fix(stripe): add error handling and resilience to subscription operations

- Record refunds immediately before cancellation to prevent retry issues if cancel fails
- Wrap Stripe API calls in try-catch for refunds and quantity reverts with internal notifications
- Add null check in Team.subscriptionEnded() to prevent NPE when subscription doesn't exist
- Fix control flow bug in StripeProcessJob (add missing break statement)
- Cap dynamic server limit with MAX_SERVER_LIMIT in subscription updates
- Add comprehensive tests for refund failures, event handling, and null safety
This commit is contained in:
Andras Bacsai committed 2026-03-18 15:21:59 +01:00
1 parent 23f9156c73
commit 566744b2e0
9 files changed
+350 -18

No files matched your search

@@ -251,6 +251,56 @@ describe('execute', function () {
expect($result['error'])->toContain('No payment intent');
});
test('records refund and proceeds when cancel fails', function () {
$stripeSubscription = (object) [
'status' => 'active',
'start_date' => now()->subDays(10)->timestamp,
];
$this->mockSubscriptions
->shouldReceive('retrieve')
->with('sub_test_123')
->andReturn($stripeSubscription);
$invoiceCollection = (object) ['data' => [
(object) ['payment_intent' => 'pi_test_123'],
]];
$this->mockInvoices
->shouldReceive('all')
->with([
'subscription' => 'sub_test_123',
'status' => 'paid',
'limit' => 1,
])
->andReturn($invoiceCollection);
$this->mockRefunds
->shouldReceive('create')
->with(['payment_intent' => 'pi_test_123'])
->andReturn((object) ['id' => 're_test_123']);
// Cancel throws — simulating Stripe failure after refund
$this->mockSubscriptions
->shouldReceive('cancel')
->with('sub_test_123')
->andThrow(new \Exception('Stripe cancel API error'));
$action = new RefundSubscription($this->mockStripe);
$result = $action->execute($this->team);
// Should still succeed — refund went through
expect($result['success'])->toBeTrue();
expect($result['error'])->toBeNull();
$this->subscription->refresh();
// Refund timestamp must be recorded
expect($this->subscription->stripe_refunded_at)->not->toBeNull();
// Subscription should still be marked as ended locally
expect($this->subscription->stripe_invoice_paid)->toBeFalsy();
expect($this->subscription->stripe_subscription_id)->toBeNull();
});
test('fails when subscription is past refund window', function () {
$stripeSubscription = (object) [
'status' => 'active',