@easypayment/medusa-paypal 0.6.2 → 0.6.4
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.
- package/.medusa/server/src/admin/index.js +12 -15
- package/.medusa/server/src/admin/index.mjs +12 -15
- package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/capture-order/route.js +1 -11
- package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/create-order/route.js +0 -9
- package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/webhook/route.js +162 -115
- package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal-complete/route.js +0 -6
- package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
- package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -1
- package/.medusa/server/src/jobs/paypal-webhook-retry.js +97 -43
- package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
- package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.d.ts +6 -0
- package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.js +20 -0
- package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.js.map +1 -0
- package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/service.js +0 -42
- package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/service.d.ts +0 -8
- package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/service.js +6 -114
- package/.medusa/server/src/modules/paypal/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/types/config.d.ts +0 -2
- package/.medusa/server/src/modules/paypal/types/config.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/types/config.js +0 -9
- package/.medusa/server/src/modules/paypal/types/config.js.map +1 -1
- package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +21 -17
- package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/webhook-processor.js +195 -99
- package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
- package/README.md +156 -159
- package/package.json +1 -1
- package/src/admin/routes/settings/paypal/_components/Tabs.tsx +48 -52
- package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +0 -23
- package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +56 -65
- package/src/api/store/paypal/capture-order/route.ts +266 -276
- package/src/api/store/paypal/create-order/route.ts +0 -9
- package/src/api/store/paypal/webhook/route.ts +325 -246
- package/src/api/store/paypal-complete/route.ts +69 -75
- package/src/jobs/paypal-webhook-retry.ts +149 -85
- package/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.ts +17 -0
- package/src/modules/paypal/payment-provider/service.ts +1079 -1121
- package/src/modules/paypal/service.ts +6 -127
- package/src/modules/paypal/types/config.ts +33 -47
- package/src/modules/paypal/webhook-processor.ts +377 -215
- package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.d.ts +0 -3
- package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.d.ts.map +0 -1
- package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.js +0 -9
- package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.js.map +0 -1
- package/.medusa/server/src/jobs/paypal-reconcile.d.ts +0 -7
- package/.medusa/server/src/jobs/paypal-reconcile.d.ts.map +0 -1
- package/.medusa/server/src/jobs/paypal-reconcile.js +0 -109
- package/.medusa/server/src/jobs/paypal-reconcile.js.map +0 -1
- package/.medusa/server/src/modules/paypal/utils/crypto.d.ts +0 -4
- package/.medusa/server/src/modules/paypal/utils/crypto.d.ts.map +0 -1
- package/.medusa/server/src/modules/paypal/utils/crypto.js +0 -47
- package/.medusa/server/src/modules/paypal/utils/crypto.js.map +0 -1
- package/src/api/admin/paypal/rotate-credentials/route.ts +0 -8
- package/src/jobs/paypal-reconcile.ts +0 -113
- package/src/modules/paypal/utils/crypto.ts +0 -51
|
@@ -1,1121 +1,1079 @@
|
|
|
1
|
-
import { AbstractPaymentProvider } from "@medusajs/framework/utils"
|
|
2
|
-
import { randomUUID } from "crypto"
|
|
3
|
-
import type {
|
|
4
|
-
AuthorizePaymentInput,
|
|
5
|
-
AuthorizePaymentOutput,
|
|
6
|
-
CapturePaymentInput,
|
|
7
|
-
CapturePaymentOutput,
|
|
8
|
-
CancelPaymentInput,
|
|
9
|
-
CancelPaymentOutput,
|
|
10
|
-
CreateAccountHolderInput,
|
|
11
|
-
CreateAccountHolderOutput,
|
|
12
|
-
DeletePaymentInput,
|
|
13
|
-
DeletePaymentOutput,
|
|
14
|
-
GetPaymentStatusInput,
|
|
15
|
-
GetPaymentStatusOutput,
|
|
16
|
-
InitiatePaymentInput,
|
|
17
|
-
InitiatePaymentOutput,
|
|
18
|
-
RefundPaymentInput,
|
|
19
|
-
RefundPaymentOutput,
|
|
20
|
-
RetrievePaymentInput,
|
|
21
|
-
RetrievePaymentOutput,
|
|
22
|
-
UpdatePaymentInput,
|
|
23
|
-
UpdatePaymentOutput,
|
|
24
|
-
ProviderWebhookPayload,
|
|
25
|
-
WebhookActionResult,
|
|
26
|
-
} from "@medusajs/framework/types"
|
|
27
|
-
import { formatAmountForPayPal } from "../utils/amounts"
|
|
28
|
-
import {
|
|
29
|
-
assertPayPalCurrencySupported,
|
|
30
|
-
normalizeCurrencyCode,
|
|
31
|
-
} from "../utils/currencies"
|
|
32
|
-
import type PayPalModuleService from "../service"
|
|
33
|
-
import { getPayPalWebhookActionAndData } from "./webhook-utils"
|
|
34
|
-
|
|
35
|
-
type Options = {}
|
|
36
|
-
|
|
37
|
-
function generateSessionId() {
|
|
38
|
-
try {
|
|
39
|
-
return randomUUID()
|
|
40
|
-
} catch {
|
|
41
|
-
// Fallback for environments where randomUUID isn't available
|
|
42
|
-
return `pp_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
47
|
-
static identifier = "paypal"
|
|
48
|
-
|
|
49
|
-
protected readonly options_: Options
|
|
50
|
-
|
|
51
|
-
constructor(cradle: Record<string, any>, options: Options) {
|
|
52
|
-
super(cradle, options)
|
|
53
|
-
this.options_ = options
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private resolvePayPalService() {
|
|
57
|
-
const container = this.container as {
|
|
58
|
-
resolve<T>(key: string): T
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
return container.resolve<PayPalModuleService>("paypal_onboarding")
|
|
62
|
-
} catch {
|
|
63
|
-
return null
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async resolveSettings() {
|
|
68
|
-
const paypal = this.resolvePayPalService();
|
|
69
|
-
if (!paypal) {
|
|
70
|
-
try {
|
|
71
|
-
const { Pool: _SettingsPool } = require("pg")
|
|
72
|
-
const _sPool = new _SettingsPool({ connectionString: process.env.DATABASE_URL })
|
|
73
|
-
const _sResult = await _sPool
|
|
74
|
-
.query("SELECT data FROM paypal_settings ORDER BY created_at DESC LIMIT 1")
|
|
75
|
-
.finally(() => _sPool.end())
|
|
76
|
-
const _sData = _sResult.rows[0]?.data || {}
|
|
77
|
-
return {
|
|
78
|
-
additionalSettings: (_sData.additional_settings || {}) as Record<string, unknown>,
|
|
79
|
-
apiDetails: (_sData.api_details || {}) as Record<string, unknown>,
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
return {
|
|
83
|
-
additionalSettings: {} as Record<string, unknown>,
|
|
84
|
-
apiDetails: {} as Record<string, unknown>,
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const settings = await paypal.getSettings().catch(() => ({}))
|
|
89
|
-
const data = settings && typeof settings === "object" && "data" in settings
|
|
90
|
-
? ((settings as any).data ?? {})
|
|
91
|
-
: {}
|
|
92
|
-
return {
|
|
93
|
-
additionalSettings: (data.additional_settings || {}) as Record<string, unknown>,
|
|
94
|
-
apiDetails: (data.api_details || {}) as Record<string, unknown>,
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private async resolveCurrencyOverride() {
|
|
99
|
-
const { apiDetails } = await this.resolveSettings()
|
|
100
|
-
if (typeof apiDetails.currency_code === "string" && apiDetails.currency_code.trim()) {
|
|
101
|
-
return normalizeCurrencyCode(apiDetails.currency_code)
|
|
102
|
-
}
|
|
103
|
-
return normalizeCurrencyCode(process.env.PAYPAL_CURRENCY || "EUR")
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private async getPayPalAccessToken() {
|
|
107
|
-
const paypal = this.resolvePayPalService()
|
|
108
|
-
if (!paypal) {
|
|
109
|
-
// Fallback: load credentials directly from paypal_connection table
|
|
110
|
-
const { Pool: _FbPool } = require("pg")
|
|
111
|
-
const _fbPool = new _FbPool({ connectionString: process.env.DATABASE_URL })
|
|
112
|
-
const _fbResult = await _fbPool.query(
|
|
113
|
-
"SELECT metadata, environment, seller_client_id, seller_client_secret FROM paypal_connection WHERE status='connected' ORDER BY created_at DESC LIMIT 1"
|
|
114
|
-
).finally(() => _fbPool.end())
|
|
115
|
-
const _fbRow = _fbResult.rows[0]
|
|
116
|
-
if (!_fbRow) throw new Error("No active PayPal connection found in DB")
|
|
117
|
-
const _fbEnv = _fbRow.environment || "sandbox"
|
|
118
|
-
const _fbCreds = (_fbRow.metadata && _fbRow.metadata.credentials && _fbRow.metadata.credentials[_fbEnv]) || {}
|
|
119
|
-
const _fbId = _fbCreds.client_id || _fbRow.seller_client_id
|
|
120
|
-
const _fbSec = _fbCreds.client_secret || _fbRow.seller_client_secret
|
|
121
|
-
const _fbBase = _fbEnv === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
|
|
122
|
-
const _fbAuth = Buffer.from(`${_fbId}:${_fbSec}`).toString("base64")
|
|
123
|
-
const _fbResp = await fetch(`${_fbBase}/v1/oauth2/token`, {
|
|
124
|
-
method: "POST",
|
|
125
|
-
headers: { Authorization: `Basic ${_fbAuth}`, "Content-Type": "application/x-www-form-urlencoded" },
|
|
126
|
-
body: "grant_type=client_credentials",
|
|
127
|
-
})
|
|
128
|
-
const _fbText = await _fbResp.text()
|
|
129
|
-
if (!_fbResp.ok) throw new Error(`PayPal token error (${_fbResp.status}): ${_fbText}`)
|
|
130
|
-
const _fbJson = JSON.parse(_fbText)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
await paypal.
|
|
307
|
-
await paypal.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
*
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
paypal: {
|
|
1081
|
-
...((input.data || {}).paypal as Record<string, unknown>),
|
|
1082
|
-
order: order || undefined,
|
|
1083
|
-
authorization_id: authorizationId || storedAuthorizationId,
|
|
1084
|
-
capture_id: captureId || paypalData.capture_id,
|
|
1085
|
-
refund_id: paypalData.refund_id,
|
|
1086
|
-
refund_status: paypalData.refund_status,
|
|
1087
|
-
refunds: paypalData.refunds,
|
|
1088
|
-
},
|
|
1089
|
-
canceled_at: new Date().toISOString(),
|
|
1090
|
-
},
|
|
1091
|
-
}
|
|
1092
|
-
} catch (error: any) {
|
|
1093
|
-
await this.recordFailure("cancel_failed", {
|
|
1094
|
-
order_id: orderId,
|
|
1095
|
-
capture_id: captureId,
|
|
1096
|
-
debug_id: debugId,
|
|
1097
|
-
message: error?.message,
|
|
1098
|
-
})
|
|
1099
|
-
throw error
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
async deletePayment(
|
|
1104
|
-
_input: DeletePaymentInput
|
|
1105
|
-
): Promise<DeletePaymentOutput> {
|
|
1106
|
-
return { data: {} }
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
/**
|
|
1110
|
-
* Required by AbstractPaymentProvider in Medusa v2.
|
|
1111
|
-
* This is used by /hooks/payment/{identifier}_{providerId}
|
|
1112
|
-
*/
|
|
1113
|
-
async getWebhookActionAndData(
|
|
1114
|
-
payload: ProviderWebhookPayload["payload"]
|
|
1115
|
-
): Promise<WebhookActionResult> {
|
|
1116
|
-
return getPayPalWebhookActionAndData(payload)
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
export default PayPalPaymentProvider
|
|
1121
|
-
export { PayPalPaymentProvider }
|
|
1
|
+
import { AbstractPaymentProvider } from "@medusajs/framework/utils"
|
|
2
|
+
import { randomUUID } from "crypto"
|
|
3
|
+
import type {
|
|
4
|
+
AuthorizePaymentInput,
|
|
5
|
+
AuthorizePaymentOutput,
|
|
6
|
+
CapturePaymentInput,
|
|
7
|
+
CapturePaymentOutput,
|
|
8
|
+
CancelPaymentInput,
|
|
9
|
+
CancelPaymentOutput,
|
|
10
|
+
CreateAccountHolderInput,
|
|
11
|
+
CreateAccountHolderOutput,
|
|
12
|
+
DeletePaymentInput,
|
|
13
|
+
DeletePaymentOutput,
|
|
14
|
+
GetPaymentStatusInput,
|
|
15
|
+
GetPaymentStatusOutput,
|
|
16
|
+
InitiatePaymentInput,
|
|
17
|
+
InitiatePaymentOutput,
|
|
18
|
+
RefundPaymentInput,
|
|
19
|
+
RefundPaymentOutput,
|
|
20
|
+
RetrievePaymentInput,
|
|
21
|
+
RetrievePaymentOutput,
|
|
22
|
+
UpdatePaymentInput,
|
|
23
|
+
UpdatePaymentOutput,
|
|
24
|
+
ProviderWebhookPayload,
|
|
25
|
+
WebhookActionResult,
|
|
26
|
+
} from "@medusajs/framework/types"
|
|
27
|
+
import { formatAmountForPayPal } from "../utils/amounts"
|
|
28
|
+
import {
|
|
29
|
+
assertPayPalCurrencySupported,
|
|
30
|
+
normalizeCurrencyCode,
|
|
31
|
+
} from "../utils/currencies"
|
|
32
|
+
import type PayPalModuleService from "../service"
|
|
33
|
+
import { getPayPalWebhookActionAndData } from "./webhook-utils"
|
|
34
|
+
|
|
35
|
+
type Options = {}
|
|
36
|
+
|
|
37
|
+
function generateSessionId() {
|
|
38
|
+
try {
|
|
39
|
+
return randomUUID()
|
|
40
|
+
} catch {
|
|
41
|
+
// Fallback for environments where randomUUID isn't available
|
|
42
|
+
return `pp_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
47
|
+
static identifier = "paypal"
|
|
48
|
+
|
|
49
|
+
protected readonly options_: Options
|
|
50
|
+
|
|
51
|
+
constructor(cradle: Record<string, any>, options: Options) {
|
|
52
|
+
super(cradle, options)
|
|
53
|
+
this.options_ = options
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private resolvePayPalService() {
|
|
57
|
+
const container = this.container as {
|
|
58
|
+
resolve<T>(key: string): T
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return container.resolve<PayPalModuleService>("paypal_onboarding")
|
|
62
|
+
} catch {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async resolveSettings() {
|
|
68
|
+
const paypal = this.resolvePayPalService();
|
|
69
|
+
if (!paypal) {
|
|
70
|
+
try {
|
|
71
|
+
const { Pool: _SettingsPool } = require("pg")
|
|
72
|
+
const _sPool = new _SettingsPool({ connectionString: process.env.DATABASE_URL })
|
|
73
|
+
const _sResult = await _sPool
|
|
74
|
+
.query("SELECT data FROM paypal_settings ORDER BY created_at DESC LIMIT 1")
|
|
75
|
+
.finally(() => _sPool.end())
|
|
76
|
+
const _sData = _sResult.rows[0]?.data || {}
|
|
77
|
+
return {
|
|
78
|
+
additionalSettings: (_sData.additional_settings || {}) as Record<string, unknown>,
|
|
79
|
+
apiDetails: (_sData.api_details || {}) as Record<string, unknown>,
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
return {
|
|
83
|
+
additionalSettings: {} as Record<string, unknown>,
|
|
84
|
+
apiDetails: {} as Record<string, unknown>,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const settings = await paypal.getSettings().catch(() => ({}))
|
|
89
|
+
const data = settings && typeof settings === "object" && "data" in settings
|
|
90
|
+
? ((settings as any).data ?? {})
|
|
91
|
+
: {}
|
|
92
|
+
return {
|
|
93
|
+
additionalSettings: (data.additional_settings || {}) as Record<string, unknown>,
|
|
94
|
+
apiDetails: (data.api_details || {}) as Record<string, unknown>,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async resolveCurrencyOverride() {
|
|
99
|
+
const { apiDetails } = await this.resolveSettings()
|
|
100
|
+
if (typeof apiDetails.currency_code === "string" && apiDetails.currency_code.trim()) {
|
|
101
|
+
return normalizeCurrencyCode(apiDetails.currency_code)
|
|
102
|
+
}
|
|
103
|
+
return normalizeCurrencyCode(process.env.PAYPAL_CURRENCY || "EUR")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async getPayPalAccessToken() {
|
|
107
|
+
const paypal = this.resolvePayPalService()
|
|
108
|
+
if (!paypal) {
|
|
109
|
+
// Fallback: load credentials directly from paypal_connection table
|
|
110
|
+
const { Pool: _FbPool } = require("pg")
|
|
111
|
+
const _fbPool = new _FbPool({ connectionString: process.env.DATABASE_URL })
|
|
112
|
+
const _fbResult = await _fbPool.query(
|
|
113
|
+
"SELECT metadata, environment, seller_client_id, seller_client_secret FROM paypal_connection WHERE status='connected' ORDER BY created_at DESC LIMIT 1"
|
|
114
|
+
).finally(() => _fbPool.end())
|
|
115
|
+
const _fbRow = _fbResult.rows[0]
|
|
116
|
+
if (!_fbRow) throw new Error("No active PayPal connection found in DB")
|
|
117
|
+
const _fbEnv = _fbRow.environment || "sandbox"
|
|
118
|
+
const _fbCreds = (_fbRow.metadata && _fbRow.metadata.credentials && _fbRow.metadata.credentials[_fbEnv]) || {}
|
|
119
|
+
const _fbId = _fbCreds.client_id || _fbRow.seller_client_id
|
|
120
|
+
const _fbSec = _fbCreds.client_secret || _fbRow.seller_client_secret
|
|
121
|
+
const _fbBase = _fbEnv === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
|
|
122
|
+
const _fbAuth = Buffer.from(`${_fbId}:${_fbSec}`).toString("base64")
|
|
123
|
+
const _fbResp = await fetch(`${_fbBase}/v1/oauth2/token`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { Authorization: `Basic ${_fbAuth}`, "Content-Type": "application/x-www-form-urlencoded" },
|
|
126
|
+
body: "grant_type=client_credentials",
|
|
127
|
+
})
|
|
128
|
+
const _fbText = await _fbResp.text()
|
|
129
|
+
if (!_fbResp.ok) throw new Error(`PayPal token error (${_fbResp.status}): ${_fbText}`)
|
|
130
|
+
const _fbJson = JSON.parse(_fbText)
|
|
131
|
+
return { accessToken: String(_fbJson.access_token), base: _fbBase }
|
|
132
|
+
}
|
|
133
|
+
const creds = await paypal.getActiveCredentials()
|
|
134
|
+
const base =
|
|
135
|
+
creds.environment === "live"
|
|
136
|
+
? "https://api-m.paypal.com"
|
|
137
|
+
: "https://api-m.sandbox.paypal.com"
|
|
138
|
+
const auth = Buffer.from(`${creds.client_id}:${creds.client_secret}`).toString("base64")
|
|
139
|
+
|
|
140
|
+
const resp = await fetch(`${base}/v1/oauth2/token`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: `Basic ${auth}`,
|
|
144
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
145
|
+
},
|
|
146
|
+
body: "grant_type=client_credentials",
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const text = await resp.text()
|
|
150
|
+
if (!resp.ok) {
|
|
151
|
+
throw new Error(`PayPal token error (${resp.status}): ${text}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const json = JSON.parse(text)
|
|
155
|
+
return { accessToken: String(json.access_token), base }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
private async getOrderDetails(orderId: string) {
|
|
160
|
+
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
161
|
+
const resp = await fetch(`${base}/v2/checkout/orders/${orderId}`, {
|
|
162
|
+
method: "GET",
|
|
163
|
+
headers: {
|
|
164
|
+
Authorization: `Bearer ${accessToken}`,
|
|
165
|
+
"Content-Type": "application/json",
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const text = await resp.text()
|
|
170
|
+
if (!resp.ok) {
|
|
171
|
+
throw new Error(`PayPal get order error (${resp.status}): ${text}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return JSON.parse(text)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private getIdempotencyKey(input: { context?: { idempotency_key?: string } }, suffix: string) {
|
|
178
|
+
const key = input?.context?.idempotency_key?.trim()
|
|
179
|
+
if (key) {
|
|
180
|
+
return `${key}-${suffix}`
|
|
181
|
+
}
|
|
182
|
+
return `pp-${suffix}-${generateSessionId()}`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async normalizePaymentData(input: { data?: Record<string, unknown> }) {
|
|
186
|
+
const data = (input.data || {}) as Record<string, any>
|
|
187
|
+
const amount = Number(data.amount ?? 0)
|
|
188
|
+
const currencyOverride = await this.resolveCurrencyOverride()
|
|
189
|
+
const currencyCode = normalizeCurrencyCode(
|
|
190
|
+
data.currency_code || currencyOverride || "EUR"
|
|
191
|
+
)
|
|
192
|
+
assertPayPalCurrencySupported({
|
|
193
|
+
currencyCode,
|
|
194
|
+
paypalCurrencyOverride: currencyOverride,
|
|
195
|
+
})
|
|
196
|
+
return { data, amount, currencyCode }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private mapCaptureStatus(status?: string) {
|
|
200
|
+
const normalized = String(status || "").toUpperCase()
|
|
201
|
+
if (!normalized) {
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (normalized === "COMPLETED") {
|
|
206
|
+
return "captured"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (normalized === "PENDING") {
|
|
210
|
+
return "pending"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (["DENIED", "DECLINED", "FAILED"].includes(normalized)) {
|
|
214
|
+
return "error"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (["REFUNDED", "PARTIALLY_REFUNDED", "REVERSED"].includes(normalized)) {
|
|
218
|
+
return "canceled"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private mapAuthorizationStatus(status?: string) {
|
|
225
|
+
const normalized = String(status || "").toUpperCase()
|
|
226
|
+
if (!normalized) {
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (["CREATED", "APPROVED", "PENDING"].includes(normalized)) {
|
|
231
|
+
return "authorized"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (["VOIDED", "EXPIRED"].includes(normalized)) {
|
|
235
|
+
return "canceled"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (["DENIED", "DECLINED", "FAILED"].includes(normalized)) {
|
|
239
|
+
return "error"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private serializeError(error: unknown) {
|
|
246
|
+
if (error instanceof Error) {
|
|
247
|
+
const errorWithCause = error as Error & { cause?: unknown }
|
|
248
|
+
const cause = errorWithCause.cause
|
|
249
|
+
return {
|
|
250
|
+
name: error.name,
|
|
251
|
+
message: error.message,
|
|
252
|
+
stack: error.stack,
|
|
253
|
+
cause:
|
|
254
|
+
cause instanceof Error
|
|
255
|
+
? {
|
|
256
|
+
name: cause.name,
|
|
257
|
+
message: cause.message,
|
|
258
|
+
stack: cause.stack,
|
|
259
|
+
}
|
|
260
|
+
: cause,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
message: String(error),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private mapOrderStatus(status?: string) {
|
|
270
|
+
const normalized = String(status || "").toUpperCase()
|
|
271
|
+
if (!normalized) {
|
|
272
|
+
return "pending"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (normalized === "COMPLETED") {
|
|
276
|
+
return "captured"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (normalized === "APPROVED") {
|
|
280
|
+
return "authorized"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (["VOIDED", "CANCELLED"].includes(normalized)) {
|
|
284
|
+
return "canceled"
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (["CREATED", "SAVED", "PAYER_ACTION_REQUIRED"].includes(normalized)) {
|
|
288
|
+
return "pending"
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (["FAILED", "EXPIRED"].includes(normalized)) {
|
|
292
|
+
return "error"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return "pending"
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async recordFailure(eventType: string, metadata?: Record<string, unknown>) {
|
|
299
|
+
const paypal = this.resolvePayPalService()
|
|
300
|
+
if (!paypal) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await paypal.recordPaymentLog(eventType, metadata)
|
|
306
|
+
await paypal.recordAuditEvent(eventType, metadata)
|
|
307
|
+
await paypal.recordMetric(eventType)
|
|
308
|
+
} catch {
|
|
309
|
+
// ignore audit logging failures
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private async recordSuccess(metricName: string) {
|
|
314
|
+
const paypal = this.resolvePayPalService()
|
|
315
|
+
if (!paypal) {
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
await paypal.recordMetric(metricName)
|
|
321
|
+
} catch {
|
|
322
|
+
// ignore metrics failures
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async recordPaymentEvent(eventType: string, metadata?: Record<string, unknown>) {
|
|
327
|
+
const paypal = this.resolvePayPalService()
|
|
328
|
+
if (!paypal) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await paypal.recordPaymentLog(eventType, metadata)
|
|
334
|
+
} catch {
|
|
335
|
+
// ignore payment logging failures
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async createAccountHolder(
|
|
340
|
+
input: CreateAccountHolderInput
|
|
341
|
+
): Promise<CreateAccountHolderOutput> {
|
|
342
|
+
const customerId = input.context?.customer?.id
|
|
343
|
+
const externalId = customerId ? `paypal_${customerId}` : `paypal_${generateSessionId()}`
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
id: externalId,
|
|
347
|
+
data: {
|
|
348
|
+
email: input.context?.customer?.email || null,
|
|
349
|
+
customer_id: customerId || null,
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Create a payment session when the customer selects PayPal.
|
|
356
|
+
* Must return an object containing an `id` and `data`.
|
|
357
|
+
*/
|
|
358
|
+
async initiatePayment(
|
|
359
|
+
input: InitiatePaymentInput
|
|
360
|
+
): Promise<InitiatePaymentOutput> {
|
|
361
|
+
const providerId = (input.data as Record<string, any> | undefined)?.provider_id
|
|
362
|
+
try {
|
|
363
|
+
const currencyOverride = await this.resolveCurrencyOverride()
|
|
364
|
+
const currencyCode = normalizeCurrencyCode(
|
|
365
|
+
input.currency_code || currencyOverride || "EUR"
|
|
366
|
+
)
|
|
367
|
+
assertPayPalCurrencySupported({
|
|
368
|
+
currencyCode,
|
|
369
|
+
paypalCurrencyOverride: currencyOverride,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
id: generateSessionId(),
|
|
375
|
+
data: {
|
|
376
|
+
...(input.data || {}),
|
|
377
|
+
...(providerId ? { provider_id: providerId } : {}),
|
|
378
|
+
amount: input.amount,
|
|
379
|
+
currency_code: currencyCode,
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error("[PayPal] provider initiate failed", {
|
|
384
|
+
provider_id: providerId,
|
|
385
|
+
payment_collection_id: (input.data as Record<string, any> | undefined)
|
|
386
|
+
?.payment_collection_id,
|
|
387
|
+
cart_id: (input.data as Record<string, any> | undefined)?.cart_id,
|
|
388
|
+
amount: input.amount,
|
|
389
|
+
currency_code: input.currency_code,
|
|
390
|
+
error: this.serializeError(error),
|
|
391
|
+
})
|
|
392
|
+
await this.recordFailure("initiate_failed", {
|
|
393
|
+
error: this.serializeError(error),
|
|
394
|
+
currency_code: input.currency_code,
|
|
395
|
+
amount: input.amount,
|
|
396
|
+
provider_id: providerId,
|
|
397
|
+
data: input.data ?? null,
|
|
398
|
+
})
|
|
399
|
+
throw error
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async updatePayment(input: UpdatePaymentInput): Promise<UpdatePaymentOutput> {
|
|
404
|
+
const currencyOverride = await this.resolveCurrencyOverride()
|
|
405
|
+
const currencyCode = normalizeCurrencyCode(
|
|
406
|
+
input.currency_code || currencyOverride || "EUR"
|
|
407
|
+
)
|
|
408
|
+
assertPayPalCurrencySupported({
|
|
409
|
+
currencyCode,
|
|
410
|
+
paypalCurrencyOverride: currencyOverride,
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
const providerId = (input.data as Record<string, any> | undefined)?.provider_id
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
data: {
|
|
417
|
+
...(input.data || {}),
|
|
418
|
+
...(providerId ? { provider_id: providerId } : {}),
|
|
419
|
+
amount: input.amount,
|
|
420
|
+
currency_code: currencyCode,
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async authorizePayment(
|
|
426
|
+
input: AuthorizePaymentInput
|
|
427
|
+
): Promise<AuthorizePaymentOutput> {
|
|
428
|
+
const { data, amount, currencyCode } = await this.normalizePaymentData(input)
|
|
429
|
+
|
|
430
|
+
const existingPayPal = (data.paypal || {}) as Record<string, any>
|
|
431
|
+
if (
|
|
432
|
+
existingPayPal.capture_id ||
|
|
433
|
+
existingPayPal.authorization_id ||
|
|
434
|
+
(data as any).authorized_at ||
|
|
435
|
+
(data as any).captured_at
|
|
436
|
+
) {
|
|
437
|
+
const { additionalSettings } = await this.resolveSettings()
|
|
438
|
+
const paymentAction =
|
|
439
|
+
typeof additionalSettings.paymentAction === "string"
|
|
440
|
+
? additionalSettings.paymentAction
|
|
441
|
+
: "capture"
|
|
442
|
+
const returnStatus = paymentAction === "authorize" ? "authorized" : "captured"
|
|
443
|
+
return {
|
|
444
|
+
status: returnStatus,
|
|
445
|
+
data: {
|
|
446
|
+
...(input.data || {}),
|
|
447
|
+
...(paymentAction === "authorize"
|
|
448
|
+
? { authorized_at: new Date().toISOString() }
|
|
449
|
+
: { captured_at: new Date().toISOString() }),
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
const requestId = this.getIdempotencyKey(input, "authorize")
|
|
456
|
+
let debugId: string | null = null
|
|
457
|
+
const { additionalSettings } = await this.resolveSettings()
|
|
458
|
+
const paymentActionRaw =
|
|
459
|
+
typeof additionalSettings.paymentAction === "string"
|
|
460
|
+
? additionalSettings.paymentAction
|
|
461
|
+
: "capture"
|
|
462
|
+
const orderIntent = paymentActionRaw === "authorize" ? "AUTHORIZE" : "CAPTURE"
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
466
|
+
const existingPayPal = (data.paypal || {}) as Record<string, any>
|
|
467
|
+
let orderId = String(existingPayPal.order_id || data.order_id || "")
|
|
468
|
+
let order: Record<string, any> | null = null
|
|
469
|
+
let authorization: any = null
|
|
470
|
+
|
|
471
|
+
if (!orderId) {
|
|
472
|
+
const value = formatAmountForPayPal(amount, currencyCode || "EUR")
|
|
473
|
+
|
|
474
|
+
const orderPayload = {
|
|
475
|
+
intent: orderIntent,
|
|
476
|
+
purchase_units: [
|
|
477
|
+
{
|
|
478
|
+
reference_id: data.cart_id || data.payment_collection_id || undefined,
|
|
479
|
+
custom_id: data.session_id || data.cart_id || data.payment_collection_id || undefined,
|
|
480
|
+
amount: {
|
|
481
|
+
currency_code: currencyCode || "EUR",
|
|
482
|
+
value,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
custom_id: data.session_id || data.cart_id || data.payment_collection_id || undefined,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const ppResp = await fetch(`${base}/v2/checkout/orders`, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: {
|
|
492
|
+
Authorization: `Bearer ${accessToken}`,
|
|
493
|
+
"Content-Type": "application/json",
|
|
494
|
+
"PayPal-Request-Id": requestId,
|
|
495
|
+
},
|
|
496
|
+
body: JSON.stringify(orderPayload),
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
const ppText = await ppResp.text()
|
|
500
|
+
debugId = ppResp.headers.get("paypal-debug-id")
|
|
501
|
+
if (!ppResp.ok) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`PayPal create order error (${ppResp.status}): ${ppText}${
|
|
504
|
+
debugId ? ` debug_id=${debugId}` : ""
|
|
505
|
+
}`
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
order = JSON.parse(ppText) as Record<string, any>
|
|
510
|
+
orderId = String(order.id || "")
|
|
511
|
+
} else {
|
|
512
|
+
order = (await this.getOrderDetails(orderId)) as Record<string, any> | null
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!order || !orderId) {
|
|
516
|
+
throw new Error("Unable to resolve PayPal order details for authorization.")
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const existingAuthorization =
|
|
520
|
+
order?.purchase_units?.[0]?.payments?.authorizations?.[0] || null
|
|
521
|
+
|
|
522
|
+
if (existingAuthorization) {
|
|
523
|
+
authorization = order
|
|
524
|
+
} else {
|
|
525
|
+
const authorizeResp = await fetch(
|
|
526
|
+
`${base}/v2/checkout/orders/${orderId}/authorize`,
|
|
527
|
+
{
|
|
528
|
+
method: "POST",
|
|
529
|
+
headers: {
|
|
530
|
+
Authorization: `Bearer ${accessToken}`,
|
|
531
|
+
"Content-Type": "application/json",
|
|
532
|
+
"PayPal-Request-Id": `${requestId}-auth`,
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
const authorizeText = await authorizeResp.text()
|
|
538
|
+
const authorizeDebugId = authorizeResp.headers.get("paypal-debug-id")
|
|
539
|
+
if (!authorizeResp.ok) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`PayPal authorize order error (${authorizeResp.status}): ${authorizeText}${
|
|
542
|
+
authorizeDebugId ? ` debug_id=${authorizeDebugId}` : ""
|
|
543
|
+
}`
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
authorization = JSON.parse(authorizeText)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const authorizationId =
|
|
551
|
+
authorization?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id ||
|
|
552
|
+
existingAuthorization?.id
|
|
553
|
+
|
|
554
|
+
await this.recordSuccess("authorize_success")
|
|
555
|
+
await this.recordPaymentEvent("authorize", {
|
|
556
|
+
order_id: orderId,
|
|
557
|
+
authorization_id: authorizationId,
|
|
558
|
+
amount,
|
|
559
|
+
currency_code: currencyCode,
|
|
560
|
+
request_id: requestId,
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
status: "authorized",
|
|
565
|
+
data: {
|
|
566
|
+
...(input.data || {}),
|
|
567
|
+
paypal: {
|
|
568
|
+
...((input.data || {}).paypal as Record<string, unknown>),
|
|
569
|
+
order_id: orderId,
|
|
570
|
+
order: order || authorization,
|
|
571
|
+
authorization_id: authorizationId,
|
|
572
|
+
authorizations:
|
|
573
|
+
authorization?.purchase_units?.[0]?.payments?.authorizations || [],
|
|
574
|
+
},
|
|
575
|
+
authorized_at: new Date().toISOString(),
|
|
576
|
+
},
|
|
577
|
+
}
|
|
578
|
+
} catch (error: any) {
|
|
579
|
+
await this.recordFailure("authorize_failed", {
|
|
580
|
+
request_id: requestId,
|
|
581
|
+
cart_id: data.cart_id,
|
|
582
|
+
payment_collection_id: data.payment_collection_id,
|
|
583
|
+
debug_id: debugId,
|
|
584
|
+
message: error?.message,
|
|
585
|
+
})
|
|
586
|
+
throw error
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async retrievePayment(
|
|
591
|
+
input: RetrievePaymentInput
|
|
592
|
+
): Promise<RetrievePaymentOutput> {
|
|
593
|
+
const data = (input.data || {}) as Record<string, any>
|
|
594
|
+
const paypalData = (data.paypal || {}) as Record<string, any>
|
|
595
|
+
const orderId = String(paypalData.order_id || data.order_id || "")
|
|
596
|
+
if (!orderId) {
|
|
597
|
+
return { data: { ...(input.data || {}) } }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const order = await this.getOrderDetails(orderId)
|
|
601
|
+
const capture = order?.purchase_units?.[0]?.payments?.captures?.[0]
|
|
602
|
+
const authorization = order?.purchase_units?.[0]?.payments?.authorizations?.[0]
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
data: {
|
|
606
|
+
...(input.data || {}),
|
|
607
|
+
paypal: {
|
|
608
|
+
...((input.data || {}).paypal as Record<string, unknown>),
|
|
609
|
+
order,
|
|
610
|
+
authorization_id: authorization?.id || paypalData.authorization_id,
|
|
611
|
+
capture_id: capture?.id || paypalData.capture_id,
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async getPaymentStatus(
|
|
618
|
+
input: GetPaymentStatusInput
|
|
619
|
+
): Promise<GetPaymentStatusOutput> {
|
|
620
|
+
const data = (input.data || {}) as Record<string, any>
|
|
621
|
+
const paypalData = (data.paypal || {}) as Record<string, any>
|
|
622
|
+
const orderId = String(paypalData.order_id || data.order_id || "")
|
|
623
|
+
if (!orderId) {
|
|
624
|
+
return { status: "pending", data: { ...(input.data || {}) } }
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
const order = await this.getOrderDetails(orderId)
|
|
629
|
+
const capture = order?.purchase_units?.[0]?.payments?.captures?.[0]
|
|
630
|
+
const authorization = order?.purchase_units?.[0]?.payments?.authorizations?.[0]
|
|
631
|
+
const mappedStatus =
|
|
632
|
+
this.mapCaptureStatus(capture?.status) ||
|
|
633
|
+
this.mapAuthorizationStatus(authorization?.status) ||
|
|
634
|
+
this.mapOrderStatus(order?.status) ||
|
|
635
|
+
"pending"
|
|
636
|
+
|
|
637
|
+
await this.recordSuccess("status_success")
|
|
638
|
+
return {
|
|
639
|
+
status: mappedStatus,
|
|
640
|
+
data: {
|
|
641
|
+
...(input.data || {}),
|
|
642
|
+
paypal: {
|
|
643
|
+
...((input.data || {}).paypal as Record<string, unknown>),
|
|
644
|
+
order,
|
|
645
|
+
authorization_id: authorization?.id || paypalData.authorization_id,
|
|
646
|
+
capture_id: capture?.id || paypalData.capture_id,
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
}
|
|
650
|
+
} catch (error: any) {
|
|
651
|
+
await this.recordFailure("status_failed", {
|
|
652
|
+
order_id: orderId,
|
|
653
|
+
message: error?.message,
|
|
654
|
+
})
|
|
655
|
+
throw error
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async capturePayment(
|
|
660
|
+
input: CapturePaymentInput
|
|
661
|
+
): Promise<CapturePaymentOutput> {
|
|
662
|
+
const data = (input.data || {}) as Record<string, any>
|
|
663
|
+
const paypalData = (data.paypal || {}) as Record<string, any>
|
|
664
|
+
const orderId = String(paypalData.order_id || data.order_id || "")
|
|
665
|
+
let authorizationId = String(
|
|
666
|
+
paypalData.authorization_id || data.authorization_id || ""
|
|
667
|
+
)
|
|
668
|
+
if (!orderId) {
|
|
669
|
+
throw new Error("PayPal order_id is required to capture payment")
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (paypalData.capture_id || paypalData.capture) {
|
|
673
|
+
return {
|
|
674
|
+
data: {
|
|
675
|
+
...(input.data || {}),
|
|
676
|
+
paypal: {
|
|
677
|
+
...((input.data || {}).paypal as Record<string, unknown>),
|
|
678
|
+
capture_id: paypalData.capture_id,
|
|
679
|
+
capture: paypalData.capture,
|
|
680
|
+
},
|
|
681
|
+
captured_at: new Date().toISOString(),
|
|
682
|
+
},
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const requestId = this.getIdempotencyKey(input, `capture-${orderId}`)
|
|
687
|
+
const { amount, currencyCode } = await this.normalizePaymentData(input)
|
|
688
|
+
let debugId: string | null = null
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
692
|
+
const order = await this.getOrderDetails(orderId).catch(() => null)
|
|
693
|
+
const existingCapture = order?.purchase_units?.[0]?.payments?.captures?.[0]
|
|
694
|
+
if (existingCapture?.id) {
|
|
695
|
+
return {
|
|
696
|
+
data: {
|
|
697
|
+
...(input.data || {}),
|
|
698
|
+
paypal: {
|
|
699
|
+
...((input.data || {}).paypal as Record<string, unknown>),
|
|
700
|
+
capture_id: existingCapture.id,
|
|
701
|
+
capture: existingCapture,
|
|
702
|
+
},
|
|
703
|
+
captured_at: new Date().toISOString(),
|
|
704
|
+
},
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const resolvedIntent = String(
|
|
708
|
+
order?.intent || paypalData.order?.intent || data.intent || ""
|
|
709
|
+
).toUpperCase()
|
|
710
|
+
if (!authorizationId && resolvedIntent === "AUTHORIZE") {
|
|
711
|
+
const authorizeResp = await fetch(
|
|
712
|
+
`${base}/v2/checkout/orders/${orderId}/authorize`,
|
|
713
|
+
{
|
|
714
|
+
method: "POST",
|
|
715
|
+
headers: {
|
|
716
|
+
Authorization: `Bearer ${accessToken}`,
|
|
717
|
+
"Content-Type": "application/json",
|
|
718
|
+
"PayPal-Request-Id": `${requestId}-auth`,
|
|
719
|
+
},
|
|
720
|
+
}
|
|
721
|
+
)
|
|
722
|
+
const authorizeText = await authorizeResp.text()
|
|
723
|
+
debugId = authorizeResp.headers.get("paypal-debug-id")
|
|
724
|
+
if (!authorizeResp.ok) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`PayPal authorize order error (${authorizeResp.status}): ${authorizeText}${
|
|
727
|
+
debugId ? ` debug_id=${debugId}` : ""
|
|
728
|
+
}`
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
const authorization = JSON.parse(authorizeText)
|
|
732
|
+
authorizationId =
|
|
733
|
+
authorization?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const isFinalCapture =
|
|
737
|
+
paypalData.is_final_capture ??
|
|
738
|
+
data.is_final_capture ??
|
|
739
|
+
data.final_capture ??
|
|
740
|
+
undefined
|
|
741
|
+
const capturePayload =
|
|
742
|
+
amount > 0
|
|
743
|
+
? {
|
|
744
|
+
amount: {
|
|
745
|
+
currency_code: currencyCode || "EUR",
|
|
746
|
+
value: formatAmountForPayPal(amount, currencyCode || "EUR"),
|
|
747
|
+
},
|
|
748
|
+
...(typeof isFinalCapture === "boolean"
|
|
749
|
+
? { is_final_capture: isFinalCapture }
|
|
750
|
+
: {}),
|
|
751
|
+
}
|
|
752
|
+
: {
|
|
753
|
+
...(typeof isFinalCapture === "boolean"
|
|
754
|
+
? { is_final_capture: isFinalCapture }
|
|
755
|
+
: {}),
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const captureUrl = authorizationId
|
|
759
|
+
? `${base}/v2/payments/authorizations/${authorizationId}/capture`
|
|
760
|
+
: `${base}/v2/checkout/orders/${orderId}/capture`
|
|
761
|
+
|
|
762
|
+
const ppResp = await fetch(captureUrl, {
|
|
763
|
+
method: "POST",
|
|
764
|
+
headers: {
|
|
765
|
+
Authorization: `Bearer ${accessToken}`,
|
|
766
|
+
"Content-Type": "application/json",
|
|
767
|
+
"PayPal-Request-Id": requestId,
|
|
768
|
+
},
|
|
769
|
+
body: JSON.stringify(capturePayload),
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
const ppText = await ppResp.text()
|
|
773
|
+
debugId = ppResp.headers.get("paypal-debug-id")
|
|
774
|
+
if (!ppResp.ok) {
|
|
775
|
+
throw new Error(
|
|
776
|
+
`PayPal capture error (${ppResp.status}): ${ppText}${
|
|
777
|
+
debugId ? ` debug_id=${debugId}` : ""
|
|
778
|
+
}`
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const capture = JSON.parse(ppText)
|
|
783
|
+
const captureId =
|
|
784
|
+
capture?.id || capture?.purchase_units?.[0]?.payments?.captures?.[0]?.id
|
|
785
|
+
const existingCaptures = Array.isArray(paypalData.captures)
|
|
786
|
+
? paypalData.captures
|
|
787
|
+
: []
|
|
788
|
+
const captureEntry = {
|
|
789
|
+
id: captureId,
|
|
790
|
+
status: capture?.status,
|
|
791
|
+
amount: capture?.amount,
|
|
792
|
+
raw: capture,
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
await this.recordSuccess("capture_success")
|
|
796
|
+
await this.recordPaymentEvent("capture", {
|
|
797
|
+
order_id: orderId,
|
|
798
|
+
capture_id: captureId,
|
|
799
|
+
authorization_id: authorizationId || undefined,
|
|
800
|
+
amount,
|
|
801
|
+
currency_code: currencyCode,
|
|
802
|
+
request_id: requestId,
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
data: {
|
|
807
|
+
...(input.data || {}),
|
|
808
|
+
paypal: {
|
|
809
|
+
...((input.data || {}).paypal as Record<string, unknown>),
|
|
810
|
+
order_id: orderId,
|
|
811
|
+
capture_id: captureId,
|
|
812
|
+
capture,
|
|
813
|
+
authorization_id: authorizationId || paypalData.authorization_id,
|
|
814
|
+
captures: [...existingCaptures, captureEntry],
|
|
815
|
+
},
|
|
816
|
+
captured_at: new Date().toISOString(),
|
|
817
|
+
},
|
|
818
|
+
}
|
|
819
|
+
} catch (error: any) {
|
|
820
|
+
await this.recordFailure("capture_failed", {
|
|
821
|
+
order_id: orderId,
|
|
822
|
+
request_id: requestId,
|
|
823
|
+
debug_id: debugId,
|
|
824
|
+
message: error?.message,
|
|
825
|
+
})
|
|
826
|
+
throw error
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async refundPayment(
|
|
831
|
+
input: RefundPaymentInput
|
|
832
|
+
): Promise<RefundPaymentOutput> {
|
|
833
|
+
const data = (input.data || {}) as Record<string, any>
|
|
834
|
+
const paypalData = (data.paypal || {}) as Record<string, any>
|
|
835
|
+
const captureId = String(paypalData.capture_id || data.capture_id || "")
|
|
836
|
+
const refundReason = String(
|
|
837
|
+
paypalData.refund_reason || data.refund_reason || data.reason || ""
|
|
838
|
+
).trim()
|
|
839
|
+
const refundReasonCode = String(
|
|
840
|
+
paypalData.refund_reason_code || data.refund_reason_code || data.reason_code || ""
|
|
841
|
+
).trim()
|
|
842
|
+
if (!captureId) {
|
|
843
|
+
return {
|
|
844
|
+
data: {
|
|
845
|
+
...(input.data || {}),
|
|
846
|
+
refunded_at: new Date().toISOString(),
|
|
847
|
+
},
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const requestId = this.getIdempotencyKey(input, `refund-${captureId}`)
|
|
852
|
+
const { amount, currencyCode } = await this.normalizePaymentData(input)
|
|
853
|
+
let debugId: string | null = null
|
|
854
|
+
|
|
855
|
+
try {
|
|
856
|
+
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
857
|
+
const refundPayload: Record<string, any> =
|
|
858
|
+
amount > 0
|
|
859
|
+
? {
|
|
860
|
+
amount: {
|
|
861
|
+
currency_code: currencyCode || "EUR",
|
|
862
|
+
value: formatAmountForPayPal(amount, currencyCode || "EUR"),
|
|
863
|
+
},
|
|
864
|
+
}
|
|
865
|
+
: {}
|
|
866
|
+
|
|
867
|
+
if (refundReason) {
|
|
868
|
+
refundPayload.note_to_payer = refundReason
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const ppResp = await fetch(`${base}/v2/payments/captures/${captureId}/refund`, {
|
|
872
|
+
method: "POST",
|
|
873
|
+
headers: {
|
|
874
|
+
Authorization: `Bearer ${accessToken}`,
|
|
875
|
+
"Content-Type": "application/json",
|
|
876
|
+
"PayPal-Request-Id": requestId,
|
|
877
|
+
},
|
|
878
|
+
body: JSON.stringify(refundPayload),
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
const ppText = await ppResp.text()
|
|
882
|
+
debugId = ppResp.headers.get("paypal-debug-id")
|
|
883
|
+
if (!ppResp.ok) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
`PayPal refund error (${ppResp.status}): ${ppText}${
|
|
886
|
+
debugId ? ` debug_id=${debugId}` : ""
|
|
887
|
+
}`
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const refund = JSON.parse(ppText)
|
|
892
|
+
const existingRefunds = Array.isArray(paypalData.refunds) ? paypalData.refunds : []
|
|
893
|
+
const refundEntry = {
|
|
894
|
+
id: refund?.id,
|
|
895
|
+
status: refund?.status,
|
|
896
|
+
amount: refund?.amount,
|
|
897
|
+
reason: refundReason || refund?.note_to_payer,
|
|
898
|
+
reason_code: refundReasonCode || refund?.reason_code,
|
|
899
|
+
raw: refund,
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
await this.recordSuccess("refund_success")
|
|
903
|
+
await this.recordPaymentEvent("refund", {
|
|
904
|
+
capture_id: captureId,
|
|
905
|
+
refund_id: refund?.id,
|
|
906
|
+
amount,
|
|
907
|
+
currency_code: currencyCode,
|
|
908
|
+
request_id: requestId,
|
|
909
|
+
reason: refundReason,
|
|
910
|
+
reason_code: refundReasonCode,
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
data: {
|
|
915
|
+
...(input.data || {}),
|
|
916
|
+
paypal: {
|
|
917
|
+
...((input.data || {}).paypal as Record<string, unknown>),
|
|
918
|
+
refund_id: refund?.id,
|
|
919
|
+
refund_status: refund?.status,
|
|
920
|
+
refund_reason: refundReason || refund?.note_to_payer,
|
|
921
|
+
refund_reason_code: refundReasonCode || refund?.reason_code,
|
|
922
|
+
refunds: [...existingRefunds, refundEntry],
|
|
923
|
+
refund,
|
|
924
|
+
},
|
|
925
|
+
refunded_at: new Date().toISOString(),
|
|
926
|
+
},
|
|
927
|
+
}
|
|
928
|
+
} catch (error: any) {
|
|
929
|
+
await this.recordFailure("refund_failed", {
|
|
930
|
+
capture_id: captureId,
|
|
931
|
+
request_id: requestId,
|
|
932
|
+
debug_id: debugId,
|
|
933
|
+
message: error?.message,
|
|
934
|
+
})
|
|
935
|
+
throw error
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async cancelPayment(
|
|
940
|
+
input: CancelPaymentInput
|
|
941
|
+
): Promise<CancelPaymentOutput> {
|
|
942
|
+
const data = (input.data || {}) as Record<string, any>
|
|
943
|
+
const paypalData = (data.paypal || {}) as Record<string, any>
|
|
944
|
+
const orderId = String(paypalData.order_id || data.order_id || "")
|
|
945
|
+
const captureId = String(paypalData.capture_id || data.capture_id || "")
|
|
946
|
+
const storedAuthorizationId = String(
|
|
947
|
+
paypalData.authorization_id || data.authorization_id || ""
|
|
948
|
+
)
|
|
949
|
+
let debugId: string | null = null
|
|
950
|
+
|
|
951
|
+
try {
|
|
952
|
+
const order = orderId ? await this.getOrderDetails(orderId) : null
|
|
953
|
+
const intent = String(order?.intent || "").toUpperCase()
|
|
954
|
+
const authorizationId =
|
|
955
|
+
order?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id ||
|
|
956
|
+
storedAuthorizationId
|
|
957
|
+
|
|
958
|
+
if (intent === "AUTHORIZE" && authorizationId) {
|
|
959
|
+
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
960
|
+
const requestId = this.getIdempotencyKey(input, `void-${authorizationId}`)
|
|
961
|
+
|
|
962
|
+
const resp = await fetch(
|
|
963
|
+
`${base}/v2/payments/authorizations/${authorizationId}/void`,
|
|
964
|
+
{
|
|
965
|
+
method: "POST",
|
|
966
|
+
headers: {
|
|
967
|
+
Authorization: `Bearer ${accessToken}`,
|
|
968
|
+
"Content-Type": "application/json",
|
|
969
|
+
"PayPal-Request-Id": requestId,
|
|
970
|
+
},
|
|
971
|
+
}
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
if (!resp.ok) {
|
|
975
|
+
const text = await resp.text()
|
|
976
|
+
debugId = resp.headers.get("paypal-debug-id")
|
|
977
|
+
throw new Error(
|
|
978
|
+
`PayPal void error (${resp.status}): ${text}${
|
|
979
|
+
debugId ? ` debug_id=${debugId}` : ""
|
|
980
|
+
}`
|
|
981
|
+
)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
await this.recordSuccess("void_success")
|
|
985
|
+
await this.recordPaymentEvent("void", {
|
|
986
|
+
order_id: orderId,
|
|
987
|
+
authorization_id: authorizationId,
|
|
988
|
+
request_id: requestId,
|
|
989
|
+
})
|
|
990
|
+
} else if (captureId) {
|
|
991
|
+
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
992
|
+
const requestId = this.getIdempotencyKey(input, `refund-${captureId}`)
|
|
993
|
+
|
|
994
|
+
const resp = await fetch(`${base}/v2/payments/captures/${captureId}/refund`, {
|
|
995
|
+
method: "POST",
|
|
996
|
+
headers: {
|
|
997
|
+
Authorization: `Bearer ${accessToken}`,
|
|
998
|
+
"Content-Type": "application/json",
|
|
999
|
+
"PayPal-Request-Id": requestId,
|
|
1000
|
+
},
|
|
1001
|
+
body: JSON.stringify({}),
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
if (!resp.ok) {
|
|
1005
|
+
const text = await resp.text()
|
|
1006
|
+
debugId = resp.headers.get("paypal-debug-id")
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
`PayPal refund error (${resp.status}): ${text}${
|
|
1009
|
+
debugId ? ` debug_id=${debugId}` : ""
|
|
1010
|
+
}`
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const refund = await resp.json().catch(() => ({}))
|
|
1015
|
+
const existingRefunds = Array.isArray(paypalData.refunds) ? paypalData.refunds : []
|
|
1016
|
+
const refundEntry = {
|
|
1017
|
+
id: refund?.id,
|
|
1018
|
+
status: refund?.status,
|
|
1019
|
+
amount: refund?.amount,
|
|
1020
|
+
raw: refund,
|
|
1021
|
+
}
|
|
1022
|
+
paypalData.refund_id = refund?.id
|
|
1023
|
+
paypalData.refund_status = refund?.status
|
|
1024
|
+
paypalData.refunds = [...existingRefunds, refundEntry]
|
|
1025
|
+
|
|
1026
|
+
await this.recordSuccess("cancel_refund_success")
|
|
1027
|
+
await this.recordPaymentEvent("cancel_refund", {
|
|
1028
|
+
order_id: orderId,
|
|
1029
|
+
capture_id: captureId,
|
|
1030
|
+
refund_id: refund?.id,
|
|
1031
|
+
request_id: requestId,
|
|
1032
|
+
})
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
data: {
|
|
1037
|
+
...(input.data || {}),
|
|
1038
|
+
paypal: {
|
|
1039
|
+
...((input.data || {}).paypal as Record<string, unknown>),
|
|
1040
|
+
order: order || undefined,
|
|
1041
|
+
authorization_id: authorizationId || storedAuthorizationId,
|
|
1042
|
+
capture_id: captureId || paypalData.capture_id,
|
|
1043
|
+
refund_id: paypalData.refund_id,
|
|
1044
|
+
refund_status: paypalData.refund_status,
|
|
1045
|
+
refunds: paypalData.refunds,
|
|
1046
|
+
},
|
|
1047
|
+
canceled_at: new Date().toISOString(),
|
|
1048
|
+
},
|
|
1049
|
+
}
|
|
1050
|
+
} catch (error: any) {
|
|
1051
|
+
await this.recordFailure("cancel_failed", {
|
|
1052
|
+
order_id: orderId,
|
|
1053
|
+
capture_id: captureId,
|
|
1054
|
+
debug_id: debugId,
|
|
1055
|
+
message: error?.message,
|
|
1056
|
+
})
|
|
1057
|
+
throw error
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async deletePayment(
|
|
1062
|
+
_input: DeletePaymentInput
|
|
1063
|
+
): Promise<DeletePaymentOutput> {
|
|
1064
|
+
return { data: {} }
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Required by AbstractPaymentProvider in Medusa v2.
|
|
1069
|
+
* This is used by /hooks/payment/{identifier}_{providerId}
|
|
1070
|
+
*/
|
|
1071
|
+
async getWebhookActionAndData(
|
|
1072
|
+
payload: ProviderWebhookPayload["payload"]
|
|
1073
|
+
): Promise<WebhookActionResult> {
|
|
1074
|
+
return getPayPalWebhookActionAndData(payload)
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export default PayPalPaymentProvider
|
|
1079
|
+
export { PayPalPaymentProvider }
|