From 638f1d37f1f9e3a53fbb8e7f5eaa5314ba34419d Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 27 Mar 2026 19:05:13 +0100
Subject: [PATCH] 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.
---
.../Stripe/UpdateSubscriptionQuantity.php | 5 +-
.../livewire/subscription/actions.blade.php | 2 +-
.../UpdateSubscriptionQuantityTest.php | 49 +++++++++++++++++--
3 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/app/Actions/Stripe/UpdateSubscriptionQuantity.php b/app/Actions/Stripe/UpdateSubscriptionQuantity.php
index a3eab4dca..d4d29af20 100644
--- a/app/Actions/Stripe/UpdateSubscriptionQuantity.php
+++ b/app/Actions/Stripe/UpdateSubscriptionQuantity.php
@@ -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()];
diff --git a/resources/views/livewire/subscription/actions.blade.php b/resources/views/livewire/subscription/actions.blade.php
index 6fba0ed83..aa129043b 100644
--- a/resources/views/livewire/subscription/actions.blade.php
+++ b/resources/views/livewire/subscription/actions.blade.php
@@ -160,7 +160,7 @@ class="w-20 px-2 py-1 text-xl font-bold text-center rounded border dark:bg-coolg
- Total / {{ $billingInterval === 'yearly' ? 'year' : 'month' }}
+ Total / month
diff --git a/tests/Feature/Subscription/UpdateSubscriptionQuantityTest.php b/tests/Feature/Subscription/UpdateSubscriptionQuantityTest.php
index 3e13170f0..3eda322e8 100644
--- a/tests/Feature/Subscription/UpdateSubscriptionQuantityTest.php
+++ b/tests/Feature/Subscription/UpdateSubscriptionQuantityTest.php
@@ -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);