support-frontend/assets/helpers/subscriptionsForms/submit.ts (400 lines of code) (raw):

import type { PaymentMethod as StripePaymentMethod } from '@stripe/stripe-js'; import type { Dispatch } from 'redux'; import type { PaymentAuthorisation, PaymentResult, RegularPaymentRequest, RegularPaymentRequestAddress, SubscriptionProductFields, } from 'helpers/forms/paymentIntegrations/readerRevenueApis'; import { postRegularPaymentRequest, regularPaymentFieldsFromAuthorisation, } from 'helpers/forms/paymentIntegrations/readerRevenueApis'; import type { PaymentMethod } from 'helpers/forms/paymentMethods'; import { DirectDebit, PayPal, Stripe } from 'helpers/forms/paymentMethods'; import type { IsoCurrency } from 'helpers/internationalisation/currency'; import { Quarterly } from 'helpers/productPrice/billingPeriods'; import type { FulfilmentOptions } from 'helpers/productPrice/fulfilmentOptions'; import { HomeDelivery, NationalDelivery, } from 'helpers/productPrice/fulfilmentOptions'; import type { ProductOptions } from 'helpers/productPrice/productOptions'; import { NoProductOptions } from 'helpers/productPrice/productOptions'; import { getCurrency, getProductPrice, } from 'helpers/productPrice/productPrices'; import type { ProductPrice } from 'helpers/productPrice/productPrices'; import type { Promotion } from 'helpers/productPrice/promotions'; import { finalPrice, getAppliedPromo } from 'helpers/productPrice/promotions'; import { Direct, Gift } from 'helpers/productPrice/readerType'; import type { SubscriptionProduct } from 'helpers/productPrice/subscriptions'; import { DigitalPack, GuardianWeekly, isPhysicalProduct, Paper, } from 'helpers/productPrice/subscriptions'; import type { GiftingState } from 'helpers/redux/checkout/giftingState/state'; import type { DirectDebitState } from 'helpers/redux/checkout/payment/directDebit/state'; import { getSubscriptionType } from 'helpers/redux/checkout/product/selectors/productType'; import type { SubscriptionsState } from 'helpers/redux/subscriptionsStore'; import type { Action } from 'helpers/subscriptionsForms/formActions'; import { setFormSubmitted, setStage, setSubmissionError, } from 'helpers/subscriptionsForms/formActions'; import { validateWithDeliveryForm } from 'helpers/subscriptionsForms/formValidation'; import type { AnyCheckoutState } from 'helpers/subscriptionsForms/subscriptionCheckoutReducer'; import { getOphanIds, getSupportAbTests } from 'helpers/tracking/acquisitions'; import { successfulSubscriptionConversion } from 'helpers/tracking/googleTagManager'; import { sendEventSubscriptionCheckoutConversion } from 'helpers/tracking/quantumMetric'; import type { Option } from 'helpers/types/option'; import { routes } from 'helpers/urls/routes'; import type { SupportInternationalisationId } from '../internationalisation/countryGroup'; import { countryGroups } from '../internationalisation/countryGroup'; import { trackCheckoutSubmitAttempt } from '../tracking/behaviour'; type Addresses = { deliveryAddress?: RegularPaymentRequestAddress; billingAddress: RegularPaymentRequestAddress; }; // ----- Functions ----- // function getAddresses(state: SubscriptionsState): Addresses { const product = getSubscriptionType(state); if (isPhysicalProduct(product)) { const deliveryAddressFields = state.page.checkoutForm.deliveryAddress.fields; const billingAddressFields = state.page.checkoutForm.billingAddress.fields; return { deliveryAddress: deliveryAddressFields, billingAddress: state.page.checkoutForm.addressMeta .billingAddressMatchesDelivery ? deliveryAddressFields : billingAddressFields, }; } return { billingAddress: state.page.checkoutForm.billingAddress.fields, }; } const getProduct = ( state: SubscriptionsState, currencyId?: Option<IsoCurrency>, deliveryAgent?: number, ): SubscriptionProductFields => { const { billingPeriod, fulfilmentOption, productOption, orderIsAGift } = state.page.checkoutForm.product; const product = getSubscriptionType(state); const readerType = orderIsAGift ? Gift : Direct; if (product === DigitalPack) { return { productType: DigitalPack, currency: currencyId ?? state.common.internationalisation.currencyId, billingPeriod, readerType, }; } else if (product === GuardianWeekly) { return { productType: GuardianWeekly, currency: currencyId ?? state.common.internationalisation.currencyId, billingPeriod, fulfilmentOptions: fulfilmentOption, }; } /* Paper or PaperAndDigital */ return { productType: Paper, currency: currencyId ?? state.common.internationalisation.currencyId, billingPeriod, fulfilmentOptions: getPaperFulfilmentOption(fulfilmentOption, state), productOptions: productOption, deliveryAgent, }; }; const getPaperFulfilmentOption = ( fulfilmentOption: FulfilmentOptions, state: SubscriptionsState, ) => { return fulfilmentOption === HomeDelivery && state.page.checkoutForm.addressMeta.deliveryAgent.chosenAgent ? NationalDelivery : fulfilmentOption; }; const getAppliedPromotion = ( supportInternationalisationId: SupportInternationalisationId, promotions?: Promotion[], ) => { const promotion = getAppliedPromo(promotions); return promotion?.promoCode !== undefined ? { promoCode: promotion.promoCode, countryGroupId: supportInternationalisationId, } : undefined; }; function getGiftRecipient(giftingState: GiftingState) { const { title, firstName, lastName, email, giftMessage, giftDeliveryDate } = giftingState; if (firstName && lastName) { return { giftRecipient: { title, firstName, lastName, email, message: giftMessage, deliveryDate: giftDeliveryDate, }, }; } return {}; } function getPrintDiscountedPrice( productPrice: ProductPrice, promoCode?: Option<string>, ): number { const defaultPrice = productPrice.price; if (productPrice.promotions && productPrice.promotions.length > 0) { const validPromo = productPrice.promotions.find( (promotion) => promotion.promoCode === promoCode, ); if (validPromo) { return validPromo.discountedPrice ?? defaultPrice; } } return defaultPrice; } function buildRegularPaymentRequest( state: SubscriptionsState, paymentAuthorisation: PaymentAuthorisation, addresses: Addresses, promotions?: Promotion[], currencyId?: Option<IsoCurrency>, ): RegularPaymentRequest { const { actionHistory } = state.debug; const { title, firstName, lastName, email, telephone } = state.page.checkoutForm.personalDetails; const { deliveryInstructions, deliveryAgent: { chosenAgent: chosenDeliveryAgent }, } = state.page.checkoutForm.addressMeta; const { csrUsername, salesforceCaseId } = state.page.checkout; const product = getProduct(state, currencyId, chosenDeliveryAgent); const recaptchaToken = state.page.checkoutForm.recaptcha.token; const paymentFields = regularPaymentFieldsFromAuthorisation( paymentAuthorisation, state.page.checkoutForm.payment.stripeAccountDetails.publicKey, recaptchaToken, ); const giftRecipient = getGiftRecipient(state.page.checkoutForm.gifting); const appliedPromotion = getAppliedPromotion( countryGroups[state.common.internationalisation.countryGroupId] .supportInternationalisationId, promotions, ); return { title, firstName: firstName.trim(), lastName: lastName.trim(), ...addresses, email: email.trim(), ...giftRecipient, telephoneNumber: telephone, product, firstDeliveryDate: state.page.checkoutForm.product.startDate, paymentFields, ophanIds: getOphanIds(), referrerAcquisitionData: state.common.referrerAcquisitionData, supportAbTests: getSupportAbTests(state.common.abParticipations), appliedPromotion, deliveryInstructions, csrUsername, salesforceCaseId, debugInfo: actionHistory, }; } function onPaymentAuthorised( paymentAuthorisation: PaymentAuthorisation, dispatch: Dispatch<Action>, state: SubscriptionsState, currency?: IsoCurrency, ): void { const { billingPeriod, fulfilmentOption, orderIsAGift, productOption, productPrices, } = state.page.checkoutForm.product; const productType = getSubscriptionType(state); const { paymentMethod } = state.page.checkoutForm.payment; const { csrf } = state.page.checkoutForm; const addresses = getAddresses(state); const pricingCountry = addresses.deliveryAddress?.country ?? addresses.billingAddress.country; const productPrice = getProductPrice( productPrices, pricingCountry, billingPeriod, fulfilmentOption, productOption, ); const data = buildRegularPaymentRequest( state, paymentAuthorisation, addresses, productPrice.promotions, currency, ); const handleSubscribeResult = (result: PaymentResult) => { if (result.paymentStatus === 'success') { if (result.subscriptionCreationPending) { dispatch(setStage('thankyou-pending', productType, paymentMethod.name)); } else { dispatch(setStage('thankyou', productType, paymentMethod.name)); } const printPriceDiscounted = getPrintDiscountedPrice( productPrice, data.appliedPromotion?.promoCode, ); const { currencyId } = state.common.internationalisation; // GTM: track print subscription conversion successfulSubscriptionConversion( printPriceDiscounted, currencyId, paymentMethod.name, billingPeriod, productType, ); // QM: track print subscription conversion sendEventSubscriptionCheckoutConversion( productType, !!orderIsAGift, productPrice, billingPeriod, ); } else if (result.error) { dispatch(setSubmissionError(result.error)); } }; dispatch(setFormSubmitted(true)); void postRegularPaymentRequest(routes.subscriptionCreate, data, csrf).then( handleSubscribeResult, ); } function checkStripeUserType( onAuthorised: (pa: PaymentAuthorisation) => void, stripePaymentMethodId?: string | StripePaymentMethod, ) { if (stripePaymentMethodId != null) { onAuthorised({ paymentMethod: Stripe, stripePaymentMethod: 'StripeElements', paymentMethodId: stripePaymentMethodId, }); } else { throw new Error( 'Attempting to process Stripe Payment, however Stripe Payment Method ID is missing.', ); } } const directDebitAuthorised = ( onAuthorised: (pa: PaymentAuthorisation) => void, ddState: DirectDebitState, ) => { onAuthorised({ paymentMethod: DirectDebit, accountHolderName: ddState.accountHolderName, sortCode: ddState.sortCode, accountNumber: ddState.accountNumber, }); }; function showPaymentMethod( onAuthorised: (pa: PaymentAuthorisation) => void, paymentMethod: Option<PaymentMethod>, stripePaymentMethod: string | StripePaymentMethod | undefined, state: AnyCheckoutState, ): void { switch (paymentMethod) { case Stripe: checkStripeUserType(onAuthorised, stripePaymentMethod); break; case DirectDebit: directDebitAuthorised( onAuthorised, state.page.checkoutForm.payment.directDebit, ); break; case PayPal: // PayPal is more complicated and is handled differently, see PayPalExpressButton component break; case null: case undefined: console.log('Undefined payment method'); break; default: console.log(`Unknown payment method ${paymentMethod}`); } } function trackSubmitAttempt( paymentMethod: PaymentMethod | null | undefined, productType: SubscriptionProduct, productOption: ProductOptions, ): void { const componentId = productOption === NoProductOptions ? `subs-checkout-submit-${productType}-${paymentMethod ?? ''}` : `subs-checkout-submit-${productType}-${productOption}-${ paymentMethod ?? '' }`; trackCheckoutSubmitAttempt(componentId, productType); } function getPricingCountry(product: SubscriptionProduct, addresses: Addresses) { if (product === GuardianWeekly && addresses.deliveryAddress) { return addresses.deliveryAddress.country; } return addresses.billingAddress.country; } function submitForm(dispatch: Dispatch<Action>, state: SubscriptionsState) { const { paymentMethod } = state.page.checkoutForm.payment; const { productOption, billingPeriod, fulfilmentOption, productPrices } = state.page.checkoutForm.product; const productType = getSubscriptionType(state); const addresses = getAddresses(state); const pricingCountry = getPricingCountry(productType, addresses); trackSubmitAttempt(paymentMethod.name, productType, productOption); let priceDetails = finalPrice( productPrices, pricingCountry, billingPeriod, fulfilmentOption, productOption, ); // This is a small hack to make sure we show quarterly pricing until we have promos tooling if (billingPeriod === Quarterly && priceDetails.price === 6) { priceDetails = getProductPrice( productPrices, pricingCountry, billingPeriod, fulfilmentOption, productOption, ); } const currencyId = getCurrency(pricingCountry); const stripePaymentMethod = state.page.checkoutForm.payment.stripe.stripePaymentMethod; const onAuthorised = (paymentAuthorisation: PaymentAuthorisation) => onPaymentAuthorised(paymentAuthorisation, dispatch, state, currencyId); showPaymentMethod( onAuthorised, paymentMethod.name, stripePaymentMethod, state, ); } function submitWithDeliveryForm( dispatch: Dispatch<Action>, state: SubscriptionsState, ): void { if (validateWithDeliveryForm(dispatch, state)) { submitForm(dispatch, state); } } // ----- Export ----- // export { onPaymentAuthorised, submitWithDeliveryForm, trackSubmitAttempt };