Skip to content

Commit b1877aa

Browse files
committed
feat(next): add initial support for stripe webhooks
Because: - Need to record a glean event when a customers subscription is cancelled This commit: - Adds payments-webhooks library for webhook related logic - Adds StripeWebhookService to payments-webhooks - Adds new route handler to payments-next to accept incoming Stripe events - Moves PaymentsEmitter into new payments-events library Closes #FXA-10089
1 parent 39878a8 commit b1877aa

61 files changed

Lines changed: 1533 additions & 64 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ apps/payments/next/public/locales
161161
!apps/payments/next/.env
162162

163163
# payments-metrics
164-
libs/payments/metrics/src/lib/glean/__generated__/server_events.ts
164+
libs/payments/metrics/src/lib/glean/__generated__/
165165

166166
Library
167167
.node
@@ -183,4 +183,4 @@ libs/shared/cms/src/__generated__/graphql.d.ts
183183
libs/shared/cms/src/__generated__/graphql.js
184184

185185
# Sentry Config File
186-
.env.sentry-build-plugin
186+
.env.sentry-build-plugin

apps/payments/next/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__POSTAL_CODE=11211
3737

3838
# Stripe Config
3939
STRIPE_CONFIG__API_KEY=11233
40+
STRIPE_CONFIG__WEBHOOK_SECRET=11233
4041
STRIPE_CONFIG__TAX_IDS={}
4142
STRIPE_PUBLIC_API_KEY=pk_test_VNpCidC0a2TJJB3wqXq7drhN00sF8r9mhs
4243

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { getApp } from '@fxa/payments/ui/server';
6+
7+
export const dynamic = 'force-dynamic';
8+
9+
export async function POST(request: Request) {
10+
const signature = request.headers.get('stripe-signature');
11+
if (!signature || typeof signature !== 'string') {
12+
return new Response('Bad Request', { status: 400 });
13+
}
14+
15+
const payload = await request.text();
16+
17+
const stripeWebhookService = getApp().getStripeWebhookService();
18+
19+
await stripeWebhookService.handleWebhookEvent(payload, signature);
20+
21+
return new Response('Received', { status: 200 });
22+
}

libs/payments/cart/src/lib/cart.service.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
CouponErrorLimitReached,
2020
CustomerSessionManager,
2121
PaymentIntentManager,
22+
determinePaymentMethodType,
2223
} from '@fxa/payments/customer';
2324
import { EligibilityService } from '@fxa/payments/eligibility';
2425
import {
@@ -574,9 +575,13 @@ export class CartService {
574575
}
575576

576577
let paymentInfo: PaymentInfo | undefined;
577-
if (customer?.invoice_settings.default_payment_method) {
578+
const paymentMethodType = determinePaymentMethodType(
579+
customer,
580+
subscriptions
581+
);
582+
if (paymentMethodType?.type === 'stripe') {
578583
const paymentMethodPromise = this.paymentMethodManager.retrieve(
579-
customer.invoice_settings.default_payment_method
584+
paymentMethodType.paymentMethodId
580585
);
581586
const customerSessionPromise = cart.stripeCustomerId
582587
? this.customerSessionManager.create(cart.stripeCustomerId)
@@ -591,6 +596,10 @@ export class CartService {
591596
brand: paymentMethod.card?.brand,
592597
customerSessionClientSecret: customerSession?.client_secret,
593598
};
599+
} else if (paymentMethodType?.type === 'external_paypal') {
600+
paymentInfo = {
601+
type: 'external_paypal',
602+
};
594603
} else if (subscriptions.length) {
595604
const firstListedSubscription = subscriptions[0];
596605
// fetch payment method info

libs/payments/customer/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ export * from './lib/types';
1616
export * from './lib/factories/tax-address.factory';
1717
export * from './lib/error';
1818
export * from './lib/util/stripeInvoiceToFirstInvoicePreviewDTO';
19+
export * from './lib/util/getSubplatInterval';
20+
export * from './lib/util/determinePaymentMethodType';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { Interval, SubplatInterval } from './types';
6+
7+
export const subplatIntervalToInterval = {
8+
[SubplatInterval.Daily]: {
9+
interval: 'day',
10+
intervalCount: 1,
11+
},
12+
[SubplatInterval.Weekly]: {
13+
interval: 'week',
14+
intervalCount: 1,
15+
},
16+
[SubplatInterval.Monthly]: {
17+
interval: 'month',
18+
intervalCount: 1,
19+
},
20+
[SubplatInterval.HalfYearly]: {
21+
interval: 'month',
22+
intervalCount: 6,
23+
},
24+
[SubplatInterval.Yearly]: {
25+
interval: 'year',
26+
intervalCount: 1,
27+
},
28+
} satisfies Record<SubplatInterval, Interval>;

libs/payments/customer/src/lib/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
import { StripePrice } from '@fxa/payments/stripe';
6+
57
export type InvoicePreview = {
68
currency: string;
79
listAmount: number;
@@ -16,6 +18,11 @@ export type InvoicePreview = {
1618
oneTimeCharge?: number;
1719
};
1820

21+
export interface Interval {
22+
interval: NonNullable<StripePrice['recurring']>['interval'];
23+
intervalCount: number;
24+
}
25+
1926
export interface TaxAmount {
2027
title: string;
2128
inclusive: boolean;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import {
6+
StripeCustomerFactory,
7+
StripeSubscriptionFactory,
8+
} from '@fxa/payments/stripe';
9+
import { determinePaymentMethodType } from './determinePaymentMethodType';
10+
11+
describe('determinePaymentMethodType', () => {
12+
it('returns stripe', () => {
13+
const mockCustomer = StripeCustomerFactory();
14+
expect(determinePaymentMethodType(mockCustomer)).toEqual({
15+
type: 'stripe',
16+
paymentMethodId: expect.any(String),
17+
});
18+
});
19+
20+
it('returns external_paypal', () => {
21+
const mockSubscription = StripeSubscriptionFactory({
22+
collection_method: 'send_invoice',
23+
});
24+
expect(determinePaymentMethodType(undefined, [mockSubscription])).toEqual({
25+
type: 'external_paypal',
26+
});
27+
});
28+
29+
it('returns null', () => {
30+
expect(determinePaymentMethodType(undefined, undefined));
31+
});
32+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { StripeCustomer, StripeSubscription } from '@fxa/payments/stripe';
6+
7+
interface StripePaymentMethod {
8+
type: 'stripe';
9+
paymentMethodId: string;
10+
}
11+
12+
interface PayPalPaymentMethod {
13+
type: 'external_paypal';
14+
}
15+
16+
type PaymentMethodTypeResponse =
17+
| StripePaymentMethod
18+
| PayPalPaymentMethod
19+
| null;
20+
21+
export const determinePaymentMethodType = (
22+
customer?: StripeCustomer,
23+
subscriptions?: StripeSubscription[]
24+
): PaymentMethodTypeResponse => {
25+
if (customer?.invoice_settings.default_payment_method) {
26+
return {
27+
type: 'stripe',
28+
paymentMethodId: customer.invoice_settings.default_payment_method,
29+
};
30+
} else if (subscriptions?.length) {
31+
const firstListedSubscription = subscriptions[0];
32+
// fetch payment method info
33+
if (firstListedSubscription.collection_method === 'send_invoice') {
34+
return {
35+
type: 'external_paypal',
36+
};
37+
}
38+
}
39+
40+
return null;
41+
};

libs/payments/customer/src/lib/util/doesPriceMatchSubplatInterval.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,7 @@
44

55
import { StripePrice } from '@fxa/payments/stripe';
66
import { SubplatInterval } from '../types';
7-
8-
interface Interval {
9-
interval: NonNullable<StripePrice['recurring']>['interval'];
10-
intervalCount: number;
11-
}
12-
13-
const subplatIntervalToInterval = {
14-
[SubplatInterval.Daily]: {
15-
interval: 'day',
16-
intervalCount: 1,
17-
},
18-
[SubplatInterval.Weekly]: {
19-
interval: 'week',
20-
intervalCount: 1,
21-
},
22-
[SubplatInterval.Monthly]: {
23-
interval: 'month',
24-
intervalCount: 1,
25-
},
26-
[SubplatInterval.HalfYearly]: {
27-
interval: 'month',
28-
intervalCount: 6,
29-
},
30-
[SubplatInterval.Yearly]: {
31-
interval: 'year',
32-
intervalCount: 1,
33-
},
34-
} satisfies Record<SubplatInterval, Interval>;
7+
import { subplatIntervalToInterval } from '../constants';
358

369
export const doesPriceMatchSubplatInterval = (
3710
price: StripePrice,

0 commit comments

Comments
 (0)