feat: update billing services for cycle-specific pricing and fix naming

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-03-14 23:32:57 -04:00
parent c034be820e
commit 82a069304f
6 changed files with 68 additions and 54 deletions

View File

@@ -20,7 +20,7 @@ class SyncStripePrices extends Command
{ {
Stripe::setApiKey(config('cashier.secret')); Stripe::setApiKey(config('cashier.secret'));
$plans = Plan::all(); $plans = Plan::where('status', 'active')->with('prices')->get();
$this->info("Syncing {$plans->count()} plans with Stripe..."); $this->info("Syncing {$plans->count()} plans with Stripe...");
@@ -29,52 +29,59 @@ class SyncStripePrices extends Command
foreach ($plans as $plan) { foreach ($plans as $plan) {
try { try {
// Create or get Stripe product $product = $plan->stripe_product_id && ! $this->option('force')
$product = Product::create([ ? Product::retrieve($plan->stripe_product_id)
'name' => $plan->name, : Product::create([
'description' => "EZSCALE {$plan->service_type} - {$plan->name}", 'name' => $plan->name,
'metadata' => [ 'description' => "EZSCALE {$plan->service_type} - {$plan->name}",
'plan_id' => $plan->id, 'metadata' => ['plan_id' => $plan->id, 'plan_slug' => $plan->slug],
'plan_slug' => $plan->slug, ]);
],
]);
// Create Stripe price $plan->update(['stripe_product_id' => $product->id]);
$interval = match ($plan->billing_cycle) {
'monthly' => 'month',
'quarterly' => 'month',
'semi_annually' => 'month',
'annually' => 'year',
default => 'month',
};
$intervalCount = match ($plan->billing_cycle) { foreach ($plan->prices as $planPrice) {
'monthly' => 1, if ($planPrice->stripe_price_id && ! $this->option('force')) {
'quarterly' => 3, continue;
'semi_annually' => 6, }
'annually' => 1,
default => 1,
};
$price = Price::create([ $interval = match ($planPrice->billing_cycle) {
'product' => $product->id, 'monthly' => 'month',
'currency' => 'usd', 'quarterly' => 'month',
'unit_amount' => (int) ($plan->price * 100), // Convert to cents 'semi_annual' => 'month',
'recurring' => [ 'annual' => 'year',
'interval' => $interval, default => 'month',
'interval_count' => $intervalCount, };
],
'metadata' => [
'plan_id' => $plan->id,
'plan_slug' => $plan->slug,
],
]);
// Update plan with Stripe price ID $intervalCount = match ($planPrice->billing_cycle) {
$plan->update([ 'monthly' => 1,
'stripe_price_id' => $price->id, 'quarterly' => 3,
'stripe_product_id' => $product->id, '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(); $progressBar->advance();
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -86,7 +93,7 @@ class SyncStripePrices extends Command
$progressBar->finish(); $progressBar->finish();
$this->newLine(2); $this->newLine(2);
$this->info('Stripe prices synced successfully!'); $this->info('Stripe prices synced successfully!');
return self::SUCCESS; return self::SUCCESS;
} }

View File

@@ -304,7 +304,7 @@ class CustomerController extends Controller
{ {
$request->validate([ $request->validate([
'plan_id' => 'required|exists:plans,id', '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')); $plan = Plan::query()->findOrFail($request->input('plan_id'));

View File

@@ -26,7 +26,7 @@ interface BillingServiceInterface
* *
* @return array{subscription_id: string, status: string} * @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. * Resume a cancelled subscription.

View File

@@ -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 { try {
$response = $this->client->reviseSubscription($subscriptionId, [ $response = $this->client->reviseSubscription($subscriptionId, [

View File

@@ -24,7 +24,9 @@ class StripeBillingService implements BillingServiceInterface
$user->updateDefaultPaymentMethod($paymentMethodId); $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) { if ($couponCode) {
$coupon = Coupon::where('code', $couponCode)->first(); $coupon = Coupon::where('code', $couponCode)->first();
@@ -43,7 +45,7 @@ class StripeBillingService implements BillingServiceInterface
'gateway' => 'stripe', 'gateway' => 'stripe',
'gateway_subscription_id' => $cashierSubscription->stripe_id, 'gateway_subscription_id' => $cashierSubscription->stripe_id,
'gateway_customer_id' => $cashierSubscription->user->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_start' => now(),
'current_period_end' => $this->calculatePeriodEnd($billingCycle), '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(); $subscription = $user->subscriptions()->where('stripe_id', $subscriptionId)->first();
@@ -100,12 +102,17 @@ class StripeBillingService implements BillingServiceInterface
throw new \RuntimeException('Subscription not found.'); throw new \RuntimeException('Subscription not found.');
} }
$planPrice = $newPlan->priceForCycle($billingCycle);
$stripePriceId = $planPrice?->stripe_price_id ?? $newPlan->stripe_price_id;
try { try {
$subscription->swap($newPlan->stripe_price_id); $subscription->swap($stripePriceId);
$subscription->update([ $subscription->update([
'plan_id' => $newPlan->id, '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 [ return [

View File

@@ -1147,8 +1147,8 @@ function goToAuditPage(page: number): void {
:items="[ :items="[
{ title: 'Monthly', value: 'monthly' }, { title: 'Monthly', value: 'monthly' },
{ title: 'Quarterly', value: 'quarterly' }, { title: 'Quarterly', value: 'quarterly' },
{ title: 'Semi-Annually', value: 'semi_annually' }, { title: 'Semi-Annual', value: 'semi_annual' },
{ title: 'Annually', value: 'annually' }, { title: 'Annual', value: 'annual' },
]" ]"
label="Billing Cycle" label="Billing Cycle"
:error-messages="placeOrderForm.errors.billing_cycle" :error-messages="placeOrderForm.errors.billing_cycle"