Skip to content

Commit 13621c4

Browse files
Merge pull request #20053 from mozilla/PAY-3467-error-no-such-subscription
feat(payments-next): Error: No such subscription: 'sub_1SpH7cJNcmPzuWtRUU3vAbuz
2 parents b4b12b0 + 1098e22 commit 13621c4

3 files changed

Lines changed: 85 additions & 5 deletions

File tree

libs/payments/cart/src/lib/cart.error.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,16 @@ export class PaidPaymentIntendOnFailedCartError extends CartError {
554554
}
555555
}
556556

557+
export class CartSubscriptionDeletionFailedError extends CartError {
558+
constructor(cartId: string, subscriptionId: string, cause: Error) {
559+
super('Subscription deletion failed during cart cleanup', {
560+
cartId,
561+
subscriptionId,
562+
}, cause);
563+
this.name = 'CartSubscriptionDeletionFailedError';
564+
}
565+
}
566+
557567
export class SubscriptionPaymentIntentMissingCartError extends CartError {
558568
constructor(cartId: string, subscriptionId: string) {
559569
super('Subscription on cart has no payment intent', {

libs/payments/cart/src/lib/cart.service.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,49 @@ describe('CartService', () => {
350350
);
351351
});
352352

353+
it('gracefully handles subscription not found during cancel', async () => {
354+
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
355+
const mockSubscription = StripeResponseFactory(
356+
StripeSubscriptionFactory({
357+
customer: mockCustomer.id,
358+
latest_invoice: null,
359+
})
360+
);
361+
const mockCart = ResultCartFactory({
362+
state: CartState.PROCESSING,
363+
stripeSubscriptionId: mockSubscription.id,
364+
stripeCustomerId: mockCustomer.id,
365+
eligibilityStatus: CartEligibilityStatus.CREATE,
366+
});
367+
368+
const stripeError = new Stripe.errors.StripeInvalidRequestError({
369+
type: 'invalid_request_error',
370+
message: `No such subscription: '${mockSubscription.id}'`,
371+
code: 'resource_missing',
372+
});
373+
374+
jest
375+
.spyOn(cartManager, 'fetchCartById')
376+
.mockRejectedValueOnce(new Error('test'))
377+
.mockResolvedValue(mockCart);
378+
jest.spyOn(cartManager, 'finishErrorCart').mockResolvedValue();
379+
jest
380+
.spyOn(subscriptionManager, 'retrieve')
381+
.mockResolvedValue(mockSubscription);
382+
jest
383+
.spyOn(subscriptionManager, 'cancel')
384+
.mockRejectedValue(stripeError);
385+
jest
386+
.spyOn(paymentIntentManager, 'retrieve')
387+
.mockResolvedValue(mockPaymentIntent);
388+
389+
await expect(
390+
cartService.finalizeProcessingCart(mockCart.id)
391+
).rejects.toThrow(Error);
392+
393+
expect(subscriptionManager.cancel).toHaveBeenCalled();
394+
});
395+
353396
it('cancels a created subscription with async local storage', async () => {
354397
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
355398
const mockSubscription = StripeResponseFactory(

libs/payments/cart/src/lib/cart.service.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
SubmitNeedsInputCustomerIdMissingError,
8585
SubmitNeedsInputSubscriptionIdMissingError,
8686
SubmitNeedsInputUidMissingError,
87+
CartSubscriptionDeletionFailedError,
8788
} from './cart.error';
8889
import { CartManager } from './cart.manager';
8990
import type {
@@ -278,11 +279,37 @@ export class CartService {
278279
}
279280

280281
if (cart.eligibilityStatus === CartEligibilityStatus.CREATE) {
281-
await this.subscriptionManager.cancel(subscriptionId, {
282-
cancellation_details: {
283-
comment: 'Automatic Cancellation: Cart checkout failed.',
284-
},
285-
});
282+
try {
283+
await this.subscriptionManager.cancel(subscriptionId, {
284+
cancellation_details: {
285+
comment: 'Automatic Cancellation: Cart checkout failed.',
286+
},
287+
});
288+
} catch (e) {
289+
if (
290+
e.code === 'resource_missing' ||
291+
e.message?.startsWith('No such subscription')
292+
) {
293+
this.log.log(
294+
'cartService.wrapWithCartCatch.subscriptionNotFound',
295+
{
296+
subscriptionId,
297+
eligibilityStatus: cart.eligibilityStatus,
298+
offeringId: cart.offeringConfigId,
299+
interval: cart.interval,
300+
}
301+
);
302+
this.statsd.increment(
303+
'subscription_deletion_failed_not_found'
304+
);
305+
} else {
306+
throw new CartSubscriptionDeletionFailedError(
307+
cartId,
308+
subscriptionId,
309+
e
310+
);
311+
}
312+
}
286313
} else {
287314
this.statsd.increment(
288315
'checkout_failure_subscription_not_cancelled'

0 commit comments

Comments
 (0)