@easypayment/medusa-paypal 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.medusa/server/src/admin/index.js +7 -7
  2. package/.medusa/server/src/admin/index.mjs +7 -7
  3. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  4. package/.medusa/server/src/api/store/paypal/create-order/route.js +10 -3
  5. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  6. package/.medusa/server/src/modules/paypal/migrations/20260115120000_create_paypal_connection.js +22 -22
  7. package/.medusa/server/src/modules/paypal/migrations/20260123090000_create_paypal_settings.js +11 -11
  8. package/.medusa/server/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.js +18 -18
  9. package/.medusa/server/src/modules/paypal/migrations/20260401090000_create_paypal_metric.js +16 -16
  10. package/.medusa/server/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.js +20 -20
  11. package/.medusa/server/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.js +14 -14
  12. package/.medusa/server/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.js +15 -15
  13. package/README.md +142 -142
  14. package/package.json +75 -75
  15. package/src/admin/index.ts +7 -7
  16. package/src/admin/routes/settings/paypal/_components/Tabs.tsx +52 -52
  17. package/src/admin/routes/settings/paypal/_components/Toast.tsx +51 -51
  18. package/src/admin/routes/settings/paypal/additional-settings/page.tsx +200 -200
  19. package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +183 -183
  20. package/src/admin/routes/settings/paypal/apple-pay/page.tsx +5 -5
  21. package/src/admin/routes/settings/paypal/connection/page.tsx +754 -754
  22. package/src/admin/routes/settings/paypal/google-pay/page.tsx +5 -5
  23. package/src/admin/routes/settings/paypal/pay-later-messaging/page.tsx +5 -5
  24. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +376 -376
  25. package/src/api/admin/payment-collections/[id]/payment-sessions/route.ts +24 -24
  26. package/src/api/admin/paypal/disconnect/route.ts +8 -8
  27. package/src/api/admin/paypal/environment/route.ts +25 -25
  28. package/src/api/admin/paypal/onboard-complete/route.ts +44 -44
  29. package/src/api/admin/paypal/onboarding-link/route.ts +45 -45
  30. package/src/api/admin/paypal/onboarding-status/route.ts +18 -18
  31. package/src/api/admin/paypal/rotate-credentials/route.ts +8 -8
  32. package/src/api/admin/paypal/save-credentials/route.ts +14 -14
  33. package/src/api/admin/paypal/settings/route.ts +14 -14
  34. package/src/api/admin/paypal/status/route.ts +12 -12
  35. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +65 -65
  36. package/src/api/store/paypal/capture-order/route.ts +276 -276
  37. package/src/api/store/paypal/config/route.ts +102 -102
  38. package/src/api/store/paypal/create-order/route.ts +13 -3
  39. package/src/api/store/paypal/settings/route.ts +19 -19
  40. package/src/api/store/paypal/webhook/route.ts +246 -246
  41. package/src/api/store/paypal-complete/route.ts +75 -75
  42. package/src/jobs/paypal-reconcile.ts +112 -112
  43. package/src/jobs/paypal-webhook-retry.ts +85 -85
  44. package/src/modules/paypal/clients/paypal-seller.client.ts +59 -59
  45. package/src/modules/paypal/index.ts +8 -8
  46. package/src/modules/paypal/migrations/20260115120000_create_paypal_connection.ts +33 -33
  47. package/src/modules/paypal/migrations/20260123090000_create_paypal_settings.ts +22 -22
  48. package/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.ts +29 -29
  49. package/src/modules/paypal/migrations/20260401090000_create_paypal_metric.ts +27 -27
  50. package/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.ts +31 -31
  51. package/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.ts +25 -25
  52. package/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.ts +26 -26
  53. package/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.ts +11 -11
  54. package/src/modules/paypal/models/paypal_connection.ts +21 -21
  55. package/src/modules/paypal/models/paypal_metric.ts +9 -9
  56. package/src/modules/paypal/models/paypal_settings.ts +8 -8
  57. package/src/modules/paypal/models/paypal_webhook_event.ts +19 -19
  58. package/src/modules/paypal/payment-provider/README.md +22 -22
  59. package/src/modules/paypal/payment-provider/card-service.ts +760 -760
  60. package/src/modules/paypal/payment-provider/index.ts +19 -19
  61. package/src/modules/paypal/payment-provider/service.ts +1121 -1121
  62. package/src/modules/paypal/payment-provider/webhook-utils.ts +88 -88
  63. package/src/modules/paypal/service.ts +1247 -1247
  64. package/src/modules/paypal/types/config.ts +47 -47
  65. package/src/modules/paypal/utils/amounts.ts +41 -41
  66. package/src/modules/paypal/utils/crypto.ts +51 -51
  67. package/src/modules/paypal/utils/currencies.ts +84 -84
  68. package/src/modules/paypal/utils/paypal-auth.ts +32 -32
  69. package/src/modules/paypal/utils/provider-ids.ts +15 -15
  70. package/src/modules/paypal/webhook-processor.ts +215 -215
@@ -1,276 +1,276 @@
1
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
- import type { IPaymentModuleService } from "@medusajs/framework/types"
3
- import { Modules } from "@medusajs/framework/utils"
4
- import { randomUUID } from "crypto"
5
- import type PayPalModuleService from "../../../../modules/paypal/service"
6
- import { getPayPalAccessToken } from "../../../../modules/paypal/utils/paypal-auth"
7
- import { isPayPalProviderId } from "../../../../modules/paypal/utils/provider-ids"
8
-
9
- type Body = {
10
- cart_id: string
11
- order_id: string
12
- }
13
-
14
- function resolveIdempotencyKey(req: MedusaRequest, suffix: string, fallback: string) {
15
- const header =
16
- req.headers["idempotency-key"] ||
17
- req.headers["Idempotency-Key"] ||
18
- req.headers["x-idempotency-key"] ||
19
- req.headers["X-Idempotency-Key"]
20
- const key = Array.isArray(header) ? header[0] : header
21
- if (key && String(key).trim()) {
22
- return `${String(key).trim()}-${suffix}`
23
- }
24
- return fallback || `pp-${suffix}-${randomUUID()}`
25
- }
26
-
27
- async function findPayPalSessionForCart(
28
- cartId: string,
29
- scope: any
30
- ): Promise<{
31
- session_id: string
32
- session_data: Record<string, any>
33
- session_status: string
34
- } | null> {
35
- try {
36
- const query = scope.resolve("query")
37
- const { data: carts } = await query.graph({
38
- entity: "cart",
39
- fields: [
40
- "id",
41
- "payment_collection.payment_sessions.id",
42
- "payment_collection.payment_sessions.data",
43
- "payment_collection.payment_sessions.status",
44
- "payment_collection.payment_sessions.provider_id",
45
- "payment_collection.payment_sessions.created_at",
46
- ],
47
- filters: { id: cartId },
48
- })
49
- const cart = carts?.[0]
50
- const sessions = cart?.payment_collection?.payment_sessions || []
51
- const session = sessions
52
- .filter((s: any) => isPayPalProviderId(s.provider_id))
53
- .sort(
54
- (a: any, b: any) =>
55
- new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
56
- )[0]
57
-
58
- if (!session) return null
59
-
60
- return {
61
- session_id: session.id,
62
- session_data: (session.data || {}) as Record<string, any>,
63
- session_status: session.status,
64
- }
65
- } catch (e: any) {
66
- console.warn("[PayPal] findPayPalSessionForCart failed:", e?.message)
67
- return null
68
- }
69
- }
70
-
71
- async function updatePayPalSession(
72
- sessionId: string,
73
- status: string,
74
- extraData: Record<string, any>,
75
- scope: any
76
- ): Promise<void> {
77
- try {
78
- const paymentModule = scope.resolve(Modules.PAYMENT) as IPaymentModuleService
79
- const [existing] = await paymentModule.listPaymentSessions({ id: [sessionId] }, { take: 1 })
80
- const mergedData = { ...(existing?.data || {}), ...extraData }
81
- await (paymentModule as any).updatePaymentSession({
82
- id: sessionId,
83
- data: mergedData,
84
- status: status as any,
85
- amount: existing?.amount,
86
- currency_code: existing?.currency_code, // ✅ add this
87
- })
88
- } catch (e: any) {
89
- console.error("[PayPal] updatePayPalSession failed:", e?.message)
90
- }
91
- }
92
-
93
- async function attachPayPalCaptureToSession(
94
- cartId: string,
95
- orderId: string,
96
- capture: any,
97
- scope: any
98
- ) {
99
- try {
100
- const session = await findPayPalSessionForCart(cartId, scope)
101
- if (!session) {
102
- console.warn("[PayPal] attachPayPalCaptureToSession: no session found for cart", cartId)
103
- return
104
- }
105
-
106
- const captureId = capture?.purchase_units?.[0]?.payments?.captures?.[0]?.id || capture?.id
107
-
108
- await updatePayPalSession(
109
- session.session_id,
110
- "captured",
111
- {
112
- paypal: {
113
- ...((session.session_data || {}).paypal || {}),
114
- order_id: orderId,
115
- capture_id: captureId,
116
- capture,
117
- },
118
- },
119
- scope
120
- )
121
-
122
- console.info("[PayPal] session captured via DB:", session.session_id)
123
- } catch {
124
- // ignore
125
- }
126
- }
127
-
128
- async function attachPayPalAuthorizationToSession(
129
- cartId: string,
130
- orderId: string,
131
- authorization: any,
132
- scope: any
133
- ) {
134
- try {
135
- const session = await findPayPalSessionForCart(cartId, scope)
136
- if (!session) {
137
- console.warn("[PayPal] attachPayPalAuthorizationToSession: no session found for cart", cartId)
138
- return
139
- }
140
-
141
- const authorizationId = authorization?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id
142
-
143
- await updatePayPalSession(
144
- session.session_id,
145
- "authorized",
146
- {
147
- paypal: {
148
- ...((session.session_data || {}).paypal || {}),
149
- order_id: orderId,
150
- authorization_id: authorizationId,
151
- authorization,
152
- },
153
- },
154
- scope
155
- )
156
-
157
- console.info("[PayPal] session authorized via DB:", session.session_id)
158
- } catch {
159
- // ignore
160
- }
161
- }
162
-
163
- async function getExistingCapture(cartId: string, orderId: string, scope: any) {
164
- try {
165
- const session = await findPayPalSessionForCart(cartId, scope)
166
- if (!session) return null
167
-
168
- const paypalData = (session.session_data || {}).paypal || {}
169
- const existingOrderId = String(paypalData.order_id || "")
170
- if (existingOrderId && existingOrderId !== orderId) return null
171
- if (paypalData.capture) return paypalData.capture
172
- if (paypalData.capture_id) return { id: paypalData.capture_id }
173
- return null
174
- } catch {
175
- return null
176
- }
177
- }
178
-
179
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
180
- const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
181
- const { scope } = req
182
- let debugId: string | null = null
183
-
184
- try {
185
- const body = (req.body || {}) as Body
186
- const cartId = body.cart_id
187
- const orderId = body.order_id
188
-
189
- if (!cartId || !orderId) {
190
- return res.status(400).json({ message: "cart_id and order_id are required" })
191
- }
192
-
193
- const existingCapture = await getExistingCapture(cartId, orderId, scope)
194
- if (existingCapture) {
195
- return res.json({ capture: existingCapture })
196
- }
197
-
198
- const creds = await paypal.getActiveCredentials()
199
- const { accessToken, base } = await getPayPalAccessToken(creds)
200
- const settings = await paypal.getSettings().catch(() => ({}))
201
- const data =
202
- settings && typeof settings === "object" && "data" in settings
203
- ? ((settings as { data?: Record<string, any> }).data ?? {})
204
- : {}
205
- const additionalSettings = (data.additional_settings || {}) as Record<string, any>
206
- const paymentAction =
207
- typeof additionalSettings.paymentAction === "string"
208
- ? additionalSettings.paymentAction
209
- : "capture"
210
-
211
- const requestId = resolveIdempotencyKey(req, "capture-order", `pp-capture-${orderId}`)
212
- const endpoint =
213
- paymentAction === "authorize"
214
- ? `${base}/v2/checkout/orders/${orderId}/authorize`
215
- : `${base}/v2/checkout/orders/${orderId}/capture`
216
-
217
- const ppResp = await fetch(endpoint, {
218
- method: "POST",
219
- headers: {
220
- Authorization: `Bearer ${accessToken}`,
221
- "Content-Type": "application/json",
222
- "PayPal-Request-Id": requestId,
223
- },
224
- })
225
-
226
- const ppText = await ppResp.text()
227
- debugId = ppResp.headers.get("paypal-debug-id")
228
- if (!ppResp.ok) {
229
- throw new Error(
230
- `PayPal capture error (${ppResp.status}): ${ppText}${debugId ? ` debug_id=${debugId}` : ""}`
231
- )
232
- }
233
-
234
- const payload = JSON.parse(ppText)
235
- console.info("[PayPal] capture-order raw payload:", JSON.stringify(payload, null, 2))
236
- if (paymentAction === "authorize") {
237
- await attachPayPalAuthorizationToSession(cartId, orderId, payload, req.scope)
238
- } else {
239
- await attachPayPalCaptureToSession(cartId, orderId, payload, req.scope)
240
- }
241
-
242
- console.info("[PayPal] capture-order", {
243
- cart_id: cartId,
244
- order_id: orderId,
245
- request_id: requestId,
246
- debug_id: ppResp.headers.get("paypal-debug-id"),
247
- capture_id: payload?.id,
248
- })
249
-
250
- try {
251
- await paypal.recordMetric(
252
- paymentAction === "authorize" ? "authorize_order_success" : "capture_order_success"
253
- )
254
- } catch {
255
- // metrics failure must never affect payment outcome
256
- }
257
-
258
- return paymentAction === "authorize"
259
- ? res.json({ authorization: payload })
260
- : res.json({ capture: payload })
261
- } catch (e: any) {
262
- try {
263
- const body = (req.body || {}) as Body
264
- await paypal.recordAuditEvent("capture_order_failed", {
265
- cart_id: body.cart_id,
266
- order_id: body.order_id,
267
- debug_id: debugId,
268
- message: e?.message || String(e),
269
- })
270
- await paypal.recordMetric("capture_order_failed")
271
- } catch {
272
- // ignore audit logging failures
273
- }
274
- return res.status(500).json({ message: e?.message || "Failed to capture PayPal order" })
275
- }
276
- }
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
+ import type { IPaymentModuleService } from "@medusajs/framework/types"
3
+ import { Modules } from "@medusajs/framework/utils"
4
+ import { randomUUID } from "crypto"
5
+ import type PayPalModuleService from "../../../../modules/paypal/service"
6
+ import { getPayPalAccessToken } from "../../../../modules/paypal/utils/paypal-auth"
7
+ import { isPayPalProviderId } from "../../../../modules/paypal/utils/provider-ids"
8
+
9
+ type Body = {
10
+ cart_id: string
11
+ order_id: string
12
+ }
13
+
14
+ function resolveIdempotencyKey(req: MedusaRequest, suffix: string, fallback: string) {
15
+ const header =
16
+ req.headers["idempotency-key"] ||
17
+ req.headers["Idempotency-Key"] ||
18
+ req.headers["x-idempotency-key"] ||
19
+ req.headers["X-Idempotency-Key"]
20
+ const key = Array.isArray(header) ? header[0] : header
21
+ if (key && String(key).trim()) {
22
+ return `${String(key).trim()}-${suffix}`
23
+ }
24
+ return fallback || `pp-${suffix}-${randomUUID()}`
25
+ }
26
+
27
+ async function findPayPalSessionForCart(
28
+ cartId: string,
29
+ scope: any
30
+ ): Promise<{
31
+ session_id: string
32
+ session_data: Record<string, any>
33
+ session_status: string
34
+ } | null> {
35
+ try {
36
+ const query = scope.resolve("query")
37
+ const { data: carts } = await query.graph({
38
+ entity: "cart",
39
+ fields: [
40
+ "id",
41
+ "payment_collection.payment_sessions.id",
42
+ "payment_collection.payment_sessions.data",
43
+ "payment_collection.payment_sessions.status",
44
+ "payment_collection.payment_sessions.provider_id",
45
+ "payment_collection.payment_sessions.created_at",
46
+ ],
47
+ filters: { id: cartId },
48
+ })
49
+ const cart = carts?.[0]
50
+ const sessions = cart?.payment_collection?.payment_sessions || []
51
+ const session = sessions
52
+ .filter((s: any) => isPayPalProviderId(s.provider_id))
53
+ .sort(
54
+ (a: any, b: any) =>
55
+ new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
56
+ )[0]
57
+
58
+ if (!session) return null
59
+
60
+ return {
61
+ session_id: session.id,
62
+ session_data: (session.data || {}) as Record<string, any>,
63
+ session_status: session.status,
64
+ }
65
+ } catch (e: any) {
66
+ console.warn("[PayPal] findPayPalSessionForCart failed:", e?.message)
67
+ return null
68
+ }
69
+ }
70
+
71
+ async function updatePayPalSession(
72
+ sessionId: string,
73
+ status: string,
74
+ extraData: Record<string, any>,
75
+ scope: any
76
+ ): Promise<void> {
77
+ try {
78
+ const paymentModule = scope.resolve(Modules.PAYMENT) as IPaymentModuleService
79
+ const [existing] = await paymentModule.listPaymentSessions({ id: [sessionId] }, { take: 1 })
80
+ const mergedData = { ...(existing?.data || {}), ...extraData }
81
+ await (paymentModule as any).updatePaymentSession({
82
+ id: sessionId,
83
+ data: mergedData,
84
+ status: status as any,
85
+ amount: existing?.amount,
86
+ currency_code: existing?.currency_code, // ✅ add this
87
+ })
88
+ } catch (e: any) {
89
+ console.error("[PayPal] updatePayPalSession failed:", e?.message)
90
+ }
91
+ }
92
+
93
+ async function attachPayPalCaptureToSession(
94
+ cartId: string,
95
+ orderId: string,
96
+ capture: any,
97
+ scope: any
98
+ ) {
99
+ try {
100
+ const session = await findPayPalSessionForCart(cartId, scope)
101
+ if (!session) {
102
+ console.warn("[PayPal] attachPayPalCaptureToSession: no session found for cart", cartId)
103
+ return
104
+ }
105
+
106
+ const captureId = capture?.purchase_units?.[0]?.payments?.captures?.[0]?.id || capture?.id
107
+
108
+ await updatePayPalSession(
109
+ session.session_id,
110
+ "captured",
111
+ {
112
+ paypal: {
113
+ ...((session.session_data || {}).paypal || {}),
114
+ order_id: orderId,
115
+ capture_id: captureId,
116
+ capture,
117
+ },
118
+ },
119
+ scope
120
+ )
121
+
122
+ console.info("[PayPal] session captured via DB:", session.session_id)
123
+ } catch {
124
+ // ignore
125
+ }
126
+ }
127
+
128
+ async function attachPayPalAuthorizationToSession(
129
+ cartId: string,
130
+ orderId: string,
131
+ authorization: any,
132
+ scope: any
133
+ ) {
134
+ try {
135
+ const session = await findPayPalSessionForCart(cartId, scope)
136
+ if (!session) {
137
+ console.warn("[PayPal] attachPayPalAuthorizationToSession: no session found for cart", cartId)
138
+ return
139
+ }
140
+
141
+ const authorizationId = authorization?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id
142
+
143
+ await updatePayPalSession(
144
+ session.session_id,
145
+ "authorized",
146
+ {
147
+ paypal: {
148
+ ...((session.session_data || {}).paypal || {}),
149
+ order_id: orderId,
150
+ authorization_id: authorizationId,
151
+ authorization,
152
+ },
153
+ },
154
+ scope
155
+ )
156
+
157
+ console.info("[PayPal] session authorized via DB:", session.session_id)
158
+ } catch {
159
+ // ignore
160
+ }
161
+ }
162
+
163
+ async function getExistingCapture(cartId: string, orderId: string, scope: any) {
164
+ try {
165
+ const session = await findPayPalSessionForCart(cartId, scope)
166
+ if (!session) return null
167
+
168
+ const paypalData = (session.session_data || {}).paypal || {}
169
+ const existingOrderId = String(paypalData.order_id || "")
170
+ if (existingOrderId && existingOrderId !== orderId) return null
171
+ if (paypalData.capture) return paypalData.capture
172
+ if (paypalData.capture_id) return { id: paypalData.capture_id }
173
+ return null
174
+ } catch {
175
+ return null
176
+ }
177
+ }
178
+
179
+ export async function POST(req: MedusaRequest, res: MedusaResponse) {
180
+ const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
181
+ const { scope } = req
182
+ let debugId: string | null = null
183
+
184
+ try {
185
+ const body = (req.body || {}) as Body
186
+ const cartId = body.cart_id
187
+ const orderId = body.order_id
188
+
189
+ if (!cartId || !orderId) {
190
+ return res.status(400).json({ message: "cart_id and order_id are required" })
191
+ }
192
+
193
+ const existingCapture = await getExistingCapture(cartId, orderId, scope)
194
+ if (existingCapture) {
195
+ return res.json({ capture: existingCapture })
196
+ }
197
+
198
+ const creds = await paypal.getActiveCredentials()
199
+ const { accessToken, base } = await getPayPalAccessToken(creds)
200
+ const settings = await paypal.getSettings().catch(() => ({}))
201
+ const data =
202
+ settings && typeof settings === "object" && "data" in settings
203
+ ? ((settings as { data?: Record<string, any> }).data ?? {})
204
+ : {}
205
+ const additionalSettings = (data.additional_settings || {}) as Record<string, any>
206
+ const paymentAction =
207
+ typeof additionalSettings.paymentAction === "string"
208
+ ? additionalSettings.paymentAction
209
+ : "capture"
210
+
211
+ const requestId = resolveIdempotencyKey(req, "capture-order", `pp-capture-${orderId}`)
212
+ const endpoint =
213
+ paymentAction === "authorize"
214
+ ? `${base}/v2/checkout/orders/${orderId}/authorize`
215
+ : `${base}/v2/checkout/orders/${orderId}/capture`
216
+
217
+ const ppResp = await fetch(endpoint, {
218
+ method: "POST",
219
+ headers: {
220
+ Authorization: `Bearer ${accessToken}`,
221
+ "Content-Type": "application/json",
222
+ "PayPal-Request-Id": requestId,
223
+ },
224
+ })
225
+
226
+ const ppText = await ppResp.text()
227
+ debugId = ppResp.headers.get("paypal-debug-id")
228
+ if (!ppResp.ok) {
229
+ throw new Error(
230
+ `PayPal capture error (${ppResp.status}): ${ppText}${debugId ? ` debug_id=${debugId}` : ""}`
231
+ )
232
+ }
233
+
234
+ const payload = JSON.parse(ppText)
235
+ console.info("[PayPal] capture-order raw payload:", JSON.stringify(payload, null, 2))
236
+ if (paymentAction === "authorize") {
237
+ await attachPayPalAuthorizationToSession(cartId, orderId, payload, req.scope)
238
+ } else {
239
+ await attachPayPalCaptureToSession(cartId, orderId, payload, req.scope)
240
+ }
241
+
242
+ console.info("[PayPal] capture-order", {
243
+ cart_id: cartId,
244
+ order_id: orderId,
245
+ request_id: requestId,
246
+ debug_id: ppResp.headers.get("paypal-debug-id"),
247
+ capture_id: payload?.id,
248
+ })
249
+
250
+ try {
251
+ await paypal.recordMetric(
252
+ paymentAction === "authorize" ? "authorize_order_success" : "capture_order_success"
253
+ )
254
+ } catch {
255
+ // metrics failure must never affect payment outcome
256
+ }
257
+
258
+ return paymentAction === "authorize"
259
+ ? res.json({ authorization: payload })
260
+ : res.json({ capture: payload })
261
+ } catch (e: any) {
262
+ try {
263
+ const body = (req.body || {}) as Body
264
+ await paypal.recordAuditEvent("capture_order_failed", {
265
+ cart_id: body.cart_id,
266
+ order_id: body.order_id,
267
+ debug_id: debugId,
268
+ message: e?.message || String(e),
269
+ })
270
+ await paypal.recordMetric("capture_order_failed")
271
+ } catch {
272
+ // ignore audit logging failures
273
+ }
274
+ return res.status(500).json({ message: e?.message || "Failed to capture PayPal order" })
275
+ }
276
+ }