Skip to content

Commit 6c5d51b

Browse files
fix(billing): add StripeIdempotencyError to non-transient set + cover rawType branch
Pre-push review flagged the general isNonTransientStripeError helper omitted StripeIdempotencyError (400, deterministic — reused key with conflicting params). Add it. StripeCardError (402) stays excluded since some decline codes (processing_error, issuer_unavailable) are transient. Add tests for the rawType-only branch and the card exclusion; drop a mislabeled test.
1 parent b537f9d commit 6c5d51b

2 files changed

Lines changed: 21 additions & 12 deletions

File tree

modules/billing/lib/billing.stripe-errors.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@
99

1010
const NON_TRANSIENT_STRIPE_ERROR_CLASSES = new Set([
1111
'StripeInvalidRequestError', // 400/404 — bad params, deterministic
12+
'StripeIdempotencyError', // 400 — idempotency key reused with conflicting params, deterministic
1213
'StripeAuthenticationError', // 401 — bad/missing API key, deterministic
1314
'StripePermissionError', // 403 — key lacks permission for the resource, deterministic
1415
]);
1516

1617
/**
1718
* True when a Stripe error is deterministic and will never succeed on retry
18-
* (invalid request, authentication, or permission failures). Transient errors
19-
* (api_error/500, connection, rate_limit/429) return false so they keep retrying.
19+
* (invalid request, idempotency, authentication, or permission failures). Transient
20+
* errors (api_error/500, connection, rate_limit/429) return false so they keep
21+
* retrying. StripeCardError (402) is intentionally excluded — some decline codes
22+
* (processing_error, issuer_unavailable) are transient.
2023
*
2124
* @param {unknown} err
2225
* @returns {boolean}

modules/billing/tests/billing.stripe-errors.unit.tests.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ import { isNonTransientStripeError } from '../lib/billing.stripe-errors.js';
1010
* across both SDK-wrapped (.type = class name) and raw (.type = wire string) shapes.
1111
*/
1212
describe('isNonTransientStripeError', () => {
13-
test.each(['StripeInvalidRequestError', 'StripeAuthenticationError', 'StripePermissionError'])(
14-
'returns true for SDK error class %s (err.type = class name)',
15-
(type) => {
16-
expect(isNonTransientStripeError({ type })).toBe(true);
17-
},
18-
);
13+
test.each([
14+
'StripeInvalidRequestError',
15+
'StripeIdempotencyError',
16+
'StripeAuthenticationError',
17+
'StripePermissionError',
18+
])('returns true for SDK error class %s (err.type = class name)', (type) => {
19+
expect(isNonTransientStripeError({ type })).toBe(true);
20+
});
1921

20-
test('returns true for an SDK-wrapped error carrying rawType invalid_request_error', () => {
21-
expect(
22-
isNonTransientStripeError({ type: 'StripeInvalidRequestError', rawType: 'invalid_request_error' }),
23-
).toBe(true);
22+
test('returns true via the rawType branch when the class is not listed', () => {
23+
expect(isNonTransientStripeError({ type: 'StripeFutureUnknownError', rawType: 'invalid_request_error' })).toBe(
24+
true,
25+
);
2426
});
2527

2628
test('returns true for an unwrapped API error object (type = invalid_request_error)', () => {
@@ -34,6 +36,10 @@ describe('isNonTransientStripeError', () => {
3436
},
3537
);
3638

39+
test('returns false for StripeCardError (402 — some decline codes are transient)', () => {
40+
expect(isNonTransientStripeError({ type: 'StripeCardError' })).toBe(false);
41+
});
42+
3743
test('returns false for a generic non-Stripe error', () => {
3844
expect(isNonTransientStripeError(new Error('boom'))).toBe(false);
3945
});

0 commit comments

Comments
 (0)