Deploy Ladill Woo Manager / deploy (push) Successful in 39s
Agencies can invite store-scoped managers via Identity, with access limited to one WooCommerce store while owners retain full multi-store control. Co-authored-by: Cursor <cursoragent@cursor.com>
311 lines
10 KiB
PHP
311 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Woo;
|
|
|
|
use App\Models\User;
|
|
use App\Models\Woo\ProSubscription;
|
|
use App\Models\WooProduct;
|
|
use App\Models\WooStore;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Str;
|
|
|
|
class SubscriptionService
|
|
{
|
|
public function __construct(
|
|
private readonly BillingClient $billing,
|
|
private readonly WooNotificationService $notifications,
|
|
private readonly WooProExpiryAlertService $expiryAlerts,
|
|
) {}
|
|
|
|
public function gatingActive(): bool
|
|
{
|
|
return (bool) config('woo.pro.enabled', true) && $this->billing->configured();
|
|
}
|
|
|
|
public function priceMinor(): int
|
|
{
|
|
return (int) config('woo.plans.pro.price_minor', config('woo.pro.price_minor', 4900));
|
|
}
|
|
|
|
public function enterprisePriceMinor(): int
|
|
{
|
|
return (int) config('woo.plans.enterprise.price_minor', 14900);
|
|
}
|
|
|
|
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 maxStores(User $user): int
|
|
{
|
|
return $this->isPro($user) ? PHP_INT_MAX : (int) config('woo.free.max_stores', 1);
|
|
}
|
|
|
|
public function maxProducts(User $user): int
|
|
{
|
|
return $this->isPro($user) ? PHP_INT_MAX : (int) config('woo.free.max_products', 20);
|
|
}
|
|
|
|
public function connectedStoreCount(User $user): int
|
|
{
|
|
return WooStore::query()
|
|
->where('user_id', $user->id)
|
|
->whereIn('status', [WooStore::STATUS_ACTIVE, WooStore::STATUS_PENDING])
|
|
->count();
|
|
}
|
|
|
|
public function productCount(User $user): int
|
|
{
|
|
$storeIds = WooStore::query()->where('user_id', $user->id)->pluck('id');
|
|
|
|
return WooProduct::query()->whereIn('woo_store_id', $storeIds)->count();
|
|
}
|
|
|
|
public function canConnectStore(User $user, ?string $siteUrl = null): bool
|
|
{
|
|
if ($this->isPro($user)) {
|
|
return true;
|
|
}
|
|
|
|
if ($siteUrl !== null) {
|
|
$existing = WooStore::query()
|
|
->where('user_id', $user->id)
|
|
->where('site_url', $siteUrl)
|
|
->whereIn('status', [WooStore::STATUS_ACTIVE, WooStore::STATUS_PENDING])
|
|
->exists();
|
|
|
|
if ($existing) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return $this->connectedStoreCount($user) < $this->maxStores($user);
|
|
}
|
|
|
|
public function canCreateProduct(User $user): bool
|
|
{
|
|
return $this->productCount($user) < $this->maxProducts($user);
|
|
}
|
|
|
|
public function canManageStoreTeam(User $user): bool
|
|
{
|
|
return $this->isEnterprise($user);
|
|
}
|
|
|
|
public function canIngestProduct(User $user, WooStore $store, int $externalId): bool
|
|
{
|
|
if ($this->isPro($user)) {
|
|
return true;
|
|
}
|
|
|
|
$exists = WooProduct::query()
|
|
->where('woo_store_id', $store->id)
|
|
->where('external_id', $externalId)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return true;
|
|
}
|
|
|
|
return $this->canCreateProduct($user);
|
|
}
|
|
|
|
/** @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 Woo Manager Pro — monthly subscription', 'Welcome to Ladill Woo Manager 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 Woo Manager Business — monthly subscription',
|
|
'Welcome to Ladill Woo Manager 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('woo.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 = 'woo-'.$sub->plan.'-'.$user->id.'-'.now()->format('YmdHis').'-'.Str::random(6);
|
|
$result = $this->billing->charge($user, $sub->price_minor, $reference, "Ladill Woo Manager {$planLabel} — renewal");
|
|
|
|
if ($result['ok']) {
|
|
$sub->forceFill([
|
|
'current_period_end' => Carbon::now()->addDays((int) config('woo.pro.period_days', 30)),
|
|
'last_charged_at' => now(),
|
|
'last_reference' => $reference,
|
|
'last_error' => null,
|
|
])->save();
|
|
$this->expiryAlerts->resetAlerts($sub->fresh());
|
|
|
|
return;
|
|
}
|
|
|
|
$graceEnd = optional($sub->current_period_end)->addDays((int) config('woo.pro.grace_days', 3));
|
|
if ($graceEnd && $graceEnd->isPast()) {
|
|
$wasPastDue = $sub->status === ProSubscription::STATUS_PAST_DUE;
|
|
$sub->forceFill(['status' => ProSubscription::STATUS_PAST_DUE, 'last_error' => $result['error']])->save();
|
|
if (! $wasPastDue) {
|
|
$this->notifications->proPastDue($sub->fresh());
|
|
}
|
|
} 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 = 'woo-'.$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('woo.pro.period_days', 30));
|
|
ProSubscription::updateOrCreate(
|
|
['user_id' => $user->id],
|
|
[
|
|
'plan' => $plan,
|
|
'status' => ProSubscription::STATUS_ACTIVE,
|
|
'price_minor' => $price,
|
|
'currency' => (string) config('woo.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,
|
|
'metadata' => null,
|
|
],
|
|
);
|
|
|
|
$subscription = $this->subscriptionFor($user);
|
|
if ($subscription) {
|
|
$this->expiryAlerts->resetAlerts($subscription);
|
|
}
|
|
|
|
return [true, $successMessage.' Your subscription is active.'];
|
|
}
|
|
}
|