@easypayment/medusa-payment-paypal 0.9.14 → 0.9.16

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 (76) hide show
  1. package/.medusa/server/src/admin/index.js +12 -12
  2. package/.medusa/server/src/admin/index.mjs +12 -12
  3. package/.medusa/server/src/admin/routes/settings/paypal/connection/page.js.map +1 -1
  4. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
  5. package/.medusa/server/src/api/store/paypal/capture-order/route.js +27 -12
  6. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  7. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  8. package/.medusa/server/src/api/store/paypal/create-order/route.js +83 -56
  9. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  10. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
  11. package/.medusa/server/src/api/store/paypal/webhook/route.js +2 -1
  12. package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
  13. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
  14. package/.medusa/server/src/api/store/paypal-complete/route.js +37 -32
  15. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
  16. package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -1
  17. package/.medusa/server/src/jobs/paypal-webhook-retry.js +42 -1
  18. package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
  19. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.d.ts.map +1 -1
  20. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js +5 -4
  21. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js.map +1 -1
  22. package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts.map +1 -1
  23. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js +13 -10
  24. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js.map +1 -1
  25. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
  26. package/.medusa/server/src/modules/paypal/payment-provider/service.js +10 -9
  27. package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
  28. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts +12 -0
  29. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts.map +1 -1
  30. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js +12 -0
  31. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js.map +1 -1
  32. package/.medusa/server/src/modules/paypal/service.d.ts +10 -1
  33. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  34. package/.medusa/server/src/modules/paypal/service.js +71 -21
  35. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  36. package/.medusa/server/src/modules/paypal/utils/amounts.d.ts +9 -1
  37. package/.medusa/server/src/modules/paypal/utils/amounts.d.ts.map +1 -1
  38. package/.medusa/server/src/modules/paypal/utils/amounts.js +17 -5
  39. package/.medusa/server/src/modules/paypal/utils/amounts.js.map +1 -1
  40. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +0 -8
  41. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -1
  42. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +0 -22
  43. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -1
  44. package/.medusa/server/src/modules/paypal/utils/paypal-fetch.d.ts +16 -0
  45. package/.medusa/server/src/modules/paypal/utils/paypal-fetch.d.ts.map +1 -0
  46. package/.medusa/server/src/modules/paypal/utils/paypal-fetch.js +31 -0
  47. package/.medusa/server/src/modules/paypal/utils/paypal-fetch.js.map +1 -0
  48. package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts +14 -0
  49. package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts.map +1 -0
  50. package/.medusa/server/src/modules/paypal/utils/secret-crypto.js +91 -0
  51. package/.medusa/server/src/modules/paypal/utils/secret-crypto.js.map +1 -0
  52. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
  53. package/.medusa/server/src/modules/paypal/webhook-processor.js +29 -13
  54. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
  55. package/.medusa/server/src/subscribers/paypal-order-invoice.d.ts.map +1 -1
  56. package/.medusa/server/src/subscribers/paypal-order-invoice.js +5 -3
  57. package/.medusa/server/src/subscribers/paypal-order-invoice.js.map +1 -1
  58. package/README.md +12 -0
  59. package/package.json +5 -2
  60. package/src/admin/routes/settings/paypal/connection/page.tsx +1 -1
  61. package/src/api/store/paypal/capture-order/route.ts +39 -17
  62. package/src/api/store/paypal/create-order/route.ts +106 -64
  63. package/src/api/store/paypal/webhook/route.ts +2 -1
  64. package/src/api/store/paypal-complete/route.ts +58 -44
  65. package/src/jobs/paypal-webhook-retry.ts +51 -1
  66. package/src/modules/paypal/clients/paypal-seller.client.ts +6 -4
  67. package/src/modules/paypal/payment-provider/card-service.ts +13 -10
  68. package/src/modules/paypal/payment-provider/service.ts +10 -9
  69. package/src/modules/paypal/payment-provider/webhook-utils.ts +12 -0
  70. package/src/modules/paypal/service.ts +76 -21
  71. package/src/modules/paypal/utils/amounts.ts +17 -8
  72. package/src/modules/paypal/utils/paypal-auth.ts +0 -27
  73. package/src/modules/paypal/utils/paypal-fetch.ts +30 -0
  74. package/src/modules/paypal/utils/secret-crypto.ts +94 -0
  75. package/src/modules/paypal/webhook-processor.ts +32 -15
  76. package/src/subscribers/paypal-order-invoice.ts +6 -4
@@ -1,10 +1,13 @@
1
1
  import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
- import type { IPaymentModuleService } from "@medusajs/framework/types"
3
- import { Modules } from "@medusajs/framework/utils"
2
+ import type { IPaymentModuleService, Logger } from "@medusajs/framework/types"
3
+ import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
4
4
  import { completeCartWorkflow } from "@medusajs/core-flows"
5
- import type PayPalModuleService from "../../../modules/paypal/service"
5
+ import { randomUUID } from "crypto"
6
+ import { isPayPalProviderId } from "../../../modules/paypal/utils/provider-ids"
6
7
 
7
8
  export async function POST(req: MedusaRequest, res: MedusaResponse) {
9
+ const logger = req.scope.resolve<Logger>(ContainerRegistrationKeys.LOGGER)
10
+ const requestId = randomUUID()
8
11
  const { cart_id } = req.body as { cart_id: string }
9
12
 
10
13
  if (!cart_id || typeof cart_id !== "string") {
@@ -40,7 +43,7 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
40
43
 
41
44
  const sessions = cart?.payment_collection?.payment_sessions || []
42
45
  const session = sessions
43
- .filter((s: any) => String(s.provider_id || "").includes("paypal"))
46
+ .filter((s: any) => isPayPalProviderId(s.provider_id))
44
47
  .sort(
45
48
  (a: any, b: any) =>
46
49
  new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
@@ -52,46 +55,27 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
52
55
 
53
56
  const paymentModule = req.scope.resolve(Modules.PAYMENT) as IPaymentModuleService
54
57
 
55
- const [liveSession] = await paymentModule.listPaymentSessions(
56
- { id: [session.id] },
57
- { take: 1 }
58
- )
59
-
60
- const liveData = (liveSession?.data || {}) as Record<string, any>
61
-
62
- const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
63
- const settings = await paypal.getSettings().catch(() => ({}))
64
- const settingsData =
65
- settings && typeof settings === "object" && "data" in settings
66
- ? ((settings as { data?: Record<string, any> }).data ?? {})
67
- : {}
68
- const additionalSettings = (settingsData.additional_settings || {}) as Record<string, any>
69
- const paymentAction =
70
- typeof additionalSettings.paymentAction === "string"
71
- ? additionalSettings.paymentAction
72
- : "capture"
73
-
74
- const timestampKey = paymentAction === "authorize" ? "authorized_at" : "captured_at"
75
-
76
- if (!liveData[timestampKey]) {
77
- await (paymentModule as any).updatePaymentSession({
78
- id: session.id,
79
- data: {
80
- ...liveData,
81
- [timestampKey]: new Date().toISOString(),
82
- },
83
- amount: liveSession?.amount ?? session.amount,
84
- currency_code: liveSession?.currency_code ?? session.currency_code,
85
- })
86
- }
58
+ // Do NOT fabricate captured_at/authorized_at here. The authoritative
59
+ // timestamp and status are set by the provider's authorizePayment (invoked
60
+ // via authorizePaymentSession below) only after it verifies the capture /
61
+ // authorization against PayPal. Stamping it ourselves would mark an unpaid
62
+ // session as paid.
87
63
 
88
64
  const currentStatus = String(session.status || "")
89
65
  if (currentStatus !== "authorized") {
90
66
  try {
91
67
  await (paymentModule as any).authorizePaymentSession(session.id, {})
92
- console.info("[paypal-complete] authorizePaymentSession succeeded for session", session.id)
68
+ logger.info(
69
+ `[paypal] paypal-complete authorizePaymentSession succeeded (request_id=${requestId}, session_id=${session.id})`
70
+ )
93
71
  } catch (e: any) {
94
- console.warn("[paypal-complete] authorizePaymentSession non-fatal:", e?.message)
72
+ // Authorization is the gate for completion; if it fails, the
73
+ // completeCartWorkflow below fails too and we report that honestly.
74
+ logger.warn(
75
+ `[paypal] paypal-complete authorizePaymentSession failed (request_id=${requestId}, session_id=${session.id}): ${
76
+ e?.message ?? String(e)
77
+ }`
78
+ )
95
79
  }
96
80
  }
97
81
 
@@ -99,14 +83,44 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
99
83
  const { result } = await completeCartWorkflow(req.scope).run({
100
84
  input: { id: cart_id },
101
85
  })
102
- console.info("[paypal-complete] cart completed, order:", result?.id)
103
- return res.json({ success: true, cart_id, order_id: result?.id })
86
+ logger.info(
87
+ `[paypal] paypal-complete cart completed (request_id=${requestId}, cart_id=${cart_id}, order_id=${result?.id})`
88
+ )
89
+ return res.json({ success: true, cart_id, order_id: result?.id, request_id: requestId })
104
90
  } catch (e: any) {
105
- console.warn("[paypal-complete] completeCartWorkflow failed:", e?.message)
106
- return res.json({ success: true, session_id: session.id })
91
+ // The cart may have been completed by a concurrent request between our
92
+ // initial check and now — treat that as success, not an error.
93
+ const recheck = await query
94
+ .graph({ entity: "cart", fields: ["id", "completed_at"], filters: { id: cart_id } })
95
+ .catch(() => ({ data: [] as any[] }))
96
+ if (recheck?.data?.[0]?.completed_at) {
97
+ return res.json({ success: true, cart_id, already_completed: true, request_id: requestId })
98
+ }
99
+
100
+ logger.error(
101
+ `[paypal] paypal-complete completeCartWorkflow failed (request_id=${requestId}, cart_id=${cart_id}): ${
102
+ e?.message ?? String(e)
103
+ }`,
104
+ e instanceof Error ? e : undefined
105
+ )
106
+ // The payment may have been captured at PayPal but the order was not
107
+ // finalized. Report failure (not a false success) so the storefront can
108
+ // surface a "payment taken, contact support" state.
109
+ return res.status(500).json({
110
+ success: false,
111
+ cart_id,
112
+ session_id: session.id,
113
+ message: "Payment was processed but the order could not be finalized.",
114
+ request_id: requestId,
115
+ })
107
116
  }
108
117
  } catch (e: any) {
109
- console.error("[paypal-complete] error:", e?.message || e)
110
- return res.status(500).json({ message: "Internal error" })
118
+ logger.error(
119
+ `[paypal] paypal-complete failed (request_id=${requestId}, cart_id=${cart_id}): ${
120
+ e?.message ?? String(e)
121
+ }`,
122
+ e instanceof Error ? e : undefined
123
+ )
124
+ return res.status(500).json({ message: "Internal error", request_id: requestId })
111
125
  }
112
126
  }
@@ -1,4 +1,5 @@
1
1
  import type { MedusaContainer } from "@medusajs/framework/types"
2
+ import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
2
3
  import type PayPalModuleService from "../modules/paypal/service"
3
4
  import {
4
5
  computeNextRetryAt,
@@ -8,11 +9,60 @@ import {
8
9
  processPayPalWebhookEvent,
9
10
  } from "../modules/paypal/webhook-processor"
10
11
 
12
+ // Bound how many failed events one run drains so the cron can't load an
13
+ // unbounded backlog into memory.
14
+ const RETRY_BATCH_SIZE = 100
15
+
16
+ // Stable key for the Postgres advisory lock that serializes this cron across
17
+ // instances. pg_try_advisory_xact_lock auto-releases at transaction end, so
18
+ // there is no unlock to pair (and no risk of a leaked lock).
19
+ const RETRY_ADVISORY_LOCK_KEY = 838907812
20
+
11
21
  export default async function paypalWebhookRetry(container: MedusaContainer) {
22
+ // Run the sweep under a Postgres advisory lock so that with multiple server
23
+ // instances running this cron, only one drains the queue per cycle (each
24
+ // event is processed at most once). Falls back to running directly if the
25
+ // raw connection isn't resolvable, so the job degrades gracefully rather
26
+ // than breaking.
27
+ let pg: any = null
28
+ try {
29
+ pg = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
30
+ } catch {
31
+ pg = null
32
+ }
33
+
34
+ if (!pg?.transaction) {
35
+ await runRetrySweep(container)
36
+ return
37
+ }
38
+
39
+ try {
40
+ await pg.transaction(async (trx: any) => {
41
+ const res = await trx.raw(
42
+ "SELECT pg_try_advisory_xact_lock(?) AS locked",
43
+ [RETRY_ADVISORY_LOCK_KEY]
44
+ )
45
+ const locked = res?.rows?.[0]?.locked ?? res?.[0]?.locked ?? false
46
+ if (!locked) {
47
+ console.info("[PayPal] webhook-retry: another instance holds the lock, skipping")
48
+ return
49
+ }
50
+ await runRetrySweep(container)
51
+ })
52
+ } catch (e: any) {
53
+ console.warn("[PayPal] webhook-retry: advisory lock path failed, running unlocked:", e?.message)
54
+ await runRetrySweep(container)
55
+ }
56
+ }
57
+
58
+ async function runRetrySweep(container: MedusaContainer) {
12
59
  const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
13
60
  const now = Date.now()
14
61
 
15
- const candidates = await paypal.listPayPalWebhookEvents({ status: "failed" })
62
+ const candidates = await paypal.listPayPalWebhookEvents(
63
+ { status: "failed" },
64
+ { take: RETRY_BATCH_SIZE, order: { next_retry_at: "ASC" } }
65
+ )
16
66
  if (!candidates?.length) return
17
67
 
18
68
  console.info(
@@ -1,3 +1,5 @@
1
+ import { paypalFetch } from "../utils/paypal-fetch"
2
+
1
3
  export class PayPalSellerClient {
2
4
  private static readonly BN_CODE = "MBJTechnolabs_SI_SPB"
3
5
 
@@ -19,7 +21,7 @@ export class PayPalSellerClient {
19
21
  }
20
22
 
21
23
  async createOrder(body: any) {
22
- const res = await fetch(`${this.baseUrl()}/v2/checkout/orders`, {
24
+ const res = await paypalFetch(`${this.baseUrl()}/v2/checkout/orders`, {
23
25
  method: "POST",
24
26
  headers: this.headers(),
25
27
  body: JSON.stringify(body),
@@ -30,7 +32,7 @@ export class PayPalSellerClient {
30
32
  }
31
33
 
32
34
  async getOrder(orderId: string) {
33
- const res = await fetch(`${this.baseUrl()}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
35
+ const res = await paypalFetch(`${this.baseUrl()}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
34
36
  method: "GET",
35
37
  headers: this.headers(),
36
38
  })
@@ -40,7 +42,7 @@ export class PayPalSellerClient {
40
42
  }
41
43
 
42
44
  async captureOrder(orderId: string) {
43
- const res = await fetch(`${this.baseUrl()}/v2/checkout/orders/${encodeURIComponent(orderId)}/capture`, {
45
+ const res = await paypalFetch(`${this.baseUrl()}/v2/checkout/orders/${encodeURIComponent(orderId)}/capture`, {
44
46
  method: "POST",
45
47
  headers: this.headers(),
46
48
  })
@@ -50,7 +52,7 @@ export class PayPalSellerClient {
50
52
  }
51
53
 
52
54
  async refundCapture(captureId: string, body?: any) {
53
- const res = await fetch(`${this.baseUrl()}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
55
+ const res = await paypalFetch(`${this.baseUrl()}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
54
56
  method: "POST",
55
57
  headers: this.headers(),
56
58
  body: body ? JSON.stringify(body) : "{}",
@@ -26,6 +26,7 @@ import type {
26
26
  } from "@medusajs/framework/types"
27
27
  import { getPayPalWebhookActionAndData } from "./webhook-utils"
28
28
  import { formatAmountForPayPal } from "../utils/amounts"
29
+ import { paypalFetch } from "../utils/paypal-fetch"
29
30
  import {
30
31
  assertPayPalCurrencySupported,
31
32
  normalizeCurrencyCode,
@@ -110,7 +111,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
110
111
 
111
112
  private async getOrderDetails(orderId: string) {
112
113
  const { accessToken, base } = await this.getPayPalAccessToken()
113
- const resp = await fetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
114
+ const resp = await paypalFetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
114
115
  method: "GET",
115
116
  headers: {
116
117
  Authorization: `Bearer ${accessToken}`,
@@ -350,7 +351,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
350
351
  : {}),
351
352
  }
352
353
 
353
- const ppResp = await fetch(`${base}/v2/checkout/orders`, {
354
+ const ppResp = await paypalFetch(`${base}/v2/checkout/orders`, {
354
355
  method: "POST",
355
356
  headers: {
356
357
  Authorization: `Bearer ${accessToken}`,
@@ -404,7 +405,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
404
405
  },
405
406
  }
406
407
  } else {
407
- const authorizeResp = await fetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/authorize`, {
408
+ const authorizeResp = await paypalFetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/authorize`, {
408
409
  method: "POST",
409
410
  headers: {
410
411
  Authorization: `Bearer ${accessToken}`,
@@ -495,7 +496,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
495
496
  order?.intent || paypalData.order?.intent || data.intent || ""
496
497
  ).toUpperCase()
497
498
  if (!authorizationId && resolvedIntent === "AUTHORIZE") {
498
- const authorizeResp = await fetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/authorize`, {
499
+ const authorizeResp = await paypalFetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/authorize`, {
499
500
  method: "POST",
500
501
  headers: {
501
502
  Authorization: `Bearer ${accessToken}`,
@@ -544,7 +545,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
544
545
  ? `${base}/v2/payments/authorizations/${encodeURIComponent(authorizationId)}/capture`
545
546
  : `${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/capture`
546
547
 
547
- const ppResp = await fetch(captureUrl, {
548
+ const ppResp = await paypalFetch(captureUrl, {
548
549
  method: "POST",
549
550
  headers: {
550
551
  Authorization: `Bearer ${accessToken}`,
@@ -611,7 +612,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
611
612
  const { accessToken, base } = await this.getPayPalAccessToken()
612
613
  const requestId = this.getIdempotencyKey(_input, `void-${authorizationId}`)
613
614
 
614
- const resp = await fetch(
615
+ const resp = await paypalFetch(
615
616
  `${base}/v2/payments/authorizations/${encodeURIComponent(authorizationId)}/void`,
616
617
  {
617
618
  method: "POST",
@@ -635,9 +636,9 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
635
636
  }
636
637
  } else if (captureId) {
637
638
  const { accessToken, base } = await this.getPayPalAccessToken()
638
- const requestId = this.getIdempotencyKey(_input, `refund-${captureId}`)
639
+ const requestId = this.getIdempotencyKey(_input, `cancel-refund-${captureId}`)
639
640
 
640
- const resp = await fetch(
641
+ const resp = await paypalFetch(
641
642
  `${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`,
642
643
  {
643
644
  method: "POST",
@@ -710,7 +711,9 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
710
711
  }
711
712
 
712
713
  const requestId = this.getIdempotencyKey(_input, `refund-${captureId}`)
713
- const amount = Number(data.amount ?? 0)
714
+ // Use the refund amount Medusa passes (top-level input), not the session
715
+ // amount in `data` — otherwise a partial refund would refund the full order.
716
+ const amount = Number(_input.amount ?? 0)
714
717
  const currencyOverride = await this.resolveCurrencyOverride()
715
718
  const currencyCode = normalizeCurrencyCode(
716
719
  data.currency_code || currencyOverride || "EUR"
@@ -726,7 +729,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
726
729
  }
727
730
  : {}
728
731
 
729
- const resp = await fetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
732
+ const resp = await paypalFetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
730
733
  method: "POST",
731
734
  headers: {
732
735
  Authorization: `Bearer ${accessToken}`,
@@ -31,6 +31,7 @@ import {
31
31
  } from "../utils/currencies"
32
32
  import type PayPalModuleService from "../service"
33
33
  import { getPayPalWebhookActionAndData } from "./webhook-utils"
34
+ import { paypalFetch } from "../utils/paypal-fetch"
34
35
 
35
36
  type Options = {}
36
37
 
@@ -110,7 +111,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
110
111
 
111
112
  private async getOrderDetails(orderId: string) {
112
113
  const { accessToken, base } = await this.getPayPalAccessToken()
113
- const resp = await fetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
114
+ const resp = await paypalFetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
114
115
  method: "GET",
115
116
  headers: {
116
117
  Authorization: `Bearer ${accessToken}`,
@@ -395,7 +396,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
395
396
  data.session_id || data.cart_id || data.payment_collection_id || undefined,
396
397
  }
397
398
 
398
- const ppResp = await fetch(`${base}/v2/checkout/orders`, {
399
+ const ppResp = await paypalFetch(`${base}/v2/checkout/orders`, {
399
400
  method: "POST",
400
401
  headers: {
401
402
  Authorization: `Bearer ${accessToken}`,
@@ -430,7 +431,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
430
431
  if (existingAuthorization) {
431
432
  authorization = order
432
433
  } else {
433
- const authorizeResp = await fetch(
434
+ const authorizeResp = await paypalFetch(
434
435
  `${base}/v2/checkout/orders/${encodeURIComponent(newOrderId)}/authorize`,
435
436
  {
436
437
  method: "POST",
@@ -613,7 +614,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
613
614
  ).toUpperCase()
614
615
 
615
616
  if (!authorizationId && resolvedIntent === "AUTHORIZE") {
616
- const authorizeResp = await fetch(
617
+ const authorizeResp = await paypalFetch(
617
618
  `${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/authorize`,
618
619
  {
619
620
  method: "POST",
@@ -668,7 +669,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
668
669
  ? `${base}/v2/payments/authorizations/${encodeURIComponent(authorizationId)}/capture`
669
670
  : `${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/capture`
670
671
 
671
- const ppResp = await fetch(captureUrl, {
672
+ const ppResp = await paypalFetch(captureUrl, {
672
673
  method: "POST",
673
674
  headers: {
674
675
  Authorization: `Bearer ${accessToken}`,
@@ -781,7 +782,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
781
782
  refundPayload.note_to_payer = refundReason
782
783
  }
783
784
 
784
- const ppResp = await fetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
785
+ const ppResp = await paypalFetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
785
786
  method: "POST",
786
787
  headers: {
787
788
  Authorization: `Bearer ${accessToken}`,
@@ -871,7 +872,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
871
872
  const { accessToken, base } = await this.getPayPalAccessToken()
872
873
  const requestId = this.getIdempotencyKey(input, `void-${authorizationId}`)
873
874
 
874
- const resp = await fetch(
875
+ const resp = await paypalFetch(
875
876
  `${base}/v2/payments/authorizations/${encodeURIComponent(authorizationId)}/void`,
876
877
  {
877
878
  method: "POST",
@@ -901,9 +902,9 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
901
902
  })
902
903
  } else if (captureId) {
903
904
  const { accessToken, base } = await this.getPayPalAccessToken()
904
- const requestId = this.getIdempotencyKey(input, `refund-${captureId}`)
905
+ const requestId = this.getIdempotencyKey(input, `cancel-refund-${captureId}`)
905
906
 
906
- const resp = await fetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
907
+ const resp = await paypalFetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
907
908
  method: "POST",
908
909
  headers: {
909
910
  Authorization: `Bearer ${accessToken}`,
@@ -66,6 +66,18 @@ function resolveEventType(payload: ProviderWebhookPayload["payload"]) {
66
66
  return raw?.event_type || raw?.eventType
67
67
  }
68
68
 
69
+ /**
70
+ * Maps a PayPal webhook payload to the generic Medusa payment-provider webhook
71
+ * result (the `getWebhookActionAndData` hook on AbstractPaymentProvider).
72
+ *
73
+ * NOTE: the authoritative webhook handling for this plugin is the dedicated
74
+ * `/store/paypal/webhook` route (signature verification, replay window,
75
+ * dedup, dead-letter retry). This helper exists only for the framework's
76
+ * generic `/hooks/payment/:provider` path. The `session_id` returned here is
77
+ * derived from the PayPal resource `custom_id`, which this plugin sets to the
78
+ * cart id (see create-order) — so consumers must resolve the cart, not treat
79
+ * it as a payment-session id directly.
80
+ */
69
81
  export function getPayPalWebhookActionAndData(
70
82
  payload: ProviderWebhookPayload["payload"]
71
83
  ): WebhookActionResult {