diff --git a/app/Console/Commands/RenewProSubscriptionsCommand.php b/app/Console/Commands/RenewProSubscriptionsCommand.php new file mode 100644 index 0000000..dc3cc71 --- /dev/null +++ b/app/Console/Commands/RenewProSubscriptionsCommand.php @@ -0,0 +1,22 @@ +dueOrganizations(); + $this->info("Renewing {$due->count()} due organization(s)."); + $due->each(fn ($organization) => $renewals->renewIfDue($organization)); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Care/ProController.php b/app/Http/Controllers/Care/ProController.php new file mode 100644 index 0000000..93a39d4 --- /dev/null +++ b/app/Http/Controllers/Care/ProController.php @@ -0,0 +1,92 @@ +organization($request); + $canManage = app(CarePermissions::class)->can($this->member($request), 'settings.manage'); + $settings = $organization->settings ?? []; + $expiresAt = ! empty($settings['plan_expires_at']) + ? Carbon::parse($settings['plan_expires_at']) + : null; + + return view('care.pro.index', [ + 'organization' => $organization, + 'isPro' => $plans->isPro($organization), + 'canManage' => $canManage, + 'priceMinor' => $plans->proPriceMinor(), + 'currency' => (string) config('billing.currency', 'GHS'), + 'planExpiresAt' => $expiresAt, + 'proFeatures' => [ + 'Unlimited branches', + 'Full clinical workflows', + 'Laboratory & pharmacy modules', + 'Finance reports & audit exports', + 'Queue integration', + ], + ]); + } + + public function subscribe(Request $request, PlanService $plans, BillingClient $billing): RedirectResponse + { + $this->authorizeAbility($request, 'settings.manage'); + $organization = $this->organization($request); + + if ($plans->isPro($organization)) { + return redirect()->route('care.pro.index') + ->with('success', 'Your organization is already on Care Pro.'); + } + + $priceMinor = $plans->proPriceMinor(); + + try { + if (! $billing->canAfford($organization->owner_ref, $priceMinor)) { + return redirect()->route('care.pro.index') + ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade to Pro.'); + } + + $charged = $billing->debit( + $organization->owner_ref, + $priceMinor, + 'care_pro', + 'care-pro-'.$organization->id.'-'.now()->format('Y-m-d-His'), + 'Ladill Care Pro — monthly subscription', + ); + } catch (\Throwable) { + return redirect()->route('care.pro.index') + ->with('error', 'Could not process upgrade. Please try again.'); + } + + if (! $charged) { + return redirect()->route('care.pro.index') + ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade to Pro.'); + } + + $settings = $organization->settings ?? []; + $settings['plan'] = 'pro'; + $settings['auto_renew'] = true; + $settings['plan_expires_at'] = now() + ->addDays((int) config('care.pro.period_days', 30)) + ->toIso8601String(); + unset($settings['plan_renewal_error']); + $organization->update(['settings' => $settings]); + + return redirect()->route('care.pro.index') + ->with('success', 'Welcome to Care Pro — active for one month.'); + } +} diff --git a/app/Services/Care/PlanService.php b/app/Services/Care/PlanService.php index c12ea41..47a78ae 100644 --- a/app/Services/Care/PlanService.php +++ b/app/Services/Care/PlanService.php @@ -3,22 +3,37 @@ namespace App\Services\Care; use App\Models\Organization; +use Carbon\Carbon; class PlanService { + public function planKey(Organization $organization): string + { + $settings = $organization->settings ?? []; + $plan = (string) ($settings['plan'] ?? 'free'); + + if ($plan === 'pro' && ! empty($settings['plan_expires_at'])) { + if (Carbon::parse($settings['plan_expires_at'])->isPast()) { + return 'free'; + } + } + + return array_key_exists($plan, config('care.plans', [])) ? $plan : 'free'; + } + public function plan(Organization $organization): string { - return (string) data_get($organization->settings, 'plan', 'free'); + return $this->planKey($organization); } public function isPro(Organization $organization): bool { - return $this->plan($organization) === 'pro'; + return $this->planKey($organization) === 'pro'; } public function maxBranches(Organization $organization): ?int { - $plan = config('care.plans.'.$this->plan($organization)); + $plan = config('care.plans.'.$this->planKey($organization)); return $plan['max_branches'] ?? null; } diff --git a/app/Services/Care/ProRenewalService.php b/app/Services/Care/ProRenewalService.php new file mode 100644 index 0000000..bfd5850 --- /dev/null +++ b/app/Services/Care/ProRenewalService.php @@ -0,0 +1,89 @@ + */ + public function dueOrganizations(): Collection + { + return Organization::query() + ->where('settings->plan', 'pro') + ->whereNotNull('settings->plan_expires_at') + ->get() + ->filter(fn (Organization $organization) => $this->isDue($organization)); + } + + public function isDue(Organization $organization): bool + { + $settings = $organization->settings ?? []; + if (($settings['plan'] ?? '') !== 'pro') { + return false; + } + if (array_key_exists('auto_renew', $settings) && $settings['auto_renew'] === false) { + return false; + } + if (empty($settings['plan_expires_at'])) { + return false; + } + + return Carbon::parse($settings['plan_expires_at'])->lte(now()); + } + + public function renewIfDue(Organization $organization): void + { + if (! $this->isDue($organization)) { + return; + } + + $settings = $organization->settings ?? []; + $price = $this->plans->proPriceMinor(); + $reference = 'care-pro-'.$organization->id.'-'.now()->format('YmdHis'); + + try { + $charged = $this->billing->debit( + $organization->owner_ref, + $price, + 'care_pro_renewal', + $reference, + 'Ladill Care Pro — monthly renewal', + ); + } catch (\Throwable) { + $charged = false; + } + + if ($charged) { + $settings['plan'] = 'pro'; + $settings['auto_renew'] = true; + $settings['plan_expires_at'] = now() + ->addDays((int) config('care.pro.period_days', 30)) + ->toIso8601String(); + unset($settings['plan_renewal_error']); + $organization->update(['settings' => $settings]); + + return; + } + + $expires = Carbon::parse($settings['plan_expires_at']); + $graceEnd = $expires->copy()->addDays((int) config('care.pro.grace_days', 3)); + if ($graceEnd->isPast()) { + $settings['plan'] = 'free'; + unset($settings['plan_expires_at']); + $settings['plan_renewal_error'] = 'Suspended after failed renewal.'; + } else { + $settings['plan_renewal_error'] = 'Renewal failed — top up your wallet.'; + } + + $organization->update(['settings' => $settings]); + } +} diff --git a/config/care.php b/config/care.php index ceee2e1..8c999e5 100644 --- a/config/care.php +++ b/config/care.php @@ -217,6 +217,11 @@ return [ ], ], + 'pro' => [ + 'grace_days' => (int) env('CARE_PRO_GRACE_DAYS', 3), + 'period_days' => (int) env('CARE_PRO_PERIOD_DAYS', 30), + ], + 'queue' => [ 'api_url' => env('QUEUE_API_URL', 'https://queue.ladill.com/api/v1'), 'api_key' => env('QUEUE_API_KEY_CARE'), diff --git a/resources/views/care/pro/index.blade.php b/resources/views/care/pro/index.blade.php new file mode 100644 index 0000000..d01a991 --- /dev/null +++ b/resources/views/care/pro/index.blade.php @@ -0,0 +1,54 @@ + + @php + $price = number_format($priceMinor / 100, 2); + @endphp + +
+
+
+
+ Pro + @if ($isPro) + Active + @endif +
+

Ladill Care Pro

+

Unlimited branches and the full hospital management toolkit.

+

{{ $currency }} {{ $price }} / month

+
+ +
+
    + @foreach ($proFeatures as $feature) +
  • + + {{ $feature }} +
  • + @endforeach +
+ +
+ @if ($isPro) +
+ @if ($planExpiresAt) + Pro is active until {{ $planExpiresAt->format('d M Y') }}. + @else + Your organization is on Care Pro. + @endif +
+ @elseif ($canManage) +
+ @csrf + +
+

Billed monthly from your Ladill wallet.

+ @else +

Ask an organization administrator to upgrade this workspace to Pro.

+ @endif +
+
+
+
+
diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 700c93b..0f4bedf 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -78,7 +78,7 @@ 'icon' => '']; } - $settingsActive = request()->routeIs('care.settings*'); + $settingsActive = request()->routeIs('care.settings*') && ! request()->routeIs('care.pro.*'); @endphp