import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework" import type PayPalModuleService from "../modules/paypal/service" import { getPayPalApiBase } from "../modules/paypal/utils/paypal-auth" import { paypalFetch } from "../modules/paypal/utils/paypal-fetch" import { isPayPalProviderId } from "../modules/paypal/utils/provider-ids" /** PayPal order statuses that still accept a `purchase_units` PATCH. */ const PATCHABLE_STATUSES = new Set(["CREATED", "APPROVED", "SAVED"]) /** * On `order.placed`, stamp the Medusa order's display id (with the configured * invoice prefix) onto the PayPal order as `invoice_id`, so the two are easy * to reconcile from the PayPal dashboard. Best-effort: a captured order is * immutable at PayPal, in which case the mapping is logged instead. */ export default async function paypalOrderInvoiceHandler({ event, container, }: SubscriberArgs<{ id: string }>) { const orderId = event?.data?.id if (!orderId) return try { const query = container.resolve("query") as any const paypal = container.resolve("paypal_onboarding") const { data: orders } = await query.graph({ entity: "order", fields: [ "id", "display_id", "payment_collections.payment_sessions.id", "payment_collections.payment_sessions.data", "payment_collections.payment_sessions.provider_id", "payment_collections.payment_sessions.status", "payment_collections.payment_sessions.created_at", ], filters: { id: orderId }, }) const order = orders?.[0] if (!order) return const sessions = (order.payment_collections || []).flatMap( (pc: any) => pc.payment_sessions || [] ) const paypalSession = sessions .filter((s: any) => isPayPalProviderId(s.provider_id)) .sort( (a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime() )[0] if (!paypalSession) { console.info( "[PayPal] invoice subscriber: no PayPal session for order", orderId ) return } const paypalData = ((paypalSession.data || {}).paypal || {}) as Record< string, any > const paypalOrderId = String(paypalData.order_id || "") if (!paypalOrderId) { console.info( "[PayPal] invoice subscriber: no order_id in session for order", orderId ) return } const settings = await paypal.getSettings().catch(() => ({})) const settingsData = settings && typeof settings === "object" && "data" in settings ? ((settings as { data?: Record }).data ?? {}) : {} const additionalSettings = (settingsData.additional_settings || {}) as Record const invoicePrefix = typeof additionalSettings.invoicePrefix === "string" ? additionalSettings.invoicePrefix : "" const displayId = String(order.display_id || "") const invoiceId = `${invoicePrefix}${displayId}`.trim() if (!invoiceId) return const creds = await paypal.getActiveCredentials() const base = getPayPalApiBase(creds.environment) const accessToken = await paypal.getAppAccessToken() let paypalOrderStatus = "" let currentInvoiceId = "" try { const statusResp = await paypalFetch( `${base}/v2/checkout/orders/${encodeURIComponent(paypalOrderId)}`, { headers: { Authorization: `Bearer ${accessToken}` } } ) if (statusResp.ok) { const currentOrder = await statusResp.json().catch(() => ({})) paypalOrderStatus = String(currentOrder?.status || "").toUpperCase() currentInvoiceId = currentOrder?.purchase_units?.[0]?.invoice_id || "" } } catch (e: any) { console.warn( "[PayPal] invoice subscriber: order status fetch failed:", e?.message ) } console.info("[PayPal] invoice reconciliation mapping:", { medusaOrderId: orderId, displayId, invoiceId, paypalOrderId, paypalOrderStatus, currentInvoiceId, }) if (paypalOrderStatus && !PATCHABLE_STATUSES.has(paypalOrderStatus)) { console.info( `[PayPal] invoice_id PATCH skipped — order status is ${paypalOrderStatus} (immutable).`, `Reconcile via: PayPal order ${paypalOrderId} = Medusa order #${displayId}` ) return } if (currentInvoiceId === invoiceId) { console.info( `[PayPal] invoice_id already "${invoiceId}" — skipping PATCH` ) return } const patchResp = await paypalFetch( `${base}/v2/checkout/orders/${encodeURIComponent(paypalOrderId)}`, { method: "PATCH", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify([ { op: currentInvoiceId ? "replace" : "add", path: "/purchase_units/0/invoice_id", value: invoiceId, }, ]), } ) if (patchResp.ok || patchResp.status === 204) { console.info( `[PayPal] invoice_id updated to "${invoiceId}"`, `(PayPal order ${paypalOrderId} / Medusa #${displayId})` ) } else { const errText = await patchResp.text().catch(() => "") console.warn("[PayPal] invoice_id PATCH failed", { status: patchResp.status, paypalOrderId, invoiceId, paypalOrderStatus, errText, }) } } catch (e: any) { console.warn("[PayPal] paypalOrderInvoiceHandler error:", e?.message || e) } } export const config: SubscriberConfig = { event: "order.placed", }