From 85ed4b586a9556edbf60b25f50aa08afa486009c Mon Sep 17 00:00:00 2001 From: elizabeth-ilina Date: Wed, 28 Jan 2026 11:54:50 -0500 Subject: [PATCH 1/3] feat(payments-next): Update subscription reminder emails on discount change. Because: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Currently, the SubscriptionRenewalReminder emails don't handle cases when a customer’s discount is being replaced by a different discount. This commit: * Updates subscription reminder emails to handle these situations. Closes #[PAY-3485](https://mozilla-hub.atlassian.net/browse/PAY-3485) --- .../templates/subscriptionRenewalReminder/en.ftl | 1 + .../subscriptionRenewalReminder/index.mjml | 8 ++++++++ .../templates/subscriptionRenewalReminder/index.ts | 3 ++- .../templates/subscriptionRenewalReminder/index.txt | 8 +++++--- .../lib/payments/subscription-reminders.ts | 13 ++++++++++++- packages/fxa-auth-server/lib/senders/email.js | 1 + .../lib/senders/emails/templates/_versions.json | 2 +- .../templates/subscriptionRenewalReminder/en.ftl | 1 + .../subscriptionRenewalReminder/index.mjml | 8 ++++++++ .../templates/subscriptionRenewalReminder/index.txt | 8 +++++--- 10 files changed, 44 insertions(+), 9 deletions(-) diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/en.ftl index 12657f864fb..6a7e0614efa 100644 --- a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/en.ftl +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/en.ftl @@ -8,6 +8,7 @@ subscriptionRenewalReminder-content-greeting = Dear { $productName } customer, # Variables # $reminderLength (String) - The number of days until the current subscription is set to automatically renew, e.g. 14 subscriptionRenewalReminder-content-intro = Your current subscription is set to automatically renew in { $reminderLength } days. +subscriptionRenewalReminder-content-discount-change = Your next invoice reflects a change in pricing, as a previous discount has ended and a new discount has been applied. subscriptionRenewalReminder-content-discount-ending = Because a previous discount has ended, your subscription will renew at the standard price. # Variables # $invoiceTotal (String) - The amount of the subscription invoice, including currency, e.g. $10.00 diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.mjml index 9238e2016f1..1795eb612b7 100644 --- a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.mjml +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.mjml @@ -19,11 +19,19 @@ <% if (locals.hadDiscount) { %> + <% if (locals.hasRenewalDiscount) { %> + + + Your next invoice reflects a change in pricing, as a previous discount has ended and a new discount has been applied. + + + <% } else { %> Because a previous discount has ended, your subscription will renew at the standard price. + <% } %> <% } %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.ts index 6a6d1c68f63..3ed3bd16b24 100644 --- a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.ts +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.ts @@ -15,10 +15,11 @@ export type TemplateData = SubscriptionSupportContactTemplateData & subscriptionSupportUrl: string; updateBillingUrl: string; hadDiscount?: boolean; + hasRenewalDiscount?: boolean; }; export const template = 'subscriptionRenewalReminder'; -export const version = 3; +export const version = 4; export const layout = 'subscription'; export const includes = { subject: { diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.txt index 3705d2e5502..228774f66c2 100644 --- a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.txt +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.txt @@ -7,12 +7,14 @@ subscriptionRenewalReminder-content-greeting = "Dear <%- productName %> customer subscriptionRenewalReminder-content-intro = "Your current subscription is set to automatically renew in <%- reminderLength %> days." <% if (locals.hadDiscount) { %> + <% if (locals.hasRenewalDiscount) { %> +subscriptionRenewalReminder-content-discount-change = "Your next invoice reflects a change in pricing, as a previous discount has ended and a new discount has been applied." + <% } else { %> subscriptionRenewalReminder-content-discount-ending = "Because a previous discount has ended, your subscription will renew at the standard price." + <% } %> +<% } %> subscriptionRenewalReminder-content-charge = "At that time, Mozilla will renew your <%- planIntervalCount %> <%- planInterval %> subscription and a charge of <%- invoiceTotal %> will be applied to the payment method on your account." -<% } else { %> -subscriptionRenewalReminder-content-charge = "At that time, Mozilla will renew your <%- planIntervalCount %> <%- planInterval %> subscription and a charge of <%- invoiceTotal %> will be applied to the payment method on your account." -<% } %> <%- include ('/partials/subscriptionUpdateBillingEnsure/index.txt') %> diff --git a/packages/fxa-auth-server/lib/payments/subscription-reminders.ts b/packages/fxa-auth-server/lib/payments/subscription-reminders.ts index d2e3a82b517..bc7e6f93ccd 100644 --- a/packages/fxa-auth-server/lib/payments/subscription-reminders.ts +++ b/packages/fxa-auth-server/lib/payments/subscription-reminders.ts @@ -268,7 +268,6 @@ export class SubscriptionReminders { /** * Determine if a discount is ending by checking that the subscription currently * has a discount but the upcoming invoice does not. - * TODO in PAY-3485: Handle the case where the discount changes without ending. */ private hasDiscountEnding( subscription: Stripe.Subscription, @@ -277,6 +276,15 @@ export class SubscriptionReminders { return !!subscription.discount && !invoicePreview.discount; } + /** + * Determine if the upcoming invoice has a discount. + */ + private hasRenewalDiscount( + invoicePreview: Stripe.UpcomingInvoice + ): boolean { + return !!invoicePreview.discount; + } + /** * Send out a renewal reminder email if we haven't already sent one. */ @@ -341,6 +349,8 @@ export class SubscriptionReminders { // Detect if discount is ending const hadDiscount = this.hasDiscountEnding(subscription, invoicePreview); + // Detect if renewal has a discount + const hasRenewalDiscount = this.hasRenewalDiscount(invoicePreview); await this.mailer.sendSubscriptionRenewalReminderEmail( account.emails, @@ -359,6 +369,7 @@ export class SubscriptionReminders { productMetadata: formattedSubscription.productMetadata, planConfig: formattedSubscription.planConfig, hadDiscount, + hasRenewalDiscount, } ); await this.updateSentEmail( diff --git a/packages/fxa-auth-server/lib/senders/email.js b/packages/fxa-auth-server/lib/senders/email.js index 61cd303f6be..ee6485740d3 100644 --- a/packages/fxa-auth-server/lib/senders/email.js +++ b/packages/fxa-auth-server/lib/senders/email.js @@ -3253,6 +3253,7 @@ module.exports = function (log, config, bounces, statsd) { message.acceptLanguage ), hadDiscount: message.hadDiscount || false, + hasRenewalDiscount: message.hasRenewalDiscount || false, }, }); }; diff --git a/packages/fxa-auth-server/lib/senders/emails/templates/_versions.json b/packages/fxa-auth-server/lib/senders/emails/templates/_versions.json index d1555bf7a97..03a6be1e370 100644 --- a/packages/fxa-auth-server/lib/senders/emails/templates/_versions.json +++ b/packages/fxa-auth-server/lib/senders/emails/templates/_versions.json @@ -1,6 +1,6 @@ { "subscriptionReactivation": 2, - "subscriptionRenewalReminder": 3, + "subscriptionRenewalReminder": 4, "subscriptionEndingReminder": 1, "subscriptionUpgrade": 7, "subscriptionDowngrade": 2, diff --git a/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/en.ftl b/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/en.ftl index 12657f864fb..6a7e0614efa 100644 --- a/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/en.ftl +++ b/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/en.ftl @@ -8,6 +8,7 @@ subscriptionRenewalReminder-content-greeting = Dear { $productName } customer, # Variables # $reminderLength (String) - The number of days until the current subscription is set to automatically renew, e.g. 14 subscriptionRenewalReminder-content-intro = Your current subscription is set to automatically renew in { $reminderLength } days. +subscriptionRenewalReminder-content-discount-change = Your next invoice reflects a change in pricing, as a previous discount has ended and a new discount has been applied. subscriptionRenewalReminder-content-discount-ending = Because a previous discount has ended, your subscription will renew at the standard price. # Variables # $invoiceTotal (String) - The amount of the subscription invoice, including currency, e.g. $10.00 diff --git a/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.mjml b/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.mjml index b3eda94dbf8..af2c1e1a601 100644 --- a/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.mjml +++ b/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.mjml @@ -19,11 +19,19 @@ <% if (hadDiscount) { %> + <% if (hasRenewalDiscount) { %> + + + Your next invoice reflects a change in pricing, as a previous discount has ended and a new discount has been applied. + + + <% } else { %> Because a previous discount has ended, your subscription will renew at the standard price. + <% } %> <% } %> diff --git a/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.txt b/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.txt index b2ebcd9b19a..faa15f5a677 100644 --- a/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.txt +++ b/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.txt @@ -7,12 +7,14 @@ subscriptionRenewalReminder-content-greeting = "Dear <%- productName %> customer subscriptionRenewalReminder-content-intro = "Your current subscription is set to automatically renew in <%- reminderLength %> days." <% if (hadDiscount) { %> + <% if (hasRenewalDiscount) { %> +subscriptionRenewalReminder-content-discount-change = "Your next invoice reflects a change in pricing, as a previous discount has ended and a new discount has been applied." + <% } else { %> subscriptionRenewalReminder-content-discount-ending = "Because a previous discount has ended, your subscription will renew at the standard price." + <% } %> +<% } %> subscriptionRenewalReminder-content-charge = "At that time, Mozilla will renew your <%- planIntervalCount %> <%- planInterval %> subscription and a charge of <%- invoiceTotal %> will be applied to the payment method on your account." -<% } else { %> -subscriptionRenewalReminder-content-charge = "At that time, Mozilla will renew your <%- planIntervalCount %> <%- planInterval %> subscription and a charge of <%- invoiceTotal %> will be applied to the payment method on your account." -<% } %> <%- include ('/partials/subscriptionUpdateBillingEnsure/index.txt') %> From e0c5c8a1e6b6f911f483321bdbbc55dd1ed9f835 Mon Sep 17 00:00:00 2001 From: elizabeth-ilina Date: Wed, 28 Jan 2026 11:55:03 -0500 Subject: [PATCH 2/3] feat(payments-next): Because: * This commit: * Closes # --- .../index.stories.ts | 23 +++++++++++++++++++ .../index.stories.ts | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.stories.ts index 4ecb3b0065a..e729f88d017 100644 --- a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.stories.ts +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.stories.ts @@ -19,6 +19,7 @@ const data = { subscriptionSupportUrl: 'http://localhost:3030/support', updateBillingUrl: 'http://localhost:3030/subscriptions', hadDiscount: false, + hasRenewalDiscount: false, }; const createStory = subplatStoryWithProps( @@ -58,6 +59,28 @@ export const YearlyPlanDiscountEnding = createStory( reminderLength: '15', invoiceTotal: '$199.99', hadDiscount: true, + hasRenewalDiscount: false, }, 'Yearly Plan - Discount Ending' ); + +export const MonthlyPlanDiscountChanging = createStory( + { + hadDiscount: true, + hasRenewalDiscount: true, + invoiceTotal: '$14.00', + }, + 'Monthly Plan - Discount Changing' +); + +export const YearlyPlanDiscountChanging = createStory( + { + planInterval: 'year', + planIntervalCount: '1', + reminderLength: '15', + invoiceTotal: '$139.99', + hadDiscount: true, + hasRenewalDiscount: true, + }, + 'Yearly Plan - Discount Changing' +); diff --git a/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.stories.ts b/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.stories.ts index 1d439259787..b6c380fde4e 100644 --- a/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.stories.ts +++ b/packages/fxa-auth-server/lib/senders/emails/templates/subscriptionRenewalReminder/index.stories.ts @@ -21,6 +21,7 @@ const createStory = subplatStoryWithProps( subscriptionSupportUrl: 'http://localhost:3030/support', updateBillingUrl: 'http://localhost:3030/subscriptions', hadDiscount: false, + hasRenewalDiscount: false, } ); @@ -54,6 +55,28 @@ export const YearlyPlanDiscountEnding = createStory( reminderLength: '15', invoiceTotal: '$199.99', hadDiscount: true, + hasRenewalDiscount: false, }, 'Yearly Plan - Discount Ending' ); + +export const MonthlyPlanDiscountChanging = createStory( + { + hadDiscount: true, + hasRenewalDiscount: true, + invoiceTotal: '$14.00', + }, + 'Monthly Plan - Discount Changing' +); + +export const YearlyPlanDiscountChanging = createStory( + { + planInterval: 'year', + planIntervalCount: '1', + reminderLength: '15', + invoiceTotal: '$139.99', + hadDiscount: true, + hasRenewalDiscount: true, + }, + 'Yearly Plan - Discount Changing' +); From 1e2a7632c669539c788932c058e6c025cc3622da Mon Sep 17 00:00:00 2001 From: elizabeth-ilina Date: Thu, 29 Jan 2026 11:26:59 -0500 Subject: [PATCH 3/3] fix(payments-next): Fix churn offer page Because: * Cancel interstitial offer page is going to notFound() This commit: * Fixes GetInterstitialOfferContentActionResult to make reason optional --- .../validators/GetInterstitialOfferContentActionResult.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/payments/ui/src/lib/nestapp/validators/GetInterstitialOfferContentActionResult.ts b/libs/payments/ui/src/lib/nestapp/validators/GetInterstitialOfferContentActionResult.ts index 120b6bc1a72..1e79da43bb4 100644 --- a/libs/payments/ui/src/lib/nestapp/validators/GetInterstitialOfferContentActionResult.ts +++ b/libs/payments/ui/src/lib/nestapp/validators/GetInterstitialOfferContentActionResult.ts @@ -45,7 +45,8 @@ export class GetInterstitialOfferContentActionResult { pageContent!: PageContent | null; @IsString() - reason!: string; + @IsOptional() + reason?: string | null; @IsOptional() @IsString()