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 $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()], ], ); } }