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