From 99a4f94db5ae25dd1688fe29556ba46923715e5f Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:37:53 +0100 Subject: [PATCH] feat(medusa-payment-stripe): Add delay to Stripe webhook (#5838) **What** - Migrate Stripe plugin to use API Routes + Subscribers - Change the behaviour of Stripe webhook Instead of processing webhook events immediately, we fire an event to perform the work in the background. The primary reason for changing the webhook is that the previous implementation led to occasional conflicts due to a race condition between the client's and the webhook's request to complete a cart. Now, we emit an event with a configurable delay (defaults to 2s) to prevent this from happening This approach was preferred over adding a timeout directly to the webhook execution, as it is generally best practice to respond to a webhook event immediately after receiving it. --- .changeset/metal-pans-beg.md | 6 ++++ packages/medusa-payment-stripe/package.json | 2 +- .../stripe-payments/[order_id]/route.ts | 7 ++++ .../src/api/hooks/index.ts | 18 ---------- .../src/api/hooks/stripe.ts | 25 ------------- .../medusa-payment-stripe/src/api/index.ts | 35 ------------------- .../src/api/middlewares.ts | 12 +++++++ .../src/api/stripe/hooks/route.ts | 28 +++++++++++++++ .../src/api/utils/__tests__/utils.spec.ts | 6 ++-- .../src/api/utils/utils.ts | 14 +++----- .../src/core/stripe-base.ts | 2 +- packages/medusa-payment-stripe/src/index.ts | 6 ++-- .../src/subscribers/stripe.ts | 24 +++++++++++++ .../src/{types.ts => types/index.ts} | 0 .../api/routes/store/auth/create-session.ts | 3 +- yarn.lock | 2 +- 16 files changed, 92 insertions(+), 98 deletions(-) create mode 100644 .changeset/metal-pans-beg.md create mode 100644 packages/medusa-payment-stripe/src/api/admin/orders/stripe-payments/[order_id]/route.ts delete mode 100644 packages/medusa-payment-stripe/src/api/hooks/index.ts delete mode 100644 packages/medusa-payment-stripe/src/api/hooks/stripe.ts delete mode 100644 packages/medusa-payment-stripe/src/api/index.ts create mode 100644 packages/medusa-payment-stripe/src/api/middlewares.ts create mode 100644 packages/medusa-payment-stripe/src/api/stripe/hooks/route.ts create mode 100644 packages/medusa-payment-stripe/src/subscribers/stripe.ts rename packages/medusa-payment-stripe/src/{types.ts => types/index.ts} (100%) diff --git a/.changeset/metal-pans-beg.md b/.changeset/metal-pans-beg.md new file mode 100644 index 0000000000..a48e3a8af0 --- /dev/null +++ b/.changeset/metal-pans-beg.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"medusa-payment-stripe": patch +--- + +feat(medusa-payment-stripe): Add delay to Stripe webhook diff --git a/packages/medusa-payment-stripe/package.json b/packages/medusa-payment-stripe/package.json index 8fa34bcf85..d6b847fd10 100644 --- a/packages/medusa-payment-stripe/package.json +++ b/packages/medusa-payment-stripe/package.json @@ -48,9 +48,9 @@ "medusa-react": "^9.0.0" }, "dependencies": { + "@medusajs/utils": "^1.11.2", "body-parser": "^1.19.0", "express": "^4.17.1", - "medusa-core-utils": "^1.2.0", "stripe": "^11.10.0" }, "gitHead": "81a7ff73d012fda722f6e9ef0bd9ba0232d37808", diff --git a/packages/medusa-payment-stripe/src/api/admin/orders/stripe-payments/[order_id]/route.ts b/packages/medusa-payment-stripe/src/api/admin/orders/stripe-payments/[order_id]/route.ts new file mode 100644 index 0000000000..06b28b0fed --- /dev/null +++ b/packages/medusa-payment-stripe/src/api/admin/orders/stripe-payments/[order_id]/route.ts @@ -0,0 +1,7 @@ +import { MedusaRequest, MedusaResponse } from "@medusajs/medusa" +import { getStripePayments } from "../../../../../controllers/get-payments" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const payments = await getStripePayments(req) + res.json({ payments }) +} diff --git a/packages/medusa-payment-stripe/src/api/hooks/index.ts b/packages/medusa-payment-stripe/src/api/hooks/index.ts deleted file mode 100644 index 87a02f60c0..0000000000 --- a/packages/medusa-payment-stripe/src/api/hooks/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import stripeHooks from "./stripe" -import { Router } from "express" -import bodyParser from "body-parser" -import { wrapHandler } from "@medusajs/medusa" - -const route = Router() - -export default (app) => { - app.use("/stripe", route) - - route.post( - "/hooks", - // stripe constructEvent fails without body-parser - bodyParser.raw({ type: "application/json" }), - wrapHandler(stripeHooks) - ) - return app -} diff --git a/packages/medusa-payment-stripe/src/api/hooks/stripe.ts b/packages/medusa-payment-stripe/src/api/hooks/stripe.ts deleted file mode 100644 index cb66a17e91..0000000000 --- a/packages/medusa-payment-stripe/src/api/hooks/stripe.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Request, Response } from "express" -import { constructWebhook, handlePaymentHook } from "../utils/utils" - -export default async (req: Request, res: Response) => { - let event - try { - event = constructWebhook({ - signature: req.headers["stripe-signature"], - body: req.body, - container: req.scope, - }) - } catch (err) { - res.status(400).send(`Webhook Error: ${err.message}`) - return - } - - const paymentIntent = event.data.object - - const { statusCode } = await handlePaymentHook({ - event, - container: req.scope, - paymentIntent, - }) - res.sendStatus(statusCode) -} diff --git a/packages/medusa-payment-stripe/src/api/index.ts b/packages/medusa-payment-stripe/src/api/index.ts deleted file mode 100644 index 3c179b876f..0000000000 --- a/packages/medusa-payment-stripe/src/api/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { authenticate } from "@medusajs/medusa" -import { ConfigModule } from "@medusajs/types" -import getConfigFile from "@medusajs/utils/dist/common/get-config-file" -import cors from "cors" -import { Router } from "express" -import { getStripePayments } from "../controllers/get-payments" -import hooks from "./hooks" - -export default (rootDirectory) => { - const app = Router() - - hooks(app) - - const { configModule } = getConfigFile( - rootDirectory, - "medusa-config" - ) - - const corsOptions = { - origin: configModule?.projectConfig?.admin_cors?.split(","), - credentials: true, - } - - app.use( - `/admin/orders/stripe-payments/:order_id`, - cors(corsOptions), - authenticate() - ) - app.get(`/admin/orders/stripe-payments/:order_id`, async (req, res) => { - const payments = await getStripePayments(req) - res.json({ payments }) - }) - - return app -} diff --git a/packages/medusa-payment-stripe/src/api/middlewares.ts b/packages/medusa-payment-stripe/src/api/middlewares.ts new file mode 100644 index 0000000000..dcda4a8f24 --- /dev/null +++ b/packages/medusa-payment-stripe/src/api/middlewares.ts @@ -0,0 +1,12 @@ +import type { MiddlewaresConfig } from "@medusajs/medusa" +import { raw } from "body-parser" + +export const config: MiddlewaresConfig = { + routes: [ + { + bodyParser: false, + matcher: "/stripe/hooks", + middlewares: [raw({ type: "application/json" })], + }, + ], +} diff --git a/packages/medusa-payment-stripe/src/api/stripe/hooks/route.ts b/packages/medusa-payment-stripe/src/api/stripe/hooks/route.ts new file mode 100644 index 0000000000..35132700a6 --- /dev/null +++ b/packages/medusa-payment-stripe/src/api/stripe/hooks/route.ts @@ -0,0 +1,28 @@ +import { MedusaRequest, MedusaResponse } from "@medusajs/medusa" +import { constructWebhook } from "../../utils/utils" + +const WEBHOOK_DELAY = process.env.STRIPE_WEBHOOK_DELAY ?? 5000 // 5s +const WEBHOOK_RETRIES = process.env.STRIPE_WEBHOOK_RETRIES ?? 3 + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + try { + const event = constructWebhook({ + signature: req.headers["stripe-signature"], + body: req.body, + container: req.scope, + }) + + const eventBus = req.scope.resolve("eventBusService") + + // we delay the processing of the event to avoid a conflict caused by a race condition + await eventBus.emit("medusa.stripe_payment_intent_update", event, { + delay: WEBHOOK_DELAY, + attempts: WEBHOOK_RETRIES, + }) + } catch (err) { + res.status(400).send(`Webhook Error: ${err.message}`) + return + } + + res.sendStatus(200) +} diff --git a/packages/medusa-payment-stripe/src/api/utils/__tests__/utils.spec.ts b/packages/medusa-payment-stripe/src/api/utils/__tests__/utils.spec.ts index 0a46e2ff0c..5d6ccf11af 100644 --- a/packages/medusa-payment-stripe/src/api/utils/__tests__/utils.spec.ts +++ b/packages/medusa-payment-stripe/src/api/utils/__tests__/utils.spec.ts @@ -1,8 +1,7 @@ import { PostgresError } from "@medusajs/medusa" -import Stripe from "stripe" import { EOL } from "os" +import Stripe from "stripe" -import { buildError, handlePaymentHook, isPaymentCollection } from "../utils" import { container } from "../__fixtures__/container" import { existingCartId, @@ -14,6 +13,7 @@ import { paymentId, paymentIntentId, } from "../__fixtures__/data" +import { buildError, handlePaymentHook, isPaymentCollection } from "../utils" describe("Utils", () => { afterEach(() => { @@ -334,7 +334,7 @@ describe("Utils", () => { const paymentIntent = { id: paymentIntentId, metadata: { cart_id: nonExistingCartId }, - last_payment_error: { message: "error message" }, + last_payment_error: { message: "error message" } as any, } await handlePaymentHook({ event, container, paymentIntent }) diff --git a/packages/medusa-payment-stripe/src/api/utils/utils.ts b/packages/medusa-payment-stripe/src/api/utils/utils.ts index bb2fa17bba..275afe5168 100644 --- a/packages/medusa-payment-stripe/src/api/utils/utils.ts +++ b/packages/medusa-payment-stripe/src/api/utils/utils.ts @@ -4,8 +4,8 @@ import { IdempotencyKeyService, PostgresError, } from "@medusajs/medusa" +import { MedusaError } from "@medusajs/utils" import { AwilixContainer } from "awilix" -import { MedusaError } from "medusa-core-utils" import { EOL } from "os" import Stripe from "stripe" @@ -51,19 +51,15 @@ export async function handlePaymentHook({ container, paymentIntent, }: { - event: { type: string; id: string } + event: Partial container: AwilixContainer - paymentIntent: { - id: string - metadata: { cart_id?: string; resource_id?: string } - last_payment_error?: { message: string } - } + paymentIntent: Partial }): Promise<{ statusCode: number }> { const logger = container.resolve("logger") const cartId = - paymentIntent.metadata.cart_id ?? paymentIntent.metadata.resource_id // Backward compatibility - const resourceId = paymentIntent.metadata.resource_id + paymentIntent.metadata?.cart_id ?? paymentIntent.metadata?.resource_id // Backward compatibility + const resourceId = paymentIntent.metadata?.resource_id switch (event.type) { case "payment_intent.succeeded": diff --git a/packages/medusa-payment-stripe/src/core/stripe-base.ts b/packages/medusa-payment-stripe/src/core/stripe-base.ts index 0cdd4b2bee..df800aeade 100644 --- a/packages/medusa-payment-stripe/src/core/stripe-base.ts +++ b/packages/medusa-payment-stripe/src/core/stripe-base.ts @@ -6,6 +6,7 @@ import { PaymentProcessorSessionResponse, PaymentSessionStatus, } from "@medusajs/medusa" +import { MedusaError } from "@medusajs/utils" import { EOL } from "os" import Stripe from "stripe" import { @@ -14,7 +15,6 @@ import { PaymentIntentOptions, StripeOptions, } from "../types" -import { MedusaError } from "@medusajs/utils" abstract class StripeBase extends AbstractPaymentProcessor { static identifier = "" diff --git a/packages/medusa-payment-stripe/src/index.ts b/packages/medusa-payment-stripe/src/index.ts index 5184713378..6b0f5ee0f2 100644 --- a/packages/medusa-payment-stripe/src/index.ts +++ b/packages/medusa-payment-stripe/src/index.ts @@ -1,8 +1,8 @@ -export * from "./types" export * from "./core/stripe-base" -export * from "./services/stripe-blik" export * from "./services/stripe-bancontact" +export * from "./services/stripe-blik" export * from "./services/stripe-giropay" export * from "./services/stripe-ideal" -export * from "./services/stripe-przelewy24" export * from "./services/stripe-provider" +export * from "./services/stripe-przelewy24" +export * from "./types" diff --git a/packages/medusa-payment-stripe/src/subscribers/stripe.ts b/packages/medusa-payment-stripe/src/subscribers/stripe.ts new file mode 100644 index 0000000000..739bcb4350 --- /dev/null +++ b/packages/medusa-payment-stripe/src/subscribers/stripe.ts @@ -0,0 +1,24 @@ +import { type SubscriberArgs, type SubscriberConfig } from "@medusajs/medusa" +import Stripe from "stripe" +import { handlePaymentHook } from "../api/utils/utils" + +export default async function stripeHandler({ + data, + container, +}: SubscriberArgs) { + const event = data + const paymentIntent = event.data.object as Stripe.PaymentIntent + + await handlePaymentHook({ + event, + container, + paymentIntent, + }) +} + +export const config: SubscriberConfig = { + event: "medusa.stripe_payment_intent_update", + context: { + subscriberId: "medusa.stripe_payment_intent_update", + }, +} diff --git a/packages/medusa-payment-stripe/src/types.ts b/packages/medusa-payment-stripe/src/types/index.ts similarity index 100% rename from packages/medusa-payment-stripe/src/types.ts rename to packages/medusa-payment-stripe/src/types/index.ts diff --git a/packages/medusa/src/api/routes/store/auth/create-session.ts b/packages/medusa/src/api/routes/store/auth/create-session.ts index d895080542..9540e98bdc 100644 --- a/packages/medusa/src/api/routes/store/auth/create-session.ts +++ b/packages/medusa/src/api/routes/store/auth/create-session.ts @@ -1,10 +1,9 @@ import { IsEmail, IsNotEmpty } from "class-validator" -import jwt from "jsonwebtoken" import { EntityManager } from "typeorm" +import { defaultRelations } from "." import AuthService from "../../../../services/auth" import CustomerService from "../../../../services/customer" import { validator } from "../../../../utils/validator" -import { defaultRelations } from "." /** * @oas [post] /store/auth diff --git a/yarn.lock b/yarn.lock index b153d61a67..159c7348f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35980,6 +35980,7 @@ __metadata: dependencies: "@medusajs/admin": ^7.1.7 "@medusajs/medusa": ^1.17.4 + "@medusajs/utils": ^1.11.2 "@tanstack/react-table": ^8.7.9 "@types/stripe": ^8.0.417 awilix: ^8.0.1 @@ -35987,7 +35988,6 @@ __metadata: cross-env: ^5.2.1 express: ^4.17.1 jest: ^25.5.4 - medusa-core-utils: ^1.2.0 medusa-react: ^9.0.11 rimraf: ^5.0.1 stripe: ^11.10.0