From 05c401707d2b49b0d4b5c01222d6058baf15678a Mon Sep 17 00:00:00 2001 From: Lisa Chan Date: Thu, 22 Jan 2026 18:03:16 -0500 Subject: [PATCH 1/2] fix(payments): Update payment providers vs payment methods Because Sentry events frequently stating the "Server Components render" error occur for the CheckoutForm component CheckoutForm is a client component, but it is importing the Node server SDK for Stripe causing Next to try to bundle Stripe Node SDK into the client build and surfacing the Server Components render error Validation for paymentProvider was expecting an enum This occurred as some of the options for payment methods (e.g. Apple Pay, Google Pay) during checkout were not considered valid payment provider types. We should distinguish payment providers and payment methods. This pull request Updates definition/usage of payment providers vs methods Issue that this pull request solves Closes: PAY-3439 --- .../lib/factories/paymentMethod.factory.ts | 7 ++- .../src/lib/paymentMethod.manager.spec.ts | 16 +++++- .../customer/src/lib/paymentMethod.manager.ts | 12 ++++- libs/payments/customer/src/lib/types.ts | 9 ++++ .../events/src/lib/emitter.service.spec.ts | 8 +-- libs/payments/events/src/lib/emitter.types.ts | 13 +++-- .../metrics/src/lib/glean/glean.types.ts | 22 ++++---- .../ui/src/lib/actions/recordEmitterEvent.ts | 8 +-- .../client/components/CheckoutForm/index.tsx | 53 ++++++++++--------- .../src/lib/nestapp/nextjs-actions.service.ts | 18 ++++--- .../nestapp/validators/RecordEmitterEvent.ts | 8 +-- .../utils/getNextChargeChurnContent.spec.ts | 24 ++++----- .../ui/src/lib/utils/getPaymentProvider.ts | 15 ++++++ .../lib/subscription-handler.service.spec.ts | 29 +++++----- .../src/lib/subscription-handler.service.ts | 23 +++++--- 15 files changed, 165 insertions(+), 100 deletions(-) create mode 100644 libs/payments/ui/src/lib/utils/getPaymentProvider.ts diff --git a/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts b/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts index 05186a29e2c..f50930d44cb 100644 --- a/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts +++ b/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts @@ -3,11 +3,16 @@ * 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, 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..71b1a635685 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,7 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -128,6 +132,7 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.PayPal, type: SubPlatPaymentMethodType.PayPal, }); jest @@ -155,6 +160,7 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: 'pm_id', }); @@ -215,6 +221,7 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -289,6 +296,7 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: 'pm_id', }); @@ -331,6 +339,7 @@ describe('PaymentMethodManager', () => { const mockUid = faker.string.uuid(); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.PayPal, type: SubPlatPaymentMethodType.PayPal, }); jest @@ -385,6 +394,7 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.Card, paymentMethodId: expect.any(String), }); @@ -398,6 +408,7 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(undefined, [mockSubscription]) ).resolves.toEqual({ + provider: PaymentProvider.PayPal, type: SubPlatPaymentMethodType.PayPal, }); }); @@ -424,6 +435,7 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.Link, paymentMethodId: expect.any(String), }); @@ -451,6 +463,7 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: expect.any(String), }); @@ -478,6 +491,7 @@ describe('PaymentMethodManager', () => { await expect( paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ + provider: PaymentProvider.Stripe, 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..ad130557fc7 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,6 +125,7 @@ export class PaymentMethodManager { subscriptions[0].collection_method === 'send_invoice' ) { return { + provider: PaymentProvider.PayPal, type: SubPlatPaymentMethodType.PayPal, }; } @@ -134,26 +136,34 @@ export class PaymentMethodManager { ); if (paymentMethod.card?.wallet?.type === 'apple_pay') { return { + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: customer.invoice_settings.default_payment_method, }; } else if (paymentMethod.card?.wallet?.type === 'google_pay') { return { + provider: PaymentProvider.Stripe, 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, type: SubPlatPaymentMethodType.Link, paymentMethodId: customer.invoice_settings.default_payment_method, }; } else if (paymentMethod.type === 'card') { return { + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.Card, paymentMethodId: customer.invoice_settings.default_payment_method, }; } else { return { + provider: PaymentProvider.Stripe, 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..7e493463f6e 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', @@ -40,6 +47,7 @@ export enum SubPlatPaymentMethodType { } export interface StripePaymentMethod { + provider: PaymentProvider.Stripe; type: | SubPlatPaymentMethodType.Card | SubPlatPaymentMethodType.ApplePay @@ -50,6 +58,7 @@ export interface StripePaymentMethod { } export interface PayPalPaymentMethod { + provider: PaymentProvider.PayPal; type: SubPlatPaymentMethodType.PayPal; } 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..1389adc77d6 100644 --- a/libs/payments/events/src/lib/emitter.types.ts +++ b/libs/payments/events/src/lib/emitter.types.ts @@ -1,20 +1,24 @@ /* 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, + SubPlatPaymentMethodType, +} 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?: SubPlatPaymentMethodType; + paymentProvider?: PaymentProvider; }; export type SubscriptionEndedEvents = { @@ -22,7 +26,8 @@ export type SubscriptionEndedEvents = { priceId: string; priceInterval?: string; priceIntervalCount?: number; - paymentProvider?: SubPlatPaymentMethodType; + paymentMethod?: SubPlatPaymentMethodType; + 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..71fb5945411 100644 --- a/libs/payments/metrics/src/lib/glean/glean.types.ts +++ b/libs/payments/metrics/src/lib/glean/glean.types.ts @@ -2,7 +2,6 @@ * 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 { ResultCart } from '@fxa/payments/cart'; -import Stripe from 'stripe'; export const CheckoutTypes = [ 'new_account', @@ -11,21 +10,20 @@ 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', - 'external_paypal', - 'link', -] as const; export type PaymentProvidersType = - | Stripe.PaymentMethod.Type - | SubPlatPaymentMethodType + | 'stripe' | 'google_iap' | 'apple_iap' - | 'external_paypal'; + | 'paypal'; + +export type SubPlatPaymentMethodType = + | 'stripe' + | 'external_paypal' + | 'card' + | 'apple_pay' + | 'google_pay' + | 'link'; 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..feb8622b471 100644 --- a/libs/payments/ui/src/lib/actions/recordEmitterEvent.ts +++ b/libs/payments/ui/src/lib/actions/recordEmitterEvent.ts @@ -6,14 +6,14 @@ import { getApp } from '../nestapp/app'; import { flattenRouteParams } from '../utils/flatParam'; import { getAdditionalRequestArgs } from '../utils/getAdditionalRequestArgs'; -import { PaymentProvidersType } from '@fxa/payments/cart'; +import { SubPlatPaymentMethodType } from '@fxa/payments/customer'; import { PaymentsEmitterEventsKeysType } from '@fxa/payments/events'; async function recordEmitterEventAction( eventName: 'checkoutSubmit', params: Record, searchParams: Record, - paymentProvider: PaymentProvidersType + paymentMethod: SubPlatPaymentMethodType ): Promise; async function recordEmitterEventAction( eventName: @@ -28,7 +28,7 @@ async function recordEmitterEventAction( eventName: PaymentsEmitterEventsKeysType, params: Record, searchParams: Record, - paymentProvider?: PaymentProvidersType + paymentMethod?: SubPlatPaymentMethodType ) { const requestArgs = { ...getAdditionalRequestArgs(), @@ -39,7 +39,7 @@ async function recordEmitterEventAction( return getApp().getActionsService().recordEmitterEvent({ eventName, requestArgs, - paymentProvider: paymentProvider, + paymentMethod, }); } 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..dcce62f531a 100644 --- a/libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx +++ b/libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx @@ -26,7 +26,12 @@ import { } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { BaseButton, ButtonVariant, CheckoutCheckbox } from '@fxa/payments/ui'; +import { + BaseButton, + ButtonVariant, + CheckoutCheckbox, + SubPlatPaymentMethodType, +} from '@fxa/payments/ui'; import LockImage from '@fxa/shared/assets/images/lock.svg'; import { useCallbackOnce } from '../../hooks/useCallbackOnce'; import { @@ -38,7 +43,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 +81,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; @@ -185,7 +189,7 @@ export function CheckoutForm({ 'checkoutSubmit', { ...params }, Object.fromEntries(searchParams), - 'external_paypal' + 'external_paypal' as SubPlatPaymentMethodType ); await checkoutCartWithPaypal( @@ -223,12 +227,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 +262,7 @@ export function CheckoutForm({ 'checkoutSubmit', { ...params }, Object.fromEntries(searchParams), - selectedPaymentMethod as PaymentProvidersType + selectedPaymentMethod as SubPlatPaymentMethodType ); await checkoutCartWithStripe( @@ -362,9 +366,7 @@ export function CheckoutForm({ type="submit" variant={ButtonVariant.Primary} aria-disabled={ - !formEnabled || - (isStripe && !stripeFieldsComplete) || - loading + !formEnabled || (isStripe && !stripeFieldsComplete) || loading } > {loading ? ( @@ -392,7 +394,7 @@ export function CheckoutForm({ )} - + ); } @@ -425,16 +427,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 +473,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 +482,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..09472aeb294 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,14 @@ 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, PromotionCodeSanitizedError, + SubPlatPaymentMethodType, TaxAddress, type SubplatInterval, } from '@fxa/payments/customer'; @@ -133,6 +131,7 @@ import { GetExperimentsActionArgs } from './validators/GetExperimentsActionArgs' import { GetExperimentsActionResult } from './validators/GetExperimentsActionResult'; import { GetCMSChurnInterventionActionResult } from './validators/GetCMSChurnInterventionActionResult'; import { GetCMSChurnInterventionActionArgs } from './validators/GetCMSChurnInterventionActionArgs'; +import { getPaymentProvider } from '../utils/getPaymentProvider'; /** * ANY AND ALL methods exposed via this service should be considered publicly accessible and callable with any arguments. @@ -586,9 +585,9 @@ export class NextJSActionsService { async recordEmitterEvent(args: { eventName: string; requestArgs: CommonMetrics; - paymentProvider: PaymentProvidersType | undefined; + paymentMethod?: SubPlatPaymentMethodType; }) { - const { eventName, requestArgs, paymentProvider } = args; + const { eventName, requestArgs, paymentMethod } = args; switch (eventName) { case 'checkoutView': @@ -601,9 +600,14 @@ export class NextJSActionsService { case 'checkoutSubmit': case 'checkoutSuccess': case 'checkoutFail': { + let paymentProvider; + if (paymentMethod) { + paymentProvider = getPaymentProvider(paymentMethod); + } this.emitterService.getEmitter().emit(eventName, { ...requestArgs, - paymentProvider: paymentProvider, + paymentMethod: 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..dc4bce37408 100644 --- a/libs/payments/ui/src/lib/nestapp/validators/RecordEmitterEvent.ts +++ b/libs/payments/ui/src/lib/nestapp/validators/RecordEmitterEvent.ts @@ -12,10 +12,7 @@ import { } from 'class-validator'; import { PaymentsEmitterEventsKeys } from '@fxa/payments/events'; import type { PaymentsEmitterEventsKeysType } from '@fxa/payments/events'; -import { - PaymentProvidersTypePartial, - type PaymentProvidersType, -} from '@fxa/payments/metrics'; +import type { SubPlatPaymentMethodType } from '@fxa/payments/metrics'; /** * Common metrics that can be found on all events @@ -49,6 +46,5 @@ export class RecordEmitterEventArgs { requestArgs!: RequestArgs; @IsOptional() - @IsEnum(PaymentProvidersTypePartial) - paymentProvider?: PaymentProvidersType; + paymentMethod?: SubPlatPaymentMethodType; } 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/ui/src/lib/utils/getPaymentProvider.ts b/libs/payments/ui/src/lib/utils/getPaymentProvider.ts new file mode 100644 index 00000000000..47a37bcc0c1 --- /dev/null +++ b/libs/payments/ui/src/lib/utils/getPaymentProvider.ts @@ -0,0 +1,15 @@ +/* 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, + SubPlatPaymentMethodType, +} from '@fxa/payments/customer'; + +export function getPaymentProvider(paymentMethod: SubPlatPaymentMethodType) { + if (paymentMethod === SubPlatPaymentMethodType.PayPal) + return PaymentProvider.PayPal; + + return PaymentProvider.Stripe; +} 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..c601509b0b8 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,8 @@ describe('SubscriptionEventsService', () => { jest .spyOn(emitterService, 'getEmitter') .mockReturnValue(mockEmitter as any); - (paymentMethodManager.determineType as jest.Mock).mockResolvedValue({ + jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ + provider: PaymentProvider.Stripe, type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -178,17 +174,17 @@ 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, + type: SubPlatPaymentMethodType.PayPal, + }); await subscriptionEventsService.handleCustomerSubscriptionDeleted( mockEvent, mockEventObjectData @@ -198,7 +194,8 @@ describe('SubscriptionEventsService', () => { expect(mockEmitter.emit).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - paymentProvider: 'external_paypal', + paymentMethod: SubPlatPaymentMethodType.PayPal, + paymentProvider: PaymentProvider.PayPal, }) ); }); @@ -284,7 +281,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..4e81be01b55 100644 --- a/libs/payments/webhooks/src/lib/subscription-handler.service.ts +++ b/libs/payments/webhooks/src/lib/subscription-handler.service.ts @@ -9,6 +9,7 @@ import { SubscriptionManager, PaymentMethodManager, SubPlatPaymentMethodType, + PaymentProvider, } from '@fxa/payments/customer'; import { PaymentsEmitterService } from '@fxa/payments/events'; import { Injectable } from '@nestjs/common'; @@ -37,7 +38,8 @@ export class SubscriptionEventsService { ); const price = subscription.items.data[0].price; - let paymentProvider: SubPlatPaymentMethodType | undefined; + let paymentProvider: PaymentProvider | undefined; + let subplatPaymentMethod: SubPlatPaymentMethodType | undefined; let determinedCancellation: CancellationReason | undefined; let uid: string | undefined; try { @@ -45,24 +47,30 @@ 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; + subplatPaymentMethod = paymentMethod?.type; const latestInvoice = - paymentProvider === SubPlatPaymentMethodType.PayPal && + subplatPaymentMethod === SubPlatPaymentMethodType.PayPal && subscription.latest_invoice ? await this.invoiceManager.retrieve(subscription.latest_invoice) : undefined; determinedCancellation = - paymentProvider && - determineCancellation(paymentProvider, subscription, latestInvoice); + subplatPaymentMethod && + determineCancellation( + subplatPaymentMethod, + subscription, + latestInvoice + ); } catch (err) { if (err instanceof CustomerDeletedError) { paymentProvider = undefined; + subplatPaymentMethod = undefined; determinedCancellation = undefined; } else { throw err; @@ -78,7 +86,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, From ce738d2670b5651da9968474d8e2ca2f029a1b85 Mon Sep 17 00:00:00 2001 From: Lisa Chan Date: Sat, 24 Jan 2026 11:05:05 -0500 Subject: [PATCH 2/2] update --- .../lib/factories/paymentMethod.factory.ts | 6 +++ .../src/lib/paymentMethod.manager.spec.ts | 11 +++++ .../customer/src/lib/paymentMethod.manager.ts | 6 +++ libs/payments/customer/src/lib/types.ts | 16 ++++++++ libs/payments/events/src/lib/emitter.types.ts | 9 ++-- .../metrics/src/lib/glean/glean.types.ts | 19 ++++++--- .../ui/src/lib/actions/recordEmitterEvent.ts | 41 +++++++++++++++---- .../client/components/CheckoutForm/index.tsx | 11 ++--- .../src/lib/nestapp/nextjs-actions.service.ts | 14 ++++--- .../nestapp/validators/RecordEmitterEvent.ts | 9 +++- .../ui/src/lib/utils/getPaymentProvider.ts | 15 ------- .../lib/subscription-handler.service.spec.ts | 2 + .../src/lib/subscription-handler.service.ts | 15 ++++--- 13 files changed, 118 insertions(+), 56 deletions(-) delete mode 100644 libs/payments/ui/src/lib/utils/getPaymentProvider.ts diff --git a/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts b/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts index f50930d44cb..38870832d01 100644 --- a/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts +++ b/libs/payments/customer/src/lib/factories/paymentMethod.factory.ts @@ -13,6 +13,12 @@ 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 71b1a635685..f93fb3a88c7 100644 --- a/libs/payments/customer/src/lib/paymentMethod.manager.spec.ts +++ b/libs/payments/customer/src/lib/paymentMethod.manager.spec.ts @@ -103,6 +103,7 @@ describe('PaymentMethodManager', () => { jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -133,6 +134,7 @@ describe('PaymentMethodManager', () => { jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ provider: PaymentProvider.PayPal, + rawType: 'external_paypal', type: SubPlatPaymentMethodType.PayPal, }); jest @@ -161,6 +163,7 @@ describe('PaymentMethodManager', () => { jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ provider: PaymentProvider.Stripe, + rawType: 'apple_pay', type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: 'pm_id', }); @@ -222,6 +225,7 @@ describe('PaymentMethodManager', () => { jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -297,6 +301,7 @@ describe('PaymentMethodManager', () => { jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ provider: PaymentProvider.Stripe, + rawType: 'apple_pay', type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: 'pm_id', }); @@ -340,6 +345,7 @@ describe('PaymentMethodManager', () => { jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ provider: PaymentProvider.PayPal, + rawType: 'external_paypal', type: SubPlatPaymentMethodType.PayPal, }); jest @@ -395,6 +401,7 @@ describe('PaymentMethodManager', () => { paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: expect.any(String), }); @@ -409,6 +416,7 @@ describe('PaymentMethodManager', () => { paymentMethodManager.determineType(undefined, [mockSubscription]) ).resolves.toEqual({ provider: PaymentProvider.PayPal, + rawType: 'external_paypal', type: SubPlatPaymentMethodType.PayPal, }); }); @@ -436,6 +444,7 @@ describe('PaymentMethodManager', () => { paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ provider: PaymentProvider.Stripe, + rawType: 'link', type: SubPlatPaymentMethodType.Link, paymentMethodId: expect.any(String), }); @@ -464,6 +473,7 @@ describe('PaymentMethodManager', () => { paymentMethodManager.determineType(mockCustomer) ).resolves.toEqual({ provider: PaymentProvider.Stripe, + rawType: 'apple_pay', type: SubPlatPaymentMethodType.ApplePay, paymentMethodId: expect.any(String), }); @@ -492,6 +502,7 @@ describe('PaymentMethodManager', () => { 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 ad130557fc7..029081e28f3 100644 --- a/libs/payments/customer/src/lib/paymentMethod.manager.ts +++ b/libs/payments/customer/src/lib/paymentMethod.manager.ts @@ -127,6 +127,7 @@ export class PaymentMethodManager { return { provider: PaymentProvider.PayPal, type: SubPlatPaymentMethodType.PayPal, + rawType: 'external_paypal', }; } @@ -137,12 +138,14 @@ 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, }; @@ -152,18 +155,21 @@ export class PaymentMethodManager { ) { 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 7e493463f6e..c214b2358f6 100644 --- a/libs/payments/customer/src/lib/types.ts +++ b/libs/payments/customer/src/lib/types.ts @@ -46,6 +46,20 @@ 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: @@ -54,12 +68,14 @@ export interface StripePaymentMethod { | 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.types.ts b/libs/payments/events/src/lib/emitter.types.ts index 1389adc77d6..ce4d2b5c943 100644 --- a/libs/payments/events/src/lib/emitter.types.ts +++ b/libs/payments/events/src/lib/emitter.types.ts @@ -2,10 +2,7 @@ * 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, - SubPlatPaymentMethodType, -} from '@fxa/payments/customer'; +import { PaymentProvider, SubPlatPaymentMethod } from '@fxa/payments/customer'; import { CancellationReason, CartMetrics, @@ -17,7 +14,7 @@ import { TaxChangeAllowedStatus } from '@fxa/payments/cart'; export type CheckoutEvents = CommonMetrics; export type CheckoutPaymentEvents = CommonMetrics & { - paymentMethod?: SubPlatPaymentMethodType; + paymentMethod?: SubPlatPaymentMethod; paymentProvider?: PaymentProvider; }; @@ -26,7 +23,7 @@ export type SubscriptionEndedEvents = { priceId: string; priceInterval?: string; priceIntervalCount?: number; - paymentMethod?: SubPlatPaymentMethodType; + paymentMethod?: SubPlatPaymentMethod; paymentProvider?: PaymentProvider; providerEventId: string; cancellationReason: CancellationReason; diff --git a/libs/payments/metrics/src/lib/glean/glean.types.ts b/libs/payments/metrics/src/lib/glean/glean.types.ts index 71fb5945411..4ec65b4f707 100644 --- a/libs/payments/metrics/src/lib/glean/glean.types.ts +++ b/libs/payments/metrics/src/lib/glean/glean.types.ts @@ -2,6 +2,7 @@ * 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 { ResultCart } from '@fxa/payments/cart'; +import Stripe from 'stripe'; export const CheckoutTypes = [ 'new_account', @@ -17,13 +18,19 @@ export type PaymentProvidersType = | 'apple_iap' | 'paypal'; -export type SubPlatPaymentMethodType = - | 'stripe' - | 'external_paypal' - | 'card' +export const SubPlatPaymentMethodTypePartial = [ + 'external_paypal', + 'card', + 'apple_pay', + 'google_pay', + 'link', +] as const; + +export type SubPlatPaymentMethod = + | Stripe.PaymentMethod.Type | 'apple_pay' - | 'google_pay' - | 'link'; + | '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 feb8622b471..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 { SubPlatPaymentMethodType } from '@fxa/payments/customer'; +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, - paymentMethod: SubPlatPaymentMethodType + paymentMethod: string ): Promise; async function recordEmitterEventAction( eventName: @@ -28,19 +50,22 @@ async function recordEmitterEventAction( eventName: PaymentsEmitterEventsKeysType, params: Record, searchParams: Record, - paymentMethod?: SubPlatPaymentMethodType + paymentMethod?: string ) { const requestArgs = { ...getAdditionalRequestArgs(), params: flattenRouteParams(params), searchParams: flattenRouteParams(searchParams), + rawPaymentMethod: paymentMethod, }; - return getApp().getActionsService().recordEmitterEvent({ - eventName, - requestArgs, - paymentMethod, - }); + 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 dcce62f531a..c8b578be5bc 100644 --- a/libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx +++ b/libs/payments/ui/src/lib/client/components/CheckoutForm/index.tsx @@ -26,12 +26,7 @@ import { } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { - BaseButton, - ButtonVariant, - CheckoutCheckbox, - SubPlatPaymentMethodType, -} from '@fxa/payments/ui'; +import { BaseButton, ButtonVariant, CheckoutCheckbox } from '@fxa/payments/ui'; import LockImage from '@fxa/shared/assets/images/lock.svg'; import { useCallbackOnce } from '../../hooks/useCallbackOnce'; import { @@ -189,7 +184,7 @@ export function CheckoutForm({ 'checkoutSubmit', { ...params }, Object.fromEntries(searchParams), - 'external_paypal' as SubPlatPaymentMethodType + 'external_paypal' ); await checkoutCartWithPaypal( @@ -262,7 +257,7 @@ export function CheckoutForm({ 'checkoutSubmit', { ...params }, Object.fromEntries(searchParams), - selectedPaymentMethod as SubPlatPaymentMethodType + selectedPaymentMethod ); await checkoutCartWithStripe( 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 09472aeb294..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,13 +62,15 @@ import { DetermineCurrencyActionArgs } from './validators/DetermineCurrencyActio import { DetermineStaySubscribedEligibilityActionArgs } from './validators/DetermineStaySubscribedEligibilityActionArgs'; import { DetermineCancellationInterventionActionArgs } from './validators/DetermineCancellationInterventionActionArgs'; import { NextIOValidator } from './NextIOValidator'; -import type { CommonMetrics } 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, @@ -131,7 +133,6 @@ import { GetExperimentsActionArgs } from './validators/GetExperimentsActionArgs' import { GetExperimentsActionResult } from './validators/GetExperimentsActionResult'; import { GetCMSChurnInterventionActionResult } from './validators/GetCMSChurnInterventionActionResult'; import { GetCMSChurnInterventionActionArgs } from './validators/GetCMSChurnInterventionActionArgs'; -import { getPaymentProvider } from '../utils/getPaymentProvider'; /** * ANY AND ALL methods exposed via this service should be considered publicly accessible and callable with any arguments. @@ -585,7 +586,7 @@ export class NextJSActionsService { async recordEmitterEvent(args: { eventName: string; requestArgs: CommonMetrics; - paymentMethod?: SubPlatPaymentMethodType; + paymentMethod?: SubPlatPaymentMethod; }) { const { eventName, requestArgs, paymentMethod } = args; @@ -602,11 +603,14 @@ export class NextJSActionsService { case 'checkoutFail': { let paymentProvider; if (paymentMethod) { - paymentProvider = getPaymentProvider(paymentMethod); + paymentProvider = + paymentMethod === SubPlatPaymentMethodType.PayPal + ? PaymentProvider.PayPal + : PaymentProvider.Stripe; } this.emitterService.getEmitter().emit(eventName, { ...requestArgs, - paymentMethod: paymentMethod, + 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 dc4bce37408..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, @@ -12,7 +13,10 @@ import { } from 'class-validator'; import { PaymentsEmitterEventsKeys } from '@fxa/payments/events'; import type { PaymentsEmitterEventsKeysType } from '@fxa/payments/events'; -import type { SubPlatPaymentMethodType } from '@fxa/payments/metrics'; +import { + type SubPlatPaymentMethod, + SubPlatPaymentMethodTypePartial, +} from '@fxa/payments/metrics'; /** * Common metrics that can be found on all events @@ -46,5 +50,6 @@ export class RecordEmitterEventArgs { requestArgs!: RequestArgs; @IsOptional() - paymentMethod?: SubPlatPaymentMethodType; + @IsIn([...SubPlatPaymentMethodTypePartial]) + paymentMethod?: SubPlatPaymentMethod; } diff --git a/libs/payments/ui/src/lib/utils/getPaymentProvider.ts b/libs/payments/ui/src/lib/utils/getPaymentProvider.ts deleted file mode 100644 index 47a37bcc0c1..00000000000 --- a/libs/payments/ui/src/lib/utils/getPaymentProvider.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* 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, - SubPlatPaymentMethodType, -} from '@fxa/payments/customer'; - -export function getPaymentProvider(paymentMethod: SubPlatPaymentMethodType) { - if (paymentMethod === SubPlatPaymentMethodType.PayPal) - return PaymentProvider.PayPal; - - return PaymentProvider.Stripe; -} 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 c601509b0b8..7b980faaa1d 100644 --- a/libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts +++ b/libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts @@ -158,6 +158,7 @@ describe('SubscriptionEventsService', () => { .mockReturnValue(mockEmitter as any); jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ provider: PaymentProvider.Stripe, + rawType: 'card', type: SubPlatPaymentMethodType.Card, paymentMethodId: 'pm_id', }); @@ -183,6 +184,7 @@ describe('SubscriptionEventsService', () => { it('should emit the subscriptionEnded event, with paymentProvider external_paypal', async () => { jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({ provider: PaymentProvider.PayPal, + rawType: 'external_paypal', type: SubPlatPaymentMethodType.PayPal, }); await subscriptionEventsService.handleCustomerSubscriptionDeleted( diff --git a/libs/payments/webhooks/src/lib/subscription-handler.service.ts b/libs/payments/webhooks/src/lib/subscription-handler.service.ts index 4e81be01b55..941044d7b34 100644 --- a/libs/payments/webhooks/src/lib/subscription-handler.service.ts +++ b/libs/payments/webhooks/src/lib/subscription-handler.service.ts @@ -8,6 +8,7 @@ import { InvoiceManager, SubscriptionManager, PaymentMethodManager, + SubPlatPaymentMethod, SubPlatPaymentMethodType, PaymentProvider, } from '@fxa/payments/customer'; @@ -38,8 +39,9 @@ export class SubscriptionEventsService { ); const price = subscription.items.data[0].price; + let subplatPaymentMethod: SubPlatPaymentMethod | undefined; let paymentProvider: PaymentProvider | undefined; - let subplatPaymentMethod: SubPlatPaymentMethodType | undefined; + let subplatPaymentMethodType: SubPlatPaymentMethodType | undefined; let determinedCancellation: CancellationReason | undefined; let uid: string | undefined; try { @@ -52,25 +54,26 @@ export class SubscriptionEventsService { [subscription] ); paymentProvider = paymentMethod?.provider; - subplatPaymentMethod = paymentMethod?.type; + subplatPaymentMethodType = paymentMethod?.type; + subplatPaymentMethod = paymentMethod?.rawType; const latestInvoice = - subplatPaymentMethod === SubPlatPaymentMethodType.PayPal && + subplatPaymentMethodType === SubPlatPaymentMethodType.PayPal && subscription.latest_invoice ? await this.invoiceManager.retrieve(subscription.latest_invoice) : undefined; determinedCancellation = - subplatPaymentMethod && + subplatPaymentMethodType && determineCancellation( - subplatPaymentMethod, + subplatPaymentMethodType, subscription, latestInvoice ); } catch (err) { if (err instanceof CustomerDeletedError) { paymentProvider = undefined; - subplatPaymentMethod = undefined; + subplatPaymentMethodType = undefined; determinedCancellation = undefined; } else { throw err;