From f9aa4a679f84647cce41140b96114a3fed144387 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Wed, 15 Jul 2026 14:14:35 +0000 Subject: [PATCH] Tighten Care free tier and bill Pro/Enterprise per branch. Free keeps core records, appointments, and basic workflows; Pro unlocks lab, pharmacy, billing, and Queue. Both paid tiers bill per branch with unlimited branches. Enterprise adds multi-dept workflows, analytics, AI-assisted healthcare integration, and priority support (not Afia). --- .../Controllers/Care/BranchController.php | 7 +- app/Http/Controllers/Care/ProController.php | 69 ++++---- .../Controllers/Care/SettingsController.php | 14 ++ app/Http/Middleware/EnsurePaidPlan.php | 44 ++++++ app/Providers/AppServiceProvider.php | 2 +- app/Services/Care/PlanService.php | 65 +++++++- app/Services/Care/ProRenewalService.php | 17 +- bootstrap/app.php | 1 + config/care.php | 13 +- resources/views/care/pro/index.blade.php | 46 +++--- resources/views/care/settings/edit.blade.php | 19 ++- resources/views/partials/sidebar.blade.php | 49 +++--- routes/web.php | 102 ++++++------ tests/Feature/CareBillTest.php | 2 +- tests/Feature/CareLabTest.php | 2 +- tests/Feature/CarePharmacyInventoryTest.php | 2 +- tests/Feature/CarePrescriptionTest.php | 2 +- tests/Feature/CareProTest.php | 149 ++++++++++++++---- tests/Feature/CareReportTest.php | 2 +- 19 files changed, 414 insertions(+), 193 deletions(-) create mode 100644 app/Http/Middleware/EnsurePaidPlan.php diff --git a/app/Http/Controllers/Care/BranchController.php b/app/Http/Controllers/Care/BranchController.php index ef255ef..b35fbb9 100644 --- a/app/Http/Controllers/Care/BranchController.php +++ b/app/Http/Controllers/Care/BranchController.php @@ -52,8 +52,11 @@ class BranchController extends Controller ->where('organization_id', $organization->id) ->count(); - if (! app(PlanService::class)->canAddBranch($organization, $currentCount)) { - return back()->withInput()->with('error', 'Your plan allows one branch. Upgrade to Care Pro for unlimited branches.'); + $plans = app(PlanService::class); + if (! $plans->canAddBranch($organization, $currentCount)) { + return redirect() + ->route('care.pro.index') + ->with('upsell', $plans->branchLimitMessage($organization)); } $validated = $request->validate([ diff --git a/app/Http/Controllers/Care/ProController.php b/app/Http/Controllers/Care/ProController.php index 30ac6a8..51c806b 100644 --- a/app/Http/Controllers/Care/ProController.php +++ b/app/Http/Controllers/Care/ProController.php @@ -26,8 +26,9 @@ class ProController extends Controller ? Carbon::parse($settings['plan_expires_at']) : null; $planKey = $plans->planKey($organization); - $branchCount = $plans->activeBranchCount($organization); - $enterprisePrice = $plans->enterprisePriceMinor($organization); + $branchCount = max(1, $plans->activeBranchCount($organization)); + $proTotal = $plans->proPriceTotalMinor($organization); + $enterpriseTotal = $plans->enterprisePriceMinor($organization); return view('care.pro.index', [ 'organization' => $organization, @@ -36,16 +37,17 @@ class ProController extends Controller 'isEnterprise' => $planKey === 'enterprise', 'hasPaidPlan' => $plans->hasPaidPlan($organization), 'canManage' => $canManage, - 'proPriceMinor' => $plans->proPriceMinor(), + 'proPricePerBranchMinor' => $plans->proPricePerBranchMinor(), + 'proPriceMinor' => $proTotal, 'enterprisePricePerBranchMinor' => $plans->enterprisePricePerBranchMinor(), - 'enterprisePriceMinor' => $enterprisePrice, + 'enterprisePriceMinor' => $enterpriseTotal, 'branchCount' => $branchCount, 'canSubscribeEnterprise' => $plans->canSubscribeEnterprise($organization), - 'enterpriseMinBranches' => (int) config('care.enterprise.min_branches', 2), + 'enterpriseMinBranches' => (int) config('care.enterprise.min_branches', 1), 'prepaidMonths' => (array) config('care.prepaid_months', [6, 12, 24]), 'currency' => (string) config('billing.currency', 'GHS'), 'planExpiresAt' => $expiresAt, - 'enterpriseBilledBranches' => (int) ($settings['enterprise_billed_branches'] ?? $branchCount), + 'billedBranches' => (int) ($settings['billed_branches'] ?? $branchCount), 'salesUrl' => ladill_platform_url('contact-sales'), ]); } @@ -60,15 +62,18 @@ class ProController extends Controller ->with('success', 'Your organization already has an active paid plan.'); } + $branches = max(1, $plans->activeBranchCount($organization)); + return $this->chargeMonthlyWallet( $organization, $billing, - $plans->proPriceMinor(), + $plans->proPriceTotalMinor($organization), 'pro', 'care_pro', 'care-pro-'.$organization->id.'-'.now()->format('Y-m-d-His'), - 'Ladill Care Pro — monthly subscription', + 'Ladill Care Pro — '.$branches.' branch(es) monthly', 'Welcome to Care Pro — active for one month.', + $branches, ); } @@ -83,12 +88,14 @@ class ProController extends Controller } if (! $plans->canSubscribeEnterprise($organization)) { - $min = (int) config('care.enterprise.min_branches', 2); + $min = (int) config('care.enterprise.min_branches', 1); return redirect()->route('care.pro.index') - ->with('error', "Enterprise billing requires at least {$min} active branches. Add another branch or choose Pro for a single site."); + ->with('error', "Enterprise requires at least {$min} active branch. Contact sales for custom multi-site onboarding."); } + $branches = max(1, $plans->activeBranchCount($organization)); + return $this->chargeMonthlyWallet( $organization, $billing, @@ -96,9 +103,9 @@ class ProController extends Controller 'enterprise', 'care_enterprise', 'care-enterprise-'.$organization->id.'-'.now()->format('Y-m-d-His'), - 'Ladill Care Enterprise — monthly subscription', + 'Ladill Care Enterprise — '.$branches.' branch(es) monthly', 'Welcome to Care Enterprise — active for one month.', - $plans->activeBranchCount($organization), + $branches, ); } @@ -120,17 +127,15 @@ class ProController extends Controller $months = (int) $validated['months']; if ($plan === 'enterprise' && ! $plans->canSubscribeEnterprise($organization)) { - $min = (int) config('care.enterprise.min_branches', 2); + $min = (int) config('care.enterprise.min_branches', 1); return redirect()->route('care.pro.index') - ->with('error', "Enterprise billing requires at least {$min} active branches."); + ->with('error', "Enterprise requires at least {$min} active branch."); } - $monthlyMinor = $plan === 'enterprise' - ? $plans->enterprisePriceMinor($organization) - : $plans->proPriceMinor(); + $branches = max(1, $plans->activeBranchCount($organization)); + $monthlyMinor = $plans->monthlyPriceMinor($organization, $plan); $amountMinor = $monthlyMinor * $months; - $branches = $plan === 'enterprise' ? $plans->activeBranchCount($organization) : null; try { $checkout = $billing->initiatePlanCheckout( @@ -139,10 +144,10 @@ class ProController extends Controller $months, $amountMinor, route('care.pro.paystack.callback'), - array_filter([ + [ 'organization_id' => $organization->id, - 'enterprise_billed_branches' => $branches, - ], static fn ($v) => $v !== null), + 'billed_branches' => $branches, + ], ); } catch (\Throwable) { return redirect()->route('care.pro.index') @@ -191,9 +196,9 @@ class ProController extends Controller $plan = (string) ($result['plan'] ?? 'pro'); $months = (int) ($result['months'] ?? 0); - $branches = isset($metadata['enterprise_billed_branches']) - ? (int) $metadata['enterprise_billed_branches'] - : null; + $branches = isset($metadata['billed_branches']) + ? (int) $metadata['billed_branches'] + : max(1, $plans->activeBranchCount($organization)); $this->activatePrepaidPlan($organization, $plan, $months, $branches); @@ -212,7 +217,7 @@ class ProController extends Controller string $reference, string $description, string $successMessage, - ?int $enterpriseBranches = null, + int $billedBranches, ): RedirectResponse { try { if (! $billing->canAfford($organization->owner_ref, $priceMinor)) { @@ -241,13 +246,11 @@ class ProController extends Controller $settings['plan'] = $plan; $settings['auto_renew'] = true; $settings['billing_method'] = 'wallet_monthly'; + $settings['billed_branches'] = $billedBranches; $settings['plan_expires_at'] = now() ->addDays((int) config('care.pro.period_days', 30)) ->toIso8601String(); - if ($enterpriseBranches !== null) { - $settings['enterprise_billed_branches'] = $enterpriseBranches; - } - unset($settings['plan_renewal_error']); + unset($settings['plan_renewal_error'], $settings['enterprise_billed_branches']); $organization->update(['settings' => $settings]); return redirect()->route('care.pro.index')->with('success', $successMessage); @@ -257,17 +260,15 @@ class ProController extends Controller Organization $organization, string $plan, int $months, - ?int $enterpriseBranches = null, + int $billedBranches, ): void { $settings = $organization->settings ?? []; $settings['plan'] = $plan; $settings['auto_renew'] = false; $settings['billing_method'] = 'paystack_prepaid'; + $settings['billed_branches'] = $billedBranches; $settings['plan_expires_at'] = now()->addMonths($months)->toIso8601String(); - if ($enterpriseBranches !== null) { - $settings['enterprise_billed_branches'] = $enterpriseBranches; - } - unset($settings['plan_renewal_error']); + unset($settings['plan_renewal_error'], $settings['enterprise_billed_branches']); $organization->update(['settings' => $settings]); } } diff --git a/app/Http/Controllers/Care/SettingsController.php b/app/Http/Controllers/Care/SettingsController.php index fb2abe2..5672572 100644 --- a/app/Http/Controllers/Care/SettingsController.php +++ b/app/Http/Controllers/Care/SettingsController.php @@ -8,6 +8,7 @@ use App\Models\Branch; use App\Services\Care\AppointmentPatientNotifier; use App\Services\Care\AuditLogger; use App\Services\Care\CarePermissions; +use App\Services\Care\PlanService; use App\Services\Messaging\MessagingCredentialsService; use App\Services\Queue\QueueClient; use App\Support\OrganizationBranding; @@ -30,10 +31,13 @@ class SettingsController extends Controller ->where('organization_id', $organization->id) ->count(); + $plans = app(PlanService::class); + return view('care.settings.edit', [ 'organization' => $organization, 'canManage' => $canManage, 'branchCount' => $branchCount, + 'canUseQueueIntegration' => $plans->canUseQueueIntegration($organization), 'messagingSmsReady' => $credential->hasValidSms(), 'messagingEmailReady' => $credential->hasValidBird(), 'facilityTypes' => [ @@ -50,6 +54,7 @@ class SettingsController extends Controller $this->authorizeAbility($request, 'settings.manage'); $organization = $this->organization($request); $owner = $this->ownerRef($request); + $plans = app(PlanService::class); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], @@ -68,7 +73,16 @@ class SettingsController extends Controller $settings['onboarded'] = true; $settings['facility_type'] = $validated['facility_type']; + // Queue integration is Pro/Enterprise only — free plans cannot enable it. $wantsQueue = $request->boolean('queue_integration_enabled'); + if ($wantsQueue && ! $plans->canUseQueueIntegration($organization)) { + return redirect() + ->route('care.pro.index') + ->with('upsell', 'Ladill Queue integration is part of Care Pro. Upgrade to connect service queues and counters.'); + } + if (! $plans->canUseQueueIntegration($organization)) { + $wantsQueue = false; + } $settings['queue_integration_enabled'] = $wantsQueue; $settings[AppointmentPatientNotifier::SETTING_BOOKED_SMS] = $request->boolean('notify_appointment_booked_sms'); diff --git a/app/Http/Middleware/EnsurePaidPlan.php b/app/Http/Middleware/EnsurePaidPlan.php new file mode 100644 index 0000000..a93242f --- /dev/null +++ b/app/Http/Middleware/EnsurePaidPlan.php @@ -0,0 +1,44 @@ +user(); + if (! $user) { + return $next($request); + } + + $organization = $this->organizations->resolveForUser($user); + if (! $organization || $this->plans->hasPaidPlan($organization)) { + return $next($request); + } + + if ($request->expectsJson() || $request->ajax()) { + return response()->json([ + 'message' => 'This feature requires Care Pro or Enterprise.', + 'upgrade_url' => route('care.pro.index'), + ], 402); + } + + return redirect() + ->route('care.pro.index') + ->with('upsell', 'Lab, pharmacy, billing, and Queue integration are part of Care Pro. Upgrade to unlock them.'); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 39e424d..22c6c0a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -22,7 +22,7 @@ class AppServiceProvider extends ServiceProvider { Event::listen(ServiceEventOccurred::class, PlatformServiceEventListener::class); - View::composer('partials.sidebar', function ($view) { + View::composer(['partials.sidebar', 'partials.topbar-desktop-widgets', 'partials.afia', 'components.app-layout'], function ($view) { /** @var User|null $user */ $user = auth()->user(); $planKey = 'free'; diff --git a/app/Services/Care/PlanService.php b/app/Services/Care/PlanService.php index ccc9e3a..f161942 100644 --- a/app/Services/Care/PlanService.php +++ b/app/Services/Care/PlanService.php @@ -42,6 +42,17 @@ class PlanService return in_array($this->planKey($organization), ['pro', 'enterprise'], true); } + /** Lab, pharmacy, billing, and Ladill Queue integration require Pro or Enterprise. */ + public function canUseProModules(Organization $organization): bool + { + return $this->hasPaidPlan($organization); + } + + public function canUseQueueIntegration(Organization $organization): bool + { + return $this->hasPaidPlan($organization); + } + public function activeBranchCount(Organization $organization): int { return Branch::query() @@ -53,8 +64,9 @@ class PlanService public function maxBranches(Organization $organization): ?int { $plan = config('care.plans.'.$this->planKey($organization)); + $max = $plan['max_branches'] ?? null; - return $plan['max_branches'] ?? null; + return $max === null ? null : (int) $max; } public function canAddBranch(Organization $organization, int $currentCount): bool @@ -64,25 +76,66 @@ class PlanService return $max === null || $currentCount < $max; } + public function branchLimitMessage(Organization $organization): string + { + $max = $this->maxBranches($organization); + $key = $this->planKey($organization); + + if ($key === 'free') { + return 'Your Free plan allows 1 branch. Upgrade to Care Pro for unlimited branches (billed per branch).'; + } + + if ($key === 'pro' && $max !== null) { + return "Your Pro plan allows up to {$max} branches (billed per branch)."; + } + + return 'Branch limit reached for your current plan.'; + } + + public function proPricePerBranchMinor(): int + { + return (int) config( + 'care.plans.pro.price_minor_per_branch', + config('care.plans.pro.price_minor', 199000), + ); + } + + /** @deprecated Prefer proPricePerBranchMinor() — Pro is billed per branch. */ public function proPriceMinor(): int { - return (int) config('care.plans.pro.price_minor', 19900); + return $this->proPricePerBranchMinor(); } public function enterprisePricePerBranchMinor(): int { - return (int) config('care.plans.enterprise.price_minor_per_branch', 49900); + return (int) config( + 'care.plans.enterprise.price_minor_per_branch', + config('care.plans.enterprise.price_minor', 499000), + ); } + /** Total monthly amount for Pro = active branches × per-branch rate (min 1). */ + public function proPriceTotalMinor(Organization $organization): int + { + return max(1, $this->activeBranchCount($organization)) * $this->proPricePerBranchMinor(); + } + + /** Total monthly amount for Enterprise = active branches × per-branch rate (min 1). */ public function enterprisePriceMinor(Organization $organization): int { - $branches = max(1, $this->activeBranchCount($organization)); + return max(1, $this->activeBranchCount($organization)) * $this->enterprisePricePerBranchMinor(); + } - return $branches * $this->enterprisePricePerBranchMinor(); + public function monthlyPriceMinor(Organization $organization, string $plan): int + { + return $plan === 'enterprise' + ? $this->enterprisePriceMinor($organization) + : $this->proPriceTotalMinor($organization); } public function canSubscribeEnterprise(Organization $organization): bool { - return $this->activeBranchCount($organization) >= (int) config('care.enterprise.min_branches', 2); + // Enterprise is a feature tier (not branch-gated). min_branches defaults to 1. + return $this->activeBranchCount($organization) >= (int) config('care.enterprise.min_branches', 1); } } diff --git a/app/Services/Care/ProRenewalService.php b/app/Services/Care/ProRenewalService.php index 450a201..beb8848 100644 --- a/app/Services/Care/ProRenewalService.php +++ b/app/Services/Care/ProRenewalService.php @@ -50,9 +50,8 @@ class ProRenewalService $settings = $organization->settings ?? []; $plan = (string) ($settings['plan'] ?? 'free'); $isEnterprise = $plan === 'enterprise'; - $price = $isEnterprise - ? $this->plans->enterprisePriceMinor($organization) - : $this->plans->proPriceMinor(); + $branches = max(1, $this->plans->activeBranchCount($organization)); + $price = $this->plans->monthlyPriceMinor($organization, $plan); $reference = ($isEnterprise ? 'care-enterprise-' : 'care-pro-') .$organization->id.'-'.now()->format('YmdHis'); @@ -63,8 +62,8 @@ class ProRenewalService $isEnterprise ? 'care_enterprise_renewal' : 'care_pro_renewal', $reference, $isEnterprise - ? 'Ladill Care Enterprise — monthly renewal' - : 'Ladill Care Pro — monthly renewal', + ? 'Ladill Care Enterprise — '.$branches.' branch(es) monthly renewal' + : 'Ladill Care Pro — '.$branches.' branch(es) monthly renewal', ); } catch (\Throwable) { $charged = false; @@ -73,13 +72,11 @@ class ProRenewalService if ($charged) { $settings['plan'] = $plan; $settings['auto_renew'] = true; + $settings['billed_branches'] = $branches; $settings['plan_expires_at'] = now() ->addDays((int) config('care.pro.period_days', 30)) ->toIso8601String(); - if ($isEnterprise) { - $settings['enterprise_billed_branches'] = $this->plans->activeBranchCount($organization); - } - unset($settings['plan_renewal_error']); + unset($settings['plan_renewal_error'], $settings['enterprise_billed_branches']); $organization->update(['settings' => $settings]); return; @@ -89,7 +86,7 @@ class ProRenewalService $graceEnd = $expires->copy()->addDays((int) config('care.pro.grace_days', 3)); if ($graceEnd->isPast()) { $settings['plan'] = 'free'; - unset($settings['plan_expires_at'], $settings['enterprise_billed_branches']); + unset($settings['plan_expires_at'], $settings['billed_branches'], $settings['enterprise_billed_branches']); $settings['plan_renewal_error'] = 'Suspended after failed renewal.'; } else { $settings['plan_renewal_error'] = 'Renewal failed — top up your wallet.'; diff --git a/bootstrap/app.php b/bootstrap/app.php index cb75851..50729d6 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -32,6 +32,7 @@ return Application::configure(basePath: dirname(__DIR__)) 'platform.session' => EnsurePlatformSession::class, 'care.setup' => \App\Http\Middleware\EnsureOrganizationSetup::class, 'care.ability' => \App\Http\Middleware\EnsureCareAbility::class, + 'care.paid' => \App\Http\Middleware\EnsurePaidPlan::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/config/care.php b/config/care.php index 2529d8e..dacdf3a 100644 --- a/config/care.php +++ b/config/care.php @@ -215,21 +215,26 @@ return [ 'label' => 'Free', 'price_minor' => 0, 'max_branches' => 1, + // Free: core patient records, appointments & consults, basic workflows only. ], 'pro' => [ 'label' => 'Pro', - 'price_minor' => (int) env('CARE_PRO_PRICE_MINOR', 199000), + // Billed per active branch / month (unlimited branches). + 'price_minor_per_branch' => (int) env('CARE_PRO_PRICE_PER_BRANCH_MINOR', env('CARE_PRO_PRICE_MINOR', 199000)), 'max_branches' => null, ], 'enterprise' => [ 'label' => 'Enterprise', - 'price_minor_per_branch' => (int) env('CARE_ENTERPRISE_PRICE_PER_BRANCH_MINOR', 499000), + // Billed per active branch / month (unlimited branches). + // Differentiator: multi-dept custom workflows, priority support, analytics, AI. + 'price_minor_per_branch' => (int) env('CARE_ENTERPRISE_PRICE_PER_BRANCH_MINOR', env('CARE_ENTERPRISE_PRICE_MINOR', 499000)), 'max_branches' => null, ], ], 'enterprise' => [ - 'min_branches' => (int) env('CARE_ENTERPRISE_MIN_BRANCHES', 2), + // Feature tier (not branch-gated); kept for config compatibility. + 'min_branches' => (int) env('CARE_ENTERPRISE_MIN_BRANCHES', 1), ], 'prepaid_months' => [6, 12, 24], @@ -246,7 +251,7 @@ return [ 'upgrade_banner' => [ 'title' => 'Unlock Care Pro or Enterprise', - 'description' => 'Unlimited branches, lab & pharmacy modules, encounter billing — from GHS 1990/mo.', + 'description' => 'Pro from GHS 1990/branch/mo — lab, pharmacy, billing, and Queue. Enterprise adds custom multi-dept workflows, analytics, priority support, and AI-assisted healthcare integration.', 'route' => 'care.pro.index', ], diff --git a/resources/views/care/pro/index.blade.php b/resources/views/care/pro/index.blade.php index 91937e9..1f7f3e0 100644 --- a/resources/views/care/pro/index.blade.php +++ b/resources/views/care/pro/index.blade.php @@ -1,6 +1,7 @@ @php - $proPrice = number_format($proPriceMinor / 100, 0); + $proPerBranch = number_format($proPricePerBranchMinor / 100, 0); + $proTotal = number_format($proPriceMinor / 100, 0); $enterprisePerBranch = number_format($enterprisePricePerBranchMinor / 100, 0); $enterpriseTotal = number_format($enterprisePriceMinor / 100, 0); @endphp @@ -50,13 +51,22 @@ {{-- Pro --}}

Pro

- -

Unlimited branches

+ +

+ Unlimited branches · {{ $branchCount }} active = {{ $currency }} {{ $proTotal }}/mo +

    +
  • Everything in Free
  • Full clinical workflows
  • Laboratory & pharmacy
  • Encounter billing
  • -
  • Queue integration
  • +
  • Ladill Queue integration
  • +
  • Unlimited branches (billed per branch)
@if ($planKey === 'pro') @@ -65,19 +75,23 @@ @if ($planExpiresAt) until {{ $planExpiresAt->format('d M Y') }} @endif + @if ($billedBranches > 0) + · billed for {{ $billedBranches }} {{ str('branch')->plural($billedBranches) }} + @endif .

@elseif ($canManage)
@csrf - +
@else @@ -92,23 +106,19 @@

- @if ($branchCount > 0) - {{ $branchCount }} active {{ str('branch')->plural($branchCount) }} = {{ $currency }} {{ $enterpriseTotal }}/mo - @else - Per-branch billing for multi-site - @endif + Unlimited branches · {{ $branchCount }} active = {{ $currency }} {{ $enterpriseTotal }}/mo

  • Everything in Pro
  • -
  • Multi-department workflows
  • -
  • Consolidated reporting
  • -
  • Dedicated onboarding (setup fee)
  • +
  • Custom multi-department workflow integration
  • +
  • Advanced data analytics
  • +
  • AI-assisted healthcare integration
  • +
  • Priority support & dedicated onboarding
@if ($planKey === 'enterprise') @@ -117,8 +127,8 @@ @if ($planExpiresAt) until {{ $planExpiresAt->format('d M Y') }} @endif - @if ($enterpriseBilledBranches > 0) - · billed for {{ $enterpriseBilledBranches }} {{ str('branch')->plural($enterpriseBilledBranches) }} + @if ($billedBranches > 0) + · billed for {{ $billedBranches }} {{ str('branch')->plural($billedBranches) }} @endif .

@@ -140,7 +150,7 @@ @else -

Add at least {{ $enterpriseMinBranches }} active branches, or choose Pro for a single site.

+

Ask an admin to upgrade, or contact sales for custom onboarding.

@endif Contact sales for setup & SLA @else diff --git a/resources/views/care/settings/edit.blade.php b/resources/views/care/settings/edit.blade.php index a15b6fa..3c25ccb 100644 --- a/resources/views/care/settings/edit.blade.php +++ b/resources/views/care/settings/edit.blade.php @@ -111,11 +111,20 @@
- - + + @if (! empty($canUseQueueIntegration)) + + @else +
+

Queue integration is a Pro feature

+

Free includes core patient records, appointments & consults, and basic workflows. Upgrade to connect service queues and counters.

+ View Care Pro plans → +
+ + @endif
diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 61083aa..afebc9e 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -30,34 +30,37 @@ 'icon' => '']; } - if ($permissions->can($member, 'lab.view')) { - $nav[] = ['name' => 'Laboratory', 'route' => route('care.lab.queue.index'), 'active' => request()->routeIs('care.lab.*'), - 'icon' => '']; - } + // Lab, pharmacy, billing, and Queue service counters are Pro/Enterprise modules. + if (! empty($hasPaidPlan)) { + if ($permissions->can($member, 'lab.view')) { + $nav[] = ['name' => 'Laboratory', 'route' => route('care.lab.queue.index'), 'active' => request()->routeIs('care.lab.*'), + 'icon' => '']; + } - if ($permissions->can($member, 'prescriptions.view')) { - $nav[] = ['name' => 'Pharmacy queue', 'route' => route('care.prescriptions.queue'), 'active' => request()->routeIs('care.prescriptions.*') && ! request()->routeIs('care.pharmacy.*'), - 'icon' => '']; - } + if ($permissions->can($member, 'prescriptions.view')) { + $nav[] = ['name' => 'Pharmacy queue', 'route' => route('care.prescriptions.queue'), 'active' => request()->routeIs('care.prescriptions.*') && ! request()->routeIs('care.pharmacy.*'), + 'icon' => '']; + } - if ($permissions->can($member, 'pharmacy.view')) { - $nav[] = ['name' => 'Inventory', 'route' => route('care.pharmacy.drugs.index'), 'active' => request()->routeIs('care.pharmacy.*'), - 'icon' => '']; - } + if ($permissions->can($member, 'pharmacy.view')) { + $nav[] = ['name' => 'Inventory', 'route' => route('care.pharmacy.drugs.index'), 'active' => request()->routeIs('care.pharmacy.*'), + 'icon' => '']; + } - if ($permissions->can($member, 'bills.view')) { - $nav[] = ['name' => 'Billing', 'route' => route('care.bills.index'), 'active' => request()->routeIs('care.bills.*'), - 'icon' => '']; - } + if ($permissions->can($member, 'bills.view')) { + $nav[] = ['name' => 'Billing', 'route' => route('care.bills.index'), 'active' => request()->routeIs('care.bills.*'), + 'icon' => '']; + } - if ($permissions->can($member, 'reports.finance.view')) { - $nav[] = ['name' => 'Reports', 'route' => route('care.reports.index'), 'active' => request()->routeIs('care.reports.*'), - 'icon' => '']; - } + if ($permissions->can($member, 'reports.finance.view')) { + $nav[] = ['name' => 'Reports', 'route' => route('care.reports.index'), 'active' => request()->routeIs('care.reports.*'), + 'icon' => '']; + } - if ($organization && data_get($organization->settings, 'queue_integration_enabled') && $permissions->can($member, 'service_queues.console')) { - $nav[] = ['name' => 'Service queues', 'route' => route('care.service-queues.index'), 'active' => request()->routeIs('care.service-queues.*'), - 'icon' => '']; + if ($organization && data_get($organization->settings, 'queue_integration_enabled') && $permissions->can($member, 'service_queues.console')) { + $nav[] = ['name' => 'Service queues', 'route' => route('care.service-queues.index'), 'active' => request()->routeIs('care.service-queues.*'), + 'icon' => '']; + } } $adminNav = []; diff --git a/routes/web.php b/routes/web.php index 2603b33..cbdce75 100644 --- a/routes/web.php +++ b/routes/web.php @@ -82,61 +82,63 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/queue', [QueueController::class, 'index'])->name('care.queue.index'); Route::post('/queue/{appointment}/start', [QueueController::class, 'start'])->name('care.queue.start'); - Route::get('/service-queues', [ServiceQueueController::class, 'index'])->name('care.service-queues.index'); - Route::get('/service-queues/console/{counterUuid}', [ServiceQueueController::class, 'console'])->name('care.service-queues.console'); - Route::post('/service-queues/console/{counterUuid}/action', [ServiceQueueController::class, 'action'])->name('care.service-queues.action'); + Route::middleware('care.paid')->group(function () { + Route::get('/service-queues', [ServiceQueueController::class, 'index'])->name('care.service-queues.index'); + Route::get('/service-queues/console/{counterUuid}', [ServiceQueueController::class, 'console'])->name('care.service-queues.console'); + Route::post('/service-queues/console/{counterUuid}/action', [ServiceQueueController::class, 'action'])->name('care.service-queues.action'); + + Route::get('/lab/requests', [InvestigationController::class, 'index'])->name('care.lab.requests.index'); + Route::get('/lab/queue', [InvestigationController::class, 'queue'])->name('care.lab.queue.index'); + Route::get('/lab/requests/{investigation}', [InvestigationController::class, 'show'])->name('care.lab.requests.show'); + Route::post('/consultations/{consultation}/investigations', [InvestigationController::class, 'requestFromConsultation'])->name('care.lab.requests.store'); + Route::post('/lab/requests/{investigation}/collect-sample', [InvestigationController::class, 'collectSample'])->name('care.lab.requests.collect-sample'); + Route::post('/lab/requests/{investigation}/start', [InvestigationController::class, 'startProcessing'])->name('care.lab.requests.start'); + Route::post('/lab/requests/{investigation}/results', [InvestigationController::class, 'enterResults'])->name('care.lab.requests.results'); + Route::post('/lab/requests/{investigation}/approve', [InvestigationController::class, 'approve'])->name('care.lab.requests.approve'); + Route::post('/lab/requests/{investigation}/deliver', [InvestigationController::class, 'deliver'])->name('care.lab.requests.deliver'); + Route::post('/lab/requests/{investigation}/cancel', [InvestigationController::class, 'cancel'])->name('care.lab.requests.cancel'); + + Route::get('/lab/catalog', [InvestigationTypeController::class, 'index'])->name('care.lab.catalog.index'); + Route::get('/lab/catalog/create', [InvestigationTypeController::class, 'create'])->name('care.lab.catalog.create'); + Route::post('/lab/catalog', [InvestigationTypeController::class, 'store'])->name('care.lab.catalog.store'); + Route::get('/lab/catalog/{investigationType}/edit', [InvestigationTypeController::class, 'edit'])->name('care.lab.catalog.edit'); + Route::put('/lab/catalog/{investigationType}', [InvestigationTypeController::class, 'update'])->name('care.lab.catalog.update'); + + Route::get('/prescriptions', [PrescriptionController::class, 'index'])->name('care.prescriptions.index'); + Route::get('/prescriptions/queue', [PrescriptionController::class, 'queue'])->name('care.prescriptions.queue'); + Route::get('/consultations/{consultation}/prescriptions/create', [PrescriptionController::class, 'create'])->name('care.prescriptions.create'); + Route::post('/consultations/{consultation}/prescriptions', [PrescriptionController::class, 'store'])->name('care.prescriptions.store'); + Route::get('/prescriptions/{prescription}', [PrescriptionController::class, 'show'])->name('care.prescriptions.show'); + Route::post('/prescriptions/{prescription}/activate', [PrescriptionController::class, 'activate'])->name('care.prescriptions.activate'); + Route::post('/prescriptions/{prescription}/dispense', [PrescriptionController::class, 'dispense'])->name('care.prescriptions.dispense'); + Route::post('/prescriptions/{prescription}/cancel', [PrescriptionController::class, 'cancel'])->name('care.prescriptions.cancel'); + + Route::get('/bills', [BillController::class, 'index'])->name('care.bills.index'); + Route::post('/visits/{visit}/bill', [BillController::class, 'generate'])->name('care.bills.generate'); + Route::get('/bills/{bill}', [BillController::class, 'show'])->name('care.bills.show'); + Route::get('/bills/{bill}/print', [BillController::class, 'print'])->name('care.bills.print'); + Route::post('/bills/{bill}/line-items', [BillController::class, 'addLineItem'])->name('care.bills.line-items.store'); + Route::post('/bills/{bill}/adjustments', [BillController::class, 'applyAdjustments'])->name('care.bills.adjustments'); + Route::post('/bills/{bill}/payments', [BillController::class, 'recordPayment'])->name('care.bills.payments.store'); + Route::post('/bills/{bill}/void', [BillController::class, 'void'])->name('care.bills.void'); + + Route::get('/pharmacy/drugs', [DrugController::class, 'index'])->name('care.pharmacy.drugs.index'); + Route::get('/pharmacy/drugs/create', [DrugController::class, 'create'])->name('care.pharmacy.drugs.create'); + Route::post('/pharmacy/drugs', [DrugController::class, 'store'])->name('care.pharmacy.drugs.store'); + Route::get('/pharmacy/drugs/{drug}', [DrugController::class, 'show'])->name('care.pharmacy.drugs.show'); + Route::get('/pharmacy/drugs/{drug}/edit', [DrugController::class, 'edit'])->name('care.pharmacy.drugs.edit'); + Route::put('/pharmacy/drugs/{drug}', [DrugController::class, 'update'])->name('care.pharmacy.drugs.update'); + Route::post('/pharmacy/drugs/{drug}/batches', [DrugController::class, 'receiveBatch'])->name('care.pharmacy.drugs.batches.store'); + + Route::get('/reports', [ReportController::class, 'index'])->name('care.reports.index'); + Route::get('/reports/{type}', [ReportController::class, 'show'])->name('care.reports.show'); + Route::get('/reports/{type}/export', [ReportController::class, 'export'])->name('care.reports.export'); + }); Route::get('/consultations/{consultation}', [ConsultationController::class, 'show'])->name('care.consultations.show'); Route::put('/consultations/{consultation}', [ConsultationController::class, 'update'])->name('care.consultations.update'); Route::post('/consultations/{consultation}/complete', [ConsultationController::class, 'complete'])->name('care.consultations.complete'); - Route::get('/lab/requests', [InvestigationController::class, 'index'])->name('care.lab.requests.index'); - Route::get('/lab/queue', [InvestigationController::class, 'queue'])->name('care.lab.queue.index'); - Route::get('/lab/requests/{investigation}', [InvestigationController::class, 'show'])->name('care.lab.requests.show'); - Route::post('/consultations/{consultation}/investigations', [InvestigationController::class, 'requestFromConsultation'])->name('care.lab.requests.store'); - Route::post('/lab/requests/{investigation}/collect-sample', [InvestigationController::class, 'collectSample'])->name('care.lab.requests.collect-sample'); - Route::post('/lab/requests/{investigation}/start', [InvestigationController::class, 'startProcessing'])->name('care.lab.requests.start'); - Route::post('/lab/requests/{investigation}/results', [InvestigationController::class, 'enterResults'])->name('care.lab.requests.results'); - Route::post('/lab/requests/{investigation}/approve', [InvestigationController::class, 'approve'])->name('care.lab.requests.approve'); - Route::post('/lab/requests/{investigation}/deliver', [InvestigationController::class, 'deliver'])->name('care.lab.requests.deliver'); - Route::post('/lab/requests/{investigation}/cancel', [InvestigationController::class, 'cancel'])->name('care.lab.requests.cancel'); - - Route::get('/lab/catalog', [InvestigationTypeController::class, 'index'])->name('care.lab.catalog.index'); - Route::get('/lab/catalog/create', [InvestigationTypeController::class, 'create'])->name('care.lab.catalog.create'); - Route::post('/lab/catalog', [InvestigationTypeController::class, 'store'])->name('care.lab.catalog.store'); - Route::get('/lab/catalog/{investigationType}/edit', [InvestigationTypeController::class, 'edit'])->name('care.lab.catalog.edit'); - Route::put('/lab/catalog/{investigationType}', [InvestigationTypeController::class, 'update'])->name('care.lab.catalog.update'); - - Route::get('/prescriptions', [PrescriptionController::class, 'index'])->name('care.prescriptions.index'); - Route::get('/prescriptions/queue', [PrescriptionController::class, 'queue'])->name('care.prescriptions.queue'); - Route::get('/consultations/{consultation}/prescriptions/create', [PrescriptionController::class, 'create'])->name('care.prescriptions.create'); - Route::post('/consultations/{consultation}/prescriptions', [PrescriptionController::class, 'store'])->name('care.prescriptions.store'); - Route::get('/prescriptions/{prescription}', [PrescriptionController::class, 'show'])->name('care.prescriptions.show'); - Route::post('/prescriptions/{prescription}/activate', [PrescriptionController::class, 'activate'])->name('care.prescriptions.activate'); - Route::post('/prescriptions/{prescription}/dispense', [PrescriptionController::class, 'dispense'])->name('care.prescriptions.dispense'); - Route::post('/prescriptions/{prescription}/cancel', [PrescriptionController::class, 'cancel'])->name('care.prescriptions.cancel'); - - Route::get('/bills', [BillController::class, 'index'])->name('care.bills.index'); - Route::post('/visits/{visit}/bill', [BillController::class, 'generate'])->name('care.bills.generate'); - Route::get('/bills/{bill}', [BillController::class, 'show'])->name('care.bills.show'); - Route::get('/bills/{bill}/print', [BillController::class, 'print'])->name('care.bills.print'); - Route::post('/bills/{bill}/line-items', [BillController::class, 'addLineItem'])->name('care.bills.line-items.store'); - Route::post('/bills/{bill}/adjustments', [BillController::class, 'applyAdjustments'])->name('care.bills.adjustments'); - Route::post('/bills/{bill}/payments', [BillController::class, 'recordPayment'])->name('care.bills.payments.store'); - Route::post('/bills/{bill}/void', [BillController::class, 'void'])->name('care.bills.void'); - - Route::get('/pharmacy/drugs', [DrugController::class, 'index'])->name('care.pharmacy.drugs.index'); - Route::get('/pharmacy/drugs/create', [DrugController::class, 'create'])->name('care.pharmacy.drugs.create'); - Route::post('/pharmacy/drugs', [DrugController::class, 'store'])->name('care.pharmacy.drugs.store'); - Route::get('/pharmacy/drugs/{drug}', [DrugController::class, 'show'])->name('care.pharmacy.drugs.show'); - Route::get('/pharmacy/drugs/{drug}/edit', [DrugController::class, 'edit'])->name('care.pharmacy.drugs.edit'); - Route::put('/pharmacy/drugs/{drug}', [DrugController::class, 'update'])->name('care.pharmacy.drugs.update'); - Route::post('/pharmacy/drugs/{drug}/batches', [DrugController::class, 'receiveBatch'])->name('care.pharmacy.drugs.batches.store'); - - Route::get('/reports', [ReportController::class, 'index'])->name('care.reports.index'); - Route::get('/reports/{type}', [ReportController::class, 'show'])->name('care.reports.show'); - Route::get('/reports/{type}/export', [ReportController::class, 'export'])->name('care.reports.export'); - Route::get('/settings', [SettingsController::class, 'edit'])->name('care.settings'); Route::put('/settings', [SettingsController::class, 'update'])->name('care.settings.update'); diff --git a/tests/Feature/CareBillTest.php b/tests/Feature/CareBillTest.php index bc0ee5a..201ca70 100644 --- a/tests/Feature/CareBillTest.php +++ b/tests/Feature/CareBillTest.php @@ -40,7 +40,7 @@ class CareBillTest extends TestCase 'name' => 'Test Clinic', 'slug' => 'test-clinic', 'timezone' => 'UTC', - 'settings' => ['onboarded' => true], + 'settings' => ['onboarded' => true, 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String()], ]); Member::create([ diff --git a/tests/Feature/CareLabTest.php b/tests/Feature/CareLabTest.php index c5ee76b..6854523 100644 --- a/tests/Feature/CareLabTest.php +++ b/tests/Feature/CareLabTest.php @@ -50,7 +50,7 @@ class CareLabTest extends TestCase 'name' => 'Test Clinic', 'slug' => 'test-clinic', 'timezone' => 'UTC', - 'settings' => ['onboarded' => true], + 'settings' => ['onboarded' => true, 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String()], ]); Member::create([ diff --git a/tests/Feature/CarePharmacyInventoryTest.php b/tests/Feature/CarePharmacyInventoryTest.php index c18ba64..fc2ff66 100644 --- a/tests/Feature/CarePharmacyInventoryTest.php +++ b/tests/Feature/CarePharmacyInventoryTest.php @@ -50,7 +50,7 @@ class CarePharmacyInventoryTest extends TestCase 'name' => 'Test Clinic', 'slug' => 'test-clinic', 'timezone' => 'UTC', - 'settings' => ['onboarded' => true], + 'settings' => ['onboarded' => true, 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String()], ]); Member::create([ diff --git a/tests/Feature/CarePrescriptionTest.php b/tests/Feature/CarePrescriptionTest.php index 1813097..11630d4 100644 --- a/tests/Feature/CarePrescriptionTest.php +++ b/tests/Feature/CarePrescriptionTest.php @@ -42,7 +42,7 @@ class CarePrescriptionTest extends TestCase 'name' => 'Test Clinic', 'slug' => 'test-clinic', 'timezone' => 'UTC', - 'settings' => ['onboarded' => true], + 'settings' => ['onboarded' => true, 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String()], ]); Member::create([ diff --git a/tests/Feature/CareProTest.php b/tests/Feature/CareProTest.php index 4a17274..2c418e5 100644 --- a/tests/Feature/CareProTest.php +++ b/tests/Feature/CareProTest.php @@ -59,12 +59,83 @@ class CareProTest extends TestCase ->get(route('care.pro.index')) ->assertOk() ->assertSee('Choose your Care plan') - ->assertSee('GHS 199') - ->assertSee('GHS 499') + ->assertSee('1990') + ->assertSee('4990') + ->assertSee('/branch/mo') + ->assertSee('Unlimited branches') + ->assertSee('Core patient records') + ->assertSee('Queue integration') + ->assertSee('Custom multi-department workflow') + ->assertSee('AI-assisted healthcare integration') ->assertSee('Upgrade to Pro') ->assertSee('Enterprise'); } + public function test_free_plan_cannot_enable_queue_integration(): void + { + $this->actingAs($this->owner) + ->put(route('care.settings.update'), [ + 'name' => 'Care Org', + 'timezone' => 'Africa/Accra', + 'facility_type' => 'clinic', + 'queue_integration_enabled' => '1', + ]) + ->assertRedirect(route('care.pro.index')) + ->assertSessionHas('upsell'); + + $this->organization->refresh(); + $this->assertFalse((bool) data_get($this->organization->settings, 'queue_integration_enabled')); + } + + public function test_free_plan_is_blocked_from_lab_module(): void + { + $this->actingAs($this->owner) + ->get(route('care.lab.queue.index')) + ->assertRedirect(route('care.pro.index')) + ->assertSessionHas('upsell'); + } + + public function test_pro_plan_allows_unlimited_branches(): void + { + $this->organization->update([ + 'settings' => array_merge($this->organization->settings ?? [], [ + 'plan' => 'pro', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + ]), + ]); + + $this->assertNull(app(\App\Services\Care\PlanService::class)->maxBranches($this->organization->fresh())); + $this->assertTrue(app(\App\Services\Care\PlanService::class)->canAddBranch($this->organization->fresh(), 50)); + } + + public function test_enterprise_plan_allows_unlimited_branches(): void + { + $this->organization->update([ + 'settings' => array_merge($this->organization->settings ?? [], [ + 'plan' => 'enterprise', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + ]), + ]); + + $this->assertNull(app(\App\Services\Care\PlanService::class)->maxBranches($this->organization->fresh())); + $this->assertTrue(app(\App\Services\Care\PlanService::class)->canAddBranch($this->organization->fresh(), 50)); + } + + public function test_subscribe_enterprise_no_longer_requires_multiple_branches(): void + { + Http::fake([ + 'billing.test/can-afford*' => Http::response(['affordable' => true]), + 'billing.test/debit' => Http::response(['ok' => true]), + ]); + + $this->actingAs($this->owner) + ->post(route('care.pro.subscribe-enterprise')) + ->assertRedirect(route('care.pro.index')) + ->assertSessionHas('success'); + + $this->assertSame('enterprise', $this->organization->fresh()->settings['plan']); + } + public function test_sidebar_shows_upgrade_to_pro_on_dashboard(): void { $this->actingAs($this->owner) @@ -123,35 +194,6 @@ class CareProTest extends TestCase $this->assertSame('wallet_monthly', $settings['billing_method']); } - public function test_subscribe_enterprise_requires_multiple_branches(): void - { - Http::fake([ - 'billing.test/can-afford*' => Http::response(['affordable' => true]), - 'billing.test/debit' => Http::response(['ok' => true]), - ]); - - $this->actingAs($this->owner) - ->post(route('care.pro.subscribe-enterprise')) - ->assertRedirect(route('care.pro.index')) - ->assertSessionHas('error'); - - Branch::create([ - 'owner_ref' => $this->organization->owner_ref, - 'organization_id' => $this->organization->id, - 'name' => 'Second', - 'is_active' => true, - ]); - - $this->actingAs($this->owner) - ->post(route('care.pro.subscribe-enterprise')) - ->assertRedirect(route('care.pro.index')) - ->assertSessionHas('success'); - - $settings = $this->organization->fresh()->settings; - $this->assertSame('enterprise', $settings['plan']); - $this->assertSame(2, $settings['enterprise_billed_branches']); - } - public function test_subscribe_prepaid_redirects_to_paystack(): void { Http::fake([ @@ -227,14 +269,16 @@ class CareProTest extends TestCase 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'enterprise', 'auto_renew' => true, - 'enterprise_billed_branches' => 2, 'plan_expires_at' => now()->subDay()->toIso8601String(), ]), ]); + $perBranch = (int) config('care.plans.enterprise.price_minor_per_branch'); + $expected = $perBranch * 2; + Http::fake([ - 'billing.test/debit' => function ($request) { - $this->assertSame(99800, (int) $request['amount_minor']); + 'billing.test/debit' => function ($request) use ($expected) { + $this->assertSame($expected, (int) $request['amount_minor']); return Http::response(['ok' => true]); }, @@ -244,6 +288,41 @@ class CareProTest extends TestCase $settings = $this->organization->fresh()->settings; $this->assertSame('enterprise', $settings['plan']); - $this->assertSame(2, $settings['enterprise_billed_branches']); + $this->assertSame(2, $settings['billed_branches']); + } + + public function test_pro_renew_charges_per_active_branch(): void + { + Branch::create([ + 'owner_ref' => $this->organization->owner_ref, + 'organization_id' => $this->organization->id, + 'name' => 'Second', + 'is_active' => true, + ]); + + $this->organization->update([ + 'settings' => array_merge($this->organization->settings ?? [], [ + 'plan' => 'pro', + 'auto_renew' => true, + 'plan_expires_at' => now()->subDay()->toIso8601String(), + ]), + ]); + + $perBranch = (int) config('care.plans.pro.price_minor_per_branch'); + $expected = $perBranch * 2; + + Http::fake([ + 'billing.test/debit' => function ($request) use ($expected) { + $this->assertSame($expected, (int) $request['amount_minor']); + + return Http::response(['ok' => true]); + }, + ]); + + $this->artisan('care:pro-renew')->assertSuccessful(); + + $settings = $this->organization->fresh()->settings; + $this->assertSame('pro', $settings['plan']); + $this->assertSame(2, $settings['billed_branches']); } } diff --git a/tests/Feature/CareReportTest.php b/tests/Feature/CareReportTest.php index 0e70a0f..9d7796e 100644 --- a/tests/Feature/CareReportTest.php +++ b/tests/Feature/CareReportTest.php @@ -34,7 +34,7 @@ class CareReportTest extends TestCase 'name' => 'Test Clinic', 'slug' => 'test-clinic', 'timezone' => 'UTC', - 'settings' => ['onboarded' => true], + 'settings' => ['onboarded' => true, 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String()], ]); Member::create([