Skip to content

Commit 019b8fb

Browse files
committed
feat(auth): Add invoice line items and new payment methods to invoice emails
This pull request - adds invoice line items in the first invoice and subsequent invoice emails - revises format, partials, etc - adds new payment methods Closes PAY-3200
1 parent 9d5e819 commit 019b8fb

25 files changed

Lines changed: 1238 additions & 436 deletions

File tree

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

Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import { ProductConfigurationManager } from '@fxa/shared/cms';
8585
import { reportSentryError, reportSentryMessage } from '../sentry';
8686
import { StripeMapperService } from '@fxa/payments/legacy';
8787
import { VError } from 'verror';
88+
import { SubPlatPaymentMethodType } from '@fxa/payments/customer';
8889

8990
export class SubscriptionManagementPriceInfoError extends VError {
9091
constructor(message: string, priceId: string, currency: string) {
@@ -1661,16 +1662,65 @@ export class StripeHelper extends StripeHelperBase {
16611662
);
16621663
}
16631664

1664-
getPaymentProvider(customer: Stripe.Customer) {
1665+
async getPaymentProvider(
1666+
customer: Stripe.Customer,
1667+
paymentIntentId?: string
1668+
): Promise<SubPlatPaymentMethodType | 'paypal' | 'not_chosen'> {
16651669
const subscription = customer.subscriptions?.data.find((sub) =>
16661670
ACTIVE_SUBSCRIPTION_STATUSES.includes(sub.status)
16671671
);
1668-
if (subscription) {
1669-
return subscription.collection_method === 'send_invoice'
1670-
? 'paypal'
1671-
: 'stripe';
1672+
1673+
if (!subscription) return 'not_chosen';
1674+
1675+
if (subscription.collection_method === 'send_invoice') {
1676+
return 'paypal';
1677+
}
1678+
1679+
let paymentMethod: Stripe.PaymentMethod | null = null;
1680+
1681+
if (paymentIntentId) {
1682+
const paymentIntent =
1683+
await this.stripe.paymentIntents.retrieve(paymentIntentId);
1684+
paymentMethod = await this.getPaymentMethod(
1685+
paymentIntent.payment_method as string
1686+
);
1687+
} else if (typeof subscription.latest_invoice === 'string') {
1688+
const invoice = await this.stripe.invoices.retrieve(
1689+
subscription.latest_invoice
1690+
);
1691+
1692+
if (
1693+
invoice.payment_intent &&
1694+
typeof invoice.payment_intent === 'string'
1695+
) {
1696+
const paymentIntent = await this.stripe.paymentIntents.retrieve(
1697+
invoice.payment_intent
1698+
);
1699+
if (paymentIntent.payment_method) {
1700+
paymentMethod = await this.getPaymentMethod(
1701+
paymentIntent.payment_method as string
1702+
);
1703+
}
1704+
}
1705+
}
1706+
1707+
if (paymentMethod) {
1708+
const walletType = paymentMethod.card?.wallet?.type;
1709+
1710+
if (walletType === 'apple_pay') {
1711+
return SubPlatPaymentMethodType.ApplePay;
1712+
} else if (walletType === 'google_pay') {
1713+
return SubPlatPaymentMethodType.GooglePay;
1714+
} else if (paymentMethod.type === 'link') {
1715+
return SubPlatPaymentMethodType.Link;
1716+
} else if (paymentMethod.type === 'card') {
1717+
return SubPlatPaymentMethodType.Card;
1718+
} else {
1719+
return SubPlatPaymentMethodType.Stripe;
1720+
}
16721721
}
1673-
return 'not_chosen';
1722+
1723+
return SubPlatPaymentMethodType.Stripe;
16741724
}
16751725

16761726
/**
@@ -2433,7 +2483,7 @@ export class StripeHelper extends StripeHelperBase {
24332483
*/
24342484
async extractBillingDetails(customer: Stripe.Customer) {
24352485
const defaultPayment = customer.invoice_settings.default_payment_method;
2436-
const paymentProvider = this.getPaymentProvider(customer);
2486+
const paymentProvider = await this.getPaymentProvider(customer);
24372487

24382488
if (defaultPayment) {
24392489
if (typeof defaultPayment === 'string') {
@@ -2714,7 +2764,7 @@ export class StripeHelper extends StripeHelperBase {
27142764
}
27152765

27162766
// Dig up & expand objects in the invoice that usually come as just IDs
2717-
const { plan } = lineItem;
2767+
const { amount: offeringAmountInCents, plan } = lineItem;
27182768
if (!plan) {
27192769
// No plan is present if this is not a subscription or proration, which
27202770
// should never happen as we only have subscriptions.
@@ -2775,6 +2825,35 @@ export class StripeHelper extends StripeHelperBase {
27752825
);
27762826
}
27772827

2828+
let remainingAmountTotal: number | undefined;
2829+
let unusedAmountTotal = 0;
2830+
2831+
if (invoice.lines.data) {
2832+
const totals = invoice.lines.data.reduce(
2833+
(totals, line) => {
2834+
if (line.proration === true) {
2835+
const amount = line.amount || 0;
2836+
const description = line.description || '';
2837+
2838+
if (amount < 0 && /^Unused/i.test(description)) {
2839+
totals.unusedAmountTotal += amount;
2840+
} else if (amount > 0 && /^Remaining/i.test(description)) {
2841+
totals.remainingAmountTotal =
2842+
(totals.remainingAmountTotal ?? 0) + amount;
2843+
}
2844+
}
2845+
return totals;
2846+
},
2847+
{
2848+
remainingAmountTotal: undefined as number | undefined,
2849+
unusedAmountTotal: 0,
2850+
}
2851+
);
2852+
2853+
remainingAmountTotal = totals.remainingAmountTotal;
2854+
unusedAmountTotal = totals.unusedAmountTotal;
2855+
}
2856+
27782857
const {
27792858
email,
27802859
metadata: { userid: uid },
@@ -2788,10 +2867,17 @@ export class StripeHelper extends StripeHelperBase {
27882867
subtotal: invoiceSubtotalInCents,
27892868
hosted_invoice_url: invoiceLink,
27902869
tax: invoiceTaxAmountInCents,
2870+
total_tax_amounts: invoiceTotalTaxAmounts,
27912871
status: invoiceStatus,
27922872
amount_due: invoiceAmountDueInCents,
2873+
ending_balance: invoiceEndingBalance,
2874+
starting_balance: invoiceStartingBalance,
27932875
} = invoice;
27942876

2877+
const hasExclusiveTax = invoiceTotalTaxAmounts.some(
2878+
(tax) => !tax.inclusive
2879+
);
2880+
27952881
const nextInvoiceDate = lineItem.period.end;
27962882

27972883
const invoiceDiscountAmountInCents =
@@ -2800,10 +2886,6 @@ export class StripeHelper extends StripeHelperBase {
28002886
invoice.total_discount_amounts[0].amount) ||
28012887
null;
28022888

2803-
// Only show the Subtotal when there is a Discount
2804-
const showSubtotal =
2805-
invoiceDiscountAmountInCents || discountType || discountDuration;
2806-
28072889
const { id: planId, nickname: planName } = plan;
28082890
const abbrevPlan = await this.findAbbrevPlanById(planId);
28092891
const productMetadata = this.mergeMetadata(
@@ -2831,25 +2913,37 @@ export class StripeHelper extends StripeHelperBase {
28312913
charge,
28322914
});
28332915

2834-
const payment_provider = this.getPaymentProvider(customer);
2916+
const paymentIntent = invoice.payment_intent as string;
2917+
const payment_provider = await this.getPaymentProvider(
2918+
customer,
2919+
paymentIntent
2920+
);
28352921

28362922
return {
28372923
uid,
28382924
email,
28392925
cardType,
28402926
lastFour,
28412927
payment_provider,
2928+
creditAppliedInCents: invoiceEndingBalance
2929+
? invoiceStartingBalance - invoiceEndingBalance
2930+
: invoiceStartingBalance,
28422931
invoiceAmountDueInCents,
28432932
invoiceLink,
28442933
invoiceNumber,
28452934
invoiceStatus,
2935+
invoiceStartingBalance,
28462936
invoiceTotalInCents,
28472937
invoiceTotalCurrency,
2848-
invoiceSubtotalInCents: showSubtotal ? invoiceSubtotalInCents : null,
2849-
invoiceDiscountAmountInCents,
2938+
invoiceSubtotalInCents,
2939+
invoiceDiscountAmountInCents:
2940+
invoiceDiscountAmountInCents && -1 & invoiceDiscountAmountInCents,
28502941
invoiceTaxAmountInCents,
28512942
invoiceDate: new Date(invoiceDate * 1000),
28522943
nextInvoiceDate: new Date(nextInvoiceDate * 1000),
2944+
offeringPriceInCents: hasExclusiveTax
2945+
? abbrevPlan.amount
2946+
: offeringAmountInCents,
28532947
productId,
28542948
productName,
28552949
planId,
@@ -2858,8 +2952,9 @@ export class StripeHelper extends StripeHelperBase {
28582952
planSuccessActionButtonURL,
28592953
planConfig,
28602954
productMetadata,
2861-
showPaymentMethod: !!invoiceTotalInCents,
2862-
showTaxAmount: false, // Currently we do not want to show tax amounts in emails
2955+
remainingAmountTotalInCents: remainingAmountTotal,
2956+
showTaxAmount: hasExclusiveTax,
2957+
unusedAmountTotalInCents: unusedAmountTotal,
28632958
discountType,
28642959
discountDuration,
28652960
};

packages/fxa-auth-server/lib/routes/subscriptions/paypal-notifications.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,9 @@ export class PayPalNotificationHandler extends PayPalHandler {
224224
(sub) =>
225225
!sub.cancel_at_period_end && ['active', 'past_due'].includes(sub.status)
226226
);
227-
if (
228-
this.stripeHelper.getPaymentProvider(customer) === 'paypal' &&
229-
nextPeriodValidSubscription
230-
) {
227+
const paymentProvider =
228+
await this.stripeHelper.getPaymentProvider(customer);
229+
if (paymentProvider === 'paypal' && nextPeriodValidSubscription) {
231230
const { uid, email } = account;
232231
const subscriptions =
233232
await this.stripeHelper.formatSubscriptionsForEmails(customer);

0 commit comments

Comments
 (0)