Files
ladill-care/app/Services/Care/PlanService.php
T
isaaccladandCursor dec282d25d
Deploy Ladill Care / deploy (push) Successful in 53s
Embed Ladill Queue on role pages and add specialty modules under Settings.
Remove the standalone Service Queues nav so call-next/now-serving lives on clinical queue, appointments, pharmacy, lab, and specialty surfaces; Pro orgs can activate dentistry and related modules with branch departments and queue stubs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 15:55:47 +00:00

179 lines
5.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\Care;
use App\Models\Branch;
use App\Models\Organization;
use Carbon\Carbon;
class PlanService
{
public function planKey(Organization $organization): string
{
$settings = $organization->settings ?? [];
$plan = (string) ($settings['plan'] ?? 'free');
if (in_array($plan, ['pro', 'enterprise'], true) && ! 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 $this->planKey($organization);
}
public function isPro(Organization $organization): bool
{
return $this->planKey($organization) === 'pro';
}
public function isEnterprise(Organization $organization): bool
{
return $this->planKey($organization) === 'enterprise';
}
public function hasPaidPlan(Organization $organization): bool
{
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);
}
/** Specialty practice modules (dentistry, eye care, …) — Pro/Enterprise expansions. */
public function canUseSpecialtyModules(Organization $organization): bool
{
return $this->hasFeature($organization, 'specialty_modules');
}
/** Own payment gateway (Paystack / Flutterwave / Hubtel) for encounter billing requires Pro or Enterprise. */
public function canUsePaymentGateway(Organization $organization): bool
{
return $this->hasPaidPlan($organization) && $this->hasFeature($organization, 'billing');
}
public function hasFeature(Organization $organization, string $feature): bool
{
$features = config('care.plans.'.$this->planKey($organization).'.features', []);
return in_array($feature, $features, true);
}
public function activeBranchCount(Organization $organization): int
{
return Branch::query()
->where('organization_id', $organization->id)
->where('is_active', true)
->count();
}
public function maxBranches(Organization $organization): ?int
{
$plan = config('care.plans.'.$this->planKey($organization));
$max = $plan['max_branches'] ?? null;
return $max === null ? null : (int) $max;
}
public function canAddBranch(Organization $organization, int $currentCount): bool
{
$max = $this->maxBranches($organization);
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', 249000),
);
}
/** @deprecated Prefer proPricePerBranchMinor() — Pro is billed per branch. */
public function proPriceMinor(): int
{
return $this->proPricePerBranchMinor();
}
public function enterprisePricePerBranchMinor(): int
{
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 $this->priceForBranches('pro', $this->activeBranchCount($organization));
}
/** Total monthly amount for Enterprise = active branches × per-branch rate (min 1). */
public function enterprisePriceMinor(Organization $organization): int
{
return $this->priceForBranches('enterprise', $this->activeBranchCount($organization));
}
public function monthlyPriceMinor(Organization $organization, string $plan): int
{
return $this->priceForBranches($plan, $this->activeBranchCount($organization));
}
/** Fixed branch count × per-branch rate (for checkout when the user picks branch seats). */
public function priceForBranches(string $plan, int $branches): int
{
$branches = max(1, $branches);
$rate = $plan === 'enterprise'
? $this->enterprisePricePerBranchMinor()
: $this->proPricePerBranchMinor();
return $branches * $rate;
}
public function resolveCheckoutBranches(Organization $organization, int $requested): int
{
$branches = max(1, $requested);
// Don't under-bill vs sites already configured in the account.
$active = max(1, $this->activeBranchCount($organization));
return max($branches, $active);
}
public function canSubscribeEnterprise(Organization $organization): bool
{
// Enterprise is a feature tier (not branch-gated). min_branches defaults to 1.
return $this->activeBranchCount($organization) >= (int) config('care.enterprise.min_branches', 1);
}
}