Files
isaaccladandCursor dd4bc493b4
Deploy Ladill Care / deploy (push) Failing after 1m13s
Hard-delete Care remote Queue adapter (Phase 5).
Care Queue Engine is the only path; remove QueueClient, driver config, and remote HTTP tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 11:16:41 +00:00

179 lines
5.8 KiB
PHP
Raw Permalink 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 in-app service queues 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);
}
}