Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/payments/customer/src/lib/subscription.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export class SubscriptionManager {

const subscription = await this.retrieve(subscriptionId);
return subscription.discounts.some(
(discount) => discount.source?.coupon.id === stripeCouponId
(discount) => discount.source?.coupon?.id === stripeCouponId
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function stripeInvoiceToInvoicePreviewDTO(
discountAmount,
subtotal: invoice.subtotal,
discountEnd: invoice.discounts[0]?.end,
discountType: invoice.discounts[0]?.source?.coupon.duration,
discountType: invoice.discounts[0]?.source?.coupon?.duration,
number: invoice.number,
paypalTransactionId:
invoice.metadata?.[STRIPE_INVOICE_METADATA.PaypalTransactionId],
Expand All @@ -82,7 +82,7 @@ export function stripeInvoiceToInvoicePreviewDTO(
creditApplied: invoice.ending_balance
? invoice.starting_balance - invoice.ending_balance
: invoice.starting_balance,
promotionName: invoice.discounts[0]?.source?.coupon.name,
promotionName: invoice.discounts[0]?.source?.coupon?.name,
remainingAmountTotal,
startingBalance: invoice.starting_balance,
totalExcludingTax: invoice.total_excluding_tax,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ export class SubscriptionManagementService {
} = upcomingInvoice;

const nextPromotionName =
subscription.discounts[0]?.source?.coupon.name ?? null;
subscription.discounts[0]?.source?.coupon?.name ?? null;

const totalExclusiveTax = taxAmounts
.filter((tax) => !tax.inclusive)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class PaymentsMetricsAggregatorService {

return {
customerId: customer?.id,
couponCode: subscription?.discounts[0]?.source?.coupon.id,
couponCode: subscription?.discounts[0]?.source?.coupon?.id,
currency: customer?.currency || undefined,
taxAddress: taxAddress,
productId: price?.product,
Expand Down
23 changes: 21 additions & 2 deletions libs/payments/stripe/src/lib/stripe.client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ describe('StripeClient', () => {
expect(result).toEqual(mockResponse);
});

it('expands confirmation_secret, discounts, and nested tax rates', async () => {
it('expands confirmation_secret, discount coupons, and nested tax rates', async () => {
const mockInvoice = StripeInvoiceFactory();
const mockResponse = StripeResponseFactory(mockInvoice);

Expand All @@ -328,7 +328,7 @@ describe('StripeClient', () => {
expect(mockStripeInvoicesRetrieve).toHaveBeenCalledWith(mockInvoice.id, {
expand: [
'confirmation_secret',
'discounts',
'discounts.source.coupon',
'lines.data.taxes.tax_rate_details.tax_rate',
'total_taxes.tax_rate_details.tax_rate',
],
Expand All @@ -350,6 +350,25 @@ describe('StripeClient', () => {

expect(result).toEqual(mockResponse);
});

it('expands discount coupons and nested tax rates', async () => {
const mockCustomer = StripeCustomerFactory();
const mockInvoice = StripeUpcomingInvoiceFactory();
const mockResponse = StripeResponseFactory(mockInvoice);

mockStripeCreatePreviewInvoice.mockResolvedValue(mockResponse);

await stripeClient.invoicesCreatePreview({ customer: mockCustomer.id });

expect(mockStripeCreatePreviewInvoice).toHaveBeenCalledWith({
customer: mockCustomer.id,
expand: [
'discounts.source.coupon',
'lines.data.taxes.tax_rate_details.tax_rate',
'total_taxes.tax_rate_details.tax_rate',
],
});
});
});

describe('invoicesFinalizeInvoice', () => {
Expand Down
4 changes: 2 additions & 2 deletions libs/payments/stripe/src/lib/stripe.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export class StripeClient {
...params,
expand: [
'confirmation_secret',
'discounts',
'discounts.source.coupon',
'lines.data.taxes.tax_rate_details.tax_rate',
'total_taxes.tax_rate_details.tax_rate',
],
Expand All @@ -251,7 +251,7 @@ export class StripeClient {
const result = await this.stripe.invoices.createPreview({
...params,
expand: [
'discounts',
'discounts.source.coupon',
'lines.data.taxes.tax_rate_details.tax_rate',
'total_taxes.tax_rate_details.tax_rate',
],
Expand Down
5 changes: 4 additions & 1 deletion libs/payments/stripe/src/lib/stripe.client.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ export type StripeDiscount = NegotiateExpanded<
DeepOverride<
Stripe.Discount,
{
source: DeepOverride<Stripe.Discount.Source, { coupon: StripeCoupon }>;
source: DeepOverride<
Stripe.Discount.Source,
{ coupon: StripeCoupon | null }
>;
}
>,
'customer' | 'promotion_code'
Expand Down
16 changes: 12 additions & 4 deletions packages/fxa-auth-server/lib/payments/stripe-firestore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,9 @@ describe('StripeFirestore', () => {
await stripeFirestore.fetchAndInsertInvoice(invoiceId, eventTime);

expect(stripe.invoices.retrieve).toHaveBeenCalledTimes(1);
expect(stripe.invoices.retrieve).toHaveBeenCalledWith(invoiceId);
expect(stripe.invoices.retrieve).toHaveBeenCalledWith(invoiceId, {
expand: ['discounts.source.coupon'],
});
expect(
stripeFirestore.customerCollectionDbRef.where
).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -718,7 +720,9 @@ describe('StripeFirestore', () => {

expect(result).toEqual(mockInvoice);
expect(stripe.invoices.retrieve).toHaveBeenCalledTimes(1);
expect(stripe.invoices.retrieve).toHaveBeenCalledWith(invoiceId);
expect(stripe.invoices.retrieve).toHaveBeenCalledWith(invoiceId, {
expand: ['discounts.source.coupon'],
});
expect(
stripeFirestore.customerCollectionDbRef.where
).toHaveBeenCalledTimes(1);
Expand All @@ -742,7 +746,9 @@ describe('StripeFirestore', () => {

expect(result).toEqual(mockInvoiceWithoutSubscription);
expect(stripe.invoices.retrieve).toHaveBeenCalledTimes(1);
expect(stripe.invoices.retrieve).toHaveBeenCalledWith(invoiceId);
expect(stripe.invoices.retrieve).toHaveBeenCalledWith(invoiceId, {
expand: ['discounts.source.coupon'],
});
expect(
stripeFirestore.customerCollectionDbRef.where
).toHaveBeenCalledTimes(0);
Expand Down Expand Up @@ -802,7 +808,9 @@ describe('StripeFirestore', () => {

expect(result).toEqual(mockInvoice);
expect(stripe.invoices.retrieve).toHaveBeenCalledTimes(1);
expect(stripe.invoices.retrieve).toHaveBeenCalledWith(invoiceId);
expect(stripe.invoices.retrieve).toHaveBeenCalledWith(invoiceId, {
expand: ['discounts.source.coupon'],
});
expect(
stripeFirestore.customerCollectionDbRef.where
).toHaveBeenCalledTimes(1);
Expand Down
67 changes: 67 additions & 0 deletions packages/fxa-auth-server/lib/payments/stripe-formatter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,60 @@ describe('stripeInvoiceToFirstInvoicePreviewDTO', () => {
);
});

it('keeps the discount amount when the invoice discount is an unexpanded id', () => {
const invoicePreview = deepCopy(previewInvoiceWithDiscountAndTax);
invoicePreview.discounts = ['di_1234567890abcdef'];

const invoice = stripeInvoiceToFirstInvoicePreviewDTO([
invoicePreview,
undefined,
]);

expect(invoice.discount).toEqual({
amount: previewInvoiceWithDiscountAndTax.total_discount_amounts[0].amount,
amount_off: null,
percent_off: null,
});
});

it('keeps the discount amount when the discount has no source', () => {
const invoicePreview = deepCopy(previewInvoiceWithDiscountAndTax);
invoicePreview.discounts = [
{
id: 'di_1234567890abcdef',
object: 'discount',
coupon: { id: 'co_1234567890abcdef', percent_off: 20 },
},
];

const invoice = stripeInvoiceToFirstInvoicePreviewDTO([
invoicePreview,
undefined,
]);

expect(invoice.discount).toEqual({
amount: previewInvoiceWithDiscountAndTax.total_discount_amounts[0].amount,
amount_off: null,
percent_off: null,
});
});

it('keeps the discount amount when the coupon on the discount source is an unexpanded id', () => {
const invoicePreview = deepCopy(previewInvoiceWithDiscountAndTax);
invoicePreview.discounts[0].source.coupon = 'co_1234567890abcdef';

const invoice = stripeInvoiceToFirstInvoicePreviewDTO([
invoicePreview,
undefined,
]);

expect(invoice.discount).toEqual({
amount: previewInvoiceWithDiscountAndTax.total_discount_amounts[0].amount,
amount_off: null,
percent_off: null,
});
});

it('formats an invoice where tax display_name is an empty string', () => {
const invoicePreview = deepCopy(previewInvoiceWithTax);
invoicePreview.total_taxes[0].tax_rate_details.tax_rate.display_name = '';
Expand Down Expand Up @@ -161,4 +215,17 @@ describe('stripeInvoiceToLatestInvoiceItemsDTO', () => {
previewInvoiceWithDiscountAndTax.discounts[0].source.coupon.percent_off
);
});

it('keeps the discount amount when the invoice discount is an unexpanded id', () => {
const invoicePreview = deepCopy(previewInvoiceWithDiscountAndTax);
invoicePreview.discounts = ['di_1234567890abcdef'];

const invoice = stripeInvoiceToLatestInvoiceItemsDTO(invoicePreview);

expect(invoice.discount).toEqual({
amount: previewInvoiceWithDiscountAndTax.total_discount_amounts[0].amount,
amount_off: null,
percent_off: null,
});
});
});
13 changes: 8 additions & 5 deletions packages/fxa-auth-server/lib/payments/stripe-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,16 @@ export function stripeInvoiceToFirstInvoicePreviewDTO(
}

// Add discount if it exists
const discount = invoice[0].discounts?.[0];
const coupon = discount?.source.coupon;
if (coupon && invoice[0].total_discount_amounts) {
const firstDiscount = invoice[0].discounts?.[0];
const discount =
typeof firstDiscount === 'object' ? firstDiscount : undefined;
const rawCoupon = discount?.source?.coupon;
const coupon = typeof rawCoupon === 'object' ? rawCoupon : undefined;
if (invoice[0].total_discount_amounts?.length) {
invoicePreview.discount = {
amount: invoice[0].total_discount_amounts[0].amount,
amount_off: coupon.amount_off,
percent_off: coupon.percent_off,
amount_off: coupon?.amount_off ?? null,
percent_off: coupon?.percent_off ?? null,
};
}

Expand Down
Loading
Loading