From 74d0a91160a7d9d73ac3813ec01ead2b019b7c33 Mon Sep 17 00:00:00 2001 From: Richard Bangay Date: Fri, 6 Sep 2024 11:19:59 +0100 Subject: [PATCH 1/2] using discount api response to return the next payment price in the cancellation offer journey --- .../mma/cancel/Cancellation.stories.tsx | 6 ++++++ .../supporterplus/SupporterPlusOffer.tsx | 11 +++++++++- .../SupporterPlusOfferConfirmed.tsx | 20 ++++++++++++++++++- .../SupporterPlusOfferReview.tsx | 11 +++++++++- client/utilities/discountPreview.ts | 1 + .../parallel-2/cancelSupporterPlus.cy.ts | 3 +++ 6 files changed, 49 insertions(+), 3 deletions(-) diff --git a/client/components/mma/cancel/Cancellation.stories.tsx b/client/components/mma/cancel/Cancellation.stories.tsx index 870b5d84c..dd1906450 100644 --- a/client/components/mma/cancel/Cancellation.stories.tsx +++ b/client/components/mma/cancel/Cancellation.stories.tsx @@ -98,6 +98,7 @@ export const Offer: StoryObj = { upToPeriodsType: 'months', firstDiscountedPaymentDate: '2024-05-30', nextNonDiscountedPaymentDate: '2024-07-30', + nonDiscountedPayments: [{ date: '2024-07-30', amount: 14.99 }], }, }, }, @@ -116,6 +117,7 @@ export const OfferReview: StoryObj = { upToPeriodsType: 'months', firstDiscountedPaymentDate: '2024-05-30', nextNonDiscountedPaymentDate: '2024-07-30', + nonDiscountedPayments: [{ date: '2024-07-30', amount: 14.99 }], }, }, msw: [ @@ -136,6 +138,7 @@ export const OfferConfirmed: StoryObj = { reactRouter: { state: { nextNonDiscountedPaymentDate: '2024-07-30', + nonDiscountedPayments: [{ date: '2024-07-30', amount: 14.99 }], }, }, }, @@ -156,6 +159,9 @@ export const SupportplusCancelConfirm: StoryObj = upToPeriodsType: 'months', firstDiscountedPaymentDate: '2024-05-30', nextNonDiscountedPaymentDate: '2024-07-30', + nonDiscountedPayments: [ + { date: '2024-07-30', amount: 14.99 }, + ], }, }, }, diff --git a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOffer.tsx b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOffer.tsx index 9ef48f9dc..211643942 100755 --- a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOffer.tsx +++ b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOffer.tsx @@ -175,6 +175,14 @@ export const SupporterPlusOffer = () => { 'yyyy-MM-dd', ).dateStr(DATE_FNS_LONG_OUTPUT_FORMAT); + const strikethroughPrice = routerState.nonDiscountedPayments.reduce( + (prev, current) => + prev && prev.amount > current.amount ? prev : current, + ).amount; + const humanReadableStrikethroughPrice = Number.isInteger(strikethroughPrice) + ? strikethroughPrice + : strikethroughPrice.toFixed(2); + return ( <> {

{mainPlan.currency} - {mainPlan.price / 100}/{mainPlan.billingPeriod} + {humanReadableStrikethroughPrice}/ + {mainPlan.billingPeriod}

)} diff --git a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferConfirmed.tsx b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferConfirmed.tsx index 968fcf6f1..75a6c9dc4 100755 --- a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferConfirmed.tsx +++ b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferConfirmed.tsx @@ -14,6 +14,8 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { measure } from '@/client/styles/typography'; import type { DiscountPreviewResponse } from '@/client/utilities/discountPreview'; import { DATE_FNS_LONG_OUTPUT_FORMAT, parseDate } from '@/shared/dates'; +import type { PaidSubscriptionPlan } from '@/shared/productResponse'; +import { getMainPlan } from '@/shared/productResponse'; import { DownloadAppCta } from '../../../shared/DownloadAppCta'; import { Heading } from '../../../shared/Heading'; import type { @@ -170,6 +172,9 @@ export const SupporterPlusOfferConfirmed = () => { ) as CancellationContextInterface; const productDetail = cancellationContext.productDetail; + const mainPlan = getMainPlan( + productDetail.subscription, + ) as PaidSubscriptionPlan; useEffect(() => { pageTitleContext.setPageTitle('Confirmation'); @@ -181,6 +186,16 @@ export const SupporterPlusOfferConfirmed = () => { 'yyyy-MM-dd', ).dateStr(DATE_FNS_LONG_OUTPUT_FORMAT); + const nextNonDiscountedPrice = routerState.nonDiscountedPayments.reduce( + (prev, current) => + prev && prev.amount > current.amount ? prev : current, + ).amount; + const humanReadableNextNonDiscountedPrice = Number.isInteger( + nextNonDiscountedPrice, + ) + ? nextNonDiscountedPrice + : nextNonDiscountedPrice.toFixed(2); + return ( <> {
  • You will not be billed until{' '} - {nextNonDiscountedPaymentDate} + {nextNonDiscountedPaymentDate} after which you will pay{' '} + {mainPlan.currency} + {humanReadableNextNonDiscountedPrice}/ + {mainPlan.billingPeriod}
  • diff --git a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferReview.tsx b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferReview.tsx index 6974e0e06..9e1af0bee 100755 --- a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferReview.tsx +++ b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferReview.tsx @@ -127,6 +127,14 @@ export const SupporterPlusOfferReview = () => { 'yyyy-MM-dd', ).dateStr(DATE_FNS_LONG_OUTPUT_FORMAT); + const strikethroughPrice = routerState.nonDiscountedPayments.reduce( + (prev, current) => + prev && prev.amount > current.amount ? prev : current, + ).amount; + const humanReadableStrikethroughPrice = Number.isInteger(strikethroughPrice) + ? strikethroughPrice + : strikethroughPrice.toFixed(2); + const [performingDiscountStatus, setPerformingDiscountStatus] = useState('NOT_READY'); @@ -194,7 +202,8 @@ export const SupporterPlusOfferReview = () => {

    {mainPlan.currency} - {mainPlan.price / 100}/{mainPlan.billingPeriod} + {humanReadableStrikethroughPrice}/ + {mainPlan.billingPeriod}

    )} diff --git a/client/utilities/discountPreview.ts b/client/utilities/discountPreview.ts index 574f505f1..0cb0f0bf8 100644 --- a/client/utilities/discountPreview.ts +++ b/client/utilities/discountPreview.ts @@ -4,4 +4,5 @@ export type DiscountPreviewResponse = { upToPeriodsType: string; firstDiscountedPaymentDate: string; nextNonDiscountedPaymentDate: string; + nonDiscountedPayments: Array<{ date: string; amount: number }>; }; diff --git a/cypress/tests/mocked/parallel-2/cancelSupporterPlus.cy.ts b/cypress/tests/mocked/parallel-2/cancelSupporterPlus.cy.ts index 1fb1e8609..90faa6c62 100644 --- a/cypress/tests/mocked/parallel-2/cancelSupporterPlus.cy.ts +++ b/cypress/tests/mocked/parallel-2/cancelSupporterPlus.cy.ts @@ -149,6 +149,7 @@ describe('Cancel Supporter Plus', () => { upToPeriodsType: 'Months', firstDiscountedPaymentDate: '2024-05-30', nextNonDiscountedPaymentDate: '2024-07-30', + nonDiscountedPayments: [{ date: '2024-07-30', amount: 14.5 }], }; it('user accepts offer instead of cancelling', () => { cy.intercept('GET', '/api/me/mma', { @@ -184,6 +185,8 @@ describe('Cancel Supporter Plus', () => { name: 'Continue to cancellation', }).click(); + cy.findByText('£14.50/month'); + cy.findByRole('button', { name: 'Redeem your offer' }).click(); cy.findByRole('button', { From 55d3d487a270228740b1937fcb357cc70f2b55fd Mon Sep 17 00:00:00 2001 From: Richard Bangay Date: Fri, 6 Sep 2024 15:50:41 +0100 Subject: [PATCH 2/2] split the function used to get the max non discounted price out from various components into a utility function that can be shared --- .../supporterplus/SupporterPlusOffer.tsx | 12 +++++------ .../SupporterPlusOfferConfirmed.tsx | 14 +++++-------- .../SupporterPlusOfferReview.tsx | 12 +++++------ client/utilities/discountPreview.ts | 21 ++++++++++++++++++- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOffer.tsx b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOffer.tsx index 211643942..5019294fc 100755 --- a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOffer.tsx +++ b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOffer.tsx @@ -19,6 +19,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { Ribbon } from '@/client/components/shared/Ribbon'; import { measure } from '@/client/styles/typography'; import type { DiscountPreviewResponse } from '@/client/utilities/discountPreview'; +import { getMaxNonDiscountedPrice } from '@/client/utilities/discountPreview'; import { DATE_FNS_LONG_OUTPUT_FORMAT, parseDate } from '@/shared/dates'; import { number2words } from '@/shared/numberUtils'; import { getMainPlan, isPaidSubscriptionPlan } from '@/shared/productResponse'; @@ -175,13 +176,10 @@ export const SupporterPlusOffer = () => { 'yyyy-MM-dd', ).dateStr(DATE_FNS_LONG_OUTPUT_FORMAT); - const strikethroughPrice = routerState.nonDiscountedPayments.reduce( - (prev, current) => - prev && prev.amount > current.amount ? prev : current, - ).amount; - const humanReadableStrikethroughPrice = Number.isInteger(strikethroughPrice) - ? strikethroughPrice - : strikethroughPrice.toFixed(2); + const humanReadableStrikethroughPrice = getMaxNonDiscountedPrice( + routerState.nonDiscountedPayments, + true, + ); return ( <> diff --git a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferConfirmed.tsx b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferConfirmed.tsx index 75a6c9dc4..fdaf485a7 100755 --- a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferConfirmed.tsx +++ b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferConfirmed.tsx @@ -13,6 +13,7 @@ import { useContext, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { measure } from '@/client/styles/typography'; import type { DiscountPreviewResponse } from '@/client/utilities/discountPreview'; +import { getMaxNonDiscountedPrice } from '@/client/utilities/discountPreview'; import { DATE_FNS_LONG_OUTPUT_FORMAT, parseDate } from '@/shared/dates'; import type { PaidSubscriptionPlan } from '@/shared/productResponse'; import { getMainPlan } from '@/shared/productResponse'; @@ -186,15 +187,10 @@ export const SupporterPlusOfferConfirmed = () => { 'yyyy-MM-dd', ).dateStr(DATE_FNS_LONG_OUTPUT_FORMAT); - const nextNonDiscountedPrice = routerState.nonDiscountedPayments.reduce( - (prev, current) => - prev && prev.amount > current.amount ? prev : current, - ).amount; - const humanReadableNextNonDiscountedPrice = Number.isInteger( - nextNonDiscountedPrice, - ) - ? nextNonDiscountedPrice - : nextNonDiscountedPrice.toFixed(2); + const humanReadableNextNonDiscountedPrice = getMaxNonDiscountedPrice( + routerState.nonDiscountedPayments, + true, + ); return ( <> diff --git a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferReview.tsx b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferReview.tsx index 9e1af0bee..c0c1d7fea 100755 --- a/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferReview.tsx +++ b/client/components/mma/cancel/cancellationSaves/supporterplus/SupporterPlusOfferReview.tsx @@ -18,6 +18,7 @@ import { useContext, useState } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { measure } from '@/client/styles/typography'; import type { DiscountPreviewResponse } from '@/client/utilities/discountPreview'; +import { getMaxNonDiscountedPrice } from '@/client/utilities/discountPreview'; import { fetchWithDefaultParameters } from '@/client/utilities/fetch'; import { DATE_FNS_LONG_OUTPUT_FORMAT, parseDate } from '@/shared/dates'; import { number2words } from '@/shared/numberUtils'; @@ -127,13 +128,10 @@ export const SupporterPlusOfferReview = () => { 'yyyy-MM-dd', ).dateStr(DATE_FNS_LONG_OUTPUT_FORMAT); - const strikethroughPrice = routerState.nonDiscountedPayments.reduce( - (prev, current) => - prev && prev.amount > current.amount ? prev : current, - ).amount; - const humanReadableStrikethroughPrice = Number.isInteger(strikethroughPrice) - ? strikethroughPrice - : strikethroughPrice.toFixed(2); + const humanReadableStrikethroughPrice = getMaxNonDiscountedPrice( + routerState.nonDiscountedPayments, + true, + ); const [performingDiscountStatus, setPerformingDiscountStatus] = useState('NOT_READY'); diff --git a/client/utilities/discountPreview.ts b/client/utilities/discountPreview.ts index 0cb0f0bf8..2239fe7ae 100644 --- a/client/utilities/discountPreview.ts +++ b/client/utilities/discountPreview.ts @@ -1,8 +1,27 @@ +interface NonDiscountedPayments { + date: string; + amount: number; +} + export type DiscountPreviewResponse = { discountedPrice: number; upToPeriods: number; upToPeriodsType: string; firstDiscountedPaymentDate: string; nextNonDiscountedPaymentDate: string; - nonDiscountedPayments: Array<{ date: string; amount: number }>; + nonDiscountedPayments: NonDiscountedPayments[]; +}; + +export const getMaxNonDiscountedPrice = ( + nonDiscountedPayments: NonDiscountedPayments[], + asHumanReadable?: boolean, +) => { + const allNonDiscountedAmounts = nonDiscountedPayments.map((p) => p.amount); + const maxNonDiscountedPrice = Math.max(...allNonDiscountedAmounts); + if (!asHumanReadable) { + return maxNonDiscountedPrice; + } + return Number.isInteger(maxNonDiscountedPrice) + ? maxNonDiscountedPrice + : maxNonDiscountedPrice.toFixed(2); };