Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add free two months plan and allow for supporter plus monthly #2304

Merged
merged 3 commits into from
Jun 10, 2024

Conversation

johnduffell
Copy link
Member

@johnduffell johnduffell commented Jun 4, 2024

This PR is part of the cancellation save work for supporter plus.

This adds links between supporter plus monthly to the newly added 2 months free plan.

The means that people calling the endpoint will be offered the appropriate save period.

Tested in CODE on A-S00894132 and it previewed and added the 100% discount correctly
https://apisandbox.zuora.com/platform/subscriptions/8ad093fb8fe33df8018fe7cfb31a5054

I've also added a couple of tests for the "happy" cases, I wasn't sure whether to try to cover too much especially in the IT test.

Comment on lines -45 to -49
const catalog: {
[K in 'CODE' | 'PROD']: {
[K in Product]: { [K in BillingPeriod]: string };
};
} = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to remove the type because if it wanted all the billing periods for all products.

If I made them optional, the code lower down didn't compile because it assumed any might be undefined.

If I remove the type completely, it infers the exact shape and compiles as appropriate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could type it more strongly using the types in the product-catalog module:

import type { ProductBillingPeriod } from '@modules/product-catalog/productCatalog';


export const catalog: {
	[S in 'CODE' | 'PROD']: {
		[P in 'DigitalSubscription' | 'SupporterPlus']: {
			[BP in ProductBillingPeriod<P>]: string;
		};
	};
} = {
	CODE: {
		DigitalSubscription: {
			Month: '2c92c0f84bbfec8b014bc655f4852d9d',
			Quarter: '2c92c0f84bbfec58014bc6a2d43a1f5b',
			Annual: '2c92c0f94bbffaaa014bc6a4212e205b',
		},
		SupporterPlus: {
			Month: '8ad08cbd8586721c01858804e3275376',
			Annual: '',
		},
	},
	PROD: {
		DigitalSubscription: {
			Month: '2c92a0fb4edd70c8014edeaa4eae220a',
			Quarter: '2c92a0fb4edd70c8014edeaa4e8521fe',
			Annual: '2c92a0fb4edd70c8014edeaa4e972204',
		},
		SupporterPlus: {
			Month: '8a128ed885fc6ded018602296ace3eb8',
			Annual: '',
		},
	},
};

But then you would have to provide a value for all the billing periods which each product supports and it looks like you don't want to add one for S+ annual, so up to you if you think this is worthwhile. I suspect probably not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the pointer, in this case I think the type just needs to be inferred as then it only compiles if everything it actually uses statically is present. Since this is not used dynamically, that should be enough.
The one that needs to be right and isn't checked by the compiler is the one that is keyed off the ID in the subscription, but that's just business logic that we need to get right.

Comment on lines +101 to +106
cancellationFree2Mo: {
productRatePlanId: '8a1299c28fb956e8018fe2c0e12c3ae4',
name: 'Cancellation Save Discount - Free for 2 months',
upToPeriods: 2,
upToPeriodsType: 'Months',
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +81 to +86
cancellationFree2Mo: {
productRatePlanId: '8ad081dd8fd3d9df018fe2b6a7bc379d',
name: 'Cancellation Save Discount - Free for 2 months',
upToPeriods: 2,
upToPeriodsType: 'Months',
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +59 to +61
supporterPlus: {
Month: '8a128ed885fc6ded018602296ace3eb8',
},
Copy link
Member Author

@johnduffell johnduffell Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnduffell johnduffell marked this pull request as draft June 4, 2024 10:55
Base automatically changed from jd-remove-unused-fields to main June 4, 2024 11:10
@johnduffell johnduffell marked this pull request as ready for review June 5, 2024 10:01
@@ -104,10 +104,10 @@ const getDiscountPreview = async (
discount.productRatePlanId,
);

if (!previewResponse.success || previewResponse.invoiceItems.length != 2) {
if (!previewResponse.success || previewResponse.invoiceItems.length < 2) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supporter plus has 2 charges (base plus contribution)
I wasn't sure how valuable this check was/what it's protecting us from? but I kept it in a loose form. Should I keep it in the limit 2->3 or is minimum 2 ok? Or remove completely?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly related to your change but what do you think about pulling that 2 out to a const to give it a name?

Suggested change
if (!previewResponse.success || previewResponse.invoiceItems.length < 2) {
const supporterPlusChargeCount = 2;
if (!previewResponse.success || previewResponse.invoiceItems.length < supporterPlusChargeCount) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funny thing is it only supported digi sub before which has one charge.
So the 2 referred to the single digi sub charge plus the new discount charge.

Now it can be 3 as we have S+ with two charges plus one new discount charge.

Maybe we could look at the subscription and count the charges, and then check for n+1 items in the preview, but I'm not 100% sure what that check was intended to assert. Perhaps it's just checking that the new charge is being planned to trigger on the right date, otherwise it wouldn't appear in the invoice?

@rupertbates any thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's just following the principle of 'set out your expectations and if the situation you're in doesn't match them fail at that point rather than trying to fudge it'. Seems like a reasonable thing to do to me but if it becomes overly complicated when you add S+ into the mix I wouldn't be too upset to lose it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so fail fast and loudly principle rather than leaving zuora in a mess. I will think about whether to count the charges (on non removed rate plans) later.

@@ -57,7 +58,7 @@ export const digiSubSubscribeBody = (
RatePlanData: [
{
RatePlan: {
ProductRatePlanId: '2c92c0f84bbfec8b014bc655f4852d9d',
ProductRatePlanId: catalog.CODE.digiSub.Month,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the existing code to use the const, just so it's a little clearer what's going on.

@@ -0,0 +1,69 @@
import { zuoraDateFormat } from '@modules/zuora/common';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file is mostly a copy of the above one, rather than adding a parameter, but there's definitely scope for more reuse.

Comment on lines +28 to +81
subscribeBody: {
subscribes: {
Account: {
IdentityId__c: string;
InvoiceTemplateId: string;
AutoPay: boolean;
PaymentTerm: string;
CreatedRequestId__c: string;
Name: string;
sfContactId__c: string;
Batch: string;
PaymentGateway: string;
Currency: string;
BcdSettingOption: string;
BillCycleDay: number;
CrmId: string;
};
SubscribeOptions: { GenerateInvoice: boolean; ProcessPayments: boolean };
SubscriptionData: {
RatePlanData: {
RatePlan: { ProductRatePlanId: string };
SubscriptionProductFeatureList: any[];
}[];
Subscription: {
ContractEffectiveDate: string;
ContractAcceptanceDate: string;
TermStartDate: string;
AutoRenew: boolean;
RenewalTerm: number;
InitialTerm: number;
ReaderType__c: string;
TermType: string;
CreatedRequestId__c: string;
InitialTermPeriodType: string;
};
};
PaymentMethod: {
BankTransferAccountName: string;
Type: string;
BankTransferAccountNumber: string;
FirstName: string;
PaymentGateway: string;
BankTransferType: string;
Country: string;
BankCode: string;
LastName: string;
};
BillToContact: {
FirstName: string;
Country: string;
LastName: string;
WorkEmail: string;
};
}[];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type is a bit wild but it doesn't seem to exist in any defined form, it's just from the data. I wasn't sure what to do here?

Copy link
Member

@rupertbates rupertbates Jun 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you felt adventurous you could upgrade this to use the orders api and define the types outlined here:
https://developer.zuora.com/v1-api-reference/api/operation/POST_Order/
in
modules/zuora/orders.ts

Not in this PR though obviously!

@johnduffell johnduffell requested a review from rupertbates June 5, 2024 10:08
Copy link
Member

@tjmw tjmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good!

I left a few minor comments/suggestions to take or leave, no blockers though!

@@ -104,10 +104,10 @@ const getDiscountPreview = async (
discount.productRatePlanId,
);

if (!previewResponse.success || previewResponse.invoiceItems.length != 2) {
if (!previewResponse.success || previewResponse.invoiceItems.length < 2) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly related to your change but what do you think about pulling that 2 out to a const to give it a name?

Suggested change
if (!previewResponse.success || previewResponse.invoiceItems.length < 2) {
const supporterPlusChargeCount = 2;
if (!previewResponse.success || previewResponse.invoiceItems.length < supporterPlusChargeCount) {

Comment on lines +76 to +84
const eligibilityChecker = new EligibilityChecker(catalog);
expect(
dayjs(
eligibilityChecker.getNextBillingDateIfEligible(
billingPreview,
getDiscountableRatePlan(sub),
),
).format('YYYY-MM-DD'),
).toEqual('2024-07-04');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is totally subjective so feel free to ignore! I'm a fan of the arrange-act-assert pattern in tests, just to make clear what's happening at each phase. What do you think about this slight refactoring?

Suggested change
const eligibilityChecker = new EligibilityChecker(catalog);
expect(
dayjs(
eligibilityChecker.getNextBillingDateIfEligible(
billingPreview,
getDiscountableRatePlan(sub),
),
).format('YYYY-MM-DD'),
).toEqual('2024-07-04');
// Arrange
// ..other setup
const eligibilityChecker = new EligibilityChecker(catalog);
// Act
const nextDate = eligibilityChecker.getNextBillingDateIfEligible(
billingPreview,
getDiscountableRatePlan(sub),
);
// Assert
expect(dayjs(nextDate).format('YYYY-MM-DD')).toEqual('2024-07-04');

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep I'm totally on board with that, I never heard that way of describing it but I do try to follow it, it was a just a bit copy and paste at the moment.
I will check the other tests and keep them all consistent with your suggestion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although that's an interesting one for the tests that are expected to throw - because catching an exception is not referentially transparent, it's not easy to pull out the "Act" and the "Assert".
Try is a very scala-y thing so I'm guessing that or similar is not the done thing in TS?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about this? #2309

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although that's an interesting one for the tests that are expected to throw - because catching an exception is not referentially transparent, it's not easy to pull out the "Act" and the "Assert".

Yeah totally, I definitely find it harder to follow AAA in these cases.

Try is a very scala-y thing so I'm guessing that or similar is not the done thing in TS?

True, it's not a language feature, but you do occasionally see abstractions which attempt to provide something similar, it's not very common.

test('Supporter Plus subscriptions are eligible', async () => {
const zuoraClient = await ZuoraClient.create(stage);

console.log('Creating a new S+ subscription');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this console.log still be here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, but I haven't quite got my head around the logging standards in TS yet. The logging does seem very spammy in the tests. This was copied from the "creating a digital subscription" code and follows the pattern.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of logging in the code which actually runs in the lambdas so that you can tell what's happening in Cloudwatch logs, I'd usually remove logging from tests though.

I would like to find a way to lower the logging level when running tests but I haven't really looked into it too closely and it didn't look trivial to do.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a separate point, I'm not sure how useful all these integration tests are. I was experimenting with setting up a new subscription for every test run, carrying out the action under test and then cancelling the subs, but I think it was of limited use ultimately because you need subs which are in a number of different states to test effectively and that's not really possible to do like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough well it's another string to the bow. I was a bit surprised there were no "end to end" style integration tests that pass in the raw payload and expect the output.

I wonder if you've seen my PR here ? : #2307
we can use it as stated to collect requests in PROD for a short time period, but we could also use it to capture tests that we do manually in CODE (and even run locally in DEV as integration tests) and store them permanently. Perhaps not quite as easy to read and update as normal tests, but we could improve that.

@johnduffell johnduffell mentioned this pull request Jun 6, 2024
@johnduffell johnduffell merged commit e31a434 into main Jun 10, 2024
42 checks passed
@johnduffell johnduffell deleted the jd-supporterplus-free-save branch June 10, 2024 09:18
@johnduffell
Copy link
Member Author

thanks for the comments and suggestions, one comment warranted more work at this stage and it's waiting for review in this PR: #2309

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants