Skip to content

Commit 0dd57b6

Browse files
committed
feat(payments): Update Cancel and Stay subscribed pages
1 parent 298e753 commit 0dd57b6

6 files changed

Lines changed: 173 additions & 209 deletions

File tree

apps/payments/next/app/[locale]/subscriptions/[subscriptionId]/cancel/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export default async function CancelSubscriptionPage({
4040
notFound();
4141
}
4242

43+
if (pageContent.isEligibleForChurnCancel === true) {
44+
redirect(
45+
`/${locale}/subscriptions/${subscriptionId}/loyalty-discount/cancel`
46+
);
47+
}
48+
49+
if (pageContent.isEligibleForCancelInterstitialOffer === true) {
50+
redirect(`/${locale}/subscriptions/${subscriptionId}/offer`);
51+
}
52+
4353
return (
4454
<CancelSubscription
4555
userId={uid}

apps/payments/next/app/[locale]/subscriptions/[subscriptionId]/stay-subscribed/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export default async function StaySubscribedPage({
4141
notFound();
4242
}
4343

44+
if (pageContent.isEligibleforChurnStaySubscribed === true) {
45+
redirect(
46+
`/${locale}/subscriptions/${subscriptionId}/loyalty-discount/stay-subscribed`
47+
);
48+
}
49+
4450
return (
4551
<StaySubscribed
4652
userId={uid}

libs/payments/management/src/lib/churn-intervention.service.spec.ts

Lines changed: 52 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
StripeSubscriptionFactory,
2020
StripeResponseFactory,
2121
StripePriceFactory,
22-
StripePriceRecurringFactory,
2322
StripeClient,
2423
MockStripeConfigProvider,
2524
AccountCustomerManager,
@@ -36,6 +35,8 @@ import {
3635
ChurnInterventionByProductIdResultUtil,
3736
CmsOfferingContentFactory,
3837
ProductConfigurationManager,
38+
PageContentByPriceIdsResultUtil,
39+
PageContentByPriceIdsPurchaseResultFactory,
3940
CancelInterstitialOfferTransformedFactory,
4041
CancelInterstitialOfferResultFactory,
4142
CancelInterstitialOfferUtil,
@@ -95,6 +96,7 @@ describe('ChurnInterventionService', () => {
9596
useValue: {
9697
getChurnInterventionBySubscription: jest.fn(),
9798
getCancelInterstitialOffer: jest.fn(),
99+
getPageContentByPriceIds: jest.fn(),
98100
getSubplatIntervalBySubscription: jest.fn(),
99101
retrieveStripePrice: jest.fn(),
100102
},
@@ -727,9 +729,6 @@ describe('ChurnInterventionService', () => {
727729
const args = {
728730
uid: 'uid_123',
729731
subscriptionId: mockSubscription.id,
730-
offeringApiIdentifier: 'offer_123',
731-
currentInterval: SubplatInterval.Monthly,
732-
upgradeInterval: SubplatInterval.Monthly,
733732
};
734733

735734
it('returns cancel_interstitial_offer when one exists', async () => {
@@ -783,82 +782,6 @@ describe('ChurnInterventionService', () => {
783782
});
784783
});
785784

786-
it('returns cancel_interstitial_offer when all checks pass', async () => {
787-
jest
788-
.spyOn(
789-
churnInterventionService,
790-
'determineCancelChurnContentEligibility'
791-
)
792-
.mockResolvedValue({
793-
isEligible: false,
794-
reason: 'no_churn_intervention_found',
795-
cmsChurnInterventionEntry: null,
796-
});
797-
798-
jest
799-
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
800-
.mockResolvedValue(mockAccountCustomer);
801-
jest
802-
.spyOn(subscriptionManager, 'retrieve')
803-
.mockResolvedValue(mockSubscription);
804-
jest
805-
.spyOn(subscriptionManager, 'getSubscriptionStatus')
806-
.mockResolvedValue({ active: true, cancelAtPeriodEnd: false });
807-
808-
const rawResult = CancelInterstitialOfferResultFactory();
809-
const util = new CancelInterstitialOfferUtil(rawResult);
810-
jest
811-
.spyOn(productConfigurationManager, 'getCancelInterstitialOffer')
812-
.mockResolvedValue(util);
813-
const mockCancelInterstitialOffer =
814-
CancelInterstitialOfferTransformedFactory();
815-
jest
816-
.spyOn(util, 'getTransformedResult')
817-
.mockReturnValue(mockCancelInterstitialOffer);
818-
819-
jest
820-
.spyOn(productConfigurationManager, 'getSubplatIntervalBySubscription')
821-
.mockResolvedValue(SubplatInterval.Monthly);
822-
823-
const mockPrice = StripeResponseFactory(StripePriceFactory());
824-
jest
825-
.spyOn(productConfigurationManager, 'retrieveStripePrice')
826-
.mockResolvedValue(mockPrice);
827-
828-
const mockFromOfferingId = faker.string.uuid();
829-
const mockFromPrice = StripePriceFactory({
830-
recurring: StripePriceRecurringFactory({ interval: 'month' }),
831-
});
832-
jest.spyOn(eligibilityService, 'checkEligibility').mockResolvedValue({
833-
subscriptionEligibilityResult: EligibilityStatus.UPGRADE,
834-
fromOfferingConfigId: mockFromOfferingId,
835-
fromPrice: mockFromPrice,
836-
});
837-
838-
const result =
839-
await churnInterventionService.determineCancellationIntervention(args);
840-
841-
expect(subscriptionManager.getSubscriptionStatus).toHaveBeenCalledWith(
842-
mockStripeCustomer.id,
843-
args.subscriptionId
844-
);
845-
expect(
846-
productConfigurationManager.retrieveStripePrice
847-
).toHaveBeenCalledWith(args.offeringApiIdentifier, args.upgradeInterval);
848-
expect(mockStatsD.increment).toHaveBeenCalledWith(
849-
'cancel_intervention_decision',
850-
{
851-
type: 'cancel_interstitial_offer',
852-
}
853-
);
854-
855-
expect(result).toEqual({
856-
cancelChurnInterventionType: 'cancel_interstitial_offer',
857-
reason: 'eligible',
858-
cmsOfferContent: mockCancelInterstitialOffer,
859-
});
860-
});
861-
862785
it('returns cancel_churn_intervention when one exists', async () => {
863786
jest
864787
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
@@ -975,7 +898,7 @@ describe('ChurnInterventionService', () => {
975898
});
976899
});
977900

978-
it('returns none when currentInterval does not match current subscription plan interval', async () => {
901+
it('returns none when there is no upgrade plan', async () => {
979902
jest
980903
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
981904
.mockResolvedValue(mockAccountCustomer);
@@ -985,17 +908,6 @@ describe('ChurnInterventionService', () => {
985908
jest
986909
.spyOn(subscriptionManager, 'getSubscriptionStatus')
987910
.mockResolvedValue({ active: true, cancelAtPeriodEnd: false });
988-
989-
const raw = CancelInterstitialOfferResultFactory();
990-
const util = new CancelInterstitialOfferUtil(raw);
991-
jest
992-
.spyOn(productConfigurationManager, 'getCancelInterstitialOffer')
993-
.mockResolvedValue(util);
994-
995-
jest
996-
.spyOn(productConfigurationManager, 'getSubplatIntervalBySubscription')
997-
.mockResolvedValue(SubplatInterval.Yearly);
998-
999911
jest
1000912
.spyOn(
1001913
churnInterventionService,
@@ -1006,69 +918,33 @@ describe('ChurnInterventionService', () => {
1006918
reason: 'no_churn_intervention_found',
1007919
cmsChurnInterventionEntry: null,
1008920
});
1009-
1010-
const result =
1011-
await churnInterventionService.determineCancellationIntervention(args);
1012-
1013-
expect(subscriptionManager.getSubscriptionStatus).toHaveBeenCalledWith(
1014-
mockStripeCustomer.id,
1015-
args.subscriptionId
1016-
);
1017-
expect(
1018-
productConfigurationManager.retrieveStripePrice
1019-
).not.toHaveBeenCalled();
1020-
1021-
expect(mockStatsD.increment).toHaveBeenCalledWith(
1022-
'cancel_intervention_decision',
1023-
{
1024-
type: 'none',
1025-
reason: 'current_interval_mismatch',
1026-
}
1027-
);
1028-
1029-
expect(result).toEqual({
1030-
cancelChurnInterventionType: 'none',
1031-
reason: 'no_churn_intervention_found',
1032-
cmsOfferContent: null,
1033-
});
1034-
});
1035-
1036-
it('returns none when there is no upgrade plan', async () => {
1037-
jest
1038-
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
1039-
.mockResolvedValue(mockAccountCustomer);
1040921
jest
1041-
.spyOn(subscriptionManager, 'retrieve')
1042-
.mockResolvedValue(mockSubscription);
1043-
922+
.spyOn(productConfigurationManager, 'getSubplatIntervalBySubscription')
923+
.mockResolvedValue(SubplatInterval.Monthly);
924+
const mockPurchaseForPriceIdResultUtil = {
925+
purchaseForPriceId: jest.fn(),
926+
};
1044927
jest
1045-
.spyOn(subscriptionManager, 'getSubscriptionStatus')
1046-
.mockResolvedValue({ active: true, cancelAtPeriodEnd: false });
1047-
928+
.spyOn(productConfigurationManager, 'getPageContentByPriceIds')
929+
.mockResolvedValue(
930+
mockPurchaseForPriceIdResultUtil as unknown as PageContentByPriceIdsResultUtil
931+
);
932+
mockPurchaseForPriceIdResultUtil.purchaseForPriceId.mockReturnValue(
933+
PageContentByPriceIdsPurchaseResultFactory({
934+
offering: {
935+
...PageContentByPriceIdsPurchaseResultFactory().offering,
936+
apiIdentifier: 'offering_123',
937+
},
938+
})
939+
);
1048940
const rawResult = CancelInterstitialOfferResultFactory();
1049941
const util = new CancelInterstitialOfferUtil(rawResult);
1050942
jest
1051943
.spyOn(productConfigurationManager, 'getCancelInterstitialOffer')
1052944
.mockResolvedValue(util);
1053-
1054-
jest
1055-
.spyOn(productConfigurationManager, 'getSubplatIntervalBySubscription')
1056-
.mockResolvedValue(SubplatInterval.Monthly);
1057945
jest
1058946
.spyOn(productConfigurationManager, 'retrieveStripePrice')
1059947
.mockRejectedValue(new Error('error'));
1060-
1061-
jest
1062-
.spyOn(
1063-
churnInterventionService,
1064-
'determineCancelChurnContentEligibility'
1065-
)
1066-
.mockResolvedValue({
1067-
isEligible: false,
1068-
reason: 'no_churn_intervention_found',
1069-
cmsChurnInterventionEntry: null,
1070-
});
1071-
1072948
const result =
1073949
await churnInterventionService.determineCancellationIntervention(args);
1074950

@@ -1078,7 +954,7 @@ describe('ChurnInterventionService', () => {
1078954
);
1079955
expect(
1080956
productConfigurationManager.retrieveStripePrice
1081-
).toHaveBeenCalledWith(args.offeringApiIdentifier, args.upgradeInterval);
957+
).toHaveBeenCalledWith('offering_123', SubplatInterval.Yearly);
1082958
expect(mockStatsD.increment).toHaveBeenCalledWith(
1083959
'cancel_intervention_decision',
1084960
{
@@ -1104,17 +980,41 @@ describe('ChurnInterventionService', () => {
1104980
jest
1105981
.spyOn(subscriptionManager, 'getSubscriptionStatus')
1106982
.mockResolvedValue({ active: true, cancelAtPeriodEnd: false });
1107-
983+
jest
984+
.spyOn(
985+
churnInterventionService,
986+
'determineCancelChurnContentEligibility'
987+
)
988+
.mockResolvedValue({
989+
isEligible: false,
990+
reason: 'no_churn_intervention_found',
991+
cmsChurnInterventionEntry: null,
992+
});
993+
jest
994+
.spyOn(productConfigurationManager, 'getSubplatIntervalBySubscription')
995+
.mockResolvedValue(SubplatInterval.Monthly);
996+
const mockPurchaseForPriceIdResultUtil = {
997+
purchaseForPriceId: jest.fn(),
998+
};
999+
jest
1000+
.spyOn(productConfigurationManager, 'getPageContentByPriceIds')
1001+
.mockResolvedValue(
1002+
mockPurchaseForPriceIdResultUtil as unknown as PageContentByPriceIdsResultUtil
1003+
);
1004+
mockPurchaseForPriceIdResultUtil.purchaseForPriceId.mockReturnValue(
1005+
PageContentByPriceIdsPurchaseResultFactory({
1006+
offering: {
1007+
...PageContentByPriceIdsPurchaseResultFactory().offering,
1008+
apiIdentifier: 'offering_123',
1009+
},
1010+
})
1011+
);
11081012
const rawResult = CancelInterstitialOfferResultFactory();
11091013
const util = new CancelInterstitialOfferUtil(rawResult);
11101014
jest
11111015
.spyOn(productConfigurationManager, 'getCancelInterstitialOffer')
11121016
.mockResolvedValue(util);
11131017

1114-
jest
1115-
.spyOn(productConfigurationManager, 'getSubplatIntervalBySubscription')
1116-
.mockResolvedValue(SubplatInterval.Monthly);
1117-
11181018
const mockPrice = StripeResponseFactory(StripePriceFactory());
11191019
jest
11201020
.spyOn(productConfigurationManager, 'retrieveStripePrice')
@@ -1123,17 +1023,6 @@ describe('ChurnInterventionService', () => {
11231023
subscriptionEligibilityResult: EligibilityStatus.SAME,
11241024
});
11251025

1126-
jest
1127-
.spyOn(
1128-
churnInterventionService,
1129-
'determineCancelChurnContentEligibility'
1130-
)
1131-
.mockResolvedValue({
1132-
isEligible: false,
1133-
reason: 'no_churn_intervention_found',
1134-
cmsChurnInterventionEntry: null,
1135-
});
1136-
11371026
const result =
11381027
await churnInterventionService.determineCancellationIntervention(args);
11391028

@@ -1143,7 +1032,7 @@ describe('ChurnInterventionService', () => {
11431032
);
11441033
expect(
11451034
productConfigurationManager.retrieveStripePrice
1146-
).toHaveBeenCalledWith(args.offeringApiIdentifier, args.upgradeInterval);
1035+
).toHaveBeenCalledWith('offering_123', SubplatInterval.Yearly);
11471036
expect(mockStatsD.increment).toHaveBeenCalledWith(
11481037
'cancel_intervention_decision',
11491038
{

0 commit comments

Comments
 (0)