Files
ladill-meet/app/Console/Commands/DemoSeedCommand.php
T
isaaccladandCursor 1157c1c23a
Deploy Ladill Meet / deploy (push) Successful in 2m10s
Seed DemoWorld business, staff, and guest continuity for Meet demos
Organization and branch names now come from DemoWorld business/branch
catalogs, meet-role staff are mirrored as Members by email until SSO
remaps to public_id, and guest participants use DemoWorld people for
cross-app continuity. Staff logins skip the login reseed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 13:36:22 +00:00

350 lines
13 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\Branch;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Participant;
use App\Models\Room;
use App\Models\Session;
use App\Models\User;
use App\Support\DemoWorld;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* Seed local demo Meet workspace data (no LiveKit / billing network calls).
* --plan is accepted for suite contract parity and only varies data volume.
*/
class DemoSeedCommand extends Command
{
protected $signature = 'demo:seed
{identity : User email or public_id}
{--plan=free : free|pro|enterprise (volume only; Meet is PAYG)}
{--reset : Wipe this owner\'s Meet tenant data before seeding}';
protected $description = 'Seed idempotent demo organization, members, branches, rooms, sessions, and participants';
public function handle(): int
{
$plan = strtolower((string) $this->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<int, Branch>
*/
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<int, Branch> $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,
]);
}
}
}