Skip to content

Commit 6991aed

Browse files
authored
Merge pull request #18829 from mozilla/fix_11538
fix(auth): Send SubscriptionReplacedEmail for redundant cancellations
2 parents 8920277 + 84e06c5 commit 6991aed

5 files changed

Lines changed: 167 additions & 52 deletions

File tree

packages/fxa-auth-server/lib/payments/stripe.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ export const SUBSCRIPTION_UPDATE_TYPES = {
111111
DOWNGRADE: 'downgrade',
112112
REACTIVATION: 'reactivation',
113113
CANCELLATION: 'cancellation',
114-
REDUNDANT_OVERLAP: 'redundant_overlap',
115114
};
116115

117116
export type FormattedSubscriptionForEmail = {
@@ -3108,6 +3107,30 @@ export class StripeHelper extends StripeHelperBase {
31083107
return baseDetails;
31093108
}
31103109

3110+
/**
3111+
* Helper for extractSubscriptionDeletedEventDetailsForEmail to further
3112+
* extract details in redundant case
3113+
*/
3114+
async extractSubscriptionDeletedEventDetailsForEmail(
3115+
subscription: Stripe.Subscription
3116+
) {
3117+
if (typeof subscription.latest_invoice !== 'string') {
3118+
throw error.internalValidationError(
3119+
'handleSubscriptionDeletedEvent',
3120+
{
3121+
subscriptionId: subscription.id,
3122+
subscriptionInvoiceType: typeof subscription.latest_invoice,
3123+
},
3124+
'Subscription latest_invoice was not a string.'
3125+
);
3126+
}
3127+
const invoice = await this.expandResource<Stripe.Invoice>(
3128+
subscription.latest_invoice,
3129+
INVOICES_RESOURCE
3130+
);
3131+
return this.extractInvoiceDetailsForEmail(invoice);
3132+
}
3133+
31113134
/**
31123135
* Helper for extractSubscriptionUpdateEventDetailsForEmail to further
31133136
* extract details in cancellation case
@@ -3137,20 +3160,6 @@ export class StripeHelper extends StripeHelperBase {
31373160
created: invoiceDate,
31383161
} = upcomingInvoiceWithInvoiceItem || invoice;
31393162

3140-
if (subscription.metadata['autoCancelledRedundantFor']) {
3141-
return {
3142-
updateType: SUBSCRIPTION_UPDATE_TYPES.REDUNDANT_OVERLAP,
3143-
email,
3144-
uid,
3145-
productId,
3146-
planId,
3147-
planEmailIconURL,
3148-
productName,
3149-
productMetadata,
3150-
planConfig,
3151-
};
3152-
}
3153-
31543163
return {
31553164
updateType: SUBSCRIPTION_UPDATE_TYPES.CANCELLATION,
31563165
email,

packages/fxa-auth-server/lib/routes/subscriptions/stripe-webhook.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ export class StripeWebhookHandler extends StripeHandler {
457457
}
458458

459459
const uid = customer.metadata.userid;
460-
const account = await Account.findByUid(uid);
460+
const account = await Account.findByUid(uid, { include: ['emails'] });
461461
if (
462462
// When SubPlat cannot collect a PayPal customer's first payment while
463463
// attempting to subscribe to a product, the subscription is canceled.
@@ -490,6 +490,26 @@ export class StripeWebhookHandler extends StripeHandler {
490490
customer,
491491
subscription
492492
);
493+
494+
if (
495+
account &&
496+
subscription.metadata['redundantCancellation'] === 'true'
497+
) {
498+
const subscriptionDetails =
499+
await this.stripeHelper.extractSubscriptionDeletedEventDetailsForEmail(
500+
subscription
501+
);
502+
503+
await this.mailer.sendSubscriptionReplacedEmail(
504+
account.emails,
505+
account,
506+
{
507+
acceptLanguage: account.locale,
508+
...subscriptionDetails,
509+
}
510+
);
511+
}
512+
493513
await request.emitMetricsEvent('subscription.ended', eventDetails);
494514
} catch (err) {
495515
// FIXME: If the customer was deleted, we don't send an email that their subscription
@@ -1003,7 +1023,6 @@ export class StripeWebhookHandler extends StripeHandler {
10031023
await this.mailer.sendSubscriptionReactivationEmail(...mailParams);
10041024
break;
10051025
case SUBSCRIPTION_UPDATE_TYPES.CANCELLATION:
1006-
case SUBSCRIPTION_UPDATE_TYPES.REDUNDANT_OVERLAP:
10071026
await this.mailer.sendSubscriptionCancellationEmail(...mailParams);
10081027
break;
10091028
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
{
2+
"id": "evt_1GB4fgKb9q6OnNsLBGVcHQST",
3+
"object": "event",
4+
"api_version": "2019-12-03",
5+
"created": 1581450323,
6+
"data": {
7+
"object": {
8+
"id": "sub_GiX3TpyO37x5BW",
9+
"object": "subscription",
10+
"application_fee_percent": null,
11+
"billing_cycle_anchor": 1581449989,
12+
"billing_thresholds": null,
13+
"cancel_at": null,
14+
"cancel_at_period_end": false,
15+
"canceled_at": 1581450078,
16+
"collection_method": "charge_automatically",
17+
"created": 1581449989,
18+
"current_period_end": 1583955589,
19+
"current_period_start": 1581449989,
20+
"customer": "cus_GiX3P6izX4lG5p",
21+
"days_until_due": null,
22+
"default_payment_method": null,
23+
"default_source": null,
24+
"default_tax_rates": [],
25+
"discount": null,
26+
"ended_at": 1581450323,
27+
"items": {
28+
"object": "list",
29+
"data": [
30+
{
31+
"id": "si_GiVbZCo1K4JXci",
32+
"object": "subscription_item",
33+
"billing_thresholds": null,
34+
"created": 1581449989,
35+
"metadata": {},
36+
"plan": {
37+
"id": "plan_FiJ55YK6UYftPa",
38+
"object": "plan",
39+
"active": true,
40+
"aggregate_usage": null,
41+
"amount": 999,
42+
"amount_decimal": "999",
43+
"billing_scheme": "per_unit",
44+
"created": 1567103726,
45+
"currency": "usd",
46+
"interval": "month",
47+
"interval_count": 1,
48+
"livemode": false,
49+
"metadata": {
50+
"successActionButtonURL": "https://stage.guardian.nonprod.cloudops.mozgcp.net/vpn/download?utm_medium=email&utm_source=email&utm_campaign=subscription-download"
51+
},
52+
"nickname": "guardian_monthly",
53+
"product": "prod_FiJ42WCzZNRSbS",
54+
"tiers": null,
55+
"tiers_mode": null,
56+
"transform_usage": null,
57+
"trial_period_days": null,
58+
"usage_type": "licensed"
59+
},
60+
"quantity": 1,
61+
"subscription": "sub_GiVbeUvwkJj6x1",
62+
"tax_rates": []
63+
}
64+
],
65+
"has_more": false,
66+
"total_count": 1,
67+
"url": "/v1/subscription_items?subscription=sub_GiVbeUvwkJj6x1"
68+
},
69+
"latest_invoice": "in_1GB4aHKb9q6OnNsLC9pbVY5a",
70+
"livemode": false,
71+
"metadata": {
72+
"amount": "500",
73+
"autoCancelledRedundantFor": "sub_1RM8vLBVqmGyQTMaqE7jLIRh",
74+
"cancelled_for_customer_at": "1746628021",
75+
"currency": "USD",
76+
"redundantCancellation": "true"
77+
},
78+
"next_pending_invoice_item_invoice": null,
79+
"pending_invoice_item_interval": null,
80+
"pending_setup_intent": null,
81+
"pending_update": null,
82+
"plan": {
83+
"id": "plan_FiJ55YK6UYftPa",
84+
"object": "plan",
85+
"active": true,
86+
"aggregate_usage": null,
87+
"amount": 999,
88+
"amount_decimal": "999",
89+
"billing_scheme": "per_unit",
90+
"created": 1567103726,
91+
"currency": "usd",
92+
"interval": "month",
93+
"interval_count": 1,
94+
"livemode": false,
95+
"metadata": {
96+
"successActionButtonURL": "https://stage.guardian.nonprod.cloudops.mozgcp.net/vpn/download?utm_medium=email&utm_source=email&utm_campaign=subscription-download"
97+
},
98+
"nickname": "guardian_monthly",
99+
"product": "prod_FiJ42WCzZNRSbS",
100+
"tiers": null,
101+
"tiers_mode": null,
102+
"transform_usage": null,
103+
"trial_period_days": null,
104+
"usage_type": "licensed"
105+
},
106+
"quantity": 1,
107+
"schedule": null,
108+
"start_date": 1581449989,
109+
"status": "canceled",
110+
"tax_percent": null,
111+
"trial_end": null,
112+
"trial_start": null
113+
}
114+
},
115+
"livemode": false,
116+
"pending_webhooks": 1,
117+
"request": {
118+
"id": "req_0Hhb1j3UqSE7Pr",
119+
"idempotency_key": null
120+
},
121+
"type": "customer.subscription.deleted"
122+
}

packages/fxa-auth-server/test/local/payments/stripe.js

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6720,32 +6720,6 @@ describe('#integration - StripeHelper', () => {
67206720
showOutstandingBalance: true,
67216721
});
67226722
});
6723-
6724-
it('extracts expected details for a subscription replacement', async () => {
6725-
const event = deepCopy(eventCustomerSubscriptionUpdated);
6726-
const subscription = event.data.object;
6727-
subscription.metadata = {
6728-
autoCancelledRedundantFor: 'REPLACEMENT_SUB_ID',
6729-
};
6730-
const result =
6731-
await stripeHelper.extractSubscriptionUpdateCancellationDetailsForEmail(
6732-
event.data.object,
6733-
expectedBaseUpdateDetails,
6734-
mockInvoice,
6735-
undefined
6736-
);
6737-
assert.deepEqual(result, {
6738-
updateType: SUBSCRIPTION_UPDATE_TYPES.REDUNDANT_OVERLAP,
6739-
email,
6740-
uid,
6741-
productId,
6742-
planId,
6743-
planConfig: {},
6744-
planEmailIconURL: productIconURLNew,
6745-
productName,
6746-
productMetadata: expectedBaseUpdateDetails.productMetadata,
6747-
});
6748-
});
67496723
});
67506724
});
67516725

packages/fxa-auth-server/test/local/routes/subscriptions/stripe-webhooks.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2239,8 +2239,6 @@ describe('StripeWebhookHandler', () => {
22392239
'sendSubscriptionReactivationEmail',
22402240
[SUBSCRIPTION_UPDATE_TYPES.CANCELLATION]:
22412241
'sendSubscriptionCancellationEmail',
2242-
[SUBSCRIPTION_UPDATE_TYPES.REDUNDANT_OVERLAP]:
2243-
'sendSubscriptionCancellationEmail',
22442242
}[updateType];
22452243

22462244
assert.calledWith(
@@ -2279,13 +2277,6 @@ describe('StripeWebhookHandler', () => {
22792277
SUBSCRIPTION_UPDATE_TYPES.CANCELLATION
22802278
)
22812279
);
2282-
2283-
it(
2284-
'sends a replaced email on subscription replacement',
2285-
commonSendSubscriptionUpdatedEmailTest(
2286-
SUBSCRIPTION_UPDATE_TYPES.REDUNDANT_OVERLAP
2287-
)
2288-
);
22892280
});
22902281

22912282
describe('sendSubscriptionDeletedEmail', () => {

0 commit comments

Comments
 (0)