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('frontdesk.plans', [])) ? $plan : 'free'; } 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); } /** @return array */ public function plan(Organization $organization): array { $key = $this->planKey($organization); return [ 'key' => $key, ...(array) config('frontdesk.plans.'.$key, []), ]; } /** * Included host emails per calendar month. * Null means unlimited (Enterprise). Free/Pro use finite allowances. */ public function freeEmailsPerMonth(Organization $organization): ?int { $value = config('frontdesk.plans.'.$this->planKey($organization).'.free_emails_per_month', 100); return $value === null ? null : (int) $value; } /** * Included host SMS per calendar month. * Null means unlimited (Enterprise). 0 means none included. */ public function freeSmsPerMonth(Organization $organization): ?int { $plan = (array) config('frontdesk.plans.'.$this->planKey($organization), []); if (! array_key_exists('free_sms_per_month', $plan)) { return 0; } $value = $plan['free_sms_per_month']; return $value === null ? null : (int) $value; } /** * Paid plans with a finite allowance stop sending once the monthly cap is hit. * Free keeps pay-as-you-go after its free email allowance. */ public function emailAllowanceExhausted(Organization $organization, int $sentThisMonth): bool { $allowance = $this->freeEmailsPerMonth($organization); return $this->hasPaidPlan($organization) && $allowance !== null && $sentThisMonth >= $allowance; } public function smsAllowanceExhausted(Organization $organization, int $sentThisMonth): bool { $allowance = $this->freeSmsPerMonth($organization); return $this->hasPaidPlan($organization) && $allowance !== null && $sentThisMonth >= $allowance; } public function activeBranchCount(Organization $organization): int { return Branch::query() ->where('organization_id', $organization->id) ->where('is_active', true) ->count(); } /** Seat allotment purchased for the current paid period; 0 when free or unset. */ public function billedBranches(Organization $organization): int { return max(0, (int) (($organization->settings ?? [])['billed_branches'] ?? 0)); } public function canAddBranch(Organization $organization, int $currentCount): bool { $limit = config('frontdesk.plans.'.$this->planKey($organization).'.max_branches'); if ($limit !== null) { return $currentCount < (int) $limit; } // Paid plans with a purchased seat allotment cannot exceed it until they expand. $billed = $this->billedBranches($organization); if ($this->hasPaidPlan($organization) && $billed > 0) { return $this->activeBranchCount($organization) < $billed; } return true; } public function canActivateBranch(Organization $organization): bool { $limit = config('frontdesk.plans.'.$this->planKey($organization).'.max_branches'); if ($limit !== null) { return $this->activeBranchCount($organization) < (int) $limit; } $billed = $this->billedBranches($organization); if ($this->hasPaidPlan($organization) && $billed > 0) { return $this->activeBranchCount($organization) < $billed; } return true; } public function branchLimitMessage(Organization $organization): string { if ($this->hasPaidPlan($organization) && $this->billedBranches($organization) > 0) { $n = $this->billedBranches($organization); return "Your plan is billed for {$n} active ".str('branch')->plural($n) .'. Increase your branch allotment on the Plans page before adding more.'; } return 'Your plan allows one branch. Upgrade to Frontdesk Pro for more branches.'; } public function canAddKiosk(Organization $organization): bool { $limit = config('frontdesk.plans.'.$this->planKey($organization).'.max_kiosk_devices'); if ($limit === null) { return true; } $count = Device::query() ->where('organization_id', $organization->id) ->where('type', 'kiosk') ->count(); return $count < (int) $limit; } public function hasFeature(Organization $organization, string $feature): bool { $features = config('frontdesk.plans.'.$this->planKey($organization).'.features', []); return in_array($feature, $features, true); } public function proPricePerBranchMinor(): int { return (int) config( 'frontdesk.plans.pro.price_minor_per_branch', config('frontdesk.plans.pro.price_minor', 199000), ); } /** * Per-branch rate (legacy callers treated this as a flat monthly fee). * * @deprecated Prefer proPricePerBranchMinor() / priceForBranches(). */ public function proPriceMinor(): int { return $this->proPricePerBranchMinor(); } public function enterprisePricePerBranchMinor(): int { return (int) config('frontdesk.plans.enterprise.price_minor_per_branch', 299000); } public function proPriceTotalMinor(Organization $organization): int { return $this->priceForBranches('pro', $this->activeBranchCount($organization)); } public function enterprisePriceMinor(Organization $organization): int { return $this->priceForBranches('enterprise', $this->activeBranchCount($organization)); } 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); $active = max(1, $this->activeBranchCount($organization)); return max($branches, $active); } public function canSubscribeEnterprise(Organization $organization): bool { return $this->activeBranchCount($organization) >= (int) config('frontdesk.enterprise.min_branches', 1); } }