option('plan')); if (! in_array($plan, ['free', 'pro', 'enterprise'], true)) { $this->error('Invalid --plan. Use free, pro, or enterprise.'); return self::FAILURE; } $user = $this->resolveUser((string) $this->argument('identity')); if (! $user) { $this->error('User not found for identity: '.$this->argument('identity')); return self::FAILURE; } $ownerRef = $user->ownerRef(); if ($this->option('reset')) { $this->resetTenant($ownerRef); $this->info('Reset demo Meet data for '.$user->email); } $existing = Organization::query() ->where('owner_ref', $ownerRef) ->where('settings->demo', true) ->exists(); if ($existing && ! $this->option('reset')) { $this->info('Demo Meet workspace already present (idempotent). Plan only varies volume on fresh seed.'); $this->line(sprintf('user=%s plan=%s owner_ref=%s', $user->email, $plan, $ownerRef)); return self::SUCCESS; } $volume = $this->volumeFor($plan); $org = $this->ensureOrganization($user, $plan); $branches = $this->ensureBranches($org, $ownerRef, $plan, $volume['branches']); $this->ensureOwnerMember($org, $ownerRef, $branches->first()?->id); $this->seedStaffMembers($org, $ownerRef, $plan, $branches->first()?->id); $this->seedRoomsAndSessions($org, $ownerRef, $branches, $volume); $this->info(sprintf( 'Seeded demo Meet workspace for %s (plan=%s volume: %d branches, %d rooms).', $user->email, $plan, $volume['branches'], $volume['rooms'], )); return self::SUCCESS; } private function resolveUser(string $identity): ?User { return User::query() ->where(function ($query) use ($identity) { $query->where('email', $identity)->orWhere('public_id', $identity); }) ->first(); } /** * @return array{branches: int, rooms: int, past_sessions: int, upcoming: int, participants: int} */ private function volumeFor(string $plan): array { return match ($plan) { 'enterprise' => [ 'branches' => 3, 'rooms' => 6, 'past_sessions' => 4, 'upcoming' => 3, 'participants' => 5, ], 'pro' => [ 'branches' => 2, 'rooms' => 4, 'past_sessions' => 3, 'upcoming' => 2, 'participants' => 4, ], default => [ 'branches' => 1, 'rooms' => 2, 'past_sessions' => 1, 'upcoming' => 1, 'participants' => 3, ], }; } private function resetTenant(string $ownerRef): void { DB::transaction(function () use ($ownerRef) { $sessionIds = Session::query()->where('owner_ref', $ownerRef)->pluck('id'); if ($sessionIds->isNotEmpty()) { Participant::query() ->whereIn('session_id', $sessionIds) ->update(['breakout_room_id' => null]); Participant::query()->whereIn('session_id', $sessionIds)->delete(); Session::query()->whereIn('id', $sessionIds)->update(['parent_session_id' => null]); Session::query()->whereIn('id', $sessionIds)->delete(); } Room::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); Member::query()->where('owner_ref', $ownerRef)->delete(); Branch::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); Organization::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); if (DB::getSchemaBuilder()->hasTable('meet_audit_logs')) { DB::table('meet_audit_logs')->where('owner_ref', $ownerRef)->delete(); } }); } private function ensureOrganization(User $user, string $plan): Organization { $slug = 'demo-'.Str::slug(Str::before($user->email, '@') ?: $user->public_id); $business = DemoWorld::business($plan); return Organization::query()->updateOrCreate( [ 'owner_ref' => $user->ownerRef(), 'slug' => $slug, ], [ 'name' => $business['name'], 'timezone' => 'Africa/Accra', 'license_tier' => 'standard', 'settings' => [ 'onboarded' => true, 'demo' => true, 'demo_plan_hint' => $plan, ], ], ); } /** * @return \Illuminate\Support\Collection */ private function ensureBranches(Organization $org, string $ownerRef, string $plan, int $count) { $catalog = DemoWorld::branches($plan, $count); $branches = collect(); foreach ($catalog as $i => $spec) { $code = $spec['code'] !== '' ? $spec['code'] : 'DEMO-'.($i + 1); $branches->push(Branch::withTrashed()->updateOrCreate( [ 'organization_id' => $org->id, 'code' => $code, ], [ 'owner_ref' => $ownerRef, 'name' => $spec['name'], 'address' => $spec['address'], 'phone' => $spec['phone'], 'is_active' => true, 'deleted_at' => null, ], )); } return $branches; } private function ensureOwnerMember(Organization $org, string $ownerRef, ?int $branchId): void { Member::query()->updateOrCreate( [ 'organization_id' => $org->id, 'user_ref' => $ownerRef, ], [ 'owner_ref' => $ownerRef, 'role' => 'owner', 'branch_id' => $branchId, ], ); } /** * Mirror DemoWorld staff with a `meet` role as local Member rows, keyed * by staff email until SSO login remaps user_ref to the real public_id. */ private function seedStaffMembers(Organization $org, string $ownerRef, string $plan, ?int $branchId): void { foreach (DemoWorld::staff($plan) as $staff) { $role = $staff['roles']['meet'] ?? null; if (! is_string($role) || $role === '') { continue; } $email = strtolower(trim($staff['email'])); User::query()->firstOrCreate( ['email' => $email], [ 'public_id' => (string) Str::uuid(), 'name' => $staff['name'], 'password' => Hash::make(config('demo_accounts.password', DemoWorld::DEFAULT_PASSWORD)), 'email_verified_at' => now(), ], ); Member::query()->updateOrCreate( [ 'organization_id' => $org->id, 'user_ref' => $email, ], [ 'owner_ref' => $ownerRef, 'role' => $role, 'branch_id' => $branchId, ], ); } } /** * @param \Illuminate\Support\Collection $branches * @param array{branches: int, rooms: int, past_sessions: int, upcoming: int, participants: int} $volume */ private function seedRoomsAndSessions(Organization $org, string $ownerRef, $branches, array $volume): void { $settings = config('meet.default_settings', []); for ($i = 0; $i < $volume['rooms']; $i++) { $branch = $branches[$i % max($branches->count(), 1)] ?? null; $isPast = $i < $volume['past_sessions']; $isUpcoming = ! $isPast && ($i - $volume['past_sessions']) < $volume['upcoming']; $room = Room::query()->create([ 'owner_ref' => $ownerRef, 'organization_id' => $org->id, 'branch_id' => $branch?->id, 'host_user_ref' => $ownerRef, 'title' => $isPast ? 'Demo Past Meeting '.($i + 1) : ($isUpcoming ? 'Demo Upcoming Meeting '.($i + 1) : 'Demo Standing Room '.($i + 1)), 'type' => 'scheduled', 'status' => $isPast ? 'ended' : 'scheduled', 'scheduled_at' => $isPast ? now()->subDays($i + 1)->setTime(10, 0) : now()->addDays($i + 1)->setTime(15, 0), 'duration_minutes' => 60, 'timezone' => 'Africa/Accra', 'settings' => $settings, 'source' => [ 'app' => 'demo', 'entity_type' => 'seed', 'entity_id' => 'room-'.($i + 1), ], ]); if ($isPast) { $session = Session::query()->create([ 'owner_ref' => $ownerRef, 'room_id' => $room->id, 'media_room_name' => 'meet-'.$room->uuid, 'status' => 'ended', 'started_at' => $room->scheduled_at, 'ended_at' => $room->scheduled_at?->copy()->addHour(), 'peak_participants' => $volume['participants'], ]); $this->seedParticipants($session, $ownerRef, $volume['participants'], left: true); } elseif ($isUpcoming) { // Upcoming rooms have no session yet (host has not started). continue; } else { $session = Session::query()->create([ 'owner_ref' => $ownerRef, 'room_id' => $room->id, 'media_room_name' => 'meet-'.$room->uuid, 'status' => 'ended', 'started_at' => now()->subHours(3), 'ended_at' => now()->subHours(2), 'peak_participants' => max(2, $volume['participants'] - 1), ]); $this->seedParticipants($session, $ownerRef, max(2, $volume['participants'] - 1), left: true); } } } private function seedParticipants(Session $session, string $ownerRef, int $count, bool $left): void { Participant::query()->create([ 'owner_ref' => $ownerRef, 'session_id' => $session->id, 'user_ref' => $ownerRef, 'display_name' => 'Demo Host', 'email' => null, 'role' => 'host', 'status' => $left ? 'left' : 'joined', 'joined_at' => $session->started_at, 'left_at' => $left ? $session->ended_at : null, ]); $guests = DemoWorld::people($count - 1); for ($i = 1; $i < $count; $i++) { $guest = $guests[$i - 1] ?? null; Participant::query()->create([ 'owner_ref' => $ownerRef, 'session_id' => $session->id, 'user_ref' => null, 'display_name' => $guest ? DemoWorld::fullName($guest) : 'Demo Guest '.$i, 'email' => $guest['email'] ?? ('guest'.$i.'@example.com'), 'role' => 'participant', 'status' => $left ? 'left' : 'joined', 'joined_at' => $session->started_at?->copy()->addMinutes($i), 'left_at' => $left ? $session->ended_at : null, ]); } } }