Files
ladill-pos/app/Services/Pos/SubscriptionService.php
T
isaaccladandCursor f800f0c1ca
Deploy Ladill POS / deploy (push) Successful in 43s
Add POS Pro subscription billing and free-tier limits.
Wallet-backed Pro unlocks unlimited products, restaurant mode, catalog imports, and ecosystem sync features.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 00:51:35 +00:00

179 lines
5.6 KiB
PHP

<?php
namespace App\Services\Pos;
use App\Models\Pos\ProSubscription;
use App\Models\PosLocation;
use App\Models\PosProduct;
use App\Models\User;
use App\Services\Crm\CrmClient;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
class SubscriptionService
{
public function __construct(private readonly BillingClient $billing) {}
public function gatingActive(): bool
{
return (bool) config('pos.pro.enabled', true) && $this->billing->configured();
}
public function priceMinor(): int
{
return (int) config('pos.pro.price_minor', 7900);
}
public function subscriptionFor(User $user): ?ProSubscription
{
return ProSubscription::where('user_id', $user->id)->first();
}
public function isPro(User $user): bool
{
if (! $this->gatingActive()) {
return true;
}
return (bool) $this->subscriptionFor($user)?->entitled();
}
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 are already on Ladill POS Pro.'];
}
$price = $this->priceMinor();
$reference = 'pos-pro-'.$user->id.'-'.now()->format('YmdHis').'-'.Str::random(6);
$result = $this->billing->charge($user, $price, $reference, 'Ladill POS Pro — monthly subscription');
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],
[
'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, 'Welcome to Ladill POS Pro! Your subscription is active.'];
}
/** @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 Pro 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;
}
$reference = 'pos-pro-'.$user->id.'-'.now()->format('YmdHis').'-'.Str::random(6);
$result = $this->billing->charge($user, $sub->price_minor, $reference, 'Ladill POS Pro — 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();
}
}
}