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