Seed org structure, rooms, and participant history with plan-based volume only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
<?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 Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
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, $volume['branches']);
|
||||
$this->ensureOwnerMember($org, $ownerRef, $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);
|
||||
|
||||
return Organization::query()->updateOrCreate(
|
||||
[
|
||||
'owner_ref' => $user->ownerRef(),
|
||||
'slug' => $slug,
|
||||
],
|
||||
[
|
||||
'name' => $user->name.' Demo Org',
|
||||
'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, int $count)
|
||||
{
|
||||
$branches = collect();
|
||||
$names = ['Head Office', 'East Branch', 'West Branch'];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$branches->push(Branch::query()->updateOrCreate(
|
||||
[
|
||||
'organization_id' => $org->id,
|
||||
'code' => 'DEMO-'.($i + 1),
|
||||
],
|
||||
[
|
||||
'owner_ref' => $ownerRef,
|
||||
'name' => $names[$i] ?? ('Branch '.($i + 1)),
|
||||
'is_active' => true,
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
]);
|
||||
|
||||
for ($i = 1; $i < $count; $i++) {
|
||||
Participant::query()->create([
|
||||
'owner_ref' => $ownerRef,
|
||||
'session_id' => $session->id,
|
||||
'user_ref' => null,
|
||||
'display_name' => 'Demo Guest '.$i,
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
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 Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DemoSeedCommandTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Http::fake();
|
||||
}
|
||||
|
||||
public function test_seeds_org_member_branch_rooms_sessions_and_participants(): void
|
||||
{
|
||||
$user = User::create([
|
||||
'public_id' => 'meet-demo-free-001',
|
||||
'name' => 'Meet Demo Free',
|
||||
'email' => 'demo-free@ladill.com',
|
||||
]);
|
||||
|
||||
$this->artisan('demo:seed', ['identity' => $user->email, '--plan' => 'free'])
|
||||
->assertSuccessful();
|
||||
|
||||
$org = Organization::query()->where('owner_ref', $user->public_id)->first();
|
||||
$this->assertNotNull($org);
|
||||
$this->assertTrue((bool) data_get($org->settings, 'demo'));
|
||||
$this->assertTrue((bool) data_get($org->settings, 'onboarded'));
|
||||
|
||||
$this->assertDatabaseHas('meet_members', [
|
||||
'organization_id' => $org->id,
|
||||
'user_ref' => $user->public_id,
|
||||
'role' => 'owner',
|
||||
]);
|
||||
$this->assertSame(1, Branch::query()->where('organization_id', $org->id)->count());
|
||||
$this->assertSame(2, Room::query()->where('owner_ref', $user->public_id)->count());
|
||||
$this->assertGreaterThan(0, Session::query()->where('owner_ref', $user->public_id)->count());
|
||||
$this->assertGreaterThan(0, Participant::query()->where('owner_ref', $user->public_id)->count());
|
||||
$this->assertTrue(
|
||||
Room::query()->where('owner_ref', $user->public_id)->where('status', 'scheduled')->exists()
|
||||
);
|
||||
$this->assertTrue(
|
||||
Room::query()->where('owner_ref', $user->public_id)->where('status', 'ended')->exists()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_plan_only_varies_volume_and_makes_no_http_calls(): void
|
||||
{
|
||||
$user = User::create([
|
||||
'public_id' => 'meet-demo-ent-001',
|
||||
'name' => 'Meet Demo Ent',
|
||||
'email' => 'demo-enterprise@ladill.com',
|
||||
]);
|
||||
|
||||
Http::fake();
|
||||
|
||||
$this->artisan('demo:seed', ['identity' => $user->public_id, '--plan' => 'enterprise'])
|
||||
->assertSuccessful();
|
||||
|
||||
Http::assertNothingSent();
|
||||
|
||||
$this->assertSame(3, Branch::query()->where('owner_ref', $user->public_id)->count());
|
||||
$this->assertSame(6, Room::query()->where('owner_ref', $user->public_id)->count());
|
||||
}
|
||||
|
||||
public function test_idempotent_and_reset_is_tenant_scoped(): void
|
||||
{
|
||||
$user = User::create([
|
||||
'public_id' => 'meet-demo-pro-001',
|
||||
'name' => 'Meet Demo Pro',
|
||||
'email' => 'demo-pro@ladill.com',
|
||||
]);
|
||||
$other = User::create([
|
||||
'public_id' => 'meet-other-001',
|
||||
'name' => 'Other User',
|
||||
'email' => 'other@example.com',
|
||||
]);
|
||||
|
||||
Organization::create([
|
||||
'owner_ref' => $other->public_id,
|
||||
'name' => 'Other Org',
|
||||
'slug' => 'other-org',
|
||||
'timezone' => 'UTC',
|
||||
'settings' => ['onboarded' => true],
|
||||
]);
|
||||
|
||||
$this->artisan('demo:seed', ['identity' => $user->email, '--plan' => 'pro'])
|
||||
->assertSuccessful();
|
||||
$rooms = Room::query()->where('owner_ref', $user->public_id)->count();
|
||||
$this->assertSame(4, $rooms);
|
||||
|
||||
$this->artisan('demo:seed', ['identity' => $user->email, '--plan' => 'pro'])
|
||||
->assertSuccessful();
|
||||
$this->assertSame(4, Room::query()->where('owner_ref', $user->public_id)->count());
|
||||
|
||||
$this->artisan('demo:seed', [
|
||||
'identity' => $user->email,
|
||||
'--plan' => 'free',
|
||||
'--reset' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->assertSame(2, Room::query()->where('owner_ref', $user->public_id)->count());
|
||||
$this->assertSame(1, Member::query()->where('owner_ref', $user->public_id)->count());
|
||||
$this->assertDatabaseHas('meet_organizations', [
|
||||
'owner_ref' => $other->public_id,
|
||||
'slug' => 'other-org',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user