billing->configured(); } public function priceMinor(): int { return (int) config('pos.plans.pro.price_minor', config('pos.pro.price_minor', 7900)); } public function enterprisePriceMinor(): int { return (int) config('pos.plans.enterprise.price_minor', 24900); } public function subscriptionFor(User $user): ?ProSubscription { return ProSubscription::where('user_id', $user->id)->first(); } public function planKey(User $user): string { if (! $this->gatingActive()) { return ProSubscription::PLAN_PRO; } $sub = $this->subscriptionFor($user); if (! $sub || ! $sub->entitled()) { return 'free'; } return $sub->plan === ProSubscription::PLAN_ENTERPRISE ? ProSubscription::PLAN_ENTERPRISE : ProSubscription::PLAN_PRO; } public function hasPaidPlan(User $user): bool { if (! $this->gatingActive()) { return true; } return (bool) $this->subscriptionFor($user)?->entitled(); } public function isEnterprise(User $user): bool { return $this->planKey($user) === ProSubscription::PLAN_ENTERPRISE; } public function isPro(User $user): bool { return $this->hasPaidPlan($user); } public function productCount(string $ownerRef, bool $restaurant): int { if ($restaurant) { return PosProduct::owned($ownerRef)->count(); } try { $response = CrmClient::for($ownerRef)->products(['type' => 'product', 'per_page' => 1]); return (int) ($response['meta']['total'] ?? count((array) ($response['data'] ?? []))); } catch (\Throwable) { return 0; } } public function canAddProduct(User $user, string $ownerRef, bool $restaurant): bool { if ($this->isPro($user)) { return true; } $limit = (int) config('pos.free.max_products', 50); return $this->productCount($ownerRef, $restaurant) < $limit; } public function canUseRestaurantMode(User $user): bool { return $this->isPro($user); } public function canImportCatalog(User $user): bool { return $this->isPro($user); } public function locationCount(string $ownerRef): int { return PosLocation::owned($ownerRef)->count(); } /** @return array{0:bool,1:string} */ public function subscribe(User $user): array { $existing = $this->subscriptionFor($user); if ($existing && $existing->entitled() && $existing->status === ProSubscription::STATUS_ACTIVE) { return [true, 'You already have an active subscription.']; } if ($existing?->plan === ProSubscription::PLAN_ENTERPRISE) { return $this->subscribeEnterprise($user); } return $this->activateWalletPlan($user, ProSubscription::PLAN_PRO, $this->priceMinor(), 'Ladill POS Pro — monthly subscription', 'Welcome to Ladill POS Pro!'); } /** @return array{0:bool,1:string} */ public function subscribeEnterprise(User $user): array { $existing = $this->subscriptionFor($user); if ($existing && $existing->entitled() && $existing->status === ProSubscription::STATUS_ACTIVE) { return [true, 'You already have an active subscription.']; } return $this->activateWalletPlan( $user, ProSubscription::PLAN_ENTERPRISE, $this->enterprisePriceMinor(), 'Ladill POS Business — monthly subscription', 'Welcome to Ladill POS Business!', ); } public function activatePrepaid(User $user, string $plan, int $months): void { $existing = $this->subscriptionFor($user); $price = $plan === ProSubscription::PLAN_ENTERPRISE ? $this->enterprisePriceMinor() : $this->priceMinor(); ProSubscription::updateOrCreate( ['user_id' => $user->id], [ 'plan' => $plan, 'status' => ProSubscription::STATUS_ACTIVE, 'price_minor' => $price, 'currency' => (string) config('pos.pro.currency', 'GHS'), 'auto_renew' => false, 'started_at' => $existing?->started_at ?? now(), 'current_period_end' => Carbon::now()->addMonths($months), 'last_charged_at' => now(), 'canceled_at' => null, 'last_reference' => null, 'last_error' => null, ], ); } /** @return array{0:bool,1:string} */ public function cancel(User $user): array { $sub = $this->subscriptionFor($user); if (! $sub || ! $sub->entitled()) { return [false, 'You do not have an active subscription.']; } $sub->forceFill([ 'status' => ProSubscription::STATUS_CANCELED, 'auto_renew' => false, 'canceled_at' => now(), ])->save(); $until = optional($sub->current_period_end)->format('d M Y'); return [true, "Auto-renew is off. You keep access until {$until}."]; } public function renewIfDue(ProSubscription $sub): void { if (! $sub->auto_renew || $sub->status !== ProSubscription::STATUS_ACTIVE) { return; } if ($sub->current_period_end && $sub->current_period_end->isFuture()) { return; } $user = $sub->user; if (! $user) { return; } $planLabel = $sub->plan === ProSubscription::PLAN_ENTERPRISE ? 'Business' : 'Pro'; $reference = 'pos-'.$sub->plan.'-'.$user->id.'-'.now()->format('YmdHis').'-'.Str::random(6); $result = $this->billing->charge($user, $sub->price_minor, $reference, "Ladill POS {$planLabel} — renewal"); if ($result['ok']) { $sub->forceFill([ 'current_period_end' => Carbon::now()->addDays((int) config('pos.pro.period_days', 30)), 'last_charged_at' => now(), 'last_reference' => $reference, 'last_error' => null, ])->save(); return; } $graceEnd = optional($sub->current_period_end)->addDays((int) config('pos.pro.grace_days', 3)); if ($graceEnd && $graceEnd->isPast()) { $sub->forceFill(['status' => ProSubscription::STATUS_PAST_DUE, 'last_error' => $result['error']])->save(); } else { $sub->forceFill(['last_error' => $result['error']])->save(); } } /** @return array{0:bool,1:string} */ private function activateWalletPlan(User $user, string $plan, int $price, string $description, string $successMessage): array { $existing = $this->subscriptionFor($user); $reference = 'pos-'.$plan.'-'.$user->id.'-'.now()->format('YmdHis').'-'.Str::random(6); $result = $this->billing->charge($user, $price, $reference, $description); if (! $result['ok']) { if ($result['insufficient']) { $bal = number_format(((int) $result['balance_minor']) / 100, 2); return [false, "Your wallet balance (GHS {$bal}) is too low. Top up, then subscribe."]; } return [false, $result['error'] ?? 'Could not start your subscription. Please try again.']; } $periodEnd = Carbon::now()->addDays((int) config('pos.pro.period_days', 30)); ProSubscription::updateOrCreate( ['user_id' => $user->id], [ 'plan' => $plan, 'status' => ProSubscription::STATUS_ACTIVE, 'price_minor' => $price, 'currency' => (string) config('pos.pro.currency', 'GHS'), 'auto_renew' => true, 'started_at' => $existing?->started_at ?? now(), 'current_period_end' => $periodEnd, 'last_charged_at' => now(), 'canceled_at' => null, 'last_reference' => $reference, 'last_error' => null, ], ); return [true, $successMessage.' Your subscription is active.']; } }