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:
@@ -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,37 +29,41 @@ class SyncStripePrices extends Command
|
||||
|
||||
foreach ($plans as $plan) {
|
||||
try {
|
||||
// Create or get Stripe product
|
||||
$product = Product::create([
|
||||
$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,
|
||||
],
|
||||
'metadata' => ['plan_id' => $plan->id, 'plan_slug' => $plan->slug],
|
||||
]);
|
||||
|
||||
// Create Stripe price
|
||||
$interval = match ($plan->billing_cycle) {
|
||||
$plan->update(['stripe_product_id' => $product->id]);
|
||||
|
||||
foreach ($plan->prices as $planPrice) {
|
||||
if ($planPrice->stripe_price_id && ! $this->option('force')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$interval = match ($planPrice->billing_cycle) {
|
||||
'monthly' => 'month',
|
||||
'quarterly' => 'month',
|
||||
'semi_annually' => 'month',
|
||||
'annually' => 'year',
|
||||
'semi_annual' => 'month',
|
||||
'annual' => 'year',
|
||||
default => 'month',
|
||||
};
|
||||
|
||||
$intervalCount = match ($plan->billing_cycle) {
|
||||
$intervalCount = match ($planPrice->billing_cycle) {
|
||||
'monthly' => 1,
|
||||
'quarterly' => 3,
|
||||
'semi_annually' => 6,
|
||||
'annually' => 1,
|
||||
'semi_annual' => 6,
|
||||
'annual' => 1,
|
||||
default => 1,
|
||||
};
|
||||
|
||||
$price = Price::create([
|
||||
'product' => $product->id,
|
||||
'currency' => 'usd',
|
||||
'unit_amount' => (int) ($plan->price * 100), // Convert to cents
|
||||
'unit_amount' => (int) round($planPrice->price * 100),
|
||||
'recurring' => [
|
||||
'interval' => $interval,
|
||||
'interval_count' => $intervalCount,
|
||||
@@ -67,14 +71,17 @@ class SyncStripePrices extends Command
|
||||
'metadata' => [
|
||||
'plan_id' => $plan->id,
|
||||
'plan_slug' => $plan->slug,
|
||||
'billing_cycle' => $planPrice->billing_cycle,
|
||||
],
|
||||
]);
|
||||
|
||||
// Update plan with Stripe price ID
|
||||
$plan->update([
|
||||
'stripe_price_id' => $price->id,
|
||||
'stripe_product_id' => $product->id,
|
||||
]);
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user