Deploy Ladill Frontdesk / deploy (push) Successful in 1m10s
Org name, branches, and visitors now come from DemoWorld so continuity matches Care/Queue across the suite, plus Members for DemoWorld staff with a frontdesk role. Also resolve organization by owner_ref when metadata.frontdesk.organization_id is missing, and skip reseed-on-login for DemoWorld staff emails. Co-authored-by: Cursor <cursoragent@cursor.com>
430 lines
16 KiB
PHP
430 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\Branch;
|
|
use App\Models\Building;
|
|
use App\Models\Device;
|
|
use App\Models\Employee;
|
|
use App\Models\EmployeePresence;
|
|
use App\Models\EmployeePresenceEvent;
|
|
use App\Models\Host;
|
|
use App\Models\Member;
|
|
use App\Models\MessagingCredential;
|
|
use App\Models\NotificationUsage;
|
|
use App\Models\OfflineCheckIn;
|
|
use App\Models\Organization;
|
|
use App\Models\ReceptionDesk;
|
|
use App\Models\User;
|
|
use App\Models\Visit;
|
|
use App\Models\Visitor;
|
|
use App\Models\WatchlistEntry;
|
|
use App\Models\WebhookEndpoint;
|
|
use App\Services\Frontdesk\PlanService;
|
|
use App\Support\DemoWorld;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Seed plan-aware Frontdesk demo data for a suite demo identity.
|
|
*/
|
|
class DemoSeedCommand extends Command
|
|
{
|
|
protected $signature = 'demo:seed
|
|
{identity : Email or public_id of the local mirrored user}
|
|
{--plan=free : free|pro|enterprise}
|
|
{--reset : Remove prior demo data for this owner_ref before seeding}
|
|
{--public-id= : Platform public_id when creating a local mirror}
|
|
{--email= : Email when creating a local mirror}
|
|
{--name= : Display name when creating a local mirror}';
|
|
|
|
protected $description = 'Seed Frontdesk demo data for a suite demo account';
|
|
|
|
public function handle(PlanService $plans): int
|
|
{
|
|
$plan = strtolower((string) $this->option('plan'));
|
|
if (! in_array($plan, ['free', 'pro', 'enterprise'], true)) {
|
|
$this->error('Plan must be free, pro, or enterprise.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$user = $this->resolveUser();
|
|
if (! $user) {
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if ($this->option('reset')) {
|
|
$this->resetDemoData($user);
|
|
$this->info('Reset demo data for '.$user->email);
|
|
}
|
|
|
|
$organization = $this->ensureOrganization($user, $plan);
|
|
|
|
if ($this->hasDemoVisits($organization) && ! $this->option('reset')) {
|
|
$this->info('Demo data already present; plan updated. Use --reset to reseed.');
|
|
$this->printSummary($user, $organization, $plans);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$this->seedDomain($user, $organization, $plan);
|
|
$this->printSummary($user, $organization->fresh(), $plans);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function resolveUser(): ?User
|
|
{
|
|
$identity = trim((string) $this->argument('identity'));
|
|
$publicIdOpt = trim((string) ($this->option('public-id') ?: ''));
|
|
$emailOpt = trim((string) ($this->option('email') ?: ''));
|
|
$nameOpt = trim((string) ($this->option('name') ?: ''));
|
|
|
|
$user = User::query()
|
|
->where('email', $identity)
|
|
->orWhere('public_id', $identity)
|
|
->first();
|
|
|
|
if ($user) {
|
|
return $user;
|
|
}
|
|
|
|
$publicId = $publicIdOpt !== '' ? $publicIdOpt : (Str::isUuid($identity) ? $identity : '');
|
|
$email = $emailOpt !== '' ? $emailOpt : (filter_var($identity, FILTER_VALIDATE_EMAIL) ? $identity : '');
|
|
|
|
if ($publicId === '' || $email === '') {
|
|
$this->error('Local user not found for "'.$identity.'". Pass --public-id and --email to create a mirror.');
|
|
|
|
return null;
|
|
}
|
|
|
|
return User::query()->updateOrCreate(
|
|
['public_id' => $publicId],
|
|
[
|
|
'email' => $email,
|
|
'name' => $nameOpt !== '' ? $nameOpt : Str::before($email, '@'),
|
|
'email_verified_at' => now(),
|
|
],
|
|
);
|
|
}
|
|
|
|
private function ensureOrganization(User $user, string $plan): Organization
|
|
{
|
|
$paid = $plan !== 'free';
|
|
$branchCount = $paid ? ($plan === 'enterprise' ? 4 : 3) : 1;
|
|
|
|
$settings = [
|
|
'onboarded' => true,
|
|
'plan' => $plan,
|
|
'badge_expiry_hours' => 8,
|
|
'kiosk_reset_seconds' => 30,
|
|
'notification_channels' => ['email'],
|
|
'visitor_policy' => 'standard',
|
|
];
|
|
|
|
if ($paid) {
|
|
$settings['plan_expires_at'] = now()->addYear()->toIso8601String();
|
|
$settings['billed_branches'] = $branchCount;
|
|
$settings['auto_renew'] = false;
|
|
$settings['billing_method'] = 'wallet_monthly';
|
|
} else {
|
|
$settings['plan_expires_at'] = null;
|
|
$settings['billed_branches'] = 0;
|
|
}
|
|
|
|
$org = Organization::query()->withTrashed()->where('owner_ref', $user->public_id)->first();
|
|
if (! $org) {
|
|
$org = new Organization([
|
|
'owner_ref' => $user->public_id,
|
|
'slug' => 'demo-frontdesk-'.substr(md5($user->public_id), 0, 8),
|
|
]);
|
|
} elseif ($org->trashed()) {
|
|
$org->restore();
|
|
}
|
|
|
|
$org->fill([
|
|
'name' => DemoWorld::business($plan)['name'],
|
|
'timezone' => 'Africa/Accra',
|
|
'settings' => array_merge((array) ($org->settings ?? []), $settings),
|
|
]);
|
|
$org->save();
|
|
|
|
Member::query()->updateOrCreate(
|
|
[
|
|
'organization_id' => $org->id,
|
|
'user_ref' => $user->public_id,
|
|
],
|
|
[
|
|
'owner_ref' => $user->public_id,
|
|
'role' => 'org_admin',
|
|
'branch_id' => null,
|
|
],
|
|
);
|
|
|
|
return $org->fresh();
|
|
}
|
|
|
|
/**
|
|
* Local Member rows for DemoWorld staff with a frontdesk role (user_ref =
|
|
* email until SSO remaps to public_id).
|
|
*
|
|
* @param list<Branch> $branches
|
|
*/
|
|
private function seedStaffMembers(string $ownerRef, Organization $organization, string $plan, array $branches): void
|
|
{
|
|
$hq = $branches[0] ?? null;
|
|
foreach (DemoWorld::staff($plan) as $staff) {
|
|
$role = $staff['roles']['frontdesk'] ?? null;
|
|
if (! is_string($role) || $role === '') {
|
|
continue;
|
|
}
|
|
|
|
Member::query()->updateOrCreate(
|
|
[
|
|
'organization_id' => $organization->id,
|
|
'user_ref' => strtolower($staff['email']),
|
|
],
|
|
[
|
|
'owner_ref' => $ownerRef,
|
|
'role' => $role,
|
|
'branch_id' => $hq?->id,
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
private function hasDemoVisits(Organization $organization): bool
|
|
{
|
|
return Visit::query()
|
|
->where('organization_id', $organization->id)
|
|
->where('notes', 'demo:seed')
|
|
->exists();
|
|
}
|
|
|
|
private function resetDemoData(User $user): void
|
|
{
|
|
$ownerRef = $user->public_id;
|
|
|
|
DB::transaction(function () use ($ownerRef, $user) {
|
|
$orgIds = Organization::withTrashed()->where('owner_ref', $ownerRef)->pluck('id');
|
|
if ($orgIds->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$deviceIds = Device::withTrashed()->where('owner_ref', $ownerRef)->pluck('id');
|
|
$visitIds = Visit::withTrashed()->where('owner_ref', $ownerRef)->pluck('id');
|
|
$employeeIds = Employee::withTrashed()->where('owner_ref', $ownerRef)->pluck('id');
|
|
|
|
if ($deviceIds->isNotEmpty() || $visitIds->isNotEmpty()) {
|
|
OfflineCheckIn::query()
|
|
->where(function ($q) use ($deviceIds, $visitIds) {
|
|
if ($deviceIds->isNotEmpty()) {
|
|
$q->orWhereIn('device_id', $deviceIds);
|
|
}
|
|
if ($visitIds->isNotEmpty()) {
|
|
$q->orWhereIn('visit_id', $visitIds);
|
|
}
|
|
})
|
|
->delete();
|
|
}
|
|
|
|
if ($employeeIds->isNotEmpty()) {
|
|
EmployeePresence::query()->whereIn('employee_id', $employeeIds)->delete();
|
|
}
|
|
|
|
EmployeePresenceEvent::query()->where('owner_ref', $ownerRef)->delete();
|
|
Employee::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
NotificationUsage::query()->whereIn('organization_id', $orgIds)->delete();
|
|
MessagingCredential::query()->where('owner_ref', $ownerRef)->delete();
|
|
WebhookEndpoint::query()->where('owner_ref', $ownerRef)->delete();
|
|
AuditLog::query()->where('owner_ref', $ownerRef)->delete();
|
|
WatchlistEntry::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
Visit::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
Device::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
Visitor::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
Host::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
Member::query()->where('owner_ref', $ownerRef)->delete();
|
|
ReceptionDesk::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
Building::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
Branch::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
Organization::withTrashed()->where('owner_ref', $ownerRef)->forceDelete();
|
|
|
|
DB::table('notifications')
|
|
->where('notifiable_type', User::class)
|
|
->where('notifiable_id', $user->id)
|
|
->delete();
|
|
});
|
|
}
|
|
|
|
private function seedDomain(User $user, Organization $organization, string $plan): void
|
|
{
|
|
$ownerRef = $user->public_id;
|
|
$paid = $plan !== 'free';
|
|
$branchCount = $paid ? ($plan === 'enterprise' ? 4 : 3) : 1;
|
|
$visitTarget = $paid ? ($plan === 'enterprise' ? 120 : 80) : 40;
|
|
$hostCount = $paid ? 6 : 3;
|
|
$visitorCount = $paid ? 30 : 15;
|
|
|
|
$branches = [];
|
|
foreach (DemoWorld::branches($plan, $branchCount) as $spec) {
|
|
$branches[] = Branch::query()->create([
|
|
'owner_ref' => $ownerRef,
|
|
'organization_id' => $organization->id,
|
|
'name' => $spec['name'],
|
|
'code' => $spec['code'],
|
|
'address' => $spec['address'],
|
|
'phone' => $spec['phone'],
|
|
'is_active' => true,
|
|
]);
|
|
}
|
|
|
|
$this->seedStaffMembers($ownerRef, $organization, $plan, $branches);
|
|
|
|
$hosts = [];
|
|
$desks = [];
|
|
foreach ($branches as $index => $branch) {
|
|
$building = Building::query()->create([
|
|
'owner_ref' => $ownerRef,
|
|
'branch_id' => $branch->id,
|
|
'name' => $branch->name.' Building',
|
|
'floor_count' => 3,
|
|
]);
|
|
|
|
$desk = ReceptionDesk::query()->create([
|
|
'owner_ref' => $ownerRef,
|
|
'building_id' => $building->id,
|
|
'name' => $branch->name.' Desk',
|
|
'location' => 'Lobby',
|
|
'is_active' => true,
|
|
]);
|
|
$desks[$branch->id] = $desk;
|
|
|
|
$kioskLimit = $paid ? 2 : 1;
|
|
if ($index === 0 || $paid) {
|
|
for ($k = 1; $k <= ($index === 0 ? $kioskLimit : 1); $k++) {
|
|
if (! $paid && Device::query()->where('organization_id', $organization->id)->where('type', 'kiosk')->count() >= 1) {
|
|
break;
|
|
}
|
|
|
|
Device::query()->create([
|
|
'owner_ref' => $ownerRef,
|
|
'organization_id' => $organization->id,
|
|
'branch_id' => $branch->id,
|
|
'reception_desk_id' => $desk->id,
|
|
'name' => $branch->name.' Kiosk '.$k,
|
|
'type' => 'kiosk',
|
|
'status' => 'online',
|
|
'device_token' => hash('sha256', $ownerRef.'|'.$branch->id.'|'.$k),
|
|
'config' => ['mode' => 'self_service', 'demo' => true],
|
|
'last_online_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for ($h = 1; $h <= $hostCount; $h++) {
|
|
$branch = $branches[($h - 1) % count($branches)];
|
|
$hosts[] = Host::query()->create([
|
|
'owner_ref' => $ownerRef,
|
|
'organization_id' => $organization->id,
|
|
'branch_id' => $branch->id,
|
|
'name' => 'Host '.$h,
|
|
'department' => $h % 2 === 0 ? 'Operations' : 'People',
|
|
'office' => 'Office '.$h,
|
|
'phone' => '+23324'.str_pad((string) $h, 7, '0', STR_PAD_LEFT),
|
|
'email' => 'host'.$h.'@demo.ladill.com',
|
|
'extension' => (string) (100 + $h),
|
|
'is_available' => true,
|
|
]);
|
|
}
|
|
|
|
$people = DemoWorld::people();
|
|
$visitors = [];
|
|
for ($v = 1; $v <= $visitorCount; $v++) {
|
|
$person = $people[($v - 1) % count($people)];
|
|
$visitors[] = Visitor::query()->create([
|
|
'owner_ref' => $ownerRef,
|
|
'organization_id' => $organization->id,
|
|
'full_name' => DemoWorld::fullName($person),
|
|
'company' => DemoWorld::companyNameForPerson($person),
|
|
'phone' => $person['phone'],
|
|
'email' => $person['email'],
|
|
'watchlist_status' => Visitor::WATCHLIST_ALLOWED,
|
|
'visit_count' => 0,
|
|
'is_frequent' => $v <= 3,
|
|
]);
|
|
}
|
|
|
|
if ($paid && isset($visitors[0])) {
|
|
WatchlistEntry::query()->create([
|
|
'owner_ref' => $ownerRef,
|
|
'organization_id' => $organization->id,
|
|
'visitor_id' => $visitors[0]->id,
|
|
'full_name' => $visitors[0]->full_name,
|
|
'company' => $visitors[0]->company,
|
|
'status' => Visitor::WATCHLIST_REQUIRES_APPROVAL,
|
|
'reason' => 'Demo watchlist entry',
|
|
'created_by' => $ownerRef,
|
|
]);
|
|
}
|
|
|
|
$statuses = [
|
|
Visit::STATUS_CHECKED_OUT,
|
|
Visit::STATUS_CHECKED_IN,
|
|
Visit::STATUS_WAITING,
|
|
Visit::STATUS_EXPECTED,
|
|
Visit::STATUS_SCHEDULED,
|
|
];
|
|
|
|
for ($i = 1; $i <= $visitTarget; $i++) {
|
|
$branch = $branches[($i - 1) % count($branches)];
|
|
$host = $hosts[($i - 1) % count($hosts)];
|
|
$visitor = $visitors[($i - 1) % count($visitors)];
|
|
$status = $statuses[($i - 1) % count($statuses)];
|
|
$checkedIn = in_array($status, [Visit::STATUS_CHECKED_IN, Visit::STATUS_CHECKED_OUT, Visit::STATUS_WAITING], true)
|
|
? now()->subDays($i % 20)->subHours($i % 8)
|
|
: null;
|
|
|
|
Visit::query()->create([
|
|
'owner_ref' => $ownerRef,
|
|
'organization_id' => $organization->id,
|
|
'branch_id' => $branch->id,
|
|
'reception_desk_id' => $desks[$branch->id]->id ?? null,
|
|
'visitor_id' => $visitor->id,
|
|
'host_id' => $host->id,
|
|
'visitor_type' => 'guest',
|
|
'status' => $status,
|
|
'purpose' => 'Demo visit '.$i,
|
|
'expected_duration_minutes' => 60,
|
|
'scheduled_at' => now()->subDays($i % 25),
|
|
'checked_in_at' => $checkedIn,
|
|
'checked_out_at' => $status === Visit::STATUS_CHECKED_OUT ? ($checkedIn?->copy()->addHour()) : null,
|
|
'badge_expires_at' => $checkedIn?->copy()->addHours(8),
|
|
'notes' => 'demo:seed',
|
|
'source' => 'demo',
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function printSummary(User $user, Organization $organization, PlanService $plans): void
|
|
{
|
|
$this->table(
|
|
['Field', 'Value'],
|
|
[
|
|
['email', $user->email],
|
|
['public_id', $user->public_id],
|
|
['org', $organization->name],
|
|
['effective_plan', $plans->planKey($organization)],
|
|
['branches', (string) Branch::query()->where('organization_id', $organization->id)->count()],
|
|
['kiosks', (string) Device::query()->where('organization_id', $organization->id)->where('type', 'kiosk')->count()],
|
|
['hosts', (string) Host::query()->where('organization_id', $organization->id)->count()],
|
|
['visitors', (string) Visitor::query()->where('organization_id', $organization->id)->count()],
|
|
['visits', (string) Visit::query()->where('organization_id', $organization->id)->count()],
|
|
],
|
|
);
|
|
}
|
|
}
|