@easypayment/medusa-paypal 0.2.7 → 0.2.9

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 (90) hide show
  1. package/.medusa/server/src/admin/index.js +536 -938
  2. package/.medusa/server/src/admin/index.mjs +536 -938
  3. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -1
  4. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js +1 -0
  5. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js.map +1 -1
  6. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
  7. package/.medusa/server/src/api/store/paypal/capture-order/route.js +61 -74
  8. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  9. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  10. package/.medusa/server/src/api/store/paypal/create-order/route.js +3 -24
  11. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  12. package/.medusa/server/src/api/store/paypal/settings/route.d.ts.map +1 -1
  13. package/.medusa/server/src/api/store/paypal/settings/route.js +7 -1
  14. package/.medusa/server/src/api/store/paypal/settings/route.js.map +1 -1
  15. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
  16. package/.medusa/server/src/api/store/paypal/webhook/route.js +1 -1
  17. package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
  18. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
  19. package/.medusa/server/src/api/store/paypal-complete/route.js +46 -24
  20. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
  21. package/.medusa/server/src/jobs/paypal-reconcile.d.ts.map +1 -1
  22. package/.medusa/server/src/jobs/paypal-reconcile.js +19 -5
  23. package/.medusa/server/src/jobs/paypal-reconcile.js.map +1 -1
  24. package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -1
  25. package/.medusa/server/src/jobs/paypal-webhook-retry.js +1 -1
  26. package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
  27. package/.medusa/server/src/modules/paypal/index.d.ts +0 -14
  28. package/.medusa/server/src/modules/paypal/index.d.ts.map +1 -1
  29. package/.medusa/server/src/modules/paypal/service.d.ts +56 -93
  30. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  31. package/.medusa/server/src/modules/paypal/service.js +34 -47
  32. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  33. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +14 -0
  34. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -0
  35. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +32 -0
  36. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -0
  37. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +2 -15
  38. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
  39. package/.medusa/server/src/modules/paypal/webhook-processor.js +17 -100
  40. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/admin/routes/settings/paypal/_components/Tabs.tsx +0 -1
  43. package/src/admin/routes/settings/paypal/additional-settings/page.tsx +226 -346
  44. package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +227 -381
  45. package/src/admin/routes/settings/paypal/audit-logs/page.tsx +127 -131
  46. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +599 -557
  47. package/src/admin/routes/settings/paypal/reconciliation-status/page.tsx +120 -165
  48. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +12 -1
  49. package/src/api/store/paypal/capture-order/route.ts +276 -284
  50. package/src/api/store/paypal/create-order/route.ts +2 -32
  51. package/src/api/store/paypal/settings/route.ts +8 -1
  52. package/src/api/store/paypal/webhook/route.ts +1 -2
  53. package/src/api/store/paypal-complete/route.ts +75 -45
  54. package/src/jobs/paypal-reconcile.ts +21 -6
  55. package/src/jobs/paypal-webhook-retry.ts +1 -2
  56. package/src/modules/paypal/service.ts +39 -62
  57. package/src/modules/paypal/utils/paypal-auth.ts +32 -0
  58. package/src/modules/paypal/webhook-processor.ts +18 -116
  59. package/tsconfig.json +1 -1
  60. package/.medusa/server/src/api/admin/paypal/disputes/[id]/route.d.ts +0 -3
  61. package/.medusa/server/src/api/admin/paypal/disputes/[id]/route.d.ts.map +0 -1
  62. package/.medusa/server/src/api/admin/paypal/disputes/[id]/route.js +0 -17
  63. package/.medusa/server/src/api/admin/paypal/disputes/[id]/route.js.map +0 -1
  64. package/.medusa/server/src/api/admin/paypal/disputes/route.d.ts +0 -3
  65. package/.medusa/server/src/api/admin/paypal/disputes/route.d.ts.map +0 -1
  66. package/.medusa/server/src/api/admin/paypal/disputes/route.js +0 -27
  67. package/.medusa/server/src/api/admin/paypal/disputes/route.js.map +0 -1
  68. package/.medusa/server/src/api/admin/paypal/disputes/summary/route.d.ts +0 -3
  69. package/.medusa/server/src/api/admin/paypal/disputes/summary/route.d.ts.map +0 -1
  70. package/.medusa/server/src/api/admin/paypal/disputes/summary/route.js +0 -17
  71. package/.medusa/server/src/api/admin/paypal/disputes/summary/route.js.map +0 -1
  72. package/.medusa/server/src/api/store/paypal/disputes/route.d.ts +0 -3
  73. package/.medusa/server/src/api/store/paypal/disputes/route.d.ts.map +0 -1
  74. package/.medusa/server/src/api/store/paypal/disputes/route.js +0 -46
  75. package/.medusa/server/src/api/store/paypal/disputes/route.js.map +0 -1
  76. package/.medusa/server/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.d.ts +0 -6
  77. package/.medusa/server/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.d.ts.map +0 -1
  78. package/.medusa/server/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.js +0 -43
  79. package/.medusa/server/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.js.map +0 -1
  80. package/.medusa/server/src/modules/paypal/models/paypal_dispute.d.ts +0 -16
  81. package/.medusa/server/src/modules/paypal/models/paypal_dispute.d.ts.map +0 -1
  82. package/.medusa/server/src/modules/paypal/models/paypal_dispute.js +0 -19
  83. package/.medusa/server/src/modules/paypal/models/paypal_dispute.js.map +0 -1
  84. package/src/admin/routes/settings/paypal/disputes/page.tsx +0 -259
  85. package/src/api/admin/paypal/disputes/[id]/route.ts +0 -19
  86. package/src/api/admin/paypal/disputes/route.ts +0 -30
  87. package/src/api/admin/paypal/disputes/summary/route.ts +0 -18
  88. package/src/api/store/paypal/disputes/route.ts +0 -67
  89. package/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.ts +0 -40
  90. package/src/modules/paypal/models/paypal_dispute.ts +0 -18
@@ -1,284 +1,276 @@
1
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
- import { randomUUID } from "crypto"
3
- import { Pool } from "pg"
4
- import type PayPalModuleService from "../../../../modules/paypal/service"
5
- import { isPayPalProviderId } from "../../../../modules/paypal/utils/provider-ids"
6
-
7
- type Body = {
8
- cart_id: string
9
- order_id: string
10
- }
11
-
12
- async function getPayPalApiBase(environment: string) {
13
- return environment === "live"
14
- ? "https://api-m.paypal.com"
15
- : "https://api-m.sandbox.paypal.com"
16
- }
17
-
18
- async function getPayPalAccessToken(opts: {
19
- environment: string
20
- client_id: string
21
- client_secret: string
22
- }) {
23
- const base = await getPayPalApiBase(opts.environment)
24
- const auth = Buffer.from(`${opts.client_id}:${opts.client_secret}`).toString("base64")
25
-
26
- const resp = await fetch(`${base}/v1/oauth2/token`, {
27
- method: "POST",
28
- headers: {
29
- Authorization: `Basic ${auth}`,
30
- "Content-Type": "application/x-www-form-urlencoded",
31
- },
32
- body: "grant_type=client_credentials",
33
- })
34
-
35
- const text = await resp.text()
36
- if (!resp.ok) {
37
- throw new Error(`PayPal token error (${resp.status}): ${text}`)
38
- }
39
-
40
- const json = JSON.parse(text)
41
- return { accessToken: String(json.access_token), base }
42
- }
43
-
44
- function resolveIdempotencyKey(req: MedusaRequest, suffix: string, fallback: string) {
45
- const header =
46
- req.headers["idempotency-key"] ||
47
- req.headers["Idempotency-Key"] ||
48
- req.headers["x-idempotency-key"] ||
49
- req.headers["X-Idempotency-Key"]
50
- const key = Array.isArray(header) ? header[0] : header
51
- if (key && String(key).trim()) {
52
- return `${String(key).trim()}-${suffix}`
53
- }
54
- return fallback || `pp-${suffix}-${randomUUID()}`
55
- }
56
-
57
- /**
58
- * Find the PayPal payment session for a cart using direct DB query.
59
- * Replaces the broken paymentCollectionService.retrieveByCartId() call.
60
- */
61
- async function findPayPalSessionForCart(cartId: string): Promise<{
62
- session_id: string
63
- session_data: Record<string, any>
64
- session_status: string
65
- } | null> {
66
- const pool = new Pool({ connectionString: process.env.DATABASE_URL })
67
- try {
68
- const { rows } = await pool.query(
69
- `SELECT ps.id as session_id, ps.data as session_data, ps.status as session_status
70
- FROM payment_session ps
71
- JOIN payment_collection pc ON ps.payment_collection_id = pc.id
72
- JOIN cart_payment_collection cpc ON cpc.payment_collection_id = pc.id
73
- WHERE cpc.cart_id = $1
74
- AND ps.provider_id LIKE '%paypal%'
75
- ORDER BY ps.created_at DESC
76
- LIMIT 1`,
77
- [cartId]
78
- )
79
- return rows[0] ?? null
80
- } catch {
81
- return null
82
- } finally {
83
- await pool.end()
84
- }
85
- }
86
-
87
- /**
88
- * Update the PayPal payment session status and data directly via DB.
89
- */
90
- async function updatePayPalSession(
91
- sessionId: string,
92
- status: string,
93
- extraData: Record<string, any>
94
- ): Promise<void> {
95
- const pool = new Pool({ connectionString: process.env.DATABASE_URL })
96
- try {
97
- await pool.query(
98
- `UPDATE payment_session
99
- SET status = $1,
100
- data = data || $2::jsonb
101
- WHERE id = $3`,
102
- [status, JSON.stringify(extraData), sessionId]
103
- )
104
- } catch {
105
- // ignore
106
- } finally {
107
- await pool.end()
108
- }
109
- }
110
-
111
- async function attachPayPalCaptureToSession(
112
- cartId: string,
113
- orderId: string,
114
- capture: any
115
- ) {
116
- try {
117
- const session = await findPayPalSessionForCart(cartId)
118
- if (!session) {
119
- console.warn("[PayPal] attachPayPalCaptureToSession: no session found for cart", cartId)
120
- return
121
- }
122
-
123
- const captureId =
124
- capture?.purchase_units?.[0]?.payments?.captures?.[0]?.id ||
125
- capture?.id
126
-
127
- await updatePayPalSession(session.session_id, "authorized", {
128
- paypal: {
129
- ...((session.session_data || {}).paypal || {}),
130
- order_id: orderId,
131
- capture_id: captureId,
132
- capture,
133
- },
134
- })
135
-
136
- console.info("[PayPal] session authorized via DB:", session.session_id)
137
- } catch {
138
- // ignore
139
- }
140
- }
141
-
142
- async function attachPayPalAuthorizationToSession(
143
- cartId: string,
144
- orderId: string,
145
- authorization: any
146
- ) {
147
- try {
148
- const session = await findPayPalSessionForCart(cartId)
149
- if (!session) {
150
- console.warn("[PayPal] attachPayPalAuthorizationToSession: no session found for cart", cartId)
151
- return
152
- }
153
-
154
- const authorizationId =
155
- authorization?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id
156
-
157
- await updatePayPalSession(session.session_id, "authorized", {
158
- paypal: {
159
- ...((session.session_data || {}).paypal || {}),
160
- order_id: orderId,
161
- authorization_id: authorizationId,
162
- authorization,
163
- },
164
- })
165
-
166
- console.info("[PayPal] session authorized via DB:", session.session_id)
167
- } catch {
168
- // ignore
169
- }
170
- }
171
-
172
- async function getExistingCapture(cartId: string, orderId: string) {
173
- try {
174
- const session = await findPayPalSessionForCart(cartId)
175
- if (!session) return null
176
-
177
- const paypalData = (session.session_data || {}).paypal || {}
178
- const existingOrderId = String(paypalData.order_id || "")
179
- if (existingOrderId && existingOrderId !== orderId) return null
180
- if (paypalData.capture) return paypalData.capture
181
- if (paypalData.capture_id) return { id: paypalData.capture_id }
182
- return null
183
- } catch {
184
- return null
185
- }
186
- }
187
-
188
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
189
- const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
190
- let debugId: string | null = null
191
-
192
- try {
193
- const body = (req.body || {}) as Body
194
- const cartId = body.cart_id
195
- const orderId = body.order_id
196
-
197
- if (!cartId || !orderId) {
198
- return res.status(400).json({ message: "cart_id and order_id are required" })
199
- }
200
-
201
- const existingCapture = await getExistingCapture(cartId, orderId)
202
- if (existingCapture) {
203
- return res.json({ capture: existingCapture })
204
- }
205
-
206
- const creds = await paypal.getActiveCredentials()
207
- const { accessToken, base } = await getPayPalAccessToken(creds)
208
- const settings = await paypal.getSettings().catch(() => ({}))
209
- const data =
210
- settings && typeof settings === "object" && "data" in settings
211
- ? ((settings as { data?: Record<string, any> }).data ?? {})
212
- : {}
213
- const additionalSettings = (data.additional_settings || {}) as Record<string, any>
214
- const paymentAction =
215
- typeof additionalSettings.paymentAction === "string"
216
- ? additionalSettings.paymentAction
217
- : "capture"
218
-
219
- const requestId = resolveIdempotencyKey(req, "capture-order", `pp-capture-${orderId}`)
220
- const endpoint =
221
- paymentAction === "authorize"
222
- ? `${base}/v2/checkout/orders/${orderId}/authorize`
223
- : `${base}/v2/checkout/orders/${orderId}/capture`
224
-
225
- const ppResp = await fetch(endpoint, {
226
- method: "POST",
227
- headers: {
228
- Authorization: `Bearer ${accessToken}`,
229
- "Content-Type": "application/json",
230
- "PayPal-Request-Id": requestId,
231
- },
232
- })
233
-
234
- const ppText = await ppResp.text()
235
- debugId = ppResp.headers.get("paypal-debug-id")
236
- if (!ppResp.ok) {
237
- throw new Error(
238
- `PayPal capture error (${ppResp.status}): ${ppText}${debugId ? ` debug_id=${debugId}` : ""}`
239
- )
240
- }
241
-
242
- const payload = JSON.parse(ppText)
243
- console.info("[PayPal] capture-order raw payload:", JSON.stringify(payload, null, 2))
244
- if (paymentAction === "authorize") {
245
- await attachPayPalAuthorizationToSession(cartId, orderId, payload)
246
- } else {
247
- await attachPayPalCaptureToSession(cartId, orderId, payload)
248
- }
249
-
250
- console.info("[PayPal] capture-order", {
251
- cart_id: cartId,
252
- order_id: orderId,
253
- request_id: requestId,
254
- debug_id: ppResp.headers.get("paypal-debug-id"),
255
- capture_id: payload?.id,
256
- })
257
-
258
- try {
259
- await paypal.recordMetric(
260
- paymentAction === "authorize" ? "authorize_order_success" : "capture_order_success"
261
- )
262
- } catch {
263
- // ignore metrics failures
264
- }
265
-
266
- return paymentAction === "authorize"
267
- ? res.json({ authorization: payload })
268
- : res.json({ capture: payload })
269
- } catch (e: any) {
270
- try {
271
- const body = (req.body || {}) as Body
272
- await paypal.recordAuditEvent("capture_order_failed", {
273
- cart_id: body.cart_id,
274
- order_id: body.order_id,
275
- debug_id: debugId,
276
- message: e?.message || String(e),
277
- })
278
- await paypal.recordMetric("capture_order_failed")
279
- } catch {
280
- // ignore audit logging failures
281
- }
282
- return res.status(500).json({ message: e?.message || "Failed to capture PayPal order" })
283
- }
284
- }
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
+ }
@@ -5,6 +5,7 @@ import {
5
5
  assertPayPalCurrencySupported,
6
6
  normalizeCurrencyCode,
7
7
  } from "../../../../modules/paypal/utils/currencies"
8
+ import { getPayPalAccessToken } from "../../../../modules/paypal/utils/paypal-auth"
8
9
  import type PayPalModuleService from "../../../../modules/paypal/service"
9
10
  import { isPayPalProviderId } from "../../../../modules/paypal/utils/provider-ids"
10
11
 
@@ -12,38 +13,6 @@ type Body = {
12
13
  cart_id: string
13
14
  }
14
15
 
15
- async function getPayPalApiBase(environment: string) {
16
- return environment === "live"
17
- ? "https://api-m.paypal.com"
18
- : "https://api-m.sandbox.paypal.com"
19
- }
20
-
21
- async function getPayPalAccessToken(opts: {
22
- environment: string
23
- client_id: string
24
- client_secret: string
25
- }) {
26
- const base = await getPayPalApiBase(opts.environment)
27
- const auth = Buffer.from(`${opts.client_id}:${opts.client_secret}`).toString("base64")
28
-
29
- const resp = await fetch(`${base}/v1/oauth2/token`, {
30
- method: "POST",
31
- headers: {
32
- Authorization: `Basic ${auth}`,
33
- "Content-Type": "application/x-www-form-urlencoded",
34
- },
35
- body: "grant_type=client_credentials",
36
- })
37
-
38
- const text = await resp.text()
39
- if (!resp.ok) {
40
- throw new Error(`PayPal token error (${resp.status}): ${text}`)
41
- }
42
-
43
- const json = JSON.parse(text)
44
- return { accessToken: String(json.access_token), base }
45
- }
46
-
47
16
  function resolveIdempotencyKey(req: MedusaRequest, suffix: string, fallback: string) {
48
17
  const header =
49
18
  req.headers["idempotency-key"] ||
@@ -78,6 +47,7 @@ async function attachPayPalOrderToSession(
78
47
  }
79
48
 
80
49
  await paymentSessionService.update(paypalSession.id, {
50
+ amount: paypalSession.amount,
81
51
  data: {
82
52
  ...(paypalSession.data || {}),
83
53
  paypal: {
@@ -5,7 +5,14 @@ export async function GET(req: MedusaRequest, res: MedusaResponse) {
5
5
  const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
6
6
  try {
7
7
  const settings = await paypal.getSettings()
8
- return res.json(settings)
8
+ const data = (settings?.data || {}) as Record<string, any>
9
+ const additionalSettings = (data.additional_settings || {}) as Record<string, any>
10
+ const advancedCard = (data.advanced_card_payments || {}) as Record<string, any>
11
+
12
+ return res.json({
13
+ paymentAction: additionalSettings.paymentAction === "authorize" ? "authorize" : "capture",
14
+ advancedCardEnabled: advancedCard.enabled === true,
15
+ })
9
16
  } catch (e: any) {
10
17
  return res.status(500).json({ message: e?.message || "Failed to load PayPal settings" })
11
18
  }
@@ -192,8 +192,7 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
192
192
  id: recordResult.event.id,
193
193
  status: "processed",
194
194
  processed_at: new Date(),
195
- resource_id:
196
- processed.disputeId || processed.refundId || processed.captureId || processed.orderId || null,
195
+ resource_id: processed.refundId || processed.captureId || processed.orderId || null,
197
196
  })
198
197
  }
199
198