mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Server: Handle Stripe webhook receiving multiple times the same event
This commit is contained in:
parent
27c3cbdf8f
commit
252d0695a4
68
packages/server/src/routes/index/stripe.test.ts
Normal file
68
packages/server/src/routes/index/stripe.test.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { AccountType } from '../../models/UserModel';
|
||||
import { initStripe, stripeConfig } from '../../utils/stripe';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, models, koaAppContext, expectNotThrow } from '../../utils/testing/testUtils';
|
||||
import uuidgen from '../../utils/uuidgen';
|
||||
import { postHandlers } from './stripe';
|
||||
|
||||
async function createUserViaSubscription(userEmail: string, eventId: string = '') {
|
||||
eventId = eventId || uuidgen();
|
||||
const stripeSessionId = 'sess_123';
|
||||
const stripePriceId = stripeConfig().proPriceId;
|
||||
await models().keyValue().setValue(`stripeSessionToPriceId::${stripeSessionId}`, stripePriceId);
|
||||
|
||||
const ctx = await koaAppContext();
|
||||
const stripe = initStripe();
|
||||
await postHandlers.webhook(stripe, {}, ctx, {
|
||||
id: eventId,
|
||||
type: 'checkout.session.completed',
|
||||
data: {
|
||||
object: {
|
||||
id: stripeSessionId,
|
||||
customer: 'cus_123',
|
||||
subscription: 'sub_123',
|
||||
customer_details: {
|
||||
email: userEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, false);
|
||||
}
|
||||
|
||||
describe('index/stripe', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('index/stripe');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should handle the checkout.session.completed event', async function() {
|
||||
const startTime = Date.now();
|
||||
|
||||
await createUserViaSubscription('toto@example.com');
|
||||
|
||||
const user = await models().user().loadByEmail('toto@example.com');
|
||||
expect(user.account_type).toBe(AccountType.Pro);
|
||||
|
||||
const sub = await models().subscription().byUserId(user.id);
|
||||
expect(sub.stripe_subscription_id).toBe('sub_123');
|
||||
expect(sub.is_deleted).toBe(0);
|
||||
expect(sub.last_payment_time).toBeGreaterThanOrEqual(startTime);
|
||||
expect(sub.last_payment_failed_time).toBe(0);
|
||||
});
|
||||
|
||||
test('should not process the same event twice', async function() {
|
||||
await createUserViaSubscription('toto@example.com', 'evt_1');
|
||||
const v = await models().keyValue().value('stripeEventDone::evt_1');
|
||||
expect(v).toBe(1);
|
||||
// This event should simply be skipped
|
||||
await expectNotThrow(async () => createUserViaSubscription('toto@example.com', 'evt_1'));
|
||||
});
|
||||
|
||||
});
|
@ -41,7 +41,12 @@ function priceIdToAccountType(priceId: string): AccountType {
|
||||
|
||||
type StripeRouteHandler = (stripe: Stripe, path: SubPath, ctx: AppContext)=> Promise<any>;
|
||||
|
||||
const postHandlers: Record<string, StripeRouteHandler> = {
|
||||
interface PostHandlers {
|
||||
createCheckoutSession: Function;
|
||||
webhook: Function;
|
||||
}
|
||||
|
||||
export const postHandlers: PostHandlers = {
|
||||
|
||||
createCheckoutSession: async (stripe: Stripe, __path: SubPath, ctx: AppContext) => {
|
||||
const fields = await bodyFields<CreateCheckoutSessionFields>(ctx.req);
|
||||
@ -99,8 +104,18 @@ const postHandlers: Record<string, StripeRouteHandler> = {
|
||||
// - The public config is under packages/server/stripeConfig.json
|
||||
// - The private config is in the server .env file
|
||||
|
||||
webhook: async (stripe: Stripe, _path: SubPath, ctx: AppContext) => {
|
||||
const event = await stripeEvent(stripe, ctx.req);
|
||||
webhook: async (stripe: Stripe, _path: SubPath, ctx: AppContext, event: Stripe.Event = null, logErrors: boolean = true) => {
|
||||
event = event ? event : await stripeEvent(stripe, ctx.req);
|
||||
|
||||
// Webhook endpoints might occasionally receive the same event more than
|
||||
// once.
|
||||
// https://stripe.com/docs/webhooks/best-practices#duplicate-events
|
||||
const eventDoneKey = `stripeEventDone::${event.id}`;
|
||||
if (await ctx.joplin.models.keyValue().value<number>(eventDoneKey)) {
|
||||
logger.info(`Skipping event that has already been done: ${event.id}`);
|
||||
return;
|
||||
}
|
||||
await ctx.joplin.models.keyValue().setValue(eventDoneKey, 1);
|
||||
|
||||
const hooks: any = {
|
||||
|
||||
@ -235,7 +250,11 @@ const postHandlers: Record<string, StripeRouteHandler> = {
|
||||
try {
|
||||
await hooks[event.type]();
|
||||
} catch (error) {
|
||||
logger.error(`Error processing event ${event.type}:`, event, error);
|
||||
if (logErrors) {
|
||||
logger.error(`Error processing event ${event.type}:`, event, error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info(`Got Stripe event: ${event.type} [Unhandled]`);
|
||||
@ -336,8 +355,8 @@ const getHandlers: Record<string, StripeRouteHandler> = {
|
||||
};
|
||||
|
||||
router.post('stripe/:id', async (path: SubPath, ctx: AppContext) => {
|
||||
if (!postHandlers[path.id]) throw new ErrorNotFound(`No such action: ${path.id}`);
|
||||
return postHandlers[path.id](initStripe(), path, ctx);
|
||||
if (!(postHandlers as any)[path.id]) throw new ErrorNotFound(`No such action: ${path.id}`);
|
||||
return (postHandlers as any)[path.id](initStripe(), path, ctx);
|
||||
});
|
||||
|
||||
router.get('stripe/:id', async (path: SubPath, ctx: AppContext) => {
|
||||
|
Loading…
Reference in New Issue
Block a user