Wire Queue demo seeder to shared DemoWorld
Deploy Ladill Queue / deploy (push) Successful in 1m3s

Org name, branches, ticket customers, and staff Members now come from
DemoWorld so Queue continuity matches Care/Frontdesk across the suite.
Also resolve organization by owner_ref when metadata.queue.organization_id
is missing, and skip reseed-on-login for DemoWorld staff emails.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 13:35:17 +00:00
co-authored by Cursor
parent ef7ceb076f
commit 8ff100a55b
5 changed files with 705 additions and 31 deletions
+8
View File
@@ -8,6 +8,10 @@ use Illuminate\Support\Facades\Log;
/**
* Reseed Queue demo tenants after a fresh SSO login.
*
* Only owner demo accounts trigger a full reseed. DemoWorld staff logins
* (e.g. receptionist@ladill.com variants) share the owner tenant but must
* never reseed mid-demo.
*/
class DemoLoginReseeder
{
@@ -18,6 +22,10 @@ class DemoLoginReseeder
return;
}
if (\App\Support\DemoWorld::isStaffDemoEmail($user->email)) {
return;
}
$plan = $this->planForEmail($user->email);
if ($plan === null) {
return;
+103 -30
View File
@@ -22,6 +22,7 @@ use App\Models\User;
use App\Models\VoiceAnnouncement;
use App\Models\Workflow;
use App\Models\WorkflowStep;
use App\Support\DemoWorld;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@@ -74,13 +75,16 @@ class DemoSeeder
$this->resetForOwner($user->ownerRef());
}
$organization = $this->ensureOrganization($user);
$organization = $this->ensureOrganization($user, $plan);
$this->assignPlan($organization, $plan);
$volumes = self::VOLUMES[$plan];
$branches = $this->seedBranches($user->ownerRef(), $organization, $volumes['branches']);
$branches = $this->seedBranches($user->ownerRef(), $organization, $plan, $volumes['branches']);
$this->seedStaffMembers($user->ownerRef(), $organization, $plan, $branches);
$queues = [];
$counters = [];
$people = DemoWorld::people();
$personCursor = 0;
foreach ($branches as $branchIndex => $branch) {
$dept = $this->ensureDepartment($user->ownerRef(), $branch);
@@ -104,13 +108,15 @@ class DemoSeeder
$this->seedDevice($user->ownerRef(), $organization, $branch);
foreach ($branchQueues as $qi => $queue) {
$this->seedLiveTickets(
$personCursor = $this->seedLiveTickets(
$user->ownerRef(),
$organization,
$branch,
$queue,
$branchCounters[$qi % max(1, count($branchCounters))] ?? null,
$volumes['tickets_per_queue'],
$people,
$personCursor,
);
}
@@ -134,6 +140,7 @@ class DemoSeeder
$queues[0] ?? null,
$counters[0] ?? null,
$volumes['history_tickets'],
$people,
);
$organization->refresh();
@@ -248,51 +255,98 @@ class DemoSeeder
});
}
private function ensureOrganization(User $user): Organization
private function ensureOrganization(User $user, string $plan): Organization
{
$business = DemoWorld::business($plan);
$existing = $this->orgs->resolveForUser($user);
if ($existing) {
$settings = $existing->settings ?? [];
$settings['onboarded'] = true;
$existing->forceFill(['settings' => $settings])->save();
$existing->forceFill([
'name' => $business['name'],
'settings' => $settings,
])->save();
$this->orgs->ensureOwnerMember($user, $existing);
return $existing;
}
$hq = DemoWorld::branches($plan, 1)[0];
return $this->orgs->completeOnboarding($user, [
'organization_name' => 'Demo Queue Org',
'organization_name' => $business['name'],
'industry' => 'retail',
'appointment_mode' => 'hybrid',
'branch_name' => 'Main Branch',
'timezone' => 'Africa/Accra',
'branch_name' => $hq['name'],
'branch_address' => $hq['address'],
'branch_phone' => $hq['phone'],
'timezone' => $business['timezone'] ?? 'Africa/Accra',
]);
}
/** @return list<Branch> */
private function seedBranches(string $ownerRef, Organization $organization, int $count): array
private function seedBranches(string $ownerRef, Organization $organization, string $plan, int $count): array
{
$branches = Branch::query()
$catalog = DemoWorld::branches($plan, $count);
$existing = 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([
$branches = [];
foreach ($catalog as $i => $spec) {
$attrs = [
'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),
'name' => $spec['name'],
'code' => $spec['code'],
'address' => $spec['address'],
'phone' => $spec['phone'],
'is_active' => true,
]);
];
$branch = $existing[$i] ?? null;
if ($branch) {
$branch->fill($attrs)->save();
} else {
$branch = Branch::query()->create($attrs);
}
$branches[] = $branch;
}
return array_slice($branches, 0, $count);
return $branches;
}
/**
* Local Member rows for DemoWorld staff with a queue role (user_ref = email
* until SSO remaps to public_id).
*
* @param list<Branch> $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']['queue'] ?? 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 ensureDepartment(string $ownerRef, Branch $branch): Department
@@ -426,6 +480,10 @@ class DemoSeeder
]);
}
/**
* @param list<array{key: string, first_name: string, last_name: string, phone: string}> $people
* @return int next person cursor for the caller to continue from
*/
private function seedLiveTickets(
string $ownerRef,
Organization $organization,
@@ -433,16 +491,22 @@ class DemoSeeder
ServiceQueue $queue,
?Counter $counter,
int $count,
): void {
array $people,
int $personCursor,
): int {
$existing = Ticket::query()
->where('service_queue_id', $queue->id)
->whereIn('status', ['waiting', 'called', 'serving'])
->count();
$storyStart = DemoWorld::storyDay()->copy()->addHours(9);
for ($i = $existing; $i < $count; $i++) {
$seq = $queue->ticket_sequence + 1;
$queue->forceFill(['ticket_sequence' => $seq])->save();
$status = ['waiting', 'waiting', 'called', 'serving'][$i % 4];
$person = $people[$personCursor % count($people)];
$personCursor++;
Ticket::query()->create([
'owner_ref' => $ownerRef,
@@ -454,16 +518,19 @@ class DemoSeeder
'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),
'customer_name' => DemoWorld::fullName($person),
'customer_phone' => $person['phone'],
'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,
'issued_at' => $storyStart->copy()->addMinutes($i * 3),
'called_at' => in_array($status, ['called', 'serving'], true) ? $storyStart->copy()->addMinutes($i * 3 + 5) : null,
'serving_started_at' => $status === 'serving' ? $storyStart->copy()->addMinutes($i * 3 + 7) : null,
]);
}
return $personCursor;
}
/** @param list<array{key: string, first_name: string, last_name: string, phone: string}> $people */
private function seedHistoryTickets(
string $ownerRef,
Organization $organization,
@@ -471,6 +538,7 @@ class DemoSeeder
?ServiceQueue $queue,
?Counter $counter,
int $count,
array $people,
): void {
if (! $queue) {
return;
@@ -481,10 +549,14 @@ class DemoSeeder
->whereIn('status', ['completed', 'no_show', 'cancelled'])
->count();
$storyDay = DemoWorld::storyDay();
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];
$person = $people[$i % count($people)];
$day = $storyDay->copy()->subDays(($i % 14) + 1);
$ticket = Ticket::query()->create([
'owner_ref' => $ownerRef,
@@ -495,12 +567,13 @@ class DemoSeeder
'ticket_number' => $queue->prefix.str_pad((string) $seq, 3, '0', STR_PAD_LEFT),
'status' => $status,
'priority' => 'walk_in',
'customer_name' => 'History Guest '.($i + 1),
'customer_name' => DemoWorld::fullName($person),
'customer_phone' => $person['phone'],
'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,
'issued_at' => $day->copy()->subMinutes(40),
'called_at' => $day->copy()->subMinutes(20),
'serving_started_at' => $status === 'completed' ? $day->copy()->subMinutes(15) : null,
'completed_at' => $status === 'completed' ? $day->copy()->subMinutes(5) : null,
]);
TicketEvent::query()->create([
+6 -1
View File
@@ -3,6 +3,7 @@
namespace App\Services\Qms;
use App\Models\Member;
use App\Models\Organization;
use App\Models\User;
use App\Services\Identity\IdentityTeamClient;
@@ -35,7 +36,11 @@ class TeamMemberProvisioner
}
$meta = (array) data_get($grant, 'metadata.queue', []);
$ownerRef = (string) ($grant['account'] ?? '');
$organizationId = (int) ($meta['organization_id'] ?? 0);
if ($organizationId <= 0 && $ownerRef !== '') {
$organizationId = (int) Organization::query()->where('owner_ref', $ownerRef)->value('id');
}
if ($organizationId <= 0) {
continue;
}
@@ -46,7 +51,7 @@ class TeamMemberProvisioner
'user_ref' => $user->ownerRef(),
],
[
'owner_ref' => (string) ($grant['account'] ?? $user->ownerRef()),
'owner_ref' => $ownerRef !== '' ? $ownerRef : $user->ownerRef(),
'role' => (string) ($meta['role'] ?? 'member'),
'branch_id' => $meta['branch_id'] ?? null,
],