Files
medusa-store/packages/medusa-payment-stripe/src/api/utils/utils.ts

259 lines
6.6 KiB
TypeScript

import {
AbstractCartCompletionStrategy,
CartService,
IdempotencyKeyService,
PostgresError,
} from "@medusajs/medusa"
import { MedusaError } from "@medusajs/utils"
import { AwilixContainer } from "awilix"
import { EOL } from "os"
import Stripe from "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 buildError(event: string, err: Stripe.StripeRawError): string {
let message = `Stripe webhook ${event} handling failed${EOL}${
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.${EOL}${
err?.detail ?? err?.message
}`
}
if (err?.code === "409") {
message = `Stripe webhook ${event} handle failed.${EOL}${
err?.detail ?? err?.message
}`
}
return message
}
export async function handlePaymentHook({
event,
container,
paymentIntent,
}: {
event: Partial<Stripe.Event>
container: AwilixContainer
paymentIntent: Partial<Stripe.PaymentIntent>
}): 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
switch (event.type) {
case "payment_intent.succeeded":
try {
await onPaymentIntentSucceeded({
eventId: event.id,
paymentIntent,
cartId,
resourceId,
isPaymentCollection: isPaymentCollection(resourceId),
container,
})
} catch (err) {
const message = buildError(event.type, err)
logger.warn(message)
return { statusCode: 409 }
}
break
case "payment_intent.amount_capturable_updated":
try {
await onPaymentAmountCapturableUpdate({
eventId: event.id,
cartId,
container,
})
} catch (err) {
const message = buildError(event.type, err)
logger.warn(message)
return { statusCode: 409 }
}
break
case "payment_intent.payment_failed": {
const message =
paymentIntent.last_payment_error &&
paymentIntent.last_payment_error.message
logger.error(
`The payment of the payment intent ${paymentIntent.id} has failed${EOL}${message}`
)
break
}
default:
return { statusCode: 204 }
}
return { statusCode: 200 }
}
async function onPaymentIntentSucceeded({
eventId,
paymentIntent,
cartId,
resourceId,
isPaymentCollection,
container,
}) {
const manager = container.resolve("manager")
await manager.transaction(async (transactionManager) => {
if (isPaymentCollection) {
await capturePaymenCollectiontIfNecessary({
paymentIntent,
resourceId,
container,
})
} else {
await completeCartIfNecessary({
eventId,
cartId,
container,
transactionManager,
})
await capturePaymentIfNecessary({
cartId,
transactionManager,
container,
})
}
})
}
async function onPaymentAmountCapturableUpdate({ eventId, cartId, container }) {
const manager = container.resolve("manager")
await manager.transaction(async (transactionManager) => {
await completeCartIfNecessary({
eventId,
cartId,
container,
transactionManager,
})
})
}
async function capturePaymenCollectiontIfNecessary({
paymentIntent,
resourceId,
container,
}) {
const manager = container.resolve("manager")
const paymentCollectionService = container.resolve("paymentCollectionService")
const paycol = await paymentCollectionService
.retrieve(resourceId, { relations: ["payments"] })
.catch(() => undefined)
if (paycol?.payments?.length) {
const payment = paycol.payments.find(
(pay) => pay.data.id === paymentIntent.id
)
if (payment && !payment.captured_at) {
await manager.transaction(async (manager) => {
await paymentCollectionService
.withTransaction(manager)
.capture(payment.id) // TODO: revisit - this method doesn't exists ATM
})
}
}
}
async function capturePaymentIfNecessary({
cartId,
transactionManager,
container,
}) {
const orderService = container.resolve("orderService")
const order = await orderService
.withTransaction(transactionManager)
.retrieveByCartId(cartId)
.catch(() => undefined)
if (order && order.payment_status !== "captured") {
await orderService
.withTransaction(transactionManager)
.capturePayment(order.id)
}
}
async function completeCartIfNecessary({
eventId,
cartId,
container,
transactionManager,
}) {
const orderService = container.resolve("orderService")
const order = await orderService
.retrieveByCartId(cartId)
.catch(() => undefined)
if (!order) {
const completionStrat: AbstractCartCompletionStrategy = container.resolve(
"cartCompletionStrategy"
)
const cartService: CartService = container.resolve("cartService")
const idempotencyKeyService: IdempotencyKeyService = container.resolve(
"idempotencyKeyService"
)
const idempotencyKeyServiceTx =
idempotencyKeyService.withTransaction(transactionManager)
let idempotencyKey = await idempotencyKeyServiceTx
.retrieve({
request_path: "/stripe/hooks",
idempotency_key: eventId,
})
.catch(() => undefined)
if (!idempotencyKey) {
idempotencyKey = await idempotencyKeyService
.withTransaction(transactionManager)
.create({
request_path: "/stripe/hooks",
idempotency_key: eventId,
})
}
const cart = await cartService
.withTransaction(transactionManager)
.retrieve(cartId, { select: ["context"] })
const { response_code, response_body } = await completionStrat
.withTransaction(transactionManager)
.complete(cartId, idempotencyKey, { ip: cart.context?.ip as string })
if (response_code !== 200) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
response_body["message"] as string,
response_body["code"] as string
)
}
}
}