-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
243 additions
and
3 deletions.
There are no files selected for viewing
18 changes: 18 additions & 0 deletions
18
packages/medusa-payment-stripe-processor/src/api/hooks/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import stripeHooks from "./stripe" | ||
import { Router } from "express" | ||
import bodyParser from "body-parser" | ||
import middlewares from "@medusajs/medusa/dist/api/middlewares" | ||
|
||
const route = Router() | ||
|
||
export default (app) => { | ||
app.use("/stripe", route) | ||
|
||
route.post( | ||
"/hooks", | ||
// stripe constructEvent fails without body-parser | ||
bodyParser.raw({ type: "application/json" }), | ||
middlewares.wrap(stripeHooks) | ||
) | ||
return app | ||
} |
165 changes: 165 additions & 0 deletions
165
packages/medusa-payment-stripe-processor/src/api/hooks/stripe.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
import { Request, Response } from "express" | ||
import { | ||
buildHandleCartPaymentErrorMessage, | ||
constructWebhook, | ||
isPaymentCollection, | ||
} from "../utils/utils" | ||
import { | ||
AbstractCartCompletionStrategy, | ||
CartService, | ||
IdempotencyKeyService, | ||
} from "@medusajs/medusa" | ||
import { MedusaError } from "medusa-core-utils" | ||
|
||
export const RETRY_STATUS_CODE = 409 | ||
|
||
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 cartId = paymentIntent.metadata.cart_id // Backward compatibility | ||
const resourceId = paymentIntent.metadata.resource_id | ||
|
||
if (isPaymentCollection(resourceId)) { | ||
await handlePaymentCollection(event, req, res, resourceId, paymentIntent.id) | ||
} else { | ||
await handleCartPayments(event, req, res, cartId ?? resourceId) | ||
} | ||
} | ||
|
||
async function handleCartPayments(event, req, res, cartId) { | ||
const manager = req.scope.resolve("manager") | ||
const orderService = req.scope.resolve("orderService") | ||
const logger = req.scope.resolve("logger") | ||
|
||
const order = await orderService | ||
.retrieveByCartId(cartId) | ||
.catch(() => undefined) | ||
|
||
// handle payment intent events | ||
switch (event.type) { | ||
case "payment_intent.succeeded": | ||
if (order) { | ||
// If order is created but not captured, we attempt to do so | ||
if (order.payment_status !== "captured") { | ||
await manager.transaction(async (manager) => { | ||
await orderService.withTransaction(manager).capturePayment(order.id) | ||
}) | ||
} else { | ||
// Otherwise, respond with 200 preventing Stripe from retrying | ||
return res.sendStatus(200) | ||
} | ||
} else { | ||
// If order is not created, we respond with 404 to trigger Stripe retry mechanism | ||
return res.sendStatus(404) | ||
} | ||
break | ||
case "payment_intent.amount_capturable_updated": | ||
try { | ||
await manager.transaction(async (manager) => { | ||
await paymentIntentAmountCapturableEventHandler({ | ||
order, | ||
cartId, | ||
container: req.scope, | ||
transactionManager: manager, | ||
}) | ||
}) | ||
} catch (err) { | ||
const message = buildHandleCartPaymentErrorMessage(event, err) | ||
logger.warn(message) | ||
return res.sendStatus(RETRY_STATUS_CODE) | ||
} | ||
break | ||
default: | ||
res.sendStatus(204) | ||
return | ||
} | ||
|
||
res.sendStatus(200) | ||
} | ||
|
||
async function handlePaymentCollection(event, req, res, id, paymentIntentId) { | ||
const manager = req.scope.resolve("manager") | ||
const paymentCollectionService = req.scope.resolve("paymentCollectionService") | ||
|
||
const paycol = await paymentCollectionService | ||
.retrieve(id, { relations: ["payments"] }) | ||
.catch(() => undefined) | ||
|
||
if (paycol?.payments?.length) { | ||
if (event.type === "payment_intent.succeeded") { | ||
const payment = paycol.payments.find( | ||
(pay) => pay.data.id === paymentIntentId | ||
) | ||
if (payment && !payment.captured_at) { | ||
await manager.transaction(async (manager) => { | ||
await paymentCollectionService | ||
.withTransaction(manager) | ||
.capture(payment.id) | ||
}) | ||
} | ||
|
||
res.sendStatus(200) | ||
return | ||
} | ||
} | ||
res.sendStatus(204) | ||
} | ||
|
||
async function paymentIntentAmountCapturableEventHandler({ | ||
order, | ||
cartId, | ||
container, | ||
transactionManager, | ||
}) { | ||
if (!order) { | ||
const completionStrat: AbstractCartCompletionStrategy = container.resolve( | ||
"cartCompletionStrategy" | ||
) | ||
const cartService: CartService = container.resolve("cartService") | ||
const idempotencyKeyService: IdempotencyKeyService = container.resolve( | ||
"idempotencyKeyService" | ||
) | ||
|
||
let idempotencyKey | ||
try { | ||
idempotencyKey = await idempotencyKeyService | ||
.withTransaction(transactionManager) | ||
.initializeRequest("", "post", { cart_id: cartId }, "/stripe/hooks") | ||
} catch (error) { | ||
throw new MedusaError( | ||
MedusaError.Types.UNEXPECTED_STATE, | ||
"Failed to create idempotency key", | ||
"409" | ||
) | ||
} | ||
|
||
const cart = await cartService | ||
.withTransaction(transactionManager) | ||
.retrieve(cartId, { select: ["context"] }) | ||
|
||
const { response_code, response_body } = await completionStrat.complete( | ||
cartId, | ||
idempotencyKey, | ||
{ ip: cart.context?.ip as string } | ||
) | ||
|
||
if (response_code !== 200) { | ||
throw new MedusaError( | ||
MedusaError.Types.UNEXPECTED_STATE, | ||
response_body["message"], | ||
response_body["code"].toString() | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Router } from "express" | ||
import hooks from "./hooks" | ||
|
||
export default (container) => { | ||
const app = Router() | ||
|
||
hooks(app) | ||
|
||
return app | ||
} |
44 changes: 44 additions & 0 deletions
44
packages/medusa-payment-stripe-processor/src/api/utils/utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { AwilixContainer } from "awilix" | ||
import Stripe from "stripe" | ||
import { PostgresError } from "@medusajs/medusa" | ||
import { RETRY_STATUS_CODE } from "../hooks/stripe" | ||
|
||
const PAYMENT_PROVIDER_KEY = "pp_stripe" | ||
|
||
export function constructWebhook({ | ||
signature, | ||
body, | ||
container, | ||
}: { | ||
signature: string | string[] | undefined | ||
body: any | ||
container: AwilixContainer | ||
}): Stripe.Event { | ||
const stripeProviderService = container.resolve(PAYMENT_PROVIDER_KEY) | ||
return stripeProviderService.constructWebhookEvent(body, signature) | ||
} | ||
|
||
export function isPaymentCollection(id) { | ||
return id && id.startsWith("paycol") | ||
} | ||
|
||
export function buildHandleCartPaymentErrorMessage( | ||
event: string, | ||
err: Stripe.errors.StripeError | ||
): string { | ||
let message = `Stripe webhook ${event} handling failed\n${ | ||
err?.detail ?? err?.message | ||
}` | ||
if (err?.code === PostgresError.SERIALIZATION_FAILURE) { | ||
message = `Stripe webhook ${event} handle failed. This can happen when this webhook is triggered during a cart completion and can be ignored. This event should be retried automatically.\n${ | ||
err?.detail ?? err?.message | ||
}` | ||
} | ||
if (err?.code === RETRY_STATUS_CODE.toString()) { | ||
message = `Stripe webhook ${event} handle failed.\n${ | ||
err?.detail ?? err?.message | ||
}` | ||
} | ||
|
||
return message | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters