feat(subscription): add billing interval to price preview
Extract and return the billing interval (month/year) from subscription pricing data in fetchPricePreview. Update the view to dynamically display the correct billing period based on the preview response instead of using static PHP logic.
This commit is contained in:
parent
9b0088072c
commit
638f1d37f1
3 changed files with 50 additions and 6 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Jobs\ServerLimitCheckJob;
|
||||
use App\Models\Team;
|
||||
use Stripe\Exception\InvalidRequestException;
|
||||
use Stripe\StripeClient;
|
||||
|
||||
class UpdateSubscriptionQuantity
|
||||
|
|
@ -42,6 +43,7 @@ public function fetchPricePreview(Team $team, int $quantity): array
|
|||
}
|
||||
|
||||
$currency = strtoupper($item->price->currency ?? 'usd');
|
||||
$billingInterval = $item->price->recurring->interval ?? 'month';
|
||||
|
||||
// Upcoming invoice gives us the prorated amount due now
|
||||
$upcomingInvoice = $this->stripe->invoices->upcoming([
|
||||
|
|
@ -99,6 +101,7 @@ public function fetchPricePreview(Team $team, int $quantity): array
|
|||
'tax_description' => $taxDescription,
|
||||
'quantity' => $quantity,
|
||||
'currency' => $currency,
|
||||
'billing_interval' => $billingInterval,
|
||||
],
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
|
|
@ -184,7 +187,7 @@ public function execute(Team $team, int $quantity): array
|
|||
\Log::info("Subscription {$subscription->stripe_subscription_id} quantity updated to {$quantity} for team {$team->name}");
|
||||
|
||||
return ['success' => true, 'error' => null];
|
||||
} catch (\Stripe\Exception\InvalidRequestException $e) {
|
||||
} catch (InvalidRequestException $e) {
|
||||
\Log::error("Stripe update quantity error for team {$team->id}: ".$e->getMessage());
|
||||
|
||||
return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class="w-20 px-2 py-1 text-xl font-bold text-center rounded border dark:bg-coolg
|
|||
<span class="dark:text-white" x-text="fmt(preview?.recurring_tax)"></span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-6 text-sm font-bold pt-1.5 border-t dark:border-coolgray-400 border-neutral-200">
|
||||
<span class="dark:text-white">Total / {{ $billingInterval === 'yearly' ? 'year' : 'month' }}</span>
|
||||
<span class="dark:text-white">Total / <span x-text="preview?.billing_interval === 'year' ? 'year' : 'month'">month</span></span>
|
||||
<span class="dark:text-white" x-text="fmt(preview?.recurring_total)"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Stripe\Exception\InvalidRequestException;
|
||||
use Stripe\Service\InvoiceService;
|
||||
use Stripe\Service\SubscriptionService;
|
||||
use Stripe\Service\TaxRateService;
|
||||
|
|
@ -46,7 +47,7 @@
|
|||
'data' => [(object) [
|
||||
'id' => 'si_item_123',
|
||||
'quantity' => 2,
|
||||
'price' => (object) ['unit_amount' => 500, 'currency' => 'usd'],
|
||||
'price' => (object) ['unit_amount' => 500, 'currency' => 'usd', 'recurring' => (object) ['interval' => 'month']],
|
||||
]],
|
||||
],
|
||||
];
|
||||
|
|
@ -187,7 +188,7 @@
|
|||
test('handles stripe API error gracefully', function () {
|
||||
$this->mockSubscriptions
|
||||
->shouldReceive('retrieve')
|
||||
->andThrow(new \Stripe\Exception\InvalidRequestException('Subscription not found'));
|
||||
->andThrow(new InvalidRequestException('Subscription not found'));
|
||||
|
||||
$action = new UpdateSubscriptionQuantity($this->mockStripe);
|
||||
$result = $action->execute($this->team, 5);
|
||||
|
|
@ -199,7 +200,7 @@
|
|||
test('handles generic exception gracefully', function () {
|
||||
$this->mockSubscriptions
|
||||
->shouldReceive('retrieve')
|
||||
->andThrow(new \RuntimeException('Network error'));
|
||||
->andThrow(new RuntimeException('Network error'));
|
||||
|
||||
$action = new UpdateSubscriptionQuantity($this->mockStripe);
|
||||
$result = $action->execute($this->team, 5);
|
||||
|
|
@ -270,6 +271,46 @@
|
|||
expect($result['preview']['tax_description'])->toContain('27%');
|
||||
expect($result['preview']['quantity'])->toBe(3);
|
||||
expect($result['preview']['currency'])->toBe('USD');
|
||||
expect($result['preview']['billing_interval'])->toBe('month');
|
||||
});
|
||||
|
||||
test('returns yearly billing interval for annual subscriptions', function () {
|
||||
$yearlySubscriptionResponse = (object) [
|
||||
'items' => (object) [
|
||||
'data' => [(object) [
|
||||
'id' => 'si_item_123',
|
||||
'quantity' => 2,
|
||||
'price' => (object) ['unit_amount' => 500, 'currency' => 'usd', 'recurring' => (object) ['interval' => 'year']],
|
||||
]],
|
||||
],
|
||||
];
|
||||
|
||||
$this->mockSubscriptions
|
||||
->shouldReceive('retrieve')
|
||||
->with('sub_test_qty')
|
||||
->andReturn($yearlySubscriptionResponse);
|
||||
|
||||
$this->mockInvoices
|
||||
->shouldReceive('upcoming')
|
||||
->andReturn((object) [
|
||||
'amount_due' => 1000,
|
||||
'total' => 1000,
|
||||
'subtotal' => 1000,
|
||||
'tax' => 0,
|
||||
'currency' => 'usd',
|
||||
'lines' => (object) [
|
||||
'data' => [
|
||||
(object) ['amount' => 1000, 'proration' => false],
|
||||
],
|
||||
],
|
||||
'total_tax_amounts' => [],
|
||||
]);
|
||||
|
||||
$action = new UpdateSubscriptionQuantity($this->mockStripe);
|
||||
$result = $action->fetchPricePreview($this->team, 2);
|
||||
|
||||
expect($result['success'])->toBeTrue();
|
||||
expect($result['preview']['billing_interval'])->toBe('year');
|
||||
});
|
||||
|
||||
test('returns preview without tax when no tax applies', function () {
|
||||
|
|
@ -336,7 +377,7 @@
|
|||
test('handles Stripe API error gracefully', function () {
|
||||
$this->mockSubscriptions
|
||||
->shouldReceive('retrieve')
|
||||
->andThrow(new \RuntimeException('API error'));
|
||||
->andThrow(new RuntimeException('API error'));
|
||||
|
||||
$action = new UpdateSubscriptionQuantity($this->mockStripe);
|
||||
$result = $action->fetchPricePreview($this->team, 5);
|
||||
|
|
|
|||
Loading…
Reference in a new issue