diff --git a/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts b/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts index 05186a29e2c..38870832d01 100644 --- a/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts +++ b/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts @@ -3,11 +3,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { faker } from '@faker-js/faker'; -import { SubPlatPaymentMethodType, type StripePaymentMethod } from '../types'; +import { + PaymentProvider, + SubPlatPaymentMethodType, + type StripePaymentMethod, +} from '../types'; export const StripePaymentMethodTypeResponseFactory = ( override?: Partial ): StripePaymentMethod => ({ + provider: PaymentProvider.Stripe, + rawType: faker.helpers.arrayElement([ + 'card', + 'apple_pay', + 'google_pay', + 'link', + ]), type: faker.helpers.arrayElement([ SubPlatPaymentMethodType.Card, SubPlatPaymentMethodType.ApplePay, diff --git a/libs/payments/customer/src/lib/paymentMethod.manager.spec.ts b/libs/payments/customer/src/lib/paymentMethod.manager.spec.ts index ac5af7491d0..f93fb3a88c7 100644 --- a/libs/payments/customer/src/lib/paymentMethod.manager.spec.ts +++ b/libs/payments/customer/src/lib/paymentMethod.manager.spec.ts @@ -11,7 +11,10 @@ import { PayPalClient, PaypalCustomerManager, } from '@fxa/payments/paypal'; -import { SubPlatPaymentMethodType } from '@fxa/payments/customer'; +import { + PaymentProvider, + SubPlatPaymentMethodType, +} from '@fxa/payments/customer'; import { StripeClient, MockStripeConfigProvider, @@ -99,6 +102,8 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -128,6 +133,8 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.PayPal, + rawType: 'external_paypal', type: SubPlatPaymentMethodType.PayPal, }); jest @@ -155,6 +162,8 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, + rawType: 'apple_pay', type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: 'pm_id', }); @@ -215,6 +224,8 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -289,6 +300,8 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, + rawType: 'apple_pay', type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: 'pm_id', }); @@ -331,6 +344,8 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.PayPal, + rawType: 'external_paypal', type: SubPlatPaymentMethodType.PayPal, }); jest @@ -385,6 +400,8 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ + provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: expect.any(String), }); @@ -398,6 +415,8 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(undefined, [mockSubscription]) ).resolves.toEqual({ + provider: PaymentProvider.PayPal, + rawType: 'external_paypal', type: SubPlatPaymentMethodType.PayPal, }); }); @@ -424,6 +443,8 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ + provider: PaymentProvider.Stripe, + rawType: 'link', type: SubPlatPaymentMethodType.Link, paymentMethodId: expect.any(String), }); @@ -451,6 +472,8 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ + provider: PaymentProvider.Stripe, + rawType: 'apple_pay', type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: expect.any(String), }); @@ -478,6 +501,8 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ + provider: PaymentProvider.Stripe, + rawType: 'google_pay', type: SubPlatPaymentMethodType.GooglePay, paymentMethodId: expect.any(String), }); diff --git a/libs/payments/customer/src/lib/paymentMethod.manager.ts b/libs/payments/customer/src/lib/paymentMethod.manager.ts index f323f685b4d..029081e28f3 100644 --- a/libs/payments/customer/src/lib/paymentMethod.manager.ts +++ b/libs/payments/customer/src/lib/paymentMethod.manager.ts @@ -14,6 +14,7 @@ import { DefaultPaymentMethod, DefaultPaymentMethodError, PaymentMethodErrorType, + PaymentProvider, SubPlatPaymentMethodType, type PaymentMethodTypeResponse, } from './types'; @@ -124,7 +125,9 @@ export class PaymentMethodManager { subscriptions[0].collection_method === 'send_invoice' ) { return { + provider: PaymentProvider.PayPal, type: SubPlatPaymentMethodType.PayPal, + rawType: 'external_paypal', }; } @@ -134,26 +137,39 @@ export class PaymentMethodManager { ); if (paymentMethod.card?.wallet?.type === 'apple_pay') { return { + provider: PaymentProvider.Stripe, + rawType: 'apple_pay', type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: customer.invoice_settings.default_payment_method, }; } else if (paymentMethod.card?.wallet?.type === 'google_pay') { return { + provider: PaymentProvider.Stripe, + rawType: 'google_pay', type: SubPlatPaymentMethodType.GooglePay, paymentMethodId: customer.invoice_settings.default_payment_method, }; - } else if (paymentMethod.type === 'link' || paymentMethod.card?.wallet?.type === 'link') { + } else if ( + paymentMethod.type === 'link' || + paymentMethod.card?.wallet?.type === 'link' + ) { return { + provider: PaymentProvider.Stripe, + rawType: 'link', type: SubPlatPaymentMethodType.Link, paymentMethodId: customer.invoice_settings.default_payment_method, }; } else if (paymentMethod.type === 'card') { return { + provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: customer.invoice_settings.default_payment_method, }; } else { return { + provider: PaymentProvider.Stripe, + rawType: paymentMethod.type, type: SubPlatPaymentMethodType.Stripe, paymentMethodId: customer.invoice_settings.default_payment_method, }; diff --git a/libs/payments/customer/src/lib/types.ts b/libs/payments/customer/src/lib/types.ts index 9f9bfc0aef6..c214b2358f6 100644 --- a/libs/payments/customer/src/lib/types.ts +++ b/libs/payments/customer/src/lib/types.ts @@ -30,6 +30,13 @@ export type InvoicePreview = { subsequentTax?: TaxAmount[]; }; +export enum PaymentProvider { + AppleIap = 'apple_iap', + GoogleIap = 'google_iap', + PayPal = 'paypal', + Stripe = 'stripe', +} + export enum SubPlatPaymentMethodType { PayPal = 'external_paypal', Stripe = 'stripe', @@ -39,18 +46,36 @@ export enum SubPlatPaymentMethodType { Link = 'link', } +export const SubPlatPaymentMethodTypePartial = [ + 'external_paypal', + 'card', + 'apple_pay', + 'google_pay', + 'link', +] as const; + +export type SubPlatPaymentMethod = + | Stripe.PaymentMethod.Type + | 'apple_pay' + | 'external_paypal' + | 'google_pay'; + export interface StripePaymentMethod { + provider: PaymentProvider.Stripe; type: | SubPlatPaymentMethodType.Card | SubPlatPaymentMethodType.ApplePay | SubPlatPaymentMethodType.GooglePay | SubPlatPaymentMethodType.Link | SubPlatPaymentMethodType.Stripe; + rawType: SubPlatPaymentMethod; paymentMethodId: string; } export interface PayPalPaymentMethod { + provider: PaymentProvider.PayPal; type: SubPlatPaymentMethodType.PayPal; + rawType: SubPlatPaymentMethod; } export type PaymentMethodTypeResponse = diff --git a/libs/payments/events/src/lib/emitter.service.spec.ts b/libs/payments/events/src/lib/emitter.service.spec.ts index b590a50c7f5..940fdec6d1f 100644 --- a/libs/payments/events/src/lib/emitter.service.spec.ts +++ b/libs/payments/events/src/lib/emitter.service.spec.ts @@ -32,6 +32,7 @@ import { PaymentMethodManager, SubPlatPaymentMethodType, StripePaymentMethodTypeResponseFactory, + PaymentProvider, } from '@fxa/payments/customer'; import { MockFirestoreProvider } from '@fxa/shared/db/firestore'; import { @@ -39,7 +40,6 @@ import { CommonMetricsFactory, MockPaymentsGleanConfigProvider, MockPaymentsGleanFactory, - PaymentProvidersType, PaymentsGleanManager, } from '@fxa/payments/metrics'; import { CartManager } from '@fxa/payments/cart'; @@ -105,7 +105,8 @@ describe('PaymentsEmitterService', () => { }); const mockCheckoutPaymentEvents = { ...mockCommonMetricsData, - paymentProvider: 'stripe' as PaymentProvidersType, + paymentMethod: SubPlatPaymentMethodType.Link, + paymentProvider: PaymentProvider.Stripe, }; let retrieveOptOutMock: jest.SpyInstance; const mockLogger = { @@ -511,7 +512,8 @@ describe('PaymentsEmitterService', () => { priceId: additionalMetricsData.cmsMetricsData.priceId, priceInterval: mockInterval, priceIntervalCount: 1, - paymentProvider: SubPlatPaymentMethodType.Card, + paymentProvider: PaymentProvider.Stripe, + paymentMethod: SubPlatPaymentMethodType.Card, }); const mockPrice = StripeResponseFactory( diff --git a/libs/payments/events/src/lib/emitter.types.ts b/libs/payments/events/src/lib/emitter.types.ts index fc486f4c5e1..ce4d2b5c943 100644 --- a/libs/payments/events/src/lib/emitter.types.ts +++ b/libs/payments/events/src/lib/emitter.types.ts @@ -1,20 +1,21 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { PaymentProvider, SubPlatPaymentMethod } from '@fxa/payments/customer'; import { CancellationReason, CartMetrics, CmsMetricsData, CommonMetrics, - PaymentProvidersType, } from '@fxa/payments/metrics'; import { LocationStatus } from '@fxa/payments/eligibility'; import { TaxChangeAllowedStatus } from '@fxa/payments/cart'; -import { SubPlatPaymentMethodType } from '@fxa/payments/customer'; export type CheckoutEvents = CommonMetrics; export type CheckoutPaymentEvents = CommonMetrics & { - paymentProvider?: PaymentProvidersType; + paymentMethod?: SubPlatPaymentMethod; + paymentProvider?: PaymentProvider; }; export type SubscriptionEndedEvents = { @@ -22,7 +23,8 @@ export type SubscriptionEndedEvents = { priceId: string; priceInterval?: string; priceIntervalCount?: number; - paymentProvider?: SubPlatPaymentMethodType; + paymentMethod?: SubPlatPaymentMethod; + paymentProvider?: PaymentProvider; providerEventId: string; cancellationReason: CancellationReason; uid?: string; diff --git a/libs/payments/metrics/src/lib/glean/glean.types.ts b/libs/payments/metrics/src/lib/glean/glean.types.ts index 24b0668ea15..4ec65b4f707 100644 --- a/libs/payments/metrics/src/lib/glean/glean.types.ts +++ b/libs/payments/metrics/src/lib/glean/glean.types.ts @@ -11,21 +11,26 @@ export const CheckoutTypes = [ 'unknown', ] as const; export type CheckoutTypesType = (typeof CheckoutTypes)[number]; -import { SubPlatPaymentMethodType } from '@fxa/payments/customer'; -export const PaymentProvidersTypePartial = [ - 'card', - 'google_iap', - 'apple_iap', +export type PaymentProvidersType = + | 'stripe' + | 'google_iap' + | 'apple_iap' + | 'paypal'; + +export const SubPlatPaymentMethodTypePartial = [ 'external_paypal', + 'card', + 'apple_pay', + 'google_pay', 'link', ] as const; -export type PaymentProvidersType = + +export type SubPlatPaymentMethod = | Stripe.PaymentMethod.Type - | SubPlatPaymentMethodType - | 'google_iap' - | 'apple_iap' - | 'external_paypal'; + | 'apple_pay' + | 'external_paypal' + | 'google_pay'; export type CommonMetrics = { ipAddress: string; diff --git a/libs/payments/ui/src/lib/actions/recordEmitterEvent.ts b/libs/payments/ui/src/lib/actions/recordEmitterEvent.ts index 9c9be26ccd7..c58b0160557 100644 --- a/libs/payments/ui/src/lib/actions/recordEmitterEvent.ts +++ b/libs/payments/ui/src/lib/actions/recordEmitterEvent.ts @@ -6,14 +6,36 @@ import { getApp } from '../nestapp/app'; import { flattenRouteParams } from '../utils/flatParam'; import { getAdditionalRequestArgs } from '../utils/getAdditionalRequestArgs'; -import { PaymentProvidersType } from '@fxa/payments/cart'; +import { + SubPlatPaymentMethod, + SubPlatPaymentMethodTypePartial, +} from '@fxa/payments/customer'; import { PaymentsEmitterEventsKeysType } from '@fxa/payments/events'; +function isSubPlatPaymentMethod( + paymentMethod: unknown +): paymentMethod is SubPlatPaymentMethod { + return ( + typeof paymentMethod === 'string' && + (SubPlatPaymentMethodTypePartial as readonly string[]).includes( + paymentMethod + ) + ); +} + +function validatePaymentMethod( + paymentMethod?: string +): SubPlatPaymentMethod | undefined { + return paymentMethod && isSubPlatPaymentMethod(paymentMethod) + ? paymentMethod + : undefined; +} + async function recordEmitterEventAction( eventName: 'checkoutSubmit', params: Record, searchParams: Record, - paymentProvider: PaymentProvidersType + paymentMethod: string ): Promise; async function recordEmitterEventAction( eventName: @@ -28,19 +50,22 @@ async function recordEmitterEventAction( eventName: PaymentsEmitterEventsKeysType, params: Record, searchParams: Record, - paymentProvider?: PaymentProvidersType + paymentMethod?: string ) { const requestArgs = { ...getAdditionalRequestArgs(), params: flattenRouteParams(params), searchParams: flattenRouteParams(searchParams), + rawPaymentMethod: paymentMethod, }; - return getApp().getActionsService().recordEmitterEvent({ - eventName, - requestArgs, - paymentProvider: paymentProvider, - }); + return getApp() + .getActionsService() + .recordEmitterEvent({ + eventName, + requestArgs, + paymentMethod: validatePaymentMethod(paymentMethod), + }); } export { recordEmitterEventAction }; diff --git a/libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx b/libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx index ce5c902b339..c8b578be5bc 100644 --- a/libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx +++ b/libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx @@ -38,7 +38,6 @@ import { checkoutCartWithPaypal, } from '@fxa/payments/ui/actions'; import { CartErrorReasonId } from '@fxa/shared/db/mysql/account/kysely-types'; -import { PaymentProvidersType } from '@fxa/payments/cart'; import PaypalIcon from '@fxa/shared/assets/images/payment-methods/paypal.svg'; import spinnerWhiteImage from '@fxa/shared/assets/images/spinnerwhite.svg'; import spinnerImage from '@fxa/shared/assets/images/spinner.svg'; @@ -77,10 +76,10 @@ interface CheckoutFormProps { }; paymentInfo?: { type: - | Stripe.PaymentMethod.Type - | 'google_iap' - | 'apple_iap' - | 'external_paypal'; + | Stripe.PaymentMethod.Type + | 'google_iap' + | 'apple_iap' + | 'external_paypal'; last4?: string; brand?: string; customerSessionClientSecret?: string; @@ -223,12 +222,12 @@ export function CheckoutForm({ const confirmationTokenParams: ConfirmationTokenCreateParams | undefined = !isSavedPaymentMethod ? { - payment_method_data: { - billing_details: { - email: sessionEmail || undefined, + payment_method_data: { + billing_details: { + email: sessionEmail || undefined, + }, }, - }, - } + } : undefined; // Create the ConfirmationToken using the details collected by the Payment Element @@ -258,7 +257,7 @@ export function CheckoutForm({ 'checkoutSubmit', { ...params }, Object.fromEntries(searchParams), - selectedPaymentMethod as PaymentProvidersType + selectedPaymentMethod ); await checkoutCartWithStripe( @@ -362,9 +361,7 @@ export function CheckoutForm({ type="submit" variant={ButtonVariant.Primary} aria-disabled={ - !formEnabled || - (isStripe && !stripeFieldsComplete) || - loading + !formEnabled || (isStripe && !stripeFieldsComplete) || loading } > {loading ? ( @@ -392,7 +389,7 @@ export function CheckoutForm({ )} - + ); } @@ -425,16 +422,19 @@ function CheckoutPayPalButton({ alt="" className="absolute animate-spin h-8 w-8" /> - ) + ); } if (isRejected) { Sentry.captureMessage('PayPal script failed to load'); return ( -
PayPal is currently unavailable. Please use another payment option or try again later.
+
+ PayPal is currently unavailable. Please use another payment option or + try again later. +
- ) + ); } return ( @@ -468,10 +468,7 @@ function CheckoutPayPalButton({ router.push('./processing' + queryParamString); }} onError={async () => { - await finalizeCartWithError( - cartId, - CartErrorReasonId.BASIC_ERROR - ); + await finalizeCartWithError(cartId, CartErrorReasonId.BASIC_ERROR); const queryParamString = searchParams.toString() ? `?${searchParams.toString()}` : ''; @@ -480,6 +477,5 @@ function CheckoutPayPalButton({ }} disabled={disabled} /> - ) + ); } - diff --git a/libs/payments/ui/src/lib/nestapp/nextjs-actions.service.ts b/libs/payments/ui/src/lib/nestapp/nextjs-actions.service.ts index d5436cd4c81..222afa5701c 100644 --- a/libs/payments/ui/src/lib/nestapp/nextjs-actions.service.ts +++ b/libs/payments/ui/src/lib/nestapp/nextjs-actions.service.ts @@ -62,16 +62,16 @@ import { DetermineCurrencyActionArgs } from './validators/DetermineCurrencyActio import { DetermineStaySubscribedEligibilityActionArgs } from './validators/DetermineStaySubscribedEligibilityActionArgs'; import { DetermineCancellationInterventionActionArgs } from './validators/DetermineCancellationInterventionActionArgs'; import { NextIOValidator } from './NextIOValidator'; -import type { - CommonMetrics, - PaymentProvidersType, -} from '@fxa/payments/metrics'; +import { type CommonMetrics } from '@fxa/payments/metrics'; import { GetCartActionResult } from './validators/GetCartActionResult'; import { GetChurnInterventionDataActionResult } from './validators/GetChurnInterventionDataActionResult'; import { GetSuccessCartActionResult } from './validators/GetSuccessCartActionResult'; import { CouponErrorCannotRedeem, + PaymentProvider, PromotionCodeSanitizedError, + SubPlatPaymentMethod, + SubPlatPaymentMethodType, TaxAddress, type SubplatInterval, } from '@fxa/payments/customer'; @@ -586,9 +586,9 @@ export class NextJSActionsService { async recordEmitterEvent(args: { eventName: string; requestArgs: CommonMetrics; - paymentProvider: PaymentProvidersType | undefined; + paymentMethod?: SubPlatPaymentMethod; }) { - const { eventName, requestArgs, paymentProvider } = args; + const { eventName, requestArgs, paymentMethod } = args; switch (eventName) { case 'checkoutView': @@ -601,9 +601,17 @@ export class NextJSActionsService { case 'checkoutSubmit': case 'checkoutSuccess': case 'checkoutFail': { + let paymentProvider; + if (paymentMethod) { + paymentProvider = + paymentMethod === SubPlatPaymentMethodType.PayPal + ? PaymentProvider.PayPal + : PaymentProvider.Stripe; + } this.emitterService.getEmitter().emit(eventName, { ...requestArgs, - paymentProvider: paymentProvider, + paymentMethod, + paymentProvider, }); break; } diff --git a/libs/payments/ui/src/lib/nestapp/validators/RecordEmitterEvent.ts b/libs/payments/ui/src/lib/nestapp/validators/RecordEmitterEvent.ts index ecc91ba1c1b..e79d6ba9faf 100644 --- a/libs/payments/ui/src/lib/nestapp/validators/RecordEmitterEvent.ts +++ b/libs/payments/ui/src/lib/nestapp/validators/RecordEmitterEvent.ts @@ -5,6 +5,7 @@ import { Type } from 'class-transformer'; import { IsEnum, + IsIn, IsObject, IsOptional, IsString, @@ -13,8 +14,8 @@ import { import { PaymentsEmitterEventsKeys } from '@fxa/payments/events'; import type { PaymentsEmitterEventsKeysType } from '@fxa/payments/events'; import { - PaymentProvidersTypePartial, - type PaymentProvidersType, + type SubPlatPaymentMethod, + SubPlatPaymentMethodTypePartial, } from '@fxa/payments/metrics'; /** @@ -49,6 +50,6 @@ export class RecordEmitterEventArgs { requestArgs!: RequestArgs; @IsOptional() - @IsEnum(PaymentProvidersTypePartial) - paymentProvider?: PaymentProvidersType; + @IsIn([...SubPlatPaymentMethodTypePartial]) + paymentMethod?: SubPlatPaymentMethod; } diff --git a/libs/payments/ui/src/lib/utils/getNextChargeChurnContent.spec.ts b/libs/payments/ui/src/lib/utils/getNextChargeChurnContent.spec.ts index c2973588ca4..32742c05c17 100644 --- a/libs/payments/ui/src/lib/utils/getNextChargeChurnContent.spec.ts +++ b/libs/payments/ui/src/lib/utils/getNextChargeChurnContent.spec.ts @@ -39,7 +39,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.Card, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), last4: faker.string.numeric({ length: 4 }), nextInvoiceTax: faker.number.int({ min: 1, max: 1000 }), }; @@ -59,7 +59,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.PayPal, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), nextInvoiceTax: faker.number.int({ min: 1, max: 1000 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); @@ -80,7 +80,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.Link, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), nextInvoiceTax: faker.number.int({ min: 1, max: 1000 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); @@ -101,7 +101,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.ApplePay, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), nextInvoiceTax: faker.number.int({ min: 1, max: 1000 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); @@ -122,7 +122,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.GooglePay, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), nextInvoiceTax: faker.number.int({ min: 1, max: 1000 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); @@ -142,7 +142,7 @@ describe('getNextChargeChurnContent', () => { it('returns default', () => { const mockSubscriptionPageContent = { ...mockContent, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), nextInvoiceTax: faker.number.int({ min: 1, max: 1000 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); @@ -168,7 +168,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.Card, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), last4: faker.string.numeric({ length: 4 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); @@ -187,7 +187,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.PayPal, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); expect(content.l10nId).toBe( @@ -207,7 +207,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.Link, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); expect(content.l10nId).toBe( @@ -227,7 +227,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.ApplePay, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); expect(content.l10nId).toBe( @@ -247,7 +247,7 @@ describe('getNextChargeChurnContent', () => { const mockSubscriptionPageContent = { ...mockContent, defaultPaymentMethodType: SubPlatPaymentMethodType.GooglePay, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); expect(content.l10nId).toBe( @@ -266,7 +266,7 @@ describe('getNextChargeChurnContent', () => { it('returns default', () => { const mockSubscriptionPageContent = { ...mockContent, - discountAmount: faker.number.int({ min: 0, max: 100 }), + discountAmount: faker.number.int({ min: 1, max: 100 }), }; const content = getNextChargeChurnContent(mockSubscriptionPageContent); expect(content.l10nId).toBe('next-charge-with-discount-no-tax'); diff --git a/libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts b/libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts index 3172aedc51f..7b980faaa1d 100644 --- a/libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts +++ b/libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts @@ -23,6 +23,7 @@ import { SubscriptionManager, PaymentMethodManager, SubPlatPaymentMethodType, + PaymentProvider, } from '@fxa/payments/customer'; import { PaymentsEmitterService } from '@fxa/payments/events'; import { @@ -83,9 +84,6 @@ describe('SubscriptionEventsService', () => { error: jest.fn(), log: jest.fn(), }; - const paymentMethodManagerMock = { - determineType: jest.fn(), - }; const { event: mockEvent, eventObjectData: mockEventObjectData } = CustomerSubscriptionDeletedResponseFactory(); @@ -97,10 +95,7 @@ describe('SubscriptionEventsService', () => { provide: Logger, useValue: mockLogger, }, - { - provide: PaymentMethodManager, - useValue: paymentMethodManagerMock, - }, + PaymentMethodManager, MockStripeConfigProvider, MockStripeEventConfigProvider, StripeClient, @@ -161,7 +156,9 @@ describe('SubscriptionEventsService', () => { jest .spyOn(emitterService, 'getEmitter') .mockReturnValue(mockEmitter as any); - (paymentMethodManager.determineType as jest.Mock).mockResolvedValue({ + jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -178,17 +175,18 @@ describe('SubscriptionEventsService', () => { expect(mockEmitter.emit).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - paymentProvider: 'card', + paymentProvider: PaymentProvider.Stripe, + paymentMethod: SubPlatPaymentMethodType.Card, }) ); }); it('should emit the subscriptionEnded event, with paymentProvider external_paypal', async () => { - (paymentMethodManager.determineType as jest.Mock).mockResolvedValueOnce( - { - type: SubPlatPaymentMethodType.PayPal, - } - ); + jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.PayPal, + rawType: 'external_paypal', + type: SubPlatPaymentMethodType.PayPal, + }); await subscriptionEventsService.handleCustomerSubscriptionDeleted( mockEvent, mockEventObjectData @@ -198,7 +196,8 @@ describe('SubscriptionEventsService', () => { expect(mockEmitter.emit).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - paymentProvider: 'external_paypal', + paymentMethod: SubPlatPaymentMethodType.PayPal, + paymentProvider: PaymentProvider.PayPal, }) ); }); @@ -284,7 +283,7 @@ describe('SubscriptionEventsService', () => { mockEvent, mockEventObjectData ) - ).rejects.toThrowError(); + ).rejects.toThrow(); }); }); }); diff --git a/libs/payments/webhooks/src/lib/subscription-handler.service.ts b/libs/payments/webhooks/src/lib/subscription-handler.service.ts index ee77fde37f5..941044d7b34 100644 --- a/libs/payments/webhooks/src/lib/subscription-handler.service.ts +++ b/libs/payments/webhooks/src/lib/subscription-handler.service.ts @@ -8,7 +8,9 @@ import { InvoiceManager, SubscriptionManager, PaymentMethodManager, + SubPlatPaymentMethod, SubPlatPaymentMethodType, + PaymentProvider, } from '@fxa/payments/customer'; import { PaymentsEmitterService } from '@fxa/payments/events'; import { Injectable } from '@nestjs/common'; @@ -37,7 +39,9 @@ export class SubscriptionEventsService { ); const price = subscription.items.data[0].price; - let paymentProvider: SubPlatPaymentMethodType | undefined; + let subplatPaymentMethod: SubPlatPaymentMethod | undefined; + let paymentProvider: PaymentProvider | undefined; + let subplatPaymentMethodType: SubPlatPaymentMethodType | undefined; let determinedCancellation: CancellationReason | undefined; let uid: string | undefined; try { @@ -45,24 +49,31 @@ export class SubscriptionEventsService { subscription.customer ); uid = customer.metadata['userid']; - const paymentMethodType = await this.paymentMethodManager.determineType( + const paymentMethod = await this.paymentMethodManager.determineType( customer, [subscription] ); - paymentProvider = paymentMethodType?.type; + paymentProvider = paymentMethod?.provider; + subplatPaymentMethodType = paymentMethod?.type; + subplatPaymentMethod = paymentMethod?.rawType; const latestInvoice = - paymentProvider === SubPlatPaymentMethodType.PayPal && + subplatPaymentMethodType === SubPlatPaymentMethodType.PayPal && subscription.latest_invoice ? await this.invoiceManager.retrieve(subscription.latest_invoice) : undefined; determinedCancellation = - paymentProvider && - determineCancellation(paymentProvider, subscription, latestInvoice); + subplatPaymentMethodType && + determineCancellation( + subplatPaymentMethodType, + subscription, + latestInvoice + ); } catch (err) { if (err instanceof CustomerDeletedError) { paymentProvider = undefined; + subplatPaymentMethodType = undefined; determinedCancellation = undefined; } else { throw err; @@ -78,7 +89,8 @@ export class SubscriptionEventsService { priceId: price.id, priceInterval: price.recurring?.interval, priceIntervalCount: price.recurring?.interval_count, - paymentProvider: paymentProvider, + paymentMethod: subplatPaymentMethod, + paymentProvider, providerEventId: event.id, cancellationReason, uid,