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'));
$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;
}

View File

@@ -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'));

View File

@@ -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.

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

View File

@@ -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 [

View File

@@ -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"