diff --git a/app/Console/Commands/DemoSeedCommand.php b/app/Console/Commands/DemoSeedCommand.php new file mode 100644 index 0000000..4adec3c --- /dev/null +++ b/app/Console/Commands/DemoSeedCommand.php @@ -0,0 +1,62 @@ +argument('identity'); + $plan = strtolower((string) $this->option('plan')); + $reset = (bool) $this->option('reset'); + + if (! in_array($plan, ['free', 'pro', 'enterprise'], true)) { + $this->error('Invalid --plan. Use free, pro, or enterprise.'); + + return self::FAILURE; + } + + try { + $result = $seeder->seed($identity, $plan, $reset); + } catch (\InvalidArgumentException $e) { + $this->error($e->getMessage()); + + return self::FAILURE; + } + + $user = $result['user']; + $org = $result['organization']; + $effective = $plans->planKey($org); + + $this->info(sprintf( + 'Seeded Queue demo for %s (%s) org=%s plan=%s (effective=%s)%s', + $user->email, + $user->public_id, + $org->name, + $result['plan'], + $effective, + $reset ? ' [reset]' : '', + )); + + $this->table( + ['Metric', 'Count'], + collect($result['counts'])->map(fn ($v, $k) => [$k, (string) $v])->values()->all() + ); + + return self::SUCCESS; + } +} diff --git a/app/Services/Qms/DemoSeeder.php b/app/Services/Qms/DemoSeeder.php new file mode 100644 index 0000000..39b35c2 --- /dev/null +++ b/app/Services/Qms/DemoSeeder.php @@ -0,0 +1,632 @@ + */ + private const VOLUMES = [ + 'free' => [ + 'branches' => 1, + 'queues_per_branch' => 5, + 'counters_per_branch' => 3, + 'tickets_per_queue' => 8, + 'history_tickets' => 12, + ], + 'pro' => [ + 'branches' => 3, + 'queues_per_branch' => 4, + 'counters_per_branch' => 4, + 'tickets_per_queue' => 10, + 'history_tickets' => 40, + ], + 'enterprise' => [ + 'branches' => 4, + 'queues_per_branch' => 5, + 'counters_per_branch' => 5, + 'tickets_per_queue' => 12, + 'history_tickets' => 80, + ], + ]; + + private const QUEUE_NAMES = [ + ['Reception', 'A'], + ['Customer Service', 'B'], + ['Payments', 'C'], + ['Collections', 'D'], + ['Support', 'E'], + ]; + + public function __construct(private readonly OrganizationResolver $orgs) {} + + /** + * @return array{user: User, organization: Organization, plan: string, counts: array} + */ + public function seed(string $identity, string $plan, bool $reset = false): array + { + $plan = $this->normalizePlan($plan); + $user = $this->resolveUser($identity); + + if ($reset) { + $this->resetForOwner($user->ownerRef()); + } + + $organization = $this->ensureOrganization($user); + $this->assignPlan($organization, $plan); + + $volumes = self::VOLUMES[$plan]; + $branches = $this->seedBranches($user->ownerRef(), $organization, $volumes['branches']); + $queues = []; + $counters = []; + + foreach ($branches as $branchIndex => $branch) { + $dept = $this->ensureDepartment($user->ownerRef(), $branch); + $branchQueues = $this->seedQueues( + $user->ownerRef(), + $organization, + $branch, + $dept, + $volumes['queues_per_branch'], + $plan, + ); + $branchCounters = $this->seedCounters( + $user->ownerRef(), + $organization, + $branch, + $dept, + $branchQueues, + $volumes['counters_per_branch'], + ); + $this->seedDisplay($user->ownerRef(), $organization, $branch, $branchQueues); + $this->seedDevice($user->ownerRef(), $organization, $branch); + + foreach ($branchQueues as $qi => $queue) { + $this->seedLiveTickets( + $user->ownerRef(), + $organization, + $branch, + $queue, + $branchCounters[$qi % max(1, count($branchCounters))] ?? null, + $volumes['tickets_per_queue'], + ); + } + + if ($plan !== 'free' && count($branchQueues) >= 2) { + $this->seedRouting($user->ownerRef(), $branchQueues); + $this->seedTransfers($user->ownerRef(), $organization, $branch, $branchQueues, $branchCounters); + } + + if ($branchIndex === 0 && $plan !== 'free') { + $this->seedWorkflow($user->ownerRef(), $organization, $branch, $branchQueues); + } + + $queues = array_merge($queues, $branchQueues); + $counters = array_merge($counters, $branchCounters); + } + + $this->seedHistoryTickets( + $user->ownerRef(), + $organization, + $branches[0], + $queues[0] ?? null, + $counters[0] ?? null, + $volumes['history_tickets'], + ); + + $organization->refresh(); + + return [ + 'user' => $user, + 'organization' => $organization, + 'plan' => $plan, + 'counts' => [ + 'branches' => Branch::query()->where('organization_id', $organization->id)->whereNull('deleted_at')->count(), + 'queues' => ServiceQueue::query()->where('organization_id', $organization->id)->whereNull('deleted_at')->count(), + 'counters' => Counter::query()->where('organization_id', $organization->id)->whereNull('deleted_at')->count(), + 'tickets' => Ticket::query()->where('organization_id', $organization->id)->whereNull('deleted_at')->count(), + 'displays' => DisplayScreen::query()->where('organization_id', $organization->id)->whereNull('deleted_at')->count(), + 'rules' => QueueRule::query()->where('owner_ref', $user->ownerRef())->count(), + ], + ]; + } + + public function resolveUser(string $identity): User + { + $identity = trim($identity); + $user = User::query() + ->where(function ($q) use ($identity) { + $q->where('email', $identity)->orWhere('public_id', $identity); + }) + ->first(); + + if ($user) { + return $user; + } + + $email = str_contains($identity, '@') ? $identity : $identity.'@demo.ladill.local'; + $publicId = str_contains($identity, '@') ? (string) Str::uuid() : $identity; + + return User::query()->create([ + 'public_id' => $publicId, + 'name' => 'Demo User', + 'email' => $email, + 'password' => bcrypt('password'), + 'email_verified_at' => now(), + ]); + } + + public function assignPlan(Organization $organization, string $plan): void + { + $settings = $organization->settings ?? []; + $settings['onboarded'] = true; + $settings['plan'] = $plan; + $settings['plan_expires_at'] = $plan === 'free' + ? null + : now()->addYear()->toIso8601String(); + $settings['billed_branches'] = max(1, Branch::query() + ->where('organization_id', $organization->id) + ->where('is_active', true) + ->count()); + $settings['demo'] = true; + $organization->forceFill(['settings' => $settings])->save(); + } + + public function resetForOwner(string $ownerRef): void + { + DB::transaction(function () use ($ownerRef) { + $orgIds = Organization::withTrashed()->where('owner_ref', $ownerRef)->pluck('id'); + if ($orgIds->isEmpty()) { + return; + } + + $ticketIds = Ticket::withTrashed()->where('owner_ref', $ownerRef)->pluck('id'); + $queueIds = ServiceQueue::withTrashed()->where('owner_ref', $ownerRef)->pluck('id'); + $counterIds = Counter::withTrashed()->where('owner_ref', $ownerRef)->pluck('id'); + $workflowIds = Workflow::withTrashed()->where('owner_ref', $ownerRef)->pluck('id'); + + CustomerFeedback::query()->where('owner_ref', $ownerRef)->delete(); + VoiceAnnouncement::query()->where('owner_ref', $ownerRef)->delete(); + ServiceSession::query()->where('owner_ref', $ownerRef)->delete(); + TicketTransfer::query()->where('owner_ref', $ownerRef)->delete(); + TicketEvent::query()->where('owner_ref', $ownerRef)->delete(); + QueueAppointment::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); + + if ($ticketIds->isNotEmpty()) { + Ticket::withTrashed()->whereIn('id', $ticketIds)->forceDelete(); + } + + QueueRule::query()->where('owner_ref', $ownerRef)->delete(); + + if ($workflowIds->isNotEmpty()) { + WorkflowStep::query()->whereIn('workflow_id', $workflowIds)->delete(); + Workflow::withTrashed()->whereIn('id', $workflowIds)->forceDelete(); + } + + DisplayScreen::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); + Device::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); + + DB::table('queue_staff_sessions')->where('owner_ref', $ownerRef)->delete(); + DB::table('queue_counter_assignments')->where('owner_ref', $ownerRef)->delete(); + + if ($counterIds->isNotEmpty()) { + DB::table('queue_counter_service_queue')->whereIn('counter_id', $counterIds)->delete(); + Counter::withTrashed()->whereIn('id', $counterIds)->forceDelete(); + } + + if ($queueIds->isNotEmpty()) { + ServiceQueue::withTrashed()->whereIn('id', $queueIds)->forceDelete(); + } + + Department::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); + Member::query()->where('owner_ref', $ownerRef)->delete(); + AuditLog::query()->where('owner_ref', $ownerRef)->delete(); + Branch::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); + Organization::withTrashed()->where('owner_ref', $ownerRef)->forceDelete(); + }); + } + + private function ensureOrganization(User $user): Organization + { + $existing = $this->orgs->resolveForUser($user); + if ($existing) { + $settings = $existing->settings ?? []; + $settings['onboarded'] = true; + $existing->forceFill(['settings' => $settings])->save(); + $this->orgs->ensureOwnerMember($user, $existing); + + return $existing; + } + + return $this->orgs->completeOnboarding($user, [ + 'organization_name' => 'Demo Queue Org', + 'industry' => 'retail', + 'appointment_mode' => 'hybrid', + 'branch_name' => 'Main Branch', + 'timezone' => 'Africa/Accra', + ]); + } + + /** @return list */ + private function seedBranches(string $ownerRef, Organization $organization, int $count): array + { + $branches = Branch::query() + ->where('organization_id', $organization->id) + ->whereNull('deleted_at') + ->orderBy('id') + ->get() + ->all(); + + $names = ['Main Branch', 'East Legon', 'Kumasi Central', 'Tema Harbour', 'Takoradi Mall']; + for ($i = count($branches); $i < $count; $i++) { + $branches[] = Branch::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'name' => $names[$i] ?? ('Branch '.($i + 1)), + 'code' => 'BR'.($i + 1), + 'address' => 'Demo address '.($i + 1).', Accra', + 'phone' => '0302'.str_pad((string) (100000 + $i), 6, '0', STR_PAD_LEFT), + 'is_active' => true, + ]); + } + + return array_slice($branches, 0, $count); + } + + private function ensureDepartment(string $ownerRef, Branch $branch): Department + { + return Department::query()->firstOrCreate( + ['branch_id' => $branch->id, 'name' => 'General Reception'], + [ + 'owner_ref' => $ownerRef, + 'type' => 'reception', + 'is_active' => true, + ], + ); + } + + /** + * @return list + */ + private function seedQueues( + string $ownerRef, + Organization $organization, + Branch $branch, + Department $department, + int $count, + string $plan, + ): array { + $queues = ServiceQueue::query() + ->where('branch_id', $branch->id) + ->whereNull('deleted_at') + ->orderBy('id') + ->get() + ->all(); + + for ($i = count($queues); $i < $count; $i++) { + [$name, $prefix] = self::QUEUE_NAMES[$i % count(self::QUEUE_NAMES)]; + $strategy = $plan === 'free' + ? 'fifo' + : ['fifo', 'priority', 'vip', 'overflow', 'dynamic'][$i % 5]; + + $queues[] = ServiceQueue::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'department_id' => $department->id, + 'name' => $name.($i >= count(self::QUEUE_NAMES) ? ' '.($i + 1) : ''), + 'prefix' => $prefix.($i >= count(self::QUEUE_NAMES) ? (string) ($i + 1) : ''), + 'strategy' => $strategy, + 'avg_service_seconds' => 240 + ($i * 30), + 'is_active' => true, + 'ticket_sequence' => 0, + ]); + } + + return array_slice($queues, 0, $count); + } + + /** + * @param list $queues + * @return list + */ + private function seedCounters( + string $ownerRef, + Organization $organization, + Branch $branch, + Department $department, + array $queues, + int $count, + ): array { + $counters = Counter::query() + ->where('branch_id', $branch->id) + ->whereNull('deleted_at') + ->orderBy('id') + ->get() + ->all(); + + for ($i = count($counters); $i < $count; $i++) { + $counter = Counter::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'department_id' => $department->id, + 'name' => 'Counter '.($i + 1), + 'code' => 'C'.($i + 1), + 'status' => $i === 0 ? 'available' : 'busy', + 'is_active' => true, + ]); + + if ($queues !== []) { + $queue = $queues[$i % count($queues)]; + $counter->serviceQueues()->syncWithoutDetaching([$queue->id => ['priority' => 0]]); + } + + $counters[] = $counter; + } + + return array_slice($counters, 0, $count); + } + + /** @param list $queues */ + private function seedDisplay(string $ownerRef, Organization $organization, Branch $branch, array $queues): void + { + if (DisplayScreen::query()->where('branch_id', $branch->id)->whereNull('deleted_at')->exists()) { + return; + } + + DisplayScreen::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'name' => $branch->name.' Display', + 'layout' => 'standard', + 'service_queue_ids' => array_map(fn (ServiceQueue $q) => $q->id, $queues), + 'is_active' => true, + ]); + } + + private function seedDevice(string $ownerRef, Organization $organization, Branch $branch): void + { + if (Device::query()->where('branch_id', $branch->id)->whereNull('deleted_at')->exists()) { + return; + } + + Device::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'name' => $branch->name.' Kiosk', + 'type' => 'kiosk', + 'status' => 'online', + 'device_token' => Str::random(40), + 'last_online_at' => now(), + ]); + } + + private function seedLiveTickets( + string $ownerRef, + Organization $organization, + Branch $branch, + ServiceQueue $queue, + ?Counter $counter, + int $count, + ): void { + $existing = Ticket::query() + ->where('service_queue_id', $queue->id) + ->whereIn('status', ['waiting', 'called', 'serving']) + ->count(); + + for ($i = $existing; $i < $count; $i++) { + $seq = $queue->ticket_sequence + 1; + $queue->forceFill(['ticket_sequence' => $seq])->save(); + $status = ['waiting', 'waiting', 'called', 'serving'][$i % 4]; + + Ticket::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'service_queue_id' => $queue->id, + 'counter_id' => in_array($status, ['called', 'serving'], true) ? $counter?->id : null, + 'ticket_number' => $queue->prefix.str_pad((string) $seq, 3, '0', STR_PAD_LEFT), + 'status' => $status, + 'priority' => ['walk_in', 'vip', 'elderly', 'appointment'][$i % 4], + 'position' => $i + 1, + 'customer_name' => 'Guest '.($i + 1), + 'customer_phone' => '024'.str_pad((string) (3000000 + $i), 7, '0', STR_PAD_LEFT), + 'source' => ['kiosk', 'reception', 'web'][$i % 3], + 'issued_at' => now()->subMinutes(30 - $i), + 'called_at' => in_array($status, ['called', 'serving'], true) ? now()->subMinutes(5) : null, + 'serving_started_at' => $status === 'serving' ? now()->subMinutes(2) : null, + ]); + } + } + + private function seedHistoryTickets( + string $ownerRef, + Organization $organization, + Branch $branch, + ?ServiceQueue $queue, + ?Counter $counter, + int $count, + ): void { + if (! $queue) { + return; + } + + $existing = Ticket::query() + ->where('service_queue_id', $queue->id) + ->whereIn('status', ['completed', 'no_show', 'cancelled']) + ->count(); + + for ($i = $existing; $i < $count; $i++) { + $seq = $queue->ticket_sequence + 1; + $queue->forceFill(['ticket_sequence' => $seq])->save(); + $status = ['completed', 'completed', 'completed', 'no_show', 'cancelled'][$i % 5]; + + $ticket = Ticket::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'service_queue_id' => $queue->id, + 'counter_id' => $counter?->id, + 'ticket_number' => $queue->prefix.str_pad((string) $seq, 3, '0', STR_PAD_LEFT), + 'status' => $status, + 'priority' => 'walk_in', + 'customer_name' => 'History Guest '.($i + 1), + 'source' => 'kiosk', + 'issued_at' => now()->subDays(($i % 14) + 1)->subMinutes(40), + 'called_at' => now()->subDays(($i % 14) + 1)->subMinutes(20), + 'serving_started_at' => $status === 'completed' ? now()->subDays(($i % 14) + 1)->subMinutes(15) : null, + 'completed_at' => $status === 'completed' ? now()->subDays(($i % 14) + 1)->subMinutes(5) : null, + ]); + + TicketEvent::query()->create([ + 'owner_ref' => $ownerRef, + 'ticket_id' => $ticket->id, + 'event' => 'issued', + 'actor_ref' => $ownerRef, + 'created_at' => $ticket->issued_at, + ]); + + if ($status === 'completed') { + TicketEvent::query()->create([ + 'owner_ref' => $ownerRef, + 'ticket_id' => $ticket->id, + 'event' => 'completed', + 'actor_ref' => $ownerRef, + 'counter_id' => $counter?->id, + 'created_at' => $ticket->completed_at, + ]); + } + } + } + + /** @param list $queues */ + private function seedRouting(string $ownerRef, array $queues): void + { + $from = $queues[0]; + if (QueueRule::query()->where('service_queue_id', $from->id)->exists()) { + return; + } + + QueueRule::query()->create([ + 'owner_ref' => $ownerRef, + 'service_queue_id' => $from->id, + 'rule_type' => 'overflow', + 'conditions' => ['waiting_above' => 15], + 'actions' => ['route_to_queue_id' => $queues[1]->id], + 'sort_order' => 1, + 'is_active' => true, + ]); + + QueueRule::query()->create([ + 'owner_ref' => $ownerRef, + 'service_queue_id' => $from->id, + 'rule_type' => 'routing', + 'conditions' => ['priority' => 'vip'], + 'actions' => ['prefer_counter' => true], + 'sort_order' => 2, + 'is_active' => true, + ]); + } + + /** + * @param list $queues + * @param list $counters + */ + private function seedTransfers( + string $ownerRef, + Organization $organization, + Branch $branch, + array $queues, + array $counters, + ): void { + if (count($queues) < 2 || TicketTransfer::query()->where('owner_ref', $ownerRef)->exists()) { + return; + } + + $ticket = Ticket::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'service_queue_id' => $queues[1]->id, + 'counter_id' => $counters[0]->id ?? null, + 'ticket_number' => $queues[1]->prefix.'T01', + 'status' => 'transferred', + 'priority' => 'walk_in', + 'customer_name' => 'Transferred Guest', + 'source' => 'reception', + 'issued_at' => now()->subHour(), + ]); + + TicketTransfer::query()->create([ + 'owner_ref' => $ownerRef, + 'ticket_id' => $ticket->id, + 'from_service_queue_id' => $queues[0]->id, + 'to_service_queue_id' => $queues[1]->id, + 'from_counter_id' => $counters[0]->id ?? null, + 'reason' => 'Needs specialist desk', + 'transferred_by' => $ownerRef, + 'transferred_at' => now()->subMinutes(30), + ]); + } + + /** @param list $queues */ + private function seedWorkflow(string $ownerRef, Organization $organization, Branch $branch, array $queues): void + { + if (Workflow::query()->where('organization_id', $organization->id)->whereNull('deleted_at')->exists()) { + return; + } + + $workflow = Workflow::query()->create([ + 'owner_ref' => $ownerRef, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'name' => 'Demo service journey', + 'industry_template' => 'retail', + 'is_active' => true, + ]); + + foreach (array_slice($queues, 0, min(3, count($queues))) as $i => $queue) { + WorkflowStep::query()->create([ + 'workflow_id' => $workflow->id, + 'service_queue_id' => $queue->id, + 'name' => $queue->name, + 'sort_order' => $i + 1, + ]); + } + } + + private function normalizePlan(string $plan): string + { + $plan = strtolower(trim($plan)); + if (! in_array($plan, ['free', 'pro', 'enterprise'], true)) { + throw new \InvalidArgumentException('Plan must be free, pro, or enterprise.'); + } + + return $plan; + } +} diff --git a/tests/Feature/DemoSeedCommandTest.php b/tests/Feature/DemoSeedCommandTest.php new file mode 100644 index 0000000..5646acb --- /dev/null +++ b/tests/Feature/DemoSeedCommandTest.php @@ -0,0 +1,93 @@ + 'demo-queue-free', + 'name' => 'Demo Free', + 'email' => 'demo-free@ladill.com', + 'password' => bcrypt('password'), + ]); + + $exit = Artisan::call('demo:seed', ['identity' => $user->email, '--plan' => 'free']); + $this->assertSame(0, $exit); + + $org = Organization::owned($user->public_id)->first(); + $this->assertNotNull($org); + $this->assertSame('free', app(PlanService::class)->planKey($org)); + $this->assertSame(1, Branch::query()->where('organization_id', $org->id)->whereNull('deleted_at')->count()); + $this->assertLessThanOrEqual(5, ServiceQueue::query()->where('organization_id', $org->id)->whereNull('deleted_at')->count()); + $this->assertGreaterThan(0, Ticket::query()->where('organization_id', $org->id)->whereNull('deleted_at')->count()); + $this->assertTrue((bool) data_get($org->settings, 'onboarded')); + } + + public function test_pro_seed_is_multi_branch_and_reset_is_tenant_scoped(): void + { + $target = User::create([ + 'public_id' => 'demo-queue-pro', + 'name' => 'Demo Pro', + 'email' => 'demo-pro@ladill.com', + 'password' => bcrypt('password'), + ]); + $other = User::create([ + 'public_id' => 'other-queue-owner', + 'name' => 'Other', + 'email' => 'other-queue@example.com', + 'password' => bcrypt('password'), + ]); + + Artisan::call('demo:seed', ['identity' => $other->email, '--plan' => 'free']); + $otherOrgId = Organization::owned($other->public_id)->value('id'); + + Artisan::call('demo:seed', ['identity' => $target->public_id, '--plan' => 'pro']); + $org = Organization::owned($target->public_id)->first(); + $this->assertSame('pro', app(PlanService::class)->planKey($org)); + $this->assertGreaterThanOrEqual(2, Branch::query()->where('organization_id', $org->id)->whereNull('deleted_at')->count()); + $this->assertGreaterThan(0, \App\Models\QueueRule::query()->where('owner_ref', $target->public_id)->count()); + + Artisan::call('demo:seed', [ + 'identity' => $target->email, + '--plan' => 'pro', + '--reset' => true, + ]); + + $this->assertDatabaseHas('queue_organizations', ['id' => $otherOrgId, 'owner_ref' => $other->public_id]); + $this->assertSame('pro', app(PlanService::class)->planKey(Organization::owned($target->public_id)->first())); + } + + public function test_idempotent_without_reset(): void + { + $user = User::create([ + 'public_id' => 'demo-queue-idem', + 'name' => 'Demo', + 'email' => 'demo-idem@ladill.com', + 'password' => bcrypt('password'), + ]); + + Artisan::call('demo:seed', ['identity' => $user->email, '--plan' => 'enterprise']); + $org = Organization::owned($user->public_id)->first(); + $branches = Branch::query()->where('organization_id', $org->id)->whereNull('deleted_at')->count(); + $tickets = Ticket::query()->where('organization_id', $org->id)->whereNull('deleted_at')->count(); + + Artisan::call('demo:seed', ['identity' => $user->email, '--plan' => 'enterprise']); + $this->assertSame($branches, Branch::query()->where('organization_id', $org->id)->whereNull('deleted_at')->count()); + $this->assertSame($tickets, Ticket::query()->where('organization_id', $org->id)->whereNull('deleted_at')->count()); + $this->assertSame('enterprise', app(PlanService::class)->planKey($org->fresh())); + } +}