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
12 changes: 12 additions & 0 deletions .yarn/patches/stripe-npm-22.3.1-291b1ac8f8.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
diff --git a/package.json b/package.json
index 090d03f906fc7f3932fce763204c76a269575db0..3fc59877772ce64884e565cac823f7a0c3624d87 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"node": ">=18"
},
"main": "cjs/stripe.cjs.node.js",
+ "types": "./esm/stripe.esm.node.d.ts",
"devDependencies": {
"@types/chai": "^4.3.4",
"@types/chai-as-promised": "^7.1.5",
2 changes: 1 addition & 1 deletion libs/payments/api-server/src/lib/util/mapPriceInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const mapPriceInfo = (
amount =
unit_amount ??
(unit_amount_decimal != null
? Math.round(parseFloat(unit_amount_decimal))
? Math.round(parseFloat(unit_amount_decimal.toString()))
: null);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ describe('transformToWebSubscription', () => {
id: 'sub_w',
status: 'active',
created: 1,
current_period_start: 2,
current_period_end: 3,
cancel_at_period_end: false,
ended_at: null,
items: {
object: 'list',
data: [
StripeSubscriptionItemFactory({
current_period_start: 2,
current_period_end: 3,
price: { ...price, product: 'prod_w' },
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { getPriceFromSubscription } from '@fxa/payments/customer';
import {
getPriceFromSubscription,
retrieveSubscriptionItem,
} from '@fxa/payments/customer';
import type { StripeSubscription } from '@fxa/payments/stripe';

import type {
Expand All @@ -21,11 +24,12 @@ export const transformToWebSubscription = (
`Subscription has no product id (subscriptionId=${sub.id})`
);
}
const subscriptionItem = retrieveSubscriptionItem(sub);
return {
_subscription_type: 'web',
created: sub.created,
current_period_end: sub.current_period_end,
current_period_start: sub.current_period_start,
current_period_end: subscriptionItem.current_period_end,
current_period_start: subscriptionItem.current_period_start,
cancel_at_period_end: sub.cancel_at_period_end,
end_at: sub.ended_at ?? null,
plan_id: priceId,
Expand Down
2 changes: 1 addition & 1 deletion libs/payments/cart/src/lib/cart.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { faker } from '@faker-js/faker';
import { Test } from '@nestjs/testing';
import { Stripe } from 'stripe';
import Stripe from 'stripe';

import {
EligibilityManager,
Expand Down
112 changes: 94 additions & 18 deletions libs/payments/cart/src/lib/checkout.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ import {
SubPlatPaymentMethodType,
} from '@fxa/payments/customer';
import {
StripeApiListFactory,
StripeClient,
StripeConfig,
StripeCustomerFactory,
StripeInvoiceFactory,
StripeInvoicePaymentFactory,
StripePaymentIntentFactory,
StripePaymentMethodFactory,
StripePriceFactory,
Expand Down Expand Up @@ -899,7 +901,10 @@ describe('CheckoutService', () => {
const mockConfirmationToken = StripeConfirmationTokenFactory();
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: mockPaymentIntent.id,
confirmation_secret: {
type: 'payment_intent',
client_secret: `${mockPaymentIntent.id}_secret_test`,
},
})
);
const mockEligibilityResult = SubscriptionEligibilityResultFactory({
Expand Down Expand Up @@ -979,7 +984,7 @@ describe('CheckoutService', () => {
automatic_tax: {
enabled: true,
},
promotion_code: mockPromotionCode.id,
discounts: [{ promotion_code: mockPromotionCode.id }],
items: [
{
price: mockPrice.id, // TODO: fetch price from cart after FXA-8893
Expand Down Expand Up @@ -1022,7 +1027,7 @@ describe('CheckoutService', () => {

it('calls calls paymentIntentManager.confirm', async () => {
expect(paymentIntentManager.confirm).toHaveBeenCalledWith(
mockInvoice.payment_intent,
mockPaymentIntent.id,
{ confirmation_token: mockConfirmationToken.id, off_session: false }
);
});
Expand Down Expand Up @@ -1051,7 +1056,14 @@ describe('CheckoutService', () => {
it('handles zero-amount invoices by creating a setup intent', async () => {
const freeInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: mockPaymentIntent.id,
payments: StripeApiListFactory([
StripeInvoicePaymentFactory({
payment: {
type: 'payment_intent',
payment_intent: mockPaymentIntent.id,
},
}),
]),
amount_due: 0,
status: 'paid',
})
Expand All @@ -1077,6 +1089,26 @@ describe('CheckoutService', () => {
).resolves.not.toThrow();
});

it('creates the subscription without a discounts key when there is no coupon', async () => {
jest.spyOn(checkoutService, 'prePaySteps').mockResolvedValue({
...mockPrePayStepsResult,
promotionCode: undefined,
});

await checkoutService.payWithStripe(
mockCart,
mockConfirmationToken.id,
mockAttributionData,
mockRequestArgs,
mockCart.uid
);

expect(subscriptionManager.create).toHaveBeenCalledWith(
expect.not.objectContaining({ discounts: expect.anything() }),
{ idempotencyKey: mockCart.id }
);
});

describe('upgrade', () => {
const mockEligibilityResult =
SubscriptionEligibilityUpgradeDowngradeResultFactory({
Expand Down Expand Up @@ -1131,7 +1163,7 @@ describe('CheckoutService', () => {
describe('calls setup intent manager', () => {
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
payments: StripeApiListFactory([]),
amount_due: 0,
})
);
Expand Down Expand Up @@ -1186,7 +1218,7 @@ describe('CheckoutService', () => {
);
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
payments: StripeApiListFactory([]),
amount_due: 0,
})
);
Expand Down Expand Up @@ -1321,7 +1353,10 @@ describe('CheckoutService', () => {
const mockConfirmationToken = StripeConfirmationTokenFactory();
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: mockPaymentIntent.id,
confirmation_secret: {
type: 'payment_intent',
client_secret: `${mockPaymentIntent.id}_secret_test`,
},
})
);
const mockEligibilityResult = SubscriptionEligibilityResultFactory({
Expand Down Expand Up @@ -1465,7 +1500,14 @@ describe('CheckoutService', () => {
);
const mockZeroInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: mockPaymentIntent.id,
payments: StripeApiListFactory([
StripeInvoicePaymentFactory({
payment: {
type: 'payment_intent',
payment_intent: mockPaymentIntent.id,
},
}),
]),
amount_due: 0,
})
);
Expand Down Expand Up @@ -1587,7 +1629,7 @@ describe('CheckoutService', () => {
);
const mockTrialInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
payments: StripeApiListFactory([]),
amount_due: 0,
})
);
Expand Down Expand Up @@ -1697,7 +1739,10 @@ describe('CheckoutService', () => {
);
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: mockPaymentIntent.id,
confirmation_secret: {
type: 'payment_intent',
client_secret: `${mockPaymentIntent.id}_secret_test`,
},
})
);

Expand Down Expand Up @@ -1821,7 +1866,7 @@ describe('CheckoutService', () => {
);
const mockTrialInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
payments: StripeApiListFactory([]),
amount_due: 0,
})
);
Expand Down Expand Up @@ -1897,7 +1942,7 @@ describe('CheckoutService', () => {
);
const mockTrialInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
payments: StripeApiListFactory([]),
amount_due: 0,
})
);
Expand Down Expand Up @@ -1971,7 +2016,10 @@ describe('CheckoutService', () => {
);
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: mockPaymentIntent.id,
confirmation_secret: {
type: 'payment_intent',
client_secret: `${mockPaymentIntent.id}_secret_test`,
},
})
);

Expand Down Expand Up @@ -2072,7 +2120,10 @@ describe('CheckoutService', () => {
);
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: mockPaymentIntent.id,
confirmation_secret: {
type: 'payment_intent',
client_secret: `${mockPaymentIntent.id}_secret_test`,
},
})
);

Expand Down Expand Up @@ -2121,7 +2172,7 @@ describe('CheckoutService', () => {
);
const mockTrialInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
payments: StripeApiListFactory([]),
amount_due: 0,
})
);
Expand Down Expand Up @@ -2195,7 +2246,10 @@ describe('CheckoutService', () => {
);
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: mockPaymentIntent.id,
confirmation_secret: {
type: 'payment_intent',
client_secret: `${mockPaymentIntent.id}_secret_test`,
},
})
);
const mockEligibilityResult = SubscriptionEligibilityResultFactory({
Expand Down Expand Up @@ -2436,7 +2490,7 @@ describe('CheckoutService', () => {
},
collection_method: 'send_invoice',
days_until_due: 1,
promotion_code: mockPromotionCode.id,
discounts: [{ promotion_code: mockPromotionCode.id }],
items: [
{
price: mockPrice.id,
Expand All @@ -2463,6 +2517,28 @@ describe('CheckoutService', () => {
);
});

it('creates the subscription without a discounts key when there is no coupon', async () => {
jest.spyOn(checkoutService, 'prePaySteps').mockResolvedValue(
PrePayStepsResultFactory({
...mockPrePayStepsResult,
promotionCode: undefined,
})
);

await checkoutService.payWithPaypal(
mockCart,
mockAttributionData,
mockRequestArgs,
mockCart.uid,
mockToken
);

expect(subscriptionManager.create).toHaveBeenCalledWith(
expect.not.objectContaining({ discounts: expect.anything() }),
{ idempotencyKey: mockCart.id }
);
});

it('calls asyncLocalStorage.getStore', () => {
expect(asyncLocalStorage.getStore).toHaveBeenCalled();
});
Expand Down Expand Up @@ -2717,7 +2793,7 @@ describe('CheckoutService', () => {
);
const mockTrialInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
payments: StripeApiListFactory([]),
amount_due: 0,
status: 'paid',
})
Expand Down
33 changes: 23 additions & 10 deletions libs/payments/cart/src/lib/checkout.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,9 @@ export class CheckoutService {
automatic_tax: {
enabled: enableAutomaticTax,
},
promotion_code: promotionCode?.id,
...(promotionCode
? { discounts: [{ promotion_code: promotionCode.id }] }
: {}),
items: [
{
price: price.id,
Expand Down Expand Up @@ -537,14 +539,23 @@ export class CheckoutService {
subscription.latest_invoice
);

if (invoice.payment_intent && invoice.amount_due !== 0) {
intent = await this.paymentIntentManager.confirm(
invoice.payment_intent,
{
confirmation_token: confirmationTokenId,
off_session: false,
}
);
// For a default_incomplete subscription, the pending PaymentIntent's
// client secret is on the invoice's `confirmation_secret` (populated only
// when expanded — see StripeClient.invoicesRetrieve). Its `client_secret`
// is `{payment_intent_id}_secret_{...}`, so the id is the portion before
// `_secret_`. `invoice.payments` only records *attempted* payments and is
// empty here, so it cannot be used to locate the intent to confirm.
const confirmationSecret = invoice.confirmation_secret;
const paymentIntentId =
confirmationSecret?.type === 'payment_intent' &&
confirmationSecret.client_secret
? confirmationSecret.client_secret.split('_secret_')[0]
: undefined;
if (paymentIntentId && invoice.amount_due !== 0) {
intent = await this.paymentIntentManager.confirm(paymentIntentId, {
confirmation_token: confirmationTokenId,
off_session: false,
});
} else if (subscription.pending_setup_intent) {
// Subscription metadata is required by welcome email dispatcher for zero-cost subscriptions
await this.setupIntentManager.update(
Expand Down Expand Up @@ -720,7 +731,9 @@ export class CheckoutService {
...(freeTrial
? { trial_period_days: freeTrial.trialLengthDays }
: {}),
promotion_code: promotionCode?.id,
...(promotionCode
? { discounts: [{ promotion_code: promotionCode.id }] }
: {}),
items: [
{
price: price.id,
Expand Down
Loading
Loading