From 2eac90c88e719b3059613bf237c8e6e303799da1 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:23:09 +0100 Subject: [PATCH] refactor(stripe): create subscriptions and charges with the basil API version --- src/infrastructure/adapters/stripe.adapter.ts | 16 ++- src/services/payment.service.ts | 85 ++++------- .../adapters/stripe.adapter.test.ts | 24 ++-- tests/src/services/payment.service.test.ts | 135 ++++++++++++------ 4 files changed, 149 insertions(+), 111 deletions(-) diff --git a/src/infrastructure/adapters/stripe.adapter.ts b/src/infrastructure/adapters/stripe.adapter.ts index c468b969..1e2905b6 100644 --- a/src/infrastructure/adapters/stripe.adapter.ts +++ b/src/infrastructure/adapters/stripe.adapter.ts @@ -180,14 +180,14 @@ export class StripePaymentsAdapter implements PaymentsAdapter { subscriptionId: string, params: Partial, ): Promise { - const subscription = await this.provider.subscriptions.update(subscriptionId, params); + const subscription = await this.newVersionProvider.subscriptions.update(subscriptionId, params); return Subscription.toDomain({ id: subscription.id, customer: subscription.customer as string, status: subscription.status, priceId: subscription.items.data[0].price.id, - currentPeriodEnd: subscription.current_period_end, + currentPeriodEnd: subscription.items.data[0].current_period_end, metadata: subscription.metadata, startDate: subscription.start_date, trialEnd: subscription.trial_end ?? undefined, @@ -219,11 +219,11 @@ export class StripePaymentsAdapter implements PaymentsAdapter { } async cancelSubscription(subscriptionId: string): Promise { - await this.provider.subscriptions.cancel(subscriptionId); + await this.newVersionProvider.subscriptions.cancel(subscriptionId); } async createInvoice(params?: Partial): Promise { - const invoice = await this.provider.invoices.create(params); + const invoice = await this.newVersionProvider.invoices.create(params); return Invoice.toDomain({ id: invoice.id, @@ -237,7 +237,11 @@ export class StripePaymentsAdapter implements PaymentsAdapter { customerId: string, params: Partial, ): Promise { - const invoice = await this.provider.invoiceItems.create({ invoice: invoiceId, customer: customerId, ...params }); + const invoice = await this.newVersionProvider.invoiceItems.create({ + invoice: invoiceId, + customer: customerId, + ...params, + }); return InvoiceItems.toDomain({ id: invoice.id, @@ -245,7 +249,7 @@ export class StripePaymentsAdapter implements PaymentsAdapter { } async finalizeInvoice(invoiceId: Invoice['id']): Promise { - const finalizedInvoice = await this.provider.invoices.finalizeInvoice(invoiceId, { + const finalizedInvoice = await this.newVersionProvider.invoices.finalizeInvoice(invoiceId, { expand: ['payments', 'confirmation_secret'], }); diff --git a/src/services/payment.service.ts b/src/services/payment.service.ts index f2913568..a3c94549 100644 --- a/src/services/payment.service.ts +++ b/src/services/payment.service.ts @@ -160,7 +160,7 @@ export class PaymentService { couponId = await this.checkIfCouponIsAplicable(customerId, promoCodeId); } - const subscription = await this.provider.subscriptions.create({ + const subscription = await getStripeNewVersion().subscriptions.create({ customer: customerId, currency: currencyValue, items: [ @@ -187,7 +187,7 @@ export class PaymentService { ) as Stripe.SubscriptionCreateParams.PaymentSettings.PaymentMethodType[], save_default_payment_method: 'on_subscription', }, - expand: ['latest_invoice.payment_intent', 'pending_setup_intent'], + expand: ['latest_invoice.confirmation_secret', 'latest_invoice.payments', 'pending_setup_intent'], trial_end: trialEnd ? trialEnd : undefined, ...additionalOptions, }); @@ -199,11 +199,14 @@ export class PaymentService { subscriptionId: subscription.id, }; } else { + const latestInvoice = subscription.latest_invoice as Stripe.Invoice; + const paymentIntent = latestInvoice.payments?.data[0]?.payment.payment_intent; + return { type: 'payment', - clientSecret: (subscription.latest_invoice as any).payment_intent.client_secret, + clientSecret: latestInvoice.confirmation_secret?.client_secret as string, subscriptionId: subscription.id, - paymentIntentId: (subscription.latest_invoice as any).payment_intent.id, + paymentIntentId: typeof paymentIntent === 'string' ? paymentIntent : paymentIntent?.id, }; } } @@ -326,7 +329,7 @@ export class PaymentService { const isRecurring = price.type === 'recurring'; if (isRecurring) { - await this.provider.subscriptions.create({ + await getStripeNewVersion().subscriptions.create({ customer: customerId, items: [ { @@ -335,7 +338,7 @@ export class PaymentService { ], }); } else { - const invoice = await this.provider.invoices.create({ + const invoice = await getStripeNewVersion().invoices.create({ customer: customerId, auto_advance: false, pending_invoice_items_behavior: 'include', @@ -345,15 +348,15 @@ export class PaymentService { }, }); - await this.provider.invoiceItems.create({ + await getStripeNewVersion().invoiceItems.create({ customer: customerId, - price: priceId, + pricing: { price: priceId }, quantity: 0, description: licenseCode?.provider ? `Affiliate sale via ${licenseCode.provider}` : 'One-time charge', invoice: invoice.id, }); - await this.provider.invoices.pay(invoice.id, { + await getStripeNewVersion().invoices.pay(invoice.id as string, { paid_out_of_band: true, }); } @@ -395,7 +398,7 @@ export class PaymentService { const { isElegibleForCancellation, cancelAt } = this.getAnnualCommitmentCancellationInfo(subscription); if (hasAnnualCommitment && !isElegibleForCancellation) { - await this.provider.subscriptions.update(subscriptionId, { + await getStripeNewVersion().subscriptions.update(subscriptionId, { cancel_at: cancelAt, }); return; @@ -554,7 +557,7 @@ export class PaymentService { throw new IncompatibleSubscriptionTypesError('The new price is not an individual price'); } - const updatedSubscription = await this.provider.subscriptions.update(individualActiveSubscription.id, { + const updatedSubscription = await getStripeNewVersion().subscriptions.update(individualActiveSubscription.id, { cancel_at_period_end: false, proration_behavior: 'none', items: [ @@ -601,7 +604,7 @@ export class PaymentService { throw new InvalidSeatNumberError('The new price does not allow the current amount of seats'); } - const updatedSubscription = await this.provider.subscriptions.update(businessActiveSubscription.id, { + const updatedSubscription = await getStripeNewVersion().subscriptions.update(businessActiveSubscription.id, { cancel_at_period_end: false, proration_behavior: additionalOptions?.proration_behavior ?? 'none', items: [ @@ -657,20 +660,18 @@ export class PaymentService { additionalOptions, }); - const getLatestInvoice = await this.provider.invoices.retrieve(updatedSubscription.latest_invoice as string); - - if (getLatestInvoice.payment_intent) { - const getPaymentIntent: Stripe.PaymentIntent = await this.provider.paymentIntents.retrieve( - getLatestInvoice.payment_intent as string, - ); - if ( - getPaymentIntent.status === 'requires_action' && - getPaymentIntent.next_action?.type === 'use_stripe_sdk' && - getPaymentIntent.client_secret - ) { - is3DSecureRequired = true; - clientSecret = getPaymentIntent.client_secret; - } + const latestInvoice = await getStripeNewVersion().invoices.retrieve(updatedSubscription.latest_invoice as string, { + expand: ['payments.data.payment.payment_intent'], + }); + const paymentIntent = latestInvoice.payments?.data[0]?.payment.payment_intent as Stripe.PaymentIntent | undefined; + + if ( + paymentIntent?.status === 'requires_action' && + paymentIntent.next_action?.type === 'use_stripe_sdk' && + paymentIntent.client_secret + ) { + is3DSecureRequired = true; + clientSecret = paymentIntent.client_secret; } return { @@ -691,13 +692,13 @@ export class PaymentService { if (!subscriptionId) throw new Error('Subscription not found'); - const { id, customer } = await this.provider.paymentMethods.attach(paymentMethodId, { + const { id, customer } = await getStripeNewVersion().paymentMethods.attach(paymentMethodId, { customer: customerId, }); if (!id || !customer) throw new Error('Payment method not attached'); - return this.provider.subscriptions.update(subscriptionId, { + return getStripeNewVersion().subscriptions.update(subscriptionId, { default_payment_method: id, }); } @@ -708,32 +709,6 @@ export class PaymentService { return res.data; } - async getPlanIdFromLastPayment( - customerId: CustomerId, - pagination: { limit?: number; startingAfter?: string }, - ): Promise { - const res = await this.provider.paymentIntents.list({ - customer: customerId, - limit: pagination.limit, - starting_after: pagination.startingAfter, - }); - - const lastPaymentIntent = res.data - .filter((pi) => pi.status === 'succeeded') - .sort((a, b) => b.created - a.created) - .at(0); - - if (!lastPaymentIntent) { - return null; - } - - const checkout = await this.provider.checkout.sessions.list({ payment_intent: lastPaymentIntent.id }); - const checkoutLines = await this.provider.checkout.sessions.listLineItems(checkout.data[0].id); - const productId = checkoutLines.data[0].price?.product; - - return productId as string; - } - async getInvoicesFromUser( customerId: CustomerId, pagination: { limit?: number; startingAfter?: string }, @@ -1264,7 +1239,7 @@ export class PaymentService { amount: number, additionalOptions?: Partial, ) { - return this.provider.paymentIntents.create({ + return getStripeNewVersion().paymentIntents.create({ customer: customerId, currency, amount, diff --git a/tests/src/infrastructure/adapters/stripe.adapter.test.ts b/tests/src/infrastructure/adapters/stripe.adapter.test.ts index 7f5b7795..bf0f5709 100644 --- a/tests/src/infrastructure/adapters/stripe.adapter.test.ts +++ b/tests/src/infrastructure/adapters/stripe.adapter.test.ts @@ -454,7 +454,7 @@ describe('Stripe Adapter', () => { const stripeSubscription = getCreatedSubscription({ status: 'active', trial_end: null }); const updateSpy = jest - .spyOn(stripePaymentsAdapter.provider.subscriptions, 'update') + .spyOn(stripePaymentsAdapter.newVersionProvider.subscriptions, 'update') .mockResolvedValue(stripeSubscription as any); const params = { cancel_at: 1800000000 }; @@ -467,7 +467,7 @@ describe('Stripe Adapter', () => { customer: stripeSubscription.customer as string, status: stripeSubscription.status, priceId: stripeSubscription.items.data[0].price.id, - currentPeriodEnd: stripeSubscription.current_period_end, + currentPeriodEnd: stripeSubscription.items.data[0].current_period_end, metadata: stripeSubscription.metadata, startDate: stripeSubscription.start_date, }), @@ -478,7 +478,9 @@ describe('Stripe Adapter', () => { const trialEnd = 1800000000; const stripeSubscription = getCreatedSubscription({ status: 'trialing', trial_end: trialEnd }); - jest.spyOn(stripePaymentsAdapter.provider.subscriptions, 'update').mockResolvedValue(stripeSubscription as any); + jest + .spyOn(stripePaymentsAdapter.newVersionProvider.subscriptions, 'update') + .mockResolvedValue(stripeSubscription as any); const subscription = await stripePaymentsAdapter.updateSubscription(stripeSubscription.id, {}); @@ -530,9 +532,9 @@ describe('Stripe Adapter', () => { describe('Creating an invoice', () => { test('When creating an invoice, then the invoice is created and the entity is mapped and returned', async () => { - const mockedInvoice = getInvoice(); + const mockedInvoice = getInvoice({ confirmation_secret: { client_secret: 'pi_secret', type: 'payment_intent' } }); - jest.spyOn(stripePaymentsAdapter.provider.invoices, 'create').mockResolvedValue(mockedInvoice as any); + jest.spyOn(stripePaymentsAdapter.newVersionProvider.invoices, 'create').mockResolvedValue(mockedInvoice as any); const invoice = await stripePaymentsAdapter.createInvoice({ customer: mockedInvoice.customer as string, @@ -541,7 +543,7 @@ describe('Stripe Adapter', () => { expect(invoice).toStrictEqual( Invoice.toDomain({ id: mockedInvoice.id, - clientSecretId: mockedInvoice.confirmation_secret?.client_secret, + clientSecretId: 'pi_secret', status: mockedInvoice.status as InvoiceStatus, }), ); @@ -550,16 +552,18 @@ describe('Stripe Adapter', () => { describe('Finalizing an invoice', () => { test('When finalizing an invoice, then the invoice is finalized and the entity is mapped and returned', async () => { - const mockedInvoice = getInvoice(); + const mockedInvoice = getInvoice({ confirmation_secret: { client_secret: 'pi_secret', type: 'payment_intent' } }); - jest.spyOn(stripePaymentsAdapter.provider.invoices, 'finalizeInvoice').mockResolvedValue(mockedInvoice as any); + jest + .spyOn(stripePaymentsAdapter.newVersionProvider.invoices, 'finalizeInvoice') + .mockResolvedValue(mockedInvoice as any); const invoice = await stripePaymentsAdapter.finalizeInvoice(mockedInvoice.id); expect(invoice).toStrictEqual( Invoice.toDomain({ id: mockedInvoice.id, - clientSecretId: mockedInvoice.confirmation_secret?.client_secret, + clientSecretId: 'pi_secret', status: mockedInvoice.status as InvoiceStatus, }), ); @@ -570,7 +574,7 @@ describe('Stripe Adapter', () => { test('When adding invoice items, then the invoice items are added and the entity is mapped and returned', async () => { const mockedInvoiceItems = getInvoiceItems(); - jest.spyOn(stripePaymentsAdapter.provider.invoiceItems, 'create').mockResolvedValue({ + jest.spyOn(stripePaymentsAdapter.newVersionProvider.invoiceItems, 'create').mockResolvedValue({ ...mockedInvoiceItems, lastResponse: {} as any, }); diff --git a/tests/src/services/payment.service.test.ts b/tests/src/services/payment.service.test.ts index 53d22b15..7c7b43a3 100644 --- a/tests/src/services/payment.service.test.ts +++ b/tests/src/services/payment.service.test.ts @@ -4,10 +4,8 @@ import { getCharge, getCoupon, getCreatedSubscription, - getCreateSubscriptionResponse, getCustomer, getInvoice, - getInvoices, getPaymentIntent, getPaymentIntentResponse, getPaymentMethod, @@ -70,30 +68,41 @@ describe('Payments Service tests', () => { }); describe('Creating a subscription', () => { - it('When trying to create a subscription with the correct params, then it is successfully created', async () => { - const mockedSubscriptionResponse = getCreateSubscriptionResponse(); - const mockedCreateSubscription = getCreatedSubscription(); - - const subscriptionCreatedSpy = jest - .spyOn(paymentService, 'createSubscription') - .mockImplementation(() => Promise.resolve(mockedSubscriptionResponse)); + const mockSubscriptionCreation = (createdSubscription: Partial) => { + jest + .spyOn(stripe.prices, 'retrieve') + .mockResolvedValue(getPrice({ product: getProduct({ params: { metadata: {} } }) }) as any); + jest.spyOn(stripeNewVersion.subscriptions, 'list').mockResolvedValue({ data: [] } as any); + return jest + .spyOn(stripeNewVersion.subscriptions, 'create') + .mockResolvedValue({ id: 'sub_new', ...createdSubscription } as any); + }; - const subscription = await paymentService.createSubscription({ - customerId: mockedCreateSubscription.customer as string, - priceId: mockedCreateSubscription.items.data[0].price.id, - promoCodeId: ( - (mockedCreateSubscription.discounts[0] as Stripe.Discount)?.promotion_code as Stripe.PromotionCode - ).code, + test('When the subscription needs a payment, then the secret and the id of that payment are returned', async () => { + mockSubscriptionCreation({ + pending_setup_intent: null, + latest_invoice: { + confirmation_secret: { client_secret: 'pi_secret', type: 'payment_intent' }, + payments: { data: [{ payment: { type: 'payment_intent', payment_intent: 'pi_new' } }] }, + } as Stripe.Invoice, }); - expect(subscriptionCreatedSpy).toHaveBeenCalledWith({ - customerId: mockedCreateSubscription.customer as string, - priceId: mockedCreateSubscription.items.data[0].price.id, - promoCodeId: ( - (mockedCreateSubscription.discounts[0] as Stripe.Discount)?.promotion_code as Stripe.PromotionCode - ).code, + const subscription = await paymentService.createSubscription({ customerId: 'cus_test', priceId: 'price_new' }); + + expect(subscription).toStrictEqual({ + type: 'payment', + clientSecret: 'pi_secret', + subscriptionId: 'sub_new', + paymentIntentId: 'pi_new', }); - expect(subscription).toEqual(mockedSubscriptionResponse); + }); + + test('When the subscription starts without a payment, then the secret to save the payment method is returned', async () => { + mockSubscriptionCreation({ pending_setup_intent: { client_secret: 'seti_secret' } as Stripe.SetupIntent }); + + const subscription = await paymentService.createSubscription({ customerId: 'cus_test', priceId: 'price_new' }); + + expect(subscription).toStrictEqual({ type: 'setup', clientSecret: 'seti_secret', subscriptionId: 'sub_new' }); }); it('When trying to create a business subscription, then an error is thrown', async () => { @@ -766,7 +775,7 @@ describe('Payments Service tests', () => { const mockedUser = getUser(); const mockedPaymentIntent = getPaymentIntent(); const paymentIntentSpy = jest - .spyOn(stripe.paymentIntents, 'create') + .spyOn(stripeNewVersion.paymentIntents, 'create') .mockResolvedValue(mockedPaymentIntent as unknown as Stripe.Response); const paymentIntent = await paymentService.paymentIntent(mockedUser.customerId, 'eur', 100); @@ -788,7 +797,7 @@ describe('Payments Service tests', () => { metadata, }); const paymentIntentSpy = jest - .spyOn(stripe.paymentIntents, 'create') + .spyOn(stripeNewVersion.paymentIntents, 'create') .mockResolvedValue(mockedPaymentIntent as unknown as Stripe.Response); const paymentIntent = await paymentService.paymentIntent(mockedUser.customerId, 'eur', 100, { @@ -846,7 +855,7 @@ describe('Payments Service tests', () => { jest.spyOn(stripe.prices, 'retrieve').mockResolvedValue(mockedPrice as Stripe.Response); const subscriptionCreateSpy = jest - .spyOn(stripe.subscriptions, 'create') + .spyOn(stripeNewVersion.subscriptions, 'create') .mockResolvedValue(mockedSubscription as Stripe.Response); const result = await paymentService.subscribe(customerId, recurringPriceId); @@ -882,13 +891,13 @@ describe('Payments Service tests', () => { jest.spyOn(stripe.prices, 'retrieve').mockResolvedValue(mockedPrice as Stripe.Response); const invoiceCreateSpy = jest - .spyOn(stripe.invoices, 'create') + .spyOn(stripeNewVersion.invoices, 'create') .mockResolvedValue(mockedInvoice as Stripe.Response); const invoiceItemCreateSpy = jest - .spyOn(stripe.invoiceItems, 'create') + .spyOn(stripeNewVersion.invoiceItems, 'create') .mockResolvedValue({} as Stripe.Response); const invoicePaySpy = jest - .spyOn(stripe.invoices, 'pay') + .spyOn(stripeNewVersion.invoices, 'pay') .mockResolvedValue(mockedInvoice as Stripe.Response); const result = await paymentService.subscribe(customerId, oneTimePriceId); @@ -905,7 +914,7 @@ describe('Payments Service tests', () => { }); expect(invoiceItemCreateSpy).toHaveBeenCalledWith({ customer: customerId, - price: oneTimePriceId, + pricing: { price: oneTimePriceId }, quantity: 0, description: 'One-time charge', invoice: mockedInvoice.id, @@ -939,12 +948,12 @@ describe('Payments Service tests', () => { jest.spyOn(stripe.prices, 'retrieve').mockResolvedValue(mockedPrice as Stripe.Response); const invoiceCreateSpy = jest - .spyOn(stripe.invoices, 'create') + .spyOn(stripeNewVersion.invoices, 'create') .mockResolvedValue(mockedInvoice as Stripe.Response); const invoiceItemCreateSpy = jest - .spyOn(stripe.invoiceItems, 'create') + .spyOn(stripeNewVersion.invoiceItems, 'create') .mockResolvedValue({} as Stripe.Response); - jest.spyOn(stripe.invoices, 'pay').mockResolvedValue(mockedInvoice as Stripe.Response); + jest.spyOn(stripeNewVersion.invoices, 'pay').mockResolvedValue(mockedInvoice as Stripe.Response); const result = await paymentService.subscribe(customerId, oneTimePriceId, licenseCode); @@ -959,7 +968,7 @@ describe('Payments Service tests', () => { }); expect(invoiceItemCreateSpy).toHaveBeenCalledWith({ customer: customerId, - price: oneTimePriceId, + pricing: { price: oneTimePriceId }, quantity: 0, description: expect.stringContaining(licenseCode.provider), invoice: mockedInvoice.id, @@ -1071,8 +1080,10 @@ describe('Payments Service tests', () => { jest.spyOn(stripePaymentsAdapter, 'getSubscription').mockResolvedValue(subscription); jest.spyOn(stripePaymentsAdapter, 'getPriceById').mockResolvedValue(price); - const updateSpy = jest.spyOn(stripe.subscriptions, 'update').mockResolvedValue(subscription as unknown as any); - const cancelSpy = jest.spyOn(stripe.subscriptions, 'cancel').mockResolvedValue(undefined as any); + const updateSpy = jest + .spyOn(stripeNewVersion.subscriptions, 'update') + .mockResolvedValue(subscription as unknown as any); + const cancelSpy = jest.spyOn(stripeNewVersion.subscriptions, 'cancel').mockResolvedValue(undefined as any); await paymentService.cancelSubscription(subscription.id); @@ -1093,7 +1104,7 @@ describe('Payments Service tests', () => { jest.spyOn(stripePaymentsAdapter, 'getSubscription').mockResolvedValue(subscription); jest.spyOn(stripePaymentsAdapter, 'getPriceById').mockResolvedValue(price); const deleteSpy = jest.spyOn(stripePaymentsAdapter, 'cancelSubscription').mockResolvedValue(); - const updateSpy = jest.spyOn(stripe.subscriptions, 'update').mockResolvedValue(undefined as any); + const updateSpy = jest.spyOn(stripeNewVersion.subscriptions, 'update').mockResolvedValue(undefined as any); await paymentService.cancelSubscription(subscription.id); @@ -1110,7 +1121,7 @@ describe('Payments Service tests', () => { jest.spyOn(stripePaymentsAdapter, 'getSubscription').mockResolvedValue(subscription); jest.spyOn(stripePaymentsAdapter, 'getPriceById').mockResolvedValue(price); const deleteSpy = jest.spyOn(stripePaymentsAdapter, 'cancelSubscription').mockResolvedValue(); - const updateSpy = jest.spyOn(stripe.subscriptions, 'update').mockResolvedValue(undefined as any); + const updateSpy = jest.spyOn(stripeNewVersion.subscriptions, 'update').mockResolvedValue(undefined as any); await paymentService.cancelSubscription(subscription.id); @@ -1207,14 +1218,20 @@ describe('Payments Service tests', () => { }); describe('Changing the subscription plan', () => { - const mockPlanChange = () => { + const mockPlanChange = (latestInvoicePaymentIntent?: Partial) => { const subscription = getCreatedSubscription(); jest.spyOn(paymentService, 'getActiveSubscriptions').mockResolvedValue([subscription as any]); jest .spyOn(stripe.prices, 'retrieve') .mockResolvedValue(getPrice({ product: getProduct({ params: { metadata: {} } }) }) as any); - jest.spyOn(stripe.invoices, 'retrieve').mockResolvedValue({ payment_intent: null } as any); - return jest.spyOn(stripe.subscriptions, 'update').mockResolvedValue(subscription as any); + jest.spyOn(stripeNewVersion.invoices, 'retrieve').mockResolvedValue({ + payments: { + data: latestInvoicePaymentIntent + ? [{ payment: { type: 'payment_intent', payment_intent: latestInvoicePaymentIntent } }] + : [], + }, + } as any); + return jest.spyOn(stripeNewVersion.subscriptions, 'update').mockResolvedValue(subscription as any); }; test('When the plan is changed with a coupon, then the coupon is applied to the subscription', async () => { @@ -1239,6 +1256,26 @@ describe('Payments Service tests', () => { expect(updateSpy.mock.calls[0][1]).not.toHaveProperty('discounts'); }); + + test('When the payment of the new plan needs 3D Secure, then the secret to confirm it is returned', async () => { + mockPlanChange({ + status: 'requires_action', + next_action: { type: 'use_stripe_sdk' } as Stripe.PaymentIntent.NextAction, + client_secret: 'pi_secret', + }); + + const result = await paymentService.updateSubscriptionPrice({ customerId: 'cus_test', priceId: 'price_new' }); + + expect(result).toStrictEqual({ is3DSecureRequired: true, clientSecret: 'pi_secret' }); + }); + + test('When the payment of the new plan succeeds without extra steps, then 3D Secure is not requested', async () => { + mockPlanChange({ status: 'succeeded', next_action: null, client_secret: 'pi_secret' }); + + const result = await paymentService.updateSubscriptionPrice({ customerId: 'cus_test', priceId: 'price_new' }); + + expect(result).toStrictEqual({ is3DSecureRequired: false, clientSecret: '' }); + }); }); describe('Getting the user subscription', () => { @@ -1425,5 +1462,23 @@ describe('Payments Service tests', () => { }), ); }); + + test('When the remaining amount is charged, then the secret to pay the final invoice in Stripe is returned', async () => { + const subscription = getSubscriptionEntity({ paymentMethod: 'pm_test_default' }); + jest.spyOn(stripePaymentsAdapter, 'getCustomer').mockResolvedValue(customerEntity); + jest.spyOn(stripePaymentsAdapter, 'getPriceById').mockResolvedValue(priceEntity); + jest.spyOn(stripePaymentsAdapter, 'shouldCalculateTaxForCustomer').mockResolvedValue(false); + jest.spyOn(stripeNewVersion.invoices, 'create').mockResolvedValue({ id: 'in_early', status: 'draft' } as any); + jest.spyOn(stripeNewVersion.invoiceItems, 'create').mockResolvedValue({ id: 'ii_early' } as any); + jest.spyOn(stripeNewVersion.invoices, 'finalizeInvoice').mockResolvedValue({ + id: 'in_early', + status: 'open', + confirmation_secret: { client_secret: 'pi_early_secret', type: 'payment_intent' }, + } as any); + + const result = await paymentService.createEarlyCancellationCharge(subscription); + + expect(result).toStrictEqual({ clientSecret: 'pi_early_secret' }); + }); }); });