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:
@@ -8,6 +8,10 @@ use Illuminate\Support\Facades\Log;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reseed Queue demo tenants after a fresh SSO login.
|
* 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
|
class DemoLoginReseeder
|
||||||
{
|
{
|
||||||
@@ -18,6 +22,10 @@ class DemoLoginReseeder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (\App\Support\DemoWorld::isStaffDemoEmail($user->email)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$plan = $this->planForEmail($user->email);
|
$plan = $this->planForEmail($user->email);
|
||||||
if ($plan === null) {
|
if ($plan === null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+103
-30
@@ -22,6 +22,7 @@ use App\Models\User;
|
|||||||
use App\Models\VoiceAnnouncement;
|
use App\Models\VoiceAnnouncement;
|
||||||
use App\Models\Workflow;
|
use App\Models\Workflow;
|
||||||
use App\Models\WorkflowStep;
|
use App\Models\WorkflowStep;
|
||||||
|
use App\Support\DemoWorld;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
@@ -74,13 +75,16 @@ class DemoSeeder
|
|||||||
$this->resetForOwner($user->ownerRef());
|
$this->resetForOwner($user->ownerRef());
|
||||||
}
|
}
|
||||||
|
|
||||||
$organization = $this->ensureOrganization($user);
|
$organization = $this->ensureOrganization($user, $plan);
|
||||||
$this->assignPlan($organization, $plan);
|
$this->assignPlan($organization, $plan);
|
||||||
|
|
||||||
$volumes = self::VOLUMES[$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 = [];
|
$queues = [];
|
||||||
$counters = [];
|
$counters = [];
|
||||||
|
$people = DemoWorld::people();
|
||||||
|
$personCursor = 0;
|
||||||
|
|
||||||
foreach ($branches as $branchIndex => $branch) {
|
foreach ($branches as $branchIndex => $branch) {
|
||||||
$dept = $this->ensureDepartment($user->ownerRef(), $branch);
|
$dept = $this->ensureDepartment($user->ownerRef(), $branch);
|
||||||
@@ -104,13 +108,15 @@ class DemoSeeder
|
|||||||
$this->seedDevice($user->ownerRef(), $organization, $branch);
|
$this->seedDevice($user->ownerRef(), $organization, $branch);
|
||||||
|
|
||||||
foreach ($branchQueues as $qi => $queue) {
|
foreach ($branchQueues as $qi => $queue) {
|
||||||
$this->seedLiveTickets(
|
$personCursor = $this->seedLiveTickets(
|
||||||
$user->ownerRef(),
|
$user->ownerRef(),
|
||||||
$organization,
|
$organization,
|
||||||
$branch,
|
$branch,
|
||||||
$queue,
|
$queue,
|
||||||
$branchCounters[$qi % max(1, count($branchCounters))] ?? null,
|
$branchCounters[$qi % max(1, count($branchCounters))] ?? null,
|
||||||
$volumes['tickets_per_queue'],
|
$volumes['tickets_per_queue'],
|
||||||
|
$people,
|
||||||
|
$personCursor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +140,7 @@ class DemoSeeder
|
|||||||
$queues[0] ?? null,
|
$queues[0] ?? null,
|
||||||
$counters[0] ?? null,
|
$counters[0] ?? null,
|
||||||
$volumes['history_tickets'],
|
$volumes['history_tickets'],
|
||||||
|
$people,
|
||||||
);
|
);
|
||||||
|
|
||||||
$organization->refresh();
|
$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);
|
$existing = $this->orgs->resolveForUser($user);
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
$settings = $existing->settings ?? [];
|
$settings = $existing->settings ?? [];
|
||||||
$settings['onboarded'] = true;
|
$settings['onboarded'] = true;
|
||||||
$existing->forceFill(['settings' => $settings])->save();
|
$existing->forceFill([
|
||||||
|
'name' => $business['name'],
|
||||||
|
'settings' => $settings,
|
||||||
|
])->save();
|
||||||
$this->orgs->ensureOwnerMember($user, $existing);
|
$this->orgs->ensureOwnerMember($user, $existing);
|
||||||
|
|
||||||
return $existing;
|
return $existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$hq = DemoWorld::branches($plan, 1)[0];
|
||||||
|
|
||||||
return $this->orgs->completeOnboarding($user, [
|
return $this->orgs->completeOnboarding($user, [
|
||||||
'organization_name' => 'Demo Queue Org',
|
'organization_name' => $business['name'],
|
||||||
'industry' => 'retail',
|
'industry' => 'retail',
|
||||||
'appointment_mode' => 'hybrid',
|
'appointment_mode' => 'hybrid',
|
||||||
'branch_name' => 'Main Branch',
|
'branch_name' => $hq['name'],
|
||||||
'timezone' => 'Africa/Accra',
|
'branch_address' => $hq['address'],
|
||||||
|
'branch_phone' => $hq['phone'],
|
||||||
|
'timezone' => $business['timezone'] ?? 'Africa/Accra',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return list<Branch> */
|
/** @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)
|
->where('organization_id', $organization->id)
|
||||||
->whereNull('deleted_at')
|
->whereNull('deleted_at')
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get()
|
->get()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$names = ['Main Branch', 'East Legon', 'Kumasi Central', 'Tema Harbour', 'Takoradi Mall'];
|
$branches = [];
|
||||||
for ($i = count($branches); $i < $count; $i++) {
|
foreach ($catalog as $i => $spec) {
|
||||||
$branches[] = Branch::query()->create([
|
$attrs = [
|
||||||
'owner_ref' => $ownerRef,
|
'owner_ref' => $ownerRef,
|
||||||
'organization_id' => $organization->id,
|
'organization_id' => $organization->id,
|
||||||
'name' => $names[$i] ?? ('Branch '.($i + 1)),
|
'name' => $spec['name'],
|
||||||
'code' => 'BR'.($i + 1),
|
'code' => $spec['code'],
|
||||||
'address' => 'Demo address '.($i + 1).', Accra',
|
'address' => $spec['address'],
|
||||||
'phone' => '0302'.str_pad((string) (100000 + $i), 6, '0', STR_PAD_LEFT),
|
'phone' => $spec['phone'],
|
||||||
'is_active' => true,
|
'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
|
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(
|
private function seedLiveTickets(
|
||||||
string $ownerRef,
|
string $ownerRef,
|
||||||
Organization $organization,
|
Organization $organization,
|
||||||
@@ -433,16 +491,22 @@ class DemoSeeder
|
|||||||
ServiceQueue $queue,
|
ServiceQueue $queue,
|
||||||
?Counter $counter,
|
?Counter $counter,
|
||||||
int $count,
|
int $count,
|
||||||
): void {
|
array $people,
|
||||||
|
int $personCursor,
|
||||||
|
): int {
|
||||||
$existing = Ticket::query()
|
$existing = Ticket::query()
|
||||||
->where('service_queue_id', $queue->id)
|
->where('service_queue_id', $queue->id)
|
||||||
->whereIn('status', ['waiting', 'called', 'serving'])
|
->whereIn('status', ['waiting', 'called', 'serving'])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
|
$storyStart = DemoWorld::storyDay()->copy()->addHours(9);
|
||||||
|
|
||||||
for ($i = $existing; $i < $count; $i++) {
|
for ($i = $existing; $i < $count; $i++) {
|
||||||
$seq = $queue->ticket_sequence + 1;
|
$seq = $queue->ticket_sequence + 1;
|
||||||
$queue->forceFill(['ticket_sequence' => $seq])->save();
|
$queue->forceFill(['ticket_sequence' => $seq])->save();
|
||||||
$status = ['waiting', 'waiting', 'called', 'serving'][$i % 4];
|
$status = ['waiting', 'waiting', 'called', 'serving'][$i % 4];
|
||||||
|
$person = $people[$personCursor % count($people)];
|
||||||
|
$personCursor++;
|
||||||
|
|
||||||
Ticket::query()->create([
|
Ticket::query()->create([
|
||||||
'owner_ref' => $ownerRef,
|
'owner_ref' => $ownerRef,
|
||||||
@@ -454,16 +518,19 @@ class DemoSeeder
|
|||||||
'status' => $status,
|
'status' => $status,
|
||||||
'priority' => ['walk_in', 'vip', 'elderly', 'appointment'][$i % 4],
|
'priority' => ['walk_in', 'vip', 'elderly', 'appointment'][$i % 4],
|
||||||
'position' => $i + 1,
|
'position' => $i + 1,
|
||||||
'customer_name' => 'Guest '.($i + 1),
|
'customer_name' => DemoWorld::fullName($person),
|
||||||
'customer_phone' => '024'.str_pad((string) (3000000 + $i), 7, '0', STR_PAD_LEFT),
|
'customer_phone' => $person['phone'],
|
||||||
'source' => ['kiosk', 'reception', 'web'][$i % 3],
|
'source' => ['kiosk', 'reception', 'web'][$i % 3],
|
||||||
'issued_at' => now()->subMinutes(30 - $i),
|
'issued_at' => $storyStart->copy()->addMinutes($i * 3),
|
||||||
'called_at' => in_array($status, ['called', 'serving'], true) ? now()->subMinutes(5) : null,
|
'called_at' => in_array($status, ['called', 'serving'], true) ? $storyStart->copy()->addMinutes($i * 3 + 5) : null,
|
||||||
'serving_started_at' => $status === 'serving' ? now()->subMinutes(2) : 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(
|
private function seedHistoryTickets(
|
||||||
string $ownerRef,
|
string $ownerRef,
|
||||||
Organization $organization,
|
Organization $organization,
|
||||||
@@ -471,6 +538,7 @@ class DemoSeeder
|
|||||||
?ServiceQueue $queue,
|
?ServiceQueue $queue,
|
||||||
?Counter $counter,
|
?Counter $counter,
|
||||||
int $count,
|
int $count,
|
||||||
|
array $people,
|
||||||
): void {
|
): void {
|
||||||
if (! $queue) {
|
if (! $queue) {
|
||||||
return;
|
return;
|
||||||
@@ -481,10 +549,14 @@ class DemoSeeder
|
|||||||
->whereIn('status', ['completed', 'no_show', 'cancelled'])
|
->whereIn('status', ['completed', 'no_show', 'cancelled'])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
|
$storyDay = DemoWorld::storyDay();
|
||||||
|
|
||||||
for ($i = $existing; $i < $count; $i++) {
|
for ($i = $existing; $i < $count; $i++) {
|
||||||
$seq = $queue->ticket_sequence + 1;
|
$seq = $queue->ticket_sequence + 1;
|
||||||
$queue->forceFill(['ticket_sequence' => $seq])->save();
|
$queue->forceFill(['ticket_sequence' => $seq])->save();
|
||||||
$status = ['completed', 'completed', 'completed', 'no_show', 'cancelled'][$i % 5];
|
$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([
|
$ticket = Ticket::query()->create([
|
||||||
'owner_ref' => $ownerRef,
|
'owner_ref' => $ownerRef,
|
||||||
@@ -495,12 +567,13 @@ class DemoSeeder
|
|||||||
'ticket_number' => $queue->prefix.str_pad((string) $seq, 3, '0', STR_PAD_LEFT),
|
'ticket_number' => $queue->prefix.str_pad((string) $seq, 3, '0', STR_PAD_LEFT),
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'priority' => 'walk_in',
|
'priority' => 'walk_in',
|
||||||
'customer_name' => 'History Guest '.($i + 1),
|
'customer_name' => DemoWorld::fullName($person),
|
||||||
|
'customer_phone' => $person['phone'],
|
||||||
'source' => 'kiosk',
|
'source' => 'kiosk',
|
||||||
'issued_at' => now()->subDays(($i % 14) + 1)->subMinutes(40),
|
'issued_at' => $day->copy()->subMinutes(40),
|
||||||
'called_at' => now()->subDays(($i % 14) + 1)->subMinutes(20),
|
'called_at' => $day->copy()->subMinutes(20),
|
||||||
'serving_started_at' => $status === 'completed' ? now()->subDays(($i % 14) + 1)->subMinutes(15) : null,
|
'serving_started_at' => $status === 'completed' ? $day->copy()->subMinutes(15) : null,
|
||||||
'completed_at' => $status === 'completed' ? now()->subDays(($i % 14) + 1)->subMinutes(5) : null,
|
'completed_at' => $status === 'completed' ? $day->copy()->subMinutes(5) : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
TicketEvent::query()->create([
|
TicketEvent::query()->create([
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Services\Qms;
|
namespace App\Services\Qms;
|
||||||
|
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Identity\IdentityTeamClient;
|
use App\Services\Identity\IdentityTeamClient;
|
||||||
|
|
||||||
@@ -35,7 +36,11 @@ class TeamMemberProvisioner
|
|||||||
}
|
}
|
||||||
|
|
||||||
$meta = (array) data_get($grant, 'metadata.queue', []);
|
$meta = (array) data_get($grant, 'metadata.queue', []);
|
||||||
|
$ownerRef = (string) ($grant['account'] ?? '');
|
||||||
$organizationId = (int) ($meta['organization_id'] ?? 0);
|
$organizationId = (int) ($meta['organization_id'] ?? 0);
|
||||||
|
if ($organizationId <= 0 && $ownerRef !== '') {
|
||||||
|
$organizationId = (int) Organization::query()->where('owner_ref', $ownerRef)->value('id');
|
||||||
|
}
|
||||||
if ($organizationId <= 0) {
|
if ($organizationId <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -46,7 +51,7 @@ class TeamMemberProvisioner
|
|||||||
'user_ref' => $user->ownerRef(),
|
'user_ref' => $user->ownerRef(),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'owner_ref' => (string) ($grant['account'] ?? $user->ownerRef()),
|
'owner_ref' => $ownerRef !== '' ? $ownerRef : $user->ownerRef(),
|
||||||
'role' => (string) ($meta['role'] ?? 'member'),
|
'role' => (string) ($meta['role'] ?? 'member'),
|
||||||
'branch_id' => $meta['branch_id'] ?? null,
|
'branch_id' => $meta['branch_id'] ?? null,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,553 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical shared demo world for the Ladill suite.
|
||||||
|
*
|
||||||
|
* Single source of truth for business identity, people, companies, branches,
|
||||||
|
* and staff rosters used across Care, Queue, Frontdesk, CRM, Invoice,
|
||||||
|
* Accounting, POS, Meet, Events, and Woo. Suite apps copy this file via
|
||||||
|
* `php scripts/sync-demo-world.php` — keep copies byte-identical.
|
||||||
|
*
|
||||||
|
* Continuity is by matching identifiers (phone, email, national_id, company
|
||||||
|
* name), not cross-database foreign keys.
|
||||||
|
*
|
||||||
|
* Reseed policy: only owner demo emails (`demo-{tier}@ladill.com`) trigger
|
||||||
|
* full product reseed on login. Staff logins never wipe mid-demo.
|
||||||
|
*/
|
||||||
|
final class DemoWorld
|
||||||
|
{
|
||||||
|
public const PASSWORD_ENV = 'DEMO_ACCOUNT_PASSWORD';
|
||||||
|
|
||||||
|
public const DEFAULT_PASSWORD = 'DemoLadill2026!';
|
||||||
|
|
||||||
|
public const TIMEZONE = 'Africa/Accra';
|
||||||
|
|
||||||
|
public const CURRENCY = 'GHS';
|
||||||
|
|
||||||
|
/** Anchor “today” for same-day appointments / tickets / visits (relative). */
|
||||||
|
public const STORY_DAY_OFFSET = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{
|
||||||
|
* name: string,
|
||||||
|
* legal_name: string,
|
||||||
|
* phone: string,
|
||||||
|
* email: string,
|
||||||
|
* address: string,
|
||||||
|
* city: string,
|
||||||
|
* region: string,
|
||||||
|
* timezone: string
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public const BUSINESS = [
|
||||||
|
'free' => [
|
||||||
|
'name' => 'Ridge Family Clinic',
|
||||||
|
'legal_name' => 'Ridge Family Clinic Ltd',
|
||||||
|
'phone' => '+233302400100',
|
||||||
|
'email' => 'hello@ridgeclinic.demo.ladill.com',
|
||||||
|
'address' => '12 Castle Road, Ridge, Accra',
|
||||||
|
'city' => 'Accra',
|
||||||
|
'region' => 'Greater Accra',
|
||||||
|
'timezone' => self::TIMEZONE,
|
||||||
|
],
|
||||||
|
'pro' => [
|
||||||
|
'name' => 'Ridge Medical Centre',
|
||||||
|
'legal_name' => 'Ridge Medical Centre Ltd',
|
||||||
|
'phone' => '+233302400200',
|
||||||
|
'email' => 'hello@ridgemedical.demo.ladill.com',
|
||||||
|
'address' => '45 Independence Ave, Ridge, Accra',
|
||||||
|
'city' => 'Accra',
|
||||||
|
'region' => 'Greater Accra',
|
||||||
|
'timezone' => self::TIMEZONE,
|
||||||
|
],
|
||||||
|
'enterprise' => [
|
||||||
|
'name' => 'Accra Healthcare Group',
|
||||||
|
'legal_name' => 'Accra Healthcare Group Ltd',
|
||||||
|
'phone' => '+233302400300',
|
||||||
|
'email' => 'hello@accrahealth.demo.ladill.com',
|
||||||
|
'address' => '1 Liberation Road, Airport Residential, Accra',
|
||||||
|
'city' => 'Accra',
|
||||||
|
'region' => 'Greater Accra',
|
||||||
|
'timezone' => self::TIMEZONE,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared branch catalog. Plans take the first N (free=1, pro=3, enterprise=6).
|
||||||
|
*
|
||||||
|
* @var list<array{code: string, name: string, phone: string, address: string}>
|
||||||
|
*/
|
||||||
|
public const BRANCHES = [
|
||||||
|
['code' => 'HQ', 'name' => 'Ridge Clinic', 'phone' => '+233302400101', 'address' => '12 Castle Road, Ridge, Accra'],
|
||||||
|
['code' => 'EL', 'name' => 'East Legon Care', 'phone' => '+233302400102', 'address' => '18 Boundary Rd, East Legon, Accra'],
|
||||||
|
['code' => 'TM', 'name' => 'Tema Harbour Clinic', 'phone' => '+233302400103', 'address' => 'Harbour Road, Community 1, Tema'],
|
||||||
|
['code' => 'OS', 'name' => 'Osu Medical Centre', 'phone' => '+233302400104', 'address' => 'Oxford Street, Osu, Accra'],
|
||||||
|
['code' => 'SX', 'name' => 'Spintex Wellness', 'phone' => '+233302400105', 'address' => 'Spintex Road, Accra'],
|
||||||
|
['code' => 'AP', 'name' => 'Airport Residential Care', 'phone' => '+233302400106', 'address' => 'Liberation Rd, Airport Residential, Accra'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical people reused as patients / visitors / ticket guests / CRM
|
||||||
|
* contacts / invoice clients / POS customers / event attendees.
|
||||||
|
*
|
||||||
|
* @var list<array{
|
||||||
|
* key: string,
|
||||||
|
* first_name: string,
|
||||||
|
* last_name: string,
|
||||||
|
* phone: string,
|
||||||
|
* email: string,
|
||||||
|
* national_id: string,
|
||||||
|
* gender: string,
|
||||||
|
* dob: string,
|
||||||
|
* company_key: string,
|
||||||
|
* city: string,
|
||||||
|
* address: string
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public const PEOPLE = [
|
||||||
|
['key' => 'ama-mensah', 'first_name' => 'Ama', 'last_name' => 'Mensah', 'phone' => '+233241000001', 'email' => 'ama.mensah@demo.ladill.com', 'national_id' => 'GHA-729001001', 'gender' => 'female', 'dob' => '1990-03-12', 'company_key' => 'accra-retail', 'city' => 'Accra', 'address' => '14 Ring Road Central, Accra'],
|
||||||
|
['key' => 'kofi-asante', 'first_name' => 'Kofi', 'last_name' => 'Asante', 'phone' => '+233241000002', 'email' => 'kofi.asante@demo.ladill.com', 'national_id' => 'GHA-729001002', 'gender' => 'male', 'dob' => '1985-07-21', 'company_key' => 'kumasi-foods', 'city' => 'Accra', 'address' => '3 Labone Crescent, Accra'],
|
||||||
|
['key' => 'abena-osei', 'first_name' => 'Abena', 'last_name' => 'Osei', 'phone' => '+233241000003', 'email' => 'abena.osei@demo.ladill.com', 'national_id' => 'GHA-729001003', 'gender' => 'female', 'dob' => '1992-11-05', 'company_key' => 'tema-logistics', 'city' => 'Tema', 'address' => 'Community 4, Tema'],
|
||||||
|
['key' => 'kwame-boateng', 'first_name' => 'Kwame', 'last_name' => 'Boateng', 'phone' => '+233241000004', 'email' => 'kwame.boateng@demo.ladill.com', 'national_id' => 'GHA-729001004', 'gender' => 'male', 'dob' => '1978-01-30', 'company_key' => 'cape-coast-tours', 'city' => 'Accra', 'address' => '8 Cantonments Road, Accra'],
|
||||||
|
['key' => 'akosua-owusu', 'first_name' => 'Akosua', 'last_name' => 'Owusu', 'phone' => '+233241000005', 'email' => 'akosua.owusu@demo.ladill.com', 'national_id' => 'GHA-729001005', 'gender' => 'female', 'dob' => '1995-06-18', 'company_key' => 'tamale-agro', 'city' => 'Accra', 'address' => '22 Dansoman Estates, Accra'],
|
||||||
|
['key' => 'yaw-adjei', 'first_name' => 'Yaw', 'last_name' => 'Adjei', 'phone' => '+233241000006', 'email' => 'yaw.adjei@demo.ladill.com', 'national_id' => 'GHA-729001006', 'gender' => 'male', 'dob' => '1988-09-09', 'company_key' => 'spintex-clinics', 'city' => 'Accra', 'address' => 'Spintex Junction, Accra'],
|
||||||
|
['key' => 'efua-appiah', 'first_name' => 'Efua', 'last_name' => 'Appiah', 'phone' => '+233241000007', 'email' => 'efua.appiah@demo.ladill.com', 'national_id' => 'GHA-729001007', 'gender' => 'female', 'dob' => '1993-02-14', 'company_key' => 'osu-cafe', 'city' => 'Accra', 'address' => 'Oxford Street, Osu, Accra'],
|
||||||
|
['key' => 'kojo-darko', 'first_name' => 'Kojo', 'last_name' => 'Darko', 'phone' => '+233241000008', 'email' => 'kojo.darko@demo.ladill.com', 'national_id' => 'GHA-729001008', 'gender' => 'male', 'dob' => '1982-12-01', 'company_key' => 'legon-tech', 'city' => 'Accra', 'address' => 'University Area, Legon'],
|
||||||
|
['key' => 'adwoa-agyeman', 'first_name' => 'Adwoa', 'last_name' => 'Agyeman', 'phone' => '+233241000009', 'email' => 'adwoa.agyeman@demo.ladill.com', 'national_id' => 'GHA-729001009', 'gender' => 'female', 'dob' => '1991-04-22', 'company_key' => 'airport-hotels', 'city' => 'Accra', 'address' => 'Airport Residential, Accra'],
|
||||||
|
['key' => 'kwesi-sarpong', 'first_name' => 'Kwesi', 'last_name' => 'Sarpong', 'phone' => '+233241000010', 'email' => 'kwesi.sarpong@demo.ladill.com', 'national_id' => 'GHA-729001010', 'gender' => 'male', 'dob' => '1987-08-16', 'company_key' => 'kaneshie-traders', 'city' => 'Accra', 'address' => 'Kaneshie Market, Accra'],
|
||||||
|
['key' => 'afia-nyarko', 'first_name' => 'Afia', 'last_name' => 'Nyarko', 'phone' => '+233241000011', 'email' => 'afia.nyarko@demo.ladill.com', 'national_id' => 'GHA-729001011', 'gender' => 'female', 'dob' => '1994-10-03', 'company_key' => 'accra-retail', 'city' => 'Accra', 'address' => 'Adabraka, Accra'],
|
||||||
|
['key' => 'kwadwo-ankrah', 'first_name' => 'Kwadwo', 'last_name' => 'Ankrah', 'phone' => '+233241000012', 'email' => 'kwadwo.ankrah@demo.ladill.com', 'national_id' => 'GHA-729001012', 'gender' => 'male', 'dob' => '1980-05-27', 'company_key' => 'kumasi-foods', 'city' => 'Accra', 'address' => 'Mamprobi, Accra'],
|
||||||
|
['key' => 'esi-tetteh', 'first_name' => 'Esi', 'last_name' => 'Tetteh', 'phone' => '+233241000013', 'email' => 'esi.tetteh@demo.ladill.com', 'national_id' => 'GHA-729001013', 'gender' => 'female', 'dob' => '1996-01-19', 'company_key' => 'tema-logistics', 'city' => 'Tema', 'address' => 'Community 8, Tema'],
|
||||||
|
['key' => 'fiifi-quaye', 'first_name' => 'Fiifi', 'last_name' => 'Quaye', 'phone' => '+233241000014', 'email' => 'fiifi.quaye@demo.ladill.com', 'national_id' => 'GHA-729001014', 'gender' => 'male', 'dob' => '1989-07-07', 'company_key' => 'cape-coast-tours', 'city' => 'Accra', 'address' => 'Tesano, Accra'],
|
||||||
|
['key' => 'mansa-addo', 'first_name' => 'Mansa', 'last_name' => 'Addo', 'phone' => '+233241000015', 'email' => 'mansa.addo@demo.ladill.com', 'national_id' => 'GHA-729001015', 'gender' => 'female', 'dob' => '1993-09-25', 'company_key' => 'tamale-agro', 'city' => 'Accra', 'address' => 'Abelemkpe, Accra'],
|
||||||
|
['key' => 'nana-frimpong', 'first_name' => 'Nana', 'last_name' => 'Frimpong', 'phone' => '+233241000016', 'email' => 'nana.frimpong@demo.ladill.com', 'national_id' => 'GHA-729001016', 'gender' => 'male', 'dob' => '1984-03-11', 'company_key' => 'spintex-clinics', 'city' => 'Accra', 'address' => 'Teshie, Accra'],
|
||||||
|
['key' => 'ama-boateng', 'first_name' => 'Ama', 'last_name' => 'Boateng', 'phone' => '+233241000017', 'email' => 'ama.boateng@demo.ladill.com', 'national_id' => 'GHA-729001017', 'gender' => 'female', 'dob' => '1997-12-08', 'company_key' => 'osu-cafe', 'city' => 'Accra', 'address' => 'Kokomlemle, Accra'],
|
||||||
|
['key' => 'kofi-owusu', 'first_name' => 'Kofi', 'last_name' => 'Owusu', 'phone' => '+233241000018', 'email' => 'kofi.owusu@demo.ladill.com', 'national_id' => 'GHA-729001018', 'gender' => 'male', 'dob' => '1986-06-02', 'company_key' => 'legon-tech', 'city' => 'Accra', 'address' => 'Haatso, Accra'],
|
||||||
|
['key' => 'abena-darko', 'first_name' => 'Abena', 'last_name' => 'Darko', 'phone' => '+233241000019', 'email' => 'abena.darko@demo.ladill.com', 'national_id' => 'GHA-729001019', 'gender' => 'female', 'dob' => '1990-08-29', 'company_key' => 'airport-hotels', 'city' => 'Accra', 'address' => 'Dzorwulu, Accra'],
|
||||||
|
['key' => 'kwame-mensah', 'first_name' => 'Kwame', 'last_name' => 'Mensah', 'phone' => '+233241000020', 'email' => 'kwame.mensah@demo.ladill.com', 'national_id' => 'GHA-729001020', 'gender' => 'male', 'dob' => '1979-11-13', 'company_key' => 'kaneshie-traders', 'city' => 'Accra', 'address' => 'Ablekuma, Accra'],
|
||||||
|
['key' => 'akosua-asante', 'first_name' => 'Akosua', 'last_name' => 'Asante', 'phone' => '+233241000021', 'email' => 'akosua.asante@demo.ladill.com', 'national_id' => 'GHA-729001021', 'gender' => 'female', 'dob' => '1992-02-20', 'company_key' => 'accra-retail', 'city' => 'Accra', 'address' => 'Roman Ridge, Accra'],
|
||||||
|
['key' => 'yaw-osei', 'first_name' => 'Yaw', 'last_name' => 'Osei', 'phone' => '+233241000022', 'email' => 'yaw.osei@demo.ladill.com', 'national_id' => 'GHA-729001022', 'gender' => 'male', 'dob' => '1983-04-15', 'company_key' => 'kumasi-foods', 'city' => 'Accra', 'address' => 'Nungua, Accra'],
|
||||||
|
['key' => 'efua-boateng', 'first_name' => 'Efua', 'last_name' => 'Boateng', 'phone' => '+233241000023', 'email' => 'efua.boateng@demo.ladill.com', 'national_id' => 'GHA-729001023', 'gender' => 'female', 'dob' => '1995-07-01', 'company_key' => 'tema-logistics', 'city' => 'Tema', 'address' => 'Community 2, Tema'],
|
||||||
|
['key' => 'kojo-owusu', 'first_name' => 'Kojo', 'last_name' => 'Owusu', 'phone' => '+233241000024', 'email' => 'kojo.owusu@demo.ladill.com', 'national_id' => 'GHA-729001024', 'gender' => 'male', 'dob' => '1981-10-28', 'company_key' => 'cape-coast-tours', 'city' => 'Accra', 'address' => 'Madina, Accra'],
|
||||||
|
['key' => 'adwoa-adjei', 'first_name' => 'Adwoa', 'last_name' => 'Adjei', 'phone' => '+233241000025', 'email' => 'adwoa.adjei@demo.ladill.com', 'national_id' => 'GHA-729001025', 'gender' => 'female', 'dob' => '1994-05-06', 'company_key' => 'tamale-agro', 'city' => 'Accra', 'address' => 'Ashaiman, Accra'],
|
||||||
|
['key' => 'kwesi-appiah', 'first_name' => 'Kwesi', 'last_name' => 'Appiah', 'phone' => '+233241000026', 'email' => 'kwesi.appiah@demo.ladill.com', 'national_id' => 'GHA-729001026', 'gender' => 'male', 'dob' => '1988-12-17', 'company_key' => 'spintex-clinics', 'city' => 'Accra', 'address' => 'Baatsona, Accra'],
|
||||||
|
['key' => 'afia-darko', 'first_name' => 'Afia', 'last_name' => 'Darko', 'phone' => '+233241000027', 'email' => 'afia.darko@demo.ladill.com', 'national_id' => 'GHA-729001027', 'gender' => 'female', 'dob' => '1991-09-23', 'company_key' => 'osu-cafe', 'city' => 'Accra', 'address' => 'Labadi, Accra'],
|
||||||
|
['key' => 'kwadwo-agyeman', 'first_name' => 'Kwadwo', 'last_name' => 'Agyeman', 'phone' => '+233241000028', 'email' => 'kwadwo.agyeman@demo.ladill.com', 'national_id' => 'GHA-729001028', 'gender' => 'male', 'dob' => '1985-01-04', 'company_key' => 'legon-tech', 'city' => 'Accra', 'address' => 'Kwabenya, Accra'],
|
||||||
|
['key' => 'esi-sarpong', 'first_name' => 'Esi', 'last_name' => 'Sarpong', 'phone' => '+233241000029', 'email' => 'esi.sarpong@demo.ladill.com', 'national_id' => 'GHA-729001029', 'gender' => 'female', 'dob' => '1996-03-30', 'company_key' => 'airport-hotels', 'city' => 'Accra', 'address' => 'East Legon Hills, Accra'],
|
||||||
|
['key' => 'fiifi-nyarko', 'first_name' => 'Fiifi', 'last_name' => 'Nyarko', 'phone' => '+233241000030', 'email' => 'fiifi.nyarko@demo.ladill.com', 'national_id' => 'GHA-729001030', 'gender' => 'male', 'dob' => '1987-06-14', 'company_key' => 'kaneshie-traders', 'city' => 'Accra', 'address' => 'Odorkor, Accra'],
|
||||||
|
['key' => 'mansa-ankrah', 'first_name' => 'Mansa', 'last_name' => 'Ankrah', 'phone' => '+233241000031', 'email' => 'mansa.ankrah@demo.ladill.com', 'national_id' => 'GHA-729001031', 'gender' => 'female', 'dob' => '1993-11-11', 'company_key' => 'accra-retail', 'city' => 'Accra', 'address' => 'Korle Bu, Accra'],
|
||||||
|
['key' => 'nana-tetteh', 'first_name' => 'Nana', 'last_name' => 'Tetteh', 'phone' => '+233241000032', 'email' => 'nana.tetteh@demo.ladill.com', 'national_id' => 'GHA-729001032', 'gender' => 'male', 'dob' => '1982-08-08', 'company_key' => 'kumasi-foods', 'city' => 'Accra', 'address' => 'Weija, Accra'],
|
||||||
|
['key' => 'ama-quaye', 'first_name' => 'Ama', 'last_name' => 'Quaye', 'phone' => '+233241000033', 'email' => 'ama.quaye@demo.ladill.com', 'national_id' => 'GHA-729001033', 'gender' => 'female', 'dob' => '1990-04-04', 'company_key' => 'tema-logistics', 'city' => 'Tema', 'address' => 'Community 11, Tema'],
|
||||||
|
['key' => 'kofi-addo', 'first_name' => 'Kofi', 'last_name' => 'Addo', 'phone' => '+233241000034', 'email' => 'kofi.addo@demo.ladill.com', 'national_id' => 'GHA-729001034', 'gender' => 'male', 'dob' => '1989-02-26', 'company_key' => 'cape-coast-tours', 'city' => 'Accra', 'address' => 'Kasoa Road, Accra'],
|
||||||
|
['key' => 'abena-frimpong', 'first_name' => 'Abena', 'last_name' => 'Frimpong', 'phone' => '+233241000035', 'email' => 'abena.frimpong@demo.ladill.com', 'national_id' => 'GHA-729001035', 'gender' => 'female', 'dob' => '1994-07-19', 'company_key' => 'tamale-agro', 'city' => 'Accra', 'address' => 'Achimota, Accra'],
|
||||||
|
['key' => 'kwame-agyeman', 'first_name' => 'Kwame', 'last_name' => 'Agyeman', 'phone' => '+233241000036', 'email' => 'kwame.agyeman@demo.ladill.com', 'national_id' => 'GHA-729001036', 'gender' => 'male', 'dob' => '1980-09-01', 'company_key' => 'spintex-clinics', 'city' => 'Accra', 'address' => 'Sakumono, Accra'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<array{key: string, name: string, email: string, phone: string, address: string}>
|
||||||
|
*/
|
||||||
|
public const COMPANIES = [
|
||||||
|
['key' => 'accra-retail', 'name' => 'Accra Retail Ltd', 'email' => 'accounts@accra-retail.demo.ladill.com', 'phone' => '+233302500001', 'address' => 'Oxford Street, Accra'],
|
||||||
|
['key' => 'kumasi-foods', 'name' => 'Kumasi Foods', 'email' => 'billing@kumasi-foods.demo.ladill.com', 'phone' => '+233302500002', 'address' => 'Kejetia Road, Kumasi'],
|
||||||
|
['key' => 'tema-logistics', 'name' => 'Tema Logistics', 'email' => 'finance@tema-logistics.demo.ladill.com', 'phone' => '+233302500003', 'address' => 'Harbour Road, Tema'],
|
||||||
|
['key' => 'cape-coast-tours', 'name' => 'Cape Coast Tours', 'email' => 'pay@cape-coast-tours.demo.ladill.com', 'phone' => '+233302500004', 'address' => 'Castle Road, Cape Coast'],
|
||||||
|
['key' => 'tamale-agro', 'name' => 'Tamale Agro', 'email' => 'accounts@tamale-agro.demo.ladill.com', 'phone' => '+233302500005', 'address' => 'Salaga Road, Tamale'],
|
||||||
|
['key' => 'spintex-clinics', 'name' => 'Spintex Clinics', 'email' => 'billing@spintex-clinics.demo.ladill.com', 'phone' => '+233302500006', 'address' => 'Spintex Road, Accra'],
|
||||||
|
['key' => 'osu-cafe', 'name' => 'Osu Café Co', 'email' => 'hello@osu-cafe.demo.ladill.com', 'phone' => '+233302500007', 'address' => 'Oxford Street, Osu'],
|
||||||
|
['key' => 'legon-tech', 'name' => 'Legon Tech', 'email' => 'ap@legon-tech.demo.ladill.com', 'phone' => '+233302500008', 'address' => 'University Area, Legon'],
|
||||||
|
['key' => 'airport-hotels', 'name' => 'Airport City Hotels', 'email' => 'finance@airport-hotels.demo.ladill.com', 'phone' => '+233302500009', 'address' => 'Airport City, Accra'],
|
||||||
|
['key' => 'kaneshie-traders', 'name' => 'Kaneshie Market Traders', 'email' => 'pay@kaneshie-traders.demo.ladill.com', 'phone' => '+233302500010', 'address' => 'Kaneshie Market, Accra'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plausible shared story amounts (GHS major units) for Accounting / Invoice / POS labels.
|
||||||
|
*
|
||||||
|
* @var array{care_consult: int, care_lab: int, invoice_typical: int, pos_typical: int, crm_deal: int}
|
||||||
|
*/
|
||||||
|
public const STORY_AMOUNTS_GHS = [
|
||||||
|
'care_consult' => 250,
|
||||||
|
'care_lab' => 180,
|
||||||
|
'invoice_typical' => 1500,
|
||||||
|
'pos_typical' => 85,
|
||||||
|
'crm_deal' => 12000,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Staff roster keyed by tier. Free: owner + receptionist. Pro/Enterprise: full roster.
|
||||||
|
*
|
||||||
|
* @var array<string, list<array{
|
||||||
|
* key: string,
|
||||||
|
* email: string,
|
||||||
|
* name: string,
|
||||||
|
* apps: list<string>,
|
||||||
|
* roles: array<string, string>,
|
||||||
|
* account_role?: string
|
||||||
|
* }>>
|
||||||
|
*/
|
||||||
|
public const STAFF = [
|
||||||
|
'free' => [
|
||||||
|
[
|
||||||
|
'key' => 'receptionist',
|
||||||
|
'email' => 'demo-free-receptionist@ladill.com',
|
||||||
|
'name' => 'Abena Reception (Free)',
|
||||||
|
'apps' => ['care', 'frontdesk', 'queue'],
|
||||||
|
'roles' => [
|
||||||
|
'care' => 'receptionist',
|
||||||
|
'frontdesk' => 'receptionist',
|
||||||
|
'queue' => 'receptionist',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'pro' => [
|
||||||
|
[
|
||||||
|
'key' => 'doctor',
|
||||||
|
'email' => 'demo-pro-doctor@ladill.com',
|
||||||
|
'name' => 'Dr. Kwame Mensah (Pro)',
|
||||||
|
'apps' => ['care'],
|
||||||
|
'roles' => ['care' => 'doctor'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'nurse',
|
||||||
|
'email' => 'demo-pro-nurse@ladill.com',
|
||||||
|
'name' => 'Akosua Nurse (Pro)',
|
||||||
|
'apps' => ['care'],
|
||||||
|
'roles' => ['care' => 'nurse'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'receptionist',
|
||||||
|
'email' => 'demo-pro-receptionist@ladill.com',
|
||||||
|
'name' => 'Abena Reception (Pro)',
|
||||||
|
'apps' => ['care', 'frontdesk', 'queue'],
|
||||||
|
'roles' => [
|
||||||
|
'care' => 'receptionist',
|
||||||
|
'frontdesk' => 'receptionist',
|
||||||
|
'queue' => 'receptionist',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'pharmacist',
|
||||||
|
'email' => 'demo-pro-pharmacist@ladill.com',
|
||||||
|
'name' => 'Yaw Pharmacist (Pro)',
|
||||||
|
'apps' => ['care'],
|
||||||
|
'roles' => ['care' => 'pharmacist'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'lab',
|
||||||
|
'email' => 'demo-pro-lab@ladill.com',
|
||||||
|
'name' => 'Efua Lab Tech (Pro)',
|
||||||
|
'apps' => ['care'],
|
||||||
|
'roles' => ['care' => 'lab_technician'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'cashier',
|
||||||
|
'email' => 'demo-pro-cashier@ladill.com',
|
||||||
|
'name' => 'Kojo Cashier (Pro)',
|
||||||
|
'apps' => ['care', 'pos'],
|
||||||
|
'roles' => [
|
||||||
|
'care' => 'cashier',
|
||||||
|
'pos' => 'cashier',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'accountant',
|
||||||
|
'email' => 'demo-pro-accountant@ladill.com',
|
||||||
|
'name' => 'Adwoa Accountant (Pro)',
|
||||||
|
'apps' => ['accounting', 'invoice'],
|
||||||
|
'roles' => [
|
||||||
|
'accounting' => 'member',
|
||||||
|
'invoice' => 'member',
|
||||||
|
'care' => 'accountant',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'sales',
|
||||||
|
'email' => 'demo-pro-sales@ladill.com',
|
||||||
|
'name' => 'Kofi Sales (Pro)',
|
||||||
|
'apps' => ['crm', 'invoice', 'meet'],
|
||||||
|
'roles' => [
|
||||||
|
'crm' => 'member',
|
||||||
|
'invoice' => 'member',
|
||||||
|
'meet' => 'member',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'enterprise' => [
|
||||||
|
[
|
||||||
|
'key' => 'doctor',
|
||||||
|
'email' => 'demo-enterprise-doctor@ladill.com',
|
||||||
|
'name' => 'Dr. Kwame Mensah',
|
||||||
|
'apps' => ['care'],
|
||||||
|
'roles' => ['care' => 'doctor'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'nurse',
|
||||||
|
'email' => 'demo-enterprise-nurse@ladill.com',
|
||||||
|
'name' => 'Akosua Nurse',
|
||||||
|
'apps' => ['care'],
|
||||||
|
'roles' => ['care' => 'nurse'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'receptionist',
|
||||||
|
'email' => 'demo-enterprise-receptionist@ladill.com',
|
||||||
|
'name' => 'Abena Reception',
|
||||||
|
'apps' => ['care', 'frontdesk', 'queue'],
|
||||||
|
'roles' => [
|
||||||
|
'care' => 'receptionist',
|
||||||
|
'frontdesk' => 'receptionist',
|
||||||
|
'queue' => 'receptionist',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'pharmacist',
|
||||||
|
'email' => 'demo-enterprise-pharmacist@ladill.com',
|
||||||
|
'name' => 'Yaw Pharmacist',
|
||||||
|
'apps' => ['care'],
|
||||||
|
'roles' => ['care' => 'pharmacist'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'lab',
|
||||||
|
'email' => 'demo-enterprise-lab@ladill.com',
|
||||||
|
'name' => 'Efua Lab Tech',
|
||||||
|
'apps' => ['care'],
|
||||||
|
'roles' => ['care' => 'lab_technician'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'cashier',
|
||||||
|
'email' => 'demo-enterprise-cashier@ladill.com',
|
||||||
|
'name' => 'Kojo Cashier',
|
||||||
|
'apps' => ['care', 'pos'],
|
||||||
|
'roles' => [
|
||||||
|
'care' => 'cashier',
|
||||||
|
'pos' => 'cashier',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'accountant',
|
||||||
|
'email' => 'demo-enterprise-accountant@ladill.com',
|
||||||
|
'name' => 'Adwoa Accountant',
|
||||||
|
'apps' => ['accounting', 'invoice', 'care'],
|
||||||
|
'roles' => [
|
||||||
|
'accounting' => 'member',
|
||||||
|
'invoice' => 'member',
|
||||||
|
'care' => 'accountant',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'sales',
|
||||||
|
'email' => 'demo-enterprise-sales@ladill.com',
|
||||||
|
'name' => 'Kofi Sales',
|
||||||
|
'apps' => ['crm', 'invoice', 'meet', 'events'],
|
||||||
|
'roles' => [
|
||||||
|
'crm' => 'member',
|
||||||
|
'invoice' => 'member',
|
||||||
|
'meet' => 'member',
|
||||||
|
'events' => 'member',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var array<string, string> owner email → tier */
|
||||||
|
public const OWNER_EMAILS = [
|
||||||
|
'demo-free@ladill.com' => 'free',
|
||||||
|
'demo-pro@ladill.com' => 'pro',
|
||||||
|
'demo-enterprise@ladill.com' => 'enterprise',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function normalizeTier(string $tier): string
|
||||||
|
{
|
||||||
|
$tier = strtolower(trim($tier));
|
||||||
|
|
||||||
|
return in_array($tier, ['free', 'pro', 'enterprise'], true) ? $tier : 'free';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function ownerEmailForTier(string $tier): string
|
||||||
|
{
|
||||||
|
$tier = self::normalizeTier($tier);
|
||||||
|
|
||||||
|
return match ($tier) {
|
||||||
|
'pro' => 'demo-pro@ladill.com',
|
||||||
|
'enterprise' => 'demo-enterprise@ladill.com',
|
||||||
|
default => 'demo-free@ladill.com',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tierForOwnerEmail(?string $email): ?string
|
||||||
|
{
|
||||||
|
$email = strtolower(trim((string) $email));
|
||||||
|
|
||||||
|
return self::OWNER_EMAILS[$email] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{name: string, legal_name: string, phone: string, email: string, address: string, city: string, region: string, timezone: string}
|
||||||
|
*/
|
||||||
|
public static function business(string $tier): array
|
||||||
|
{
|
||||||
|
return self::BUSINESS[self::normalizeTier($tier)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{code: string, name: string, phone: string, address: string}>
|
||||||
|
*/
|
||||||
|
public static function branches(string $tier, ?int $limit = null): array
|
||||||
|
{
|
||||||
|
$tier = self::normalizeTier($tier);
|
||||||
|
$limit ??= match ($tier) {
|
||||||
|
'enterprise' => 6,
|
||||||
|
'pro' => 3,
|
||||||
|
default => 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return array_slice(self::BRANCHES, 0, max(1, $limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{key: string, first_name: string, last_name: string, phone: string, email: string, national_id: string, gender: string, dob: string, company_key: string, city: string, address: string}>
|
||||||
|
*/
|
||||||
|
public static function people(?int $limit = null): array
|
||||||
|
{
|
||||||
|
if ($limit === null) {
|
||||||
|
return self::PEOPLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_slice(self::PEOPLE, 0, max(0, $limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{key: string, first_name: string, last_name: string, phone: string, email: string, national_id: string, gender: string, dob: string, company_key: string, city: string, address: string}|null
|
||||||
|
*/
|
||||||
|
public static function personByKey(string $key): ?array
|
||||||
|
{
|
||||||
|
foreach (self::PEOPLE as $person) {
|
||||||
|
if ($person['key'] === $key) {
|
||||||
|
return $person;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fullName(array $person): string
|
||||||
|
{
|
||||||
|
return trim($person['first_name'].' '.$person['last_name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{key: string, name: string, email: string, phone: string, address: string}>
|
||||||
|
*/
|
||||||
|
public static function companies(?int $limit = null): array
|
||||||
|
{
|
||||||
|
if ($limit === null) {
|
||||||
|
return self::COMPANIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_slice(self::COMPANIES, 0, max(0, $limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{key: string, name: string, email: string, phone: string, address: string}|null
|
||||||
|
*/
|
||||||
|
public static function companyByKey(string $key): ?array
|
||||||
|
{
|
||||||
|
foreach (self::COMPANIES as $company) {
|
||||||
|
if ($company['key'] === $key) {
|
||||||
|
return $company;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function companyNameForPerson(array $person): string
|
||||||
|
{
|
||||||
|
return self::companyByKey($person['company_key'])['name'] ?? 'Demo Co';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{key: string, email: string, name: string, apps: list<string>, roles: array<string, string>, account_role?: string}>
|
||||||
|
*/
|
||||||
|
public static function staff(string $tier): array
|
||||||
|
{
|
||||||
|
return self::STAFF[self::normalizeTier($tier)] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{key: string, email: string, name: string, apps: list<string>, roles: array<string, string>, account_role?: string}|null
|
||||||
|
*/
|
||||||
|
public static function staffByEmail(?string $email): ?array
|
||||||
|
{
|
||||||
|
$email = strtolower(trim((string) $email));
|
||||||
|
if ($email === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::STAFF as $roster) {
|
||||||
|
foreach ($roster as $member) {
|
||||||
|
if (strtolower($member['email']) === $email) {
|
||||||
|
return $member;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isOwnerDemoEmail(?string $email): bool
|
||||||
|
{
|
||||||
|
return self::tierForOwnerEmail($email) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isStaffDemoEmail(?string $email): bool
|
||||||
|
{
|
||||||
|
return self::staffByEmail($email) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Owner email for a staff member, or null if not staff. */
|
||||||
|
public static function ownerEmailForStaff(?string $email): ?string
|
||||||
|
{
|
||||||
|
$email = strtolower(trim((string) $email));
|
||||||
|
foreach (self::STAFF as $tier => $roster) {
|
||||||
|
foreach ($roster as $member) {
|
||||||
|
if (strtolower($member['email']) === $email) {
|
||||||
|
return self::ownerEmailForTier($tier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Identity AccountTeamMember metadata from a staff roles map.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $roles
|
||||||
|
* @return array<string, array{role: string}>
|
||||||
|
*/
|
||||||
|
public static function metadataFromRoles(array $roles): array
|
||||||
|
{
|
||||||
|
$metadata = [];
|
||||||
|
foreach ($roles as $app => $role) {
|
||||||
|
$metadata[$app] = ['role' => $role];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function storyDay(): \Carbon\CarbonInterface
|
||||||
|
{
|
||||||
|
return now(self::TIMEZONE)->startOfDay()->addDays(self::STORY_DAY_OFFSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local phone form used by some apps (024####### without +233). */
|
||||||
|
public static function phoneLocal(string $e164): string
|
||||||
|
{
|
||||||
|
if (str_starts_with($e164, '+233')) {
|
||||||
|
return '0'.substr($e164, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $e164;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@
|
|||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\Branch;
|
use App\Models\Branch;
|
||||||
|
use App\Models\Member;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\ServiceQueue;
|
use App\Models\ServiceQueue;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Qms\PlanService;
|
use App\Services\Qms\PlanService;
|
||||||
|
use App\Support\DemoWorld;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
@@ -90,4 +92,37 @@ class DemoSeedCommandTest extends TestCase
|
|||||||
$this->assertSame($tickets, Ticket::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()));
|
$this->assertSame('enterprise', app(PlanService::class)->planKey($org->fresh()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_demo_world_continuity_and_staff_roles(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'demo-world-queue-id',
|
||||||
|
'name' => 'Ladill Demo (Pro)',
|
||||||
|
'email' => 'demo-pro@ladill.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$exit = Artisan::call('demo:seed', ['identity' => $user->email, '--plan' => 'pro']);
|
||||||
|
$this->assertSame(0, $exit);
|
||||||
|
|
||||||
|
$org = Organization::owned($user->public_id)->firstOrFail();
|
||||||
|
$this->assertSame(DemoWorld::business('pro')['name'], $org->name);
|
||||||
|
|
||||||
|
$ama = DemoWorld::personByKey('ama-mensah');
|
||||||
|
$this->assertNotNull($ama);
|
||||||
|
$this->assertTrue(
|
||||||
|
Ticket::query()
|
||||||
|
->where('owner_ref', $user->public_id)
|
||||||
|
->where('customer_phone', $ama['phone'])
|
||||||
|
->exists()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
Member::query()
|
||||||
|
->where('owner_ref', $user->public_id)
|
||||||
|
->where('user_ref', 'demo-pro-receptionist@ladill.com')
|
||||||
|
->where('role', 'receptionist')
|
||||||
|
->exists()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user