diff --git a/website/app/Console/Commands/SyncStripePrices.php b/website/app/Console/Commands/SyncStripePrices.php index 70fbed9..d392cc7 100644 --- a/website/app/Console/Commands/SyncStripePrices.php +++ b/website/app/Console/Commands/SyncStripePrices.php @@ -20,7 +20,7 @@ class SyncStripePrices extends Command { Stripe::setApiKey(config('cashier.secret')); - $plans = Plan::all(); + $plans = Plan::where('status', 'active')->with('prices')->get(); $this->info("Syncing {$plans->count()} plans with Stripe..."); @@ -29,52 +29,59 @@ class SyncStripePrices extends Command foreach ($plans as $plan) { try { - // Create or get Stripe product - $product = Product::create([ - 'name' => $plan->name, - 'description' => "EZSCALE {$plan->service_type} - {$plan->name}", - 'metadata' => [ - 'plan_id' => $plan->id, - 'plan_slug' => $plan->slug, - ], - ]); + $product = $plan->stripe_product_id && ! $this->option('force') + ? Product::retrieve($plan->stripe_product_id) + : Product::create([ + 'name' => $plan->name, + 'description' => "EZSCALE {$plan->service_type} - {$plan->name}", + 'metadata' => ['plan_id' => $plan->id, 'plan_slug' => $plan->slug], + ]); - // Create Stripe price - $interval = match ($plan->billing_cycle) { - 'monthly' => 'month', - 'quarterly' => 'month', - 'semi_annually' => 'month', - 'annually' => 'year', - default => 'month', - }; + $plan->update(['stripe_product_id' => $product->id]); - $intervalCount = match ($plan->billing_cycle) { - 'monthly' => 1, - 'quarterly' => 3, - 'semi_annually' => 6, - 'annually' => 1, - default => 1, - }; + foreach ($plan->prices as $planPrice) { + if ($planPrice->stripe_price_id && ! $this->option('force')) { + continue; + } - $price = Price::create([ - 'product' => $product->id, - 'currency' => 'usd', - 'unit_amount' => (int) ($plan->price * 100), // Convert to cents - 'recurring' => [ - 'interval' => $interval, - 'interval_count' => $intervalCount, - ], - 'metadata' => [ - 'plan_id' => $plan->id, - 'plan_slug' => $plan->slug, - ], - ]); + $interval = match ($planPrice->billing_cycle) { + 'monthly' => 'month', + 'quarterly' => 'month', + 'semi_annual' => 'month', + 'annual' => 'year', + default => 'month', + }; - // Update plan with Stripe price ID - $plan->update([ - 'stripe_price_id' => $price->id, - 'stripe_product_id' => $product->id, - ]); + $intervalCount = match ($planPrice->billing_cycle) { + 'monthly' => 1, + 'quarterly' => 3, + 'semi_annual' => 6, + 'annual' => 1, + default => 1, + }; + + $price = Price::create([ + 'product' => $product->id, + 'currency' => 'usd', + 'unit_amount' => (int) round($planPrice->price * 100), + 'recurring' => [ + 'interval' => $interval, + 'interval_count' => $intervalCount, + ], + 'metadata' => [ + 'plan_id' => $plan->id, + 'plan_slug' => $plan->slug, + 'billing_cycle' => $planPrice->billing_cycle, + ], + ]); + + $planPrice->update(['stripe_price_id' => $price->id]); + } + + $monthlyPrice = $plan->priceForCycle('monthly'); + if ($monthlyPrice?->stripe_price_id) { + $plan->update(['stripe_price_id' => $monthlyPrice->stripe_price_id]); + } $progressBar->advance(); } catch (\Exception $e) { @@ -86,7 +93,7 @@ class SyncStripePrices extends Command $progressBar->finish(); $this->newLine(2); - $this->info('✓ Stripe prices synced successfully!'); + $this->info('Stripe prices synced successfully!'); return self::SUCCESS; } diff --git a/website/app/Http/Controllers/Admin/CustomerController.php b/website/app/Http/Controllers/Admin/CustomerController.php index 19ebf6b..493bff7 100644 --- a/website/app/Http/Controllers/Admin/CustomerController.php +++ b/website/app/Http/Controllers/Admin/CustomerController.php @@ -304,7 +304,7 @@ class CustomerController extends Controller { $request->validate([ 'plan_id' => 'required|exists:plans,id', - 'billing_cycle' => 'required|in:monthly,quarterly,semi_annually,annually', + 'billing_cycle' => 'required|in:monthly,quarterly,semi_annual,annual', ]); $plan = Plan::query()->findOrFail($request->input('plan_id')); diff --git a/website/app/Services/Billing/BillingServiceInterface.php b/website/app/Services/Billing/BillingServiceInterface.php index 80b17f9..f30951c 100644 --- a/website/app/Services/Billing/BillingServiceInterface.php +++ b/website/app/Services/Billing/BillingServiceInterface.php @@ -26,7 +26,7 @@ interface BillingServiceInterface * * @return array{subscription_id: string, status: string} */ - public function swapSubscription(User $user, string $subscriptionId, Plan $newPlan): array; + public function swapSubscription(User $user, string $subscriptionId, Plan $newPlan, string $billingCycle = 'monthly'): array; /** * Resume a cancelled subscription. diff --git a/website/app/Services/Billing/PayPalBillingService.php b/website/app/Services/Billing/PayPalBillingService.php index 8357724..bb45839 100644 --- a/website/app/Services/Billing/PayPalBillingService.php +++ b/website/app/Services/Billing/PayPalBillingService.php @@ -95,7 +95,7 @@ class PayPalBillingService implements BillingServiceInterface } } - public function swapSubscription(User $user, string $subscriptionId, Plan $newPlan): array + public function swapSubscription(User $user, string $subscriptionId, Plan $newPlan, string $billingCycle = 'monthly'): array { try { $response = $this->client->reviseSubscription($subscriptionId, [ diff --git a/website/app/Services/Billing/StripeBillingService.php b/website/app/Services/Billing/StripeBillingService.php index b43ee08..04794c4 100644 --- a/website/app/Services/Billing/StripeBillingService.php +++ b/website/app/Services/Billing/StripeBillingService.php @@ -24,7 +24,9 @@ class StripeBillingService implements BillingServiceInterface $user->updateDefaultPaymentMethod($paymentMethodId); } - $subscription = $user->newSubscription($plan->slug, $plan->stripe_price_id); + $planPrice = $plan->priceForCycle($billingCycle); + $stripePriceId = $planPrice?->stripe_price_id ?? $plan->stripe_price_id; + $subscription = $user->newSubscription($plan->slug, $stripePriceId); if ($couponCode) { $coupon = Coupon::where('code', $couponCode)->first(); @@ -43,7 +45,7 @@ class StripeBillingService implements BillingServiceInterface 'gateway' => 'stripe', 'gateway_subscription_id' => $cashierSubscription->stripe_id, 'gateway_customer_id' => $cashierSubscription->user->stripe_id, - 'gateway_price_id' => $plan->stripe_price_id, + 'gateway_price_id' => $stripePriceId, 'current_period_start' => now(), 'current_period_end' => $this->calculatePeriodEnd($billingCycle), ]); @@ -92,7 +94,7 @@ class StripeBillingService implements BillingServiceInterface } } - public function swapSubscription(User $user, string $subscriptionId, Plan $newPlan): array + public function swapSubscription(User $user, string $subscriptionId, Plan $newPlan, string $billingCycle = 'monthly'): array { $subscription = $user->subscriptions()->where('stripe_id', $subscriptionId)->first(); @@ -100,12 +102,17 @@ class StripeBillingService implements BillingServiceInterface throw new \RuntimeException('Subscription not found.'); } + $planPrice = $newPlan->priceForCycle($billingCycle); + $stripePriceId = $planPrice?->stripe_price_id ?? $newPlan->stripe_price_id; + try { - $subscription->swap($newPlan->stripe_price_id); + $subscription->swap($stripePriceId); $subscription->update([ 'plan_id' => $newPlan->id, - 'gateway_price_id' => $newPlan->stripe_price_id, + 'billing_cycle' => $billingCycle, + 'gateway_price_id' => $stripePriceId, + 'current_period_end' => $this->calculatePeriodEnd($billingCycle), ]); return [ diff --git a/website/resources/ts/Pages/Admin/Customers/Show.vue b/website/resources/ts/Pages/Admin/Customers/Show.vue index de1a5c7..bb3ad01 100644 --- a/website/resources/ts/Pages/Admin/Customers/Show.vue +++ b/website/resources/ts/Pages/Admin/Customers/Show.vue @@ -1147,8 +1147,8 @@ function goToAuditPage(page: number): void { :items="[ { title: 'Monthly', value: 'monthly' }, { title: 'Quarterly', value: 'quarterly' }, - { title: 'Semi-Annually', value: 'semi_annually' }, - { title: 'Annually', value: 'annually' }, + { title: 'Semi-Annual', value: 'semi_annual' }, + { title: 'Annual', value: 'annual' }, ]" label="Billing Cycle" :error-messages="placeOrderForm.errors.billing_cycle"