@easypayment/medusa-paypal 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.medusa/server/src/admin/index.js +689 -934
  2. package/.medusa/server/src/admin/index.mjs +689 -934
  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 +62 -74
  8. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  9. package/.medusa/server/src/api/store/paypal/config/route.d.ts.map +1 -1
  10. package/.medusa/server/src/api/store/paypal/config/route.js +9 -2
  11. package/.medusa/server/src/api/store/paypal/config/route.js.map +1 -1
  12. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  13. package/.medusa/server/src/api/store/paypal/create-order/route.js +3 -24
  14. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  15. package/.medusa/server/src/api/store/paypal/settings/route.d.ts.map +1 -1
  16. package/.medusa/server/src/api/store/paypal/settings/route.js +7 -1
  17. package/.medusa/server/src/api/store/paypal/settings/route.js.map +1 -1
  18. package/.medusa/server/src/api/store/paypal-complete/route.d.ts +1 -8
  19. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
  20. package/.medusa/server/src/api/store/paypal-complete/route.js +47 -39
  21. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
  22. package/.medusa/server/src/jobs/paypal-reconcile.d.ts.map +1 -1
  23. package/.medusa/server/src/jobs/paypal-reconcile.js +19 -5
  24. package/.medusa/server/src/jobs/paypal-reconcile.js.map +1 -1
  25. package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts.map +1 -1
  26. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js +54 -4
  27. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js.map +1 -1
  28. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts +4 -1
  29. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
  30. package/.medusa/server/src/modules/paypal/payment-provider/service.js +35 -8
  31. package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
  32. package/.medusa/server/src/modules/paypal/service.d.ts +67 -61
  33. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  34. package/.medusa/server/src/modules/paypal/service.js +34 -4
  35. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  36. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +14 -0
  37. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -0
  38. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +32 -0
  39. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -0
  40. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +9 -9
  41. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
  42. package/.medusa/server/src/modules/paypal/webhook-processor.js +20 -7
  43. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/admin/routes/settings/paypal/additional-settings/page.tsx +226 -346
  46. package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +227 -381
  47. package/src/admin/routes/settings/paypal/audit-logs/page.tsx +127 -131
  48. package/src/admin/routes/settings/paypal/disputes/page.tsx +186 -259
  49. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +599 -557
  50. package/src/admin/routes/settings/paypal/reconciliation-status/page.tsx +120 -165
  51. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +12 -1
  52. package/src/api/store/paypal/capture-order/route.ts +276 -284
  53. package/src/api/store/paypal/config/route.ts +12 -8
  54. package/src/api/store/paypal/create-order/route.ts +2 -32
  55. package/src/api/store/paypal/settings/route.ts +8 -1
  56. package/src/api/store/paypal-complete/route.ts +76 -65
  57. package/src/jobs/paypal-reconcile.ts +21 -6
  58. package/src/modules/paypal/payment-provider/card-service.ts +54 -4
  59. package/src/modules/paypal/payment-provider/service.ts +47 -20
  60. package/src/modules/paypal/service.ts +39 -4
  61. package/src/modules/paypal/utils/paypal-auth.ts +32 -0
  62. package/src/modules/paypal/webhook-processor.ts +22 -8
  63. package/tsconfig.json +1 -1
@@ -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
  }
@@ -1,65 +1,76 @@
1
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework"
2
- import { Pool } from "pg"
3
-
4
- /**
5
- * POST /store/paypal-complete
6
- * Body: { cart_id, order_id, capture_id }
7
- *
8
- * Directly sets the PayPal payment session to "authorized" in the DB.
9
- * The frontend then calls placeOrder() which handles cart completion + redirect.
10
- */
11
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
12
- const { cart_id, order_id, capture_id } = req.body as {
13
- cart_id?: string
14
- order_id?: string
15
- capture_id?: string
16
- }
17
-
18
- if (!cart_id) {
19
- return res.status(400).json({ error: "cart_id is required" })
20
- }
21
-
22
- const pool = new Pool({ connectionString: process.env.DATABASE_URL })
23
-
24
- try {
25
- const { rows } = await pool.query(
26
- `UPDATE payment_session
27
- SET status = 'authorized',
28
- data = data || $1::jsonb
29
- WHERE id = (
30
- SELECT ps.id
31
- FROM payment_session ps
32
- JOIN payment_collection pc ON ps.payment_collection_id = pc.id
33
- JOIN cart_payment_collection cpc ON cpc.payment_collection_id = pc.id
34
- WHERE cpc.cart_id = $2
35
- AND ps.provider_id LIKE '%paypal%'
36
- ORDER BY ps.created_at DESC
37
- LIMIT 1
38
- )
39
- RETURNING id, status`,
40
- [
41
- JSON.stringify({
42
- paypal: {
43
- order_id: order_id ?? null,
44
- capture_id: capture_id ?? null,
45
- },
46
- authorized_at: new Date().toISOString(),
47
- }),
48
- cart_id,
49
- ]
50
- )
51
-
52
- console.log("[paypal-complete] session authorized:", rows)
53
-
54
- if (!rows.length) {
55
- return res.status(400).json({ error: "No PayPal payment session found for cart" })
56
- }
57
-
58
- return res.json({ success: true, session_id: rows[0].id })
59
- } catch (e: any) {
60
- console.error("[paypal-complete] error:", e?.message || e)
61
- return res.status(500).json({ error: e?.message || "Internal error" })
62
- } finally {
63
- await pool.end()
64
- }
65
- }
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 type PayPalModuleService from "../../../modules/paypal/service"
5
+
6
+ export async function POST(req: MedusaRequest, res: MedusaResponse) {
7
+ const { cart_id } = req.body as { cart_id: string }
8
+
9
+ if (!cart_id) {
10
+ return res.status(400).json({ error: "cart_id is required" })
11
+ }
12
+
13
+ try {
14
+ // Step 1 — read paymentAction from DB settings (same pattern as capture-order)
15
+ const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
16
+ const settings = await paypal.getSettings().catch(() => ({}))
17
+ const settingsData =
18
+ settings && typeof settings === "object" && "data" in settings
19
+ ? ((settings as { data?: Record<string, any> }).data ?? {})
20
+ : {}
21
+ const additionalSettings = (settingsData.additional_settings || {}) as Record<string, any>
22
+ const paymentAction =
23
+ typeof additionalSettings.paymentAction === "string"
24
+ ? additionalSettings.paymentAction
25
+ : "capture"
26
+
27
+ // "authorize" mode → session status = "authorized"
28
+ // "capture" mode → session status = "captured"
29
+ const sessionStatus = paymentAction === "authorize" ? "authorized" : "captured"
30
+ const timestampKey = paymentAction === "authorize" ? "authorized_at" : "captured_at"
31
+
32
+ // Step 2 find the PayPal session for this cart
33
+ const query = req.scope.resolve("query")
34
+ const { data: carts } = await query.graph({
35
+ entity: "cart",
36
+ fields: [
37
+ "id",
38
+ "payment_collection.payment_sessions.id",
39
+ "payment_collection.payment_sessions.data",
40
+ "payment_collection.payment_sessions.provider_id",
41
+ "payment_collection.payment_sessions.created_at",
42
+ "payment_collection.payment_sessions.amount",
43
+ "payment_collection.payment_sessions.currency_code",
44
+ ],
45
+ filters: { id: cart_id },
46
+ })
47
+
48
+ const sessions = carts?.[0]?.payment_collection?.payment_sessions || []
49
+ const session = sessions
50
+ .filter((s: any) => String(s.provider_id || "").includes("paypal"))
51
+ .sort(
52
+ (a: any, b: any) =>
53
+ new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
54
+ )[0]
55
+
56
+ if (!session) {
57
+ return res.status(400).json({ error: "No PayPal payment session found for cart" })
58
+ }
59
+
60
+ // Step 3 update session with correct status + amount
61
+ const paymentModule = req.scope.resolve(Modules.PAYMENT) as IPaymentModuleService
62
+ await (paymentModule as any).updatePaymentSession({
63
+ id: session.id,
64
+ data: { ...(session.data || {}), [timestampKey]: new Date().toISOString() },
65
+ status: sessionStatus as any,
66
+ amount: session.amount,
67
+ currency_code: session.currency_code,
68
+ })
69
+
70
+ console.log(`[paypal-complete] session ${sessionStatus}:`, session.id)
71
+ return res.json({ success: true, session_id: session.id })
72
+ } catch (e: any) {
73
+ console.error("[paypal-complete] error:", e?.message || e)
74
+ return res.status(500).json({ error: e?.message || "Internal error" })
75
+ }
76
+ }
@@ -1,5 +1,8 @@
1
1
  import type { MedusaContainer } from "@medusajs/framework/types"
2
+ import { Modules } from "@medusajs/framework/utils"
2
3
  import type PayPalModuleService from "../modules/paypal/service"
4
+ import { PAYPAL_MODULE } from "../modules/paypal"
5
+ import { PAYPAL_PROVIDER_IDS } from "../modules/paypal/utils/provider-ids"
3
6
 
4
7
  const STATUS_MAP: Record<string, "pending" | "authorized" | "captured" | "canceled"> = {
5
8
  CREATED: "pending",
@@ -10,11 +13,20 @@ const STATUS_MAP: Record<string, "pending" | "authorized" | "captured" | "cancel
10
13
  }
11
14
 
12
15
  export default async function paypalReconcile(container: MedusaContainer) {
13
- const paymentSessionService = container.resolve("payment_session") as any
14
- const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
16
+ // FIX 3a: was container.resolve("payment_session") as any
17
+ // Modules.PAYMENT is the official typed constant for the Medusa payment module.
18
+ // listPaymentSessions and updatePaymentSession are methods on the payment module service.
19
+ const paymentModule = container.resolve(Modules.PAYMENT) as any
15
20
 
16
- const sessions = await paymentSessionService.list({
17
- provider_id: ["pp_paypal_paypal", "pp_paypal_card_paypal_card"],
21
+ // FIX 3b: was container.resolve<PayPalModuleService>("paypal_onboarding")
22
+ // PAYPAL_MODULE is the exported constant from modules/paypal/index.ts
23
+ const paypal = container.resolve<PayPalModuleService>(PAYPAL_MODULE)
24
+
25
+ // FIX 3c: was ["pp_paypal_paypal", "pp_paypal_card_paypal_card"]
26
+ // PAYPAL_PROVIDER_IDS is the exported array from modules/paypal/utils/provider-ids.ts
27
+ // If the provider IDs ever change in one place, this file updates automatically.
28
+ const sessions = await paymentModule.listPaymentSessions({
29
+ provider_id: [...PAYPAL_PROVIDER_IDS],
18
30
  status: ["pending", "authorized"],
19
31
  })
20
32
 
@@ -66,8 +78,11 @@ export default async function paypalReconcile(container: MedusaContainer) {
66
78
  })
67
79
  }
68
80
 
69
- await paymentSessionService.update(session.id, {
81
+ // FIX 3d: was paymentSessionService.update(session.id, { ... })
82
+ // updatePaymentSession is the correct method name on the payment module service.
83
+ await paymentModule.updatePaymentSession(session.id, {
70
84
  status,
85
+ amount: session.amount,
71
86
  data: {
72
87
  ...(session.data || {}),
73
88
  paypal: {
@@ -132,4 +147,4 @@ export default async function paypalReconcile(container: MedusaContainer) {
132
147
  export const config = {
133
148
  name: "paypal-reconcile",
134
149
  schedule: "*/15 * * * *",
135
- }
150
+ }
@@ -56,11 +56,36 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
56
56
  const container = this.container as {
57
57
  resolve<T>(key: string): T
58
58
  }
59
- return container.resolve<PayPalModuleService>("paypal_onboarding")
59
+ try {
60
+ return container.resolve<PayPalModuleService>("paypal_onboarding")
61
+ } catch {
62
+ return null as any
63
+ }
60
64
  }
61
65
 
62
66
  private async resolveSettings() {
63
67
  const paypal = this.resolvePayPalService()
68
+ if (!paypal) {
69
+ try {
70
+ const { Pool: _SettingsPool } = require("pg")
71
+ const _sPool = new _SettingsPool({ connectionString: process.env.DATABASE_URL })
72
+ const _sResult = await _sPool
73
+ .query("SELECT data FROM paypal_settings ORDER BY created_at DESC LIMIT 1")
74
+ .finally(() => _sPool.end())
75
+ const _sData = _sResult.rows[0]?.data || {}
76
+ return {
77
+ additionalSettings: (_sData.additional_settings || {}) as Record<string, any>,
78
+ advancedCardSettings: (_sData.advanced_card_payments || {}) as Record<string, any>,
79
+ apiDetails: (_sData.api_details || {}) as Record<string, any>,
80
+ }
81
+ } catch {
82
+ return {
83
+ additionalSettings: {} as Record<string, any>,
84
+ advancedCardSettings: {} as Record<string, any>,
85
+ apiDetails: {} as Record<string, any>,
86
+ }
87
+ }
88
+ }
64
89
  const settings = await paypal.getSettings().catch(() => ({}))
65
90
  const data =
66
91
  settings && typeof settings === "object" && "data" in settings
@@ -83,12 +108,37 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
83
108
 
84
109
  private async getPayPalAccessToken() {
85
110
  const paypal = this.resolvePayPalService()
86
- const creds = await paypal.getActiveCredentials()
111
+ let client_id: string
112
+ let client_secret: string
113
+ let environment: string
114
+
115
+ if (!paypal) {
116
+ const { Pool: _FbPool } = require("pg")
117
+ const _fbPool = new _FbPool({ connectionString: process.env.DATABASE_URL })
118
+ const _fbResult = await _fbPool
119
+ .query(
120
+ "SELECT metadata, environment, seller_client_id, seller_client_secret FROM paypal_connection WHERE status='connected' ORDER BY created_at DESC LIMIT 1"
121
+ )
122
+ .finally(() => _fbPool.end())
123
+ const _fbRow = _fbResult.rows[0]
124
+ if (!_fbRow) throw new Error("No active PayPal connection found in DB")
125
+ environment = _fbRow.environment || "sandbox"
126
+ const _fbCreds = (_fbRow.metadata?.credentials?.[environment]) || {}
127
+ client_id = _fbCreds.client_id || _fbRow.seller_client_id
128
+ client_secret = _fbCreds.client_secret || _fbRow.seller_client_secret
129
+ console.info("[PayPal Card] getPayPalAccessToken fallback via DB for env:", environment)
130
+ } else {
131
+ const creds = await paypal.getActiveCredentials()
132
+ client_id = creds.client_id
133
+ client_secret = creds.client_secret
134
+ environment = creds.environment
135
+ }
136
+
87
137
  const base =
88
- creds.environment === "live"
138
+ environment === "live"
89
139
  ? "https://api-m.paypal.com"
90
140
  : "https://api-m.sandbox.paypal.com"
91
- const auth = Buffer.from(`${creds.client_id}:${creds.client_secret}`).toString("base64")
141
+ const auth = Buffer.from(`${client_id}:${client_secret}`).toString("base64")
92
142
 
93
143
  const resp = await fetch(`${base}/v1/oauth2/token`, {
94
144
  method: "POST",
@@ -64,25 +64,36 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
64
64
  }
65
65
  }
66
66
 
67
- private async resolveSettings() {
68
- const paypal = this.resolvePayPalService()
67
+ async resolveSettings() {
68
+ const paypal = this.resolvePayPalService();
69
69
  if (!paypal) {
70
- return {
71
- additionalSettings: {},
72
- apiDetails: {},
73
- }
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
+ }
74
87
  }
75
-
76
88
  const settings = await paypal.getSettings().catch(() => ({}))
77
- const data =
78
- settings && typeof settings === "object" && "data" in settings
79
- ? ((settings as { data?: Record<string, any> }).data ?? {})
89
+ const data = settings && typeof settings === "object" && "data" in settings
90
+ ? ((settings as any).data ?? {})
80
91
  : {}
81
92
  return {
82
- additionalSettings: (data.additional_settings || {}) as Record<string, any>,
83
- apiDetails: (data.api_details || {}) as Record<string, any>,
93
+ additionalSettings: (data.additional_settings || {}) as Record<string, unknown>,
94
+ apiDetails: (data.api_details || {}) as Record<string, unknown>,
84
95
  }
85
- }
96
+ }
86
97
 
87
98
  private async resolveCurrencyOverride() {
88
99
  const { apiDetails } = await this.resolveSettings()
@@ -426,13 +437,29 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
426
437
  const { data, amount, currencyCode } = await this.normalizePaymentData(input)
427
438
 
428
439
  const existingPayPal = (data.paypal || {}) as Record<string, any>
429
- if (existingPayPal.capture_id || existingPayPal.authorized_at) {
430
- console.info("[PayPal] authorizePayment: already captured, returning authorized")
431
- return {
432
- status: "authorized",
433
- data: { ...(input.data || {}), authorized_at: new Date().toISOString() },
434
- }
435
- }
440
+ if (
441
+ existingPayPal.capture_id ||
442
+ existingPayPal.authorization_id ||
443
+ (data as any).authorized_at ||
444
+ (data as any).captured_at
445
+ ) {
446
+ const { additionalSettings } = await this.resolveSettings()
447
+ const paymentAction =
448
+ typeof additionalSettings.paymentAction === "string"
449
+ ? additionalSettings.paymentAction
450
+ : "capture"
451
+ const returnStatus = paymentAction === "authorize" ? "authorized" : "captured"
452
+ console.info("[PayPal] authorizePayment: already processed, returning", returnStatus)
453
+ return {
454
+ status: returnStatus,
455
+ data: {
456
+ ...(input.data || {}),
457
+ ...(paymentAction === "authorize"
458
+ ? { authorized_at: new Date().toISOString() }
459
+ : { captured_at: new Date().toISOString() }),
460
+ },
461
+ }
462
+ }
436
463
 
437
464
 
438
465
  const requestId = this.getIdempotencyKey(input, "authorize")
@@ -236,7 +236,9 @@ class PayPalModuleService extends MedusaService({
236
236
  let json: any = {}
237
237
  try {
238
238
  json = text ? JSON.parse(text) : {}
239
- } catch {}
239
+ } catch (e: any) {
240
+ console.warn("[PayPal] Failed to parse response JSON — using empty object:", e?.message)
241
+ }
240
242
 
241
243
  if (!resp.ok) {
242
244
  throw new Error(
@@ -548,7 +550,9 @@ class PayPalModuleService extends MedusaService({
548
550
  let tokenJson: any = {}
549
551
  try {
550
552
  tokenJson = tokenText ? JSON.parse(tokenText) : {}
551
- } catch {}
553
+ } catch (e: any) {
554
+ console.warn("[PayPal] Failed to parse token response JSON:", e?.message)
555
+ }
552
556
 
553
557
  if (!tokenRes.ok) {
554
558
  throw new Error(
@@ -583,7 +587,9 @@ class PayPalModuleService extends MedusaService({
583
587
  let credJson: any = {}
584
588
  try {
585
589
  credJson = credText ? JSON.parse(credText) : {}
586
- } catch {}
590
+ } catch (e: any) {
591
+ console.warn("[PayPal] Failed to parse token response JSON:", e?.message)
592
+ }
587
593
 
588
594
  if (!credRes.ok) {
589
595
  throw new Error(
@@ -1032,12 +1038,41 @@ class PayPalModuleService extends MedusaService({
1032
1038
  return { data: (row?.data || {}) as Record<string, any> }
1033
1039
  }
1034
1040
 
1041
+ /**
1042
+ * Deep-merge patch into current settings.
1043
+ * Nested objects (additional_settings, api_details, etc.) are merged,
1044
+ * not replaced.
1045
+ */
1046
+ private deepMerge(
1047
+ target: Record<string, any>,
1048
+ source: Record<string, any>
1049
+ ): Record<string, any> {
1050
+ const result = { ...target }
1051
+ for (const key of Object.keys(source)) {
1052
+ const sv = source[key]
1053
+ const tv = target[key]
1054
+ if (
1055
+ sv !== null &&
1056
+ typeof sv === "object" &&
1057
+ !Array.isArray(sv) &&
1058
+ tv !== null &&
1059
+ typeof tv === "object" &&
1060
+ !Array.isArray(tv)
1061
+ ) {
1062
+ result[key] = this.deepMerge(tv, sv)
1063
+ } else {
1064
+ result[key] = sv
1065
+ }
1066
+ }
1067
+ return result
1068
+ }
1069
+
1035
1070
  async saveSettings(patch: Record<string, any>) {
1036
1071
  const rows = await this.listPayPalSettings({})
1037
1072
  const row = rows?.[0]
1038
1073
  const current = (row?.data || {}) as Record<string, any>
1039
1074
 
1040
- const next = { ...current, ...patch }
1075
+ const next = this.deepMerge(current, patch)
1041
1076
 
1042
1077
  if (!row) {
1043
1078
  const created = await this.createPayPalSettings({ data: next })
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared PayPal API authentication helpers.
3
+ * Import these instead of copying into each route.
4
+ */
5
+ export function getPayPalApiBase(environment: string): string {
6
+ return environment === "live"
7
+ ? "https://api-m.paypal.com"
8
+ : "https://api-m.sandbox.paypal.com"
9
+ }
10
+
11
+ export async function getPayPalAccessToken(opts: {
12
+ environment: string
13
+ client_id: string
14
+ client_secret: string
15
+ }): Promise<{ accessToken: string; base: string }> {
16
+ const base = getPayPalApiBase(opts.environment)
17
+ const auth = Buffer.from(`${opts.client_id}:${opts.client_secret}`).toString("base64")
18
+ const resp = await fetch(`${base}/v1/oauth2/token`, {
19
+ method: "POST",
20
+ headers: {
21
+ Authorization: `Basic ${auth}`,
22
+ "Content-Type": "application/x-www-form-urlencoded",
23
+ },
24
+ body: "grant_type=client_credentials",
25
+ })
26
+ const text = await resp.text()
27
+ if (!resp.ok) {
28
+ throw new Error(`PayPal token error (${resp.status}): ${text}`)
29
+ }
30
+ const json = JSON.parse(text)
31
+ return { accessToken: String(json.access_token), base }
32
+ }
@@ -1,6 +1,8 @@
1
1
  import type { MedusaContainer } from "@medusajs/framework/types"
2
+ import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
2
3
  import type PayPalModuleService from "./service"
3
4
  import { isPayPalProviderId } from "./utils/provider-ids"
5
+ import { PAYPAL_MODULE } from "./index"
4
6
 
5
7
  export const EVENT_STATUS_MAP: Record<string, "authorized" | "captured" | "canceled" | "error"> = {
6
8
  "CHECKOUT.ORDER.APPROVED": "authorized",
@@ -125,7 +127,9 @@ async function resolveDisputeOrderId(
125
127
  orderId?: string | null,
126
128
  cartId?: string | null
127
129
  ) {
128
- const query = container.resolve("query") as any
130
+ // FIX 4a: was container.resolve("query") as any
131
+ // ContainerRegistrationKeys.QUERY is the official typed constant for the Medusa query helper
132
+ const query = container.resolve(ContainerRegistrationKeys.QUERY)
129
133
  const cleanedOrderId = orderId?.trim()
130
134
  const cleanedCartId = cartId?.trim()
131
135
 
@@ -188,15 +192,22 @@ async function updatePaymentSession(
188
192
  status: string,
189
193
  data: Record<string, unknown>
190
194
  ) {
191
- const paymentCollectionService = container.resolve("payment_collection") as any
192
- const paymentSessionService = container.resolve("payment_session") as any
195
+ // FIX 4b: was container.resolve("payment_collection") as any
196
+ // and container.resolve("payment_session") as any
197
+ // Modules.PAYMENT is the official typed constant that resolves the Medusa payment module.
198
+ // It gives access to both payment collections and payment sessions through one service.
199
+ const paymentModule = container.resolve(Modules.PAYMENT) as any
200
+
201
+ const pc = await paymentModule.retrievePaymentCollectionByCartId?.(cartId).catch(() => null)
202
+ ?? await paymentModule.listPaymentCollections({ cart_id: cartId })
203
+ .then((r: any[]) => r?.[0] ?? null)
204
+ .catch(() => null)
193
205
 
194
- const pc = await paymentCollectionService.retrieveByCartId(cartId).catch(() => null)
195
206
  if (!pc?.id) {
196
207
  return
197
208
  }
198
209
 
199
- const sessions = await paymentSessionService.list({ payment_collection_id: pc.id })
210
+ const sessions = await paymentModule.listPaymentSessions({ payment_collection_id: pc.id })
200
211
  const paypalSession = sessions?.find((s: any) => isPayPalProviderId(s.provider_id))
201
212
  if (!paypalSession) {
202
213
  return
@@ -210,7 +221,7 @@ async function updatePaymentSession(
210
221
  ? mergeRefunds(existingRefunds, incomingRefunds)
211
222
  : existingRefunds
212
223
 
213
- await paymentSessionService.update(paypalSession.id, {
224
+ await paymentModule.updatePaymentSession(paypalSession.id, {
214
225
  status,
215
226
  data: {
216
227
  ...existingData,
@@ -239,7 +250,10 @@ export async function processPayPalWebhookEvent(
239
250
  payload: Record<string, any>
240
251
  }
241
252
  ) {
242
- const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
253
+ // FIX 4c: was container.resolve<PayPalModuleService>("paypal_onboarding")
254
+ // PAYPAL_MODULE is the exported constant from ./index.ts — value is "paypal_onboarding"
255
+ // Using the constant means if the key ever changes, this file updates automatically.
256
+ const paypal = container.resolve<PayPalModuleService>(PAYPAL_MODULE)
243
257
  const resource = normalizeResource(input.payload)
244
258
  const { orderId, captureId, refundId, cartId } = extractIdentifiers(resource)
245
259
  const disputeDetails =
@@ -310,4 +324,4 @@ export async function processPayPalWebhookEvent(
310
324
  disputeId: disputeDetails?.disputeId,
311
325
  resource,
312
326
  }
313
- }
327
+ }
package/tsconfig.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "allowSyntheticDefaultImports": true,
18
18
  "resolveJsonModule": true,
19
19
  "skipLibCheck": true,
20
- "strict": false
20
+ "strict": true
21
21
  },
22
22
  "include": [
23
23
  "src/**/*.ts",