Seed clinic tenants with Free, Pro, and Enterprise volumes plus FK-safe reset for sales demos. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Care\DemoTenantSeeder;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed plan-aware Care demo data for a platform identity (local User mirror).
|
||||||
|
*
|
||||||
|
* Documented invocation (when the local mirror already exists):
|
||||||
|
* php artisan demo:seed demo-free@ladill.com --plan=free
|
||||||
|
* php artisan demo:seed demo-pro@ladill.com --plan=pro --reset
|
||||||
|
*
|
||||||
|
* Care cannot query platform identity. To create a missing local mirror, pass
|
||||||
|
* enough identity fields, e.g.:
|
||||||
|
* php artisan demo:seed --public-id=... --email=demo-free@ladill.com --name="Ladill Demo (Free)" --plan=free
|
||||||
|
*/
|
||||||
|
class DemoSeedCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'demo:seed
|
||||||
|
{identity? : Email or public_id of the local User mirror}
|
||||||
|
{--plan=free : free|pro|enterprise}
|
||||||
|
{--reset : Delete this tenant\'s Care demo data before re-seeding}
|
||||||
|
{--public-id= : Platform public_id (OIDC sub) when creating/resolving the mirror}
|
||||||
|
{--email= : Email when creating/resolving the mirror}
|
||||||
|
{--name= : Display name when creating the local mirror}';
|
||||||
|
|
||||||
|
protected $description = 'Seed idempotent Care demo data for a suite demo account';
|
||||||
|
|
||||||
|
public function handle(DemoTenantSeeder $seeder): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$user = $this->resolveUser();
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reset = (bool) $this->option('reset');
|
||||||
|
$this->info(sprintf(
|
||||||
|
'Seeding Care demo for %s (%s) plan=%s%s',
|
||||||
|
$user->email ?: '(no email)',
|
||||||
|
$user->public_id,
|
||||||
|
$plan,
|
||||||
|
$reset ? ' [reset]' : '',
|
||||||
|
));
|
||||||
|
|
||||||
|
$result = $seeder->seed($user, $plan, $reset);
|
||||||
|
|
||||||
|
$this->table(
|
||||||
|
['Field', 'Value'],
|
||||||
|
[
|
||||||
|
['owner_ref', $user->public_id],
|
||||||
|
['organization', $result['organization']->name],
|
||||||
|
['plan', $result['plan']],
|
||||||
|
['plan_expires_at', data_get($result['organization']->settings, 'plan_expires_at')],
|
||||||
|
['billed_branches', (string) data_get($result['organization']->settings, 'billed_branches')],
|
||||||
|
['branches', (string) $result['branches']],
|
||||||
|
['patients', (string) $result['patients']],
|
||||||
|
['appointments', (string) $result['appointments']],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->info('Done.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveUser(): User
|
||||||
|
{
|
||||||
|
$identity = trim((string) ($this->argument('identity') ?? ''));
|
||||||
|
$publicId = trim((string) ($this->option('public-id') ?? ''));
|
||||||
|
$email = trim((string) ($this->option('email') ?? ''));
|
||||||
|
$name = trim((string) ($this->option('name') ?? ''));
|
||||||
|
|
||||||
|
if ($identity !== '') {
|
||||||
|
if (filter_var($identity, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$email = $email !== '' ? $email : $identity;
|
||||||
|
} else {
|
||||||
|
$publicId = $publicId !== '' ? $publicId : $identity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = null;
|
||||||
|
if ($publicId !== '') {
|
||||||
|
$user = User::query()->where('public_id', $publicId)->first();
|
||||||
|
}
|
||||||
|
if (! $user && $email !== '') {
|
||||||
|
$user = User::query()->where('email', $email)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$updates = [];
|
||||||
|
if ($email !== '' && $user->email !== $email) {
|
||||||
|
$updates['email'] = $email;
|
||||||
|
}
|
||||||
|
if ($name !== '' && $user->name !== $name) {
|
||||||
|
$updates['name'] = $name;
|
||||||
|
}
|
||||||
|
if ($updates !== []) {
|
||||||
|
$user->forceFill($updates)->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($publicId === '' || $email === '') {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Local User mirror not found. Pass an existing email/public_id, or create one with --public-id and --email (optional --name).'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = new User([
|
||||||
|
'public_id' => $publicId,
|
||||||
|
'email' => $email,
|
||||||
|
'name' => $name !== '' ? $name : Str::before($email, '@'),
|
||||||
|
]);
|
||||||
|
$user->email_verified_at = now();
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Appointment;
|
||||||
|
use App\Models\Assessment;
|
||||||
|
use App\Models\Bill;
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Models\Drug;
|
||||||
|
use App\Models\InvestigationRequest;
|
||||||
|
use App\Models\InvestigationType;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\Patient;
|
||||||
|
use App\Models\Prescription;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Care\DemoTenantSeeder;
|
||||||
|
use App\Services\Care\PlanService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DemoSeedCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_command_is_registered(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(collect(Artisan::all())->has('demo:seed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_free_plan_respects_branch_cap_and_skips_paid_modules(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'demo-free-public-id',
|
||||||
|
'name' => 'Ladill Demo (Free)',
|
||||||
|
'email' => 'demo-free@ladill.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => 'demo-free@ladill.com',
|
||||||
|
'--plan' => 'free',
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$org = Organization::query()->where('owner_ref', $user->public_id)->first();
|
||||||
|
$this->assertNotNull($org);
|
||||||
|
$this->assertSame('free', data_get($org->settings, 'plan'));
|
||||||
|
$this->assertTrue((bool) data_get($org->settings, 'onboarded'));
|
||||||
|
$this->assertSame(1, (int) data_get($org->settings, 'billed_branches'));
|
||||||
|
$this->assertNotEmpty(data_get($org->settings, 'plan_expires_at'));
|
||||||
|
$this->assertTrue(\Carbon\Carbon::parse(data_get($org->settings, 'plan_expires_at'))->isFuture());
|
||||||
|
|
||||||
|
$this->assertSame(1, Branch::query()->where('owner_ref', $user->public_id)->where('is_active', true)->count());
|
||||||
|
$this->assertSame(1, app(PlanService::class)->maxBranches($org));
|
||||||
|
$this->assertGreaterThanOrEqual(12, Patient::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertGreaterThanOrEqual(20, Appointment::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
|
||||||
|
$this->assertSame(0, InvestigationType::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame(0, InvestigationRequest::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame(0, Drug::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame(0, Prescription::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame(0, Bill::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pro_plan_seeds_multi_branch_and_paid_modules(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'demo-pro-public-id',
|
||||||
|
'name' => 'Ladill Demo (Pro)',
|
||||||
|
'email' => 'demo-pro@ladill.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => 'demo-pro@ladill.com',
|
||||||
|
'--plan' => 'pro',
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$org = Organization::query()->where('owner_ref', $user->public_id)->firstOrFail();
|
||||||
|
$this->assertSame('pro', app(PlanService::class)->planKey($org));
|
||||||
|
$this->assertSame(3, Branch::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertTrue(app(PlanService::class)->canUseProModules($org));
|
||||||
|
|
||||||
|
$this->assertGreaterThan(0, InvestigationType::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertGreaterThan(0, InvestigationRequest::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertGreaterThan(0, Drug::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertGreaterThan(0, Prescription::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertGreaterThan(0, Bill::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_enterprise_seeds_assessments_and_more_branches(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'demo-enterprise-public-id',
|
||||||
|
'name' => 'Accra Healthcare Group',
|
||||||
|
'email' => 'demo-enterprise@ladill.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => $user->public_id,
|
||||||
|
'--plan' => 'enterprise',
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$org = Organization::query()->where('owner_ref', $user->public_id)->firstOrFail();
|
||||||
|
$this->assertSame('enterprise', app(PlanService::class)->planKey($org));
|
||||||
|
$this->assertSame(6, Branch::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertGreaterThan(0, Assessment::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertGreaterThan(0, Bill::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_idempotent_reseed_without_reset_does_not_duplicate(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'demo-idempotent-id',
|
||||||
|
'name' => 'Idempotent Demo',
|
||||||
|
'email' => 'demo-idempotent@ladill.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => 'demo-idempotent@ladill.com',
|
||||||
|
'--plan' => 'free',
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$patients = Patient::query()->where('owner_ref', $user->public_id)->count();
|
||||||
|
$appointments = Appointment::query()->where('owner_ref', $user->public_id)->count();
|
||||||
|
$orgs = Organization::query()->where('owner_ref', $user->public_id)->count();
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => 'demo-idempotent@ladill.com',
|
||||||
|
'--plan' => 'free',
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$this->assertSame($patients, Patient::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame($appointments, Appointment::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame($orgs, Organization::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame(1, Branch::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reset_wipes_only_tenant_scoped_data_then_reseeds(): void
|
||||||
|
{
|
||||||
|
$keep = User::create([
|
||||||
|
'public_id' => 'other-owner',
|
||||||
|
'name' => 'Other',
|
||||||
|
'email' => 'other@ladill.com',
|
||||||
|
]);
|
||||||
|
Organization::create([
|
||||||
|
'owner_ref' => $keep->public_id,
|
||||||
|
'name' => 'Keep Me',
|
||||||
|
'slug' => 'keep-me',
|
||||||
|
'settings' => ['onboarded' => true, 'plan' => 'free'],
|
||||||
|
]);
|
||||||
|
Branch::create([
|
||||||
|
'owner_ref' => $keep->public_id,
|
||||||
|
'organization_id' => Organization::query()->where('owner_ref', $keep->public_id)->value('id'),
|
||||||
|
'name' => 'Keep Branch',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'public_id' => 'demo-reset-id',
|
||||||
|
'name' => 'Reset Demo',
|
||||||
|
'email' => 'demo-reset@ladill.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => 'demo-reset@ladill.com',
|
||||||
|
'--plan' => 'pro',
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$patientCount = Patient::query()->where('owner_ref', $user->public_id)->count();
|
||||||
|
$this->assertGreaterThan(0, $patientCount);
|
||||||
|
$this->assertGreaterThan(0, Bill::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => 'demo-reset@ladill.com',
|
||||||
|
'--plan' => 'free',
|
||||||
|
'--reset' => true,
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$this->assertSame(1, Branch::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame(0, Bill::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame(0, Drug::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
$this->assertSame('free', data_get(
|
||||||
|
Organization::query()->where('owner_ref', $user->public_id)->value('settings'),
|
||||||
|
'plan'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Other tenant untouched.
|
||||||
|
$this->assertSame(1, Organization::query()->where('owner_ref', $keep->public_id)->count());
|
||||||
|
$this->assertSame(1, Branch::query()->where('owner_ref', $keep->public_id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_creates_local_mirror_when_public_id_and_email_provided(): void
|
||||||
|
{
|
||||||
|
$this->assertDatabaseMissing('users', ['email' => 'new-demo@ladill.com']);
|
||||||
|
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'--public-id' => 'brand-new-public-id',
|
||||||
|
'--email' => 'new-demo@ladill.com',
|
||||||
|
'--name' => 'New Demo',
|
||||||
|
'--plan' => 'free',
|
||||||
|
])->assertSuccessful();
|
||||||
|
|
||||||
|
$user = User::query()->where('email', 'new-demo@ladill.com')->first();
|
||||||
|
$this->assertNotNull($user);
|
||||||
|
$this->assertSame('brand-new-public-id', $user->public_id);
|
||||||
|
$this->assertSame(1, Organization::query()->where('owner_ref', $user->public_id)->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fails_when_mirror_missing_and_insufficient_identity(): void
|
||||||
|
{
|
||||||
|
$this->artisan('demo:seed', [
|
||||||
|
'identity' => 'missing@ladill.com',
|
||||||
|
'--plan' => 'free',
|
||||||
|
])->assertFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_volumes_helper_marks_free_without_paid_modules(): void
|
||||||
|
{
|
||||||
|
$volumes = app(DemoTenantSeeder::class)->volumes('free');
|
||||||
|
$this->assertFalse($volumes['paid_modules']);
|
||||||
|
$this->assertSame(1, $volumes['branches']);
|
||||||
|
$this->assertSame(0, $volumes['assessments']);
|
||||||
|
|
||||||
|
$pro = app(DemoTenantSeeder::class)->volumes('pro');
|
||||||
|
$this->assertTrue($pro['paid_modules']);
|
||||||
|
$this->assertSame(3, $pro['branches']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user