@easypayment/medusa-paypal 0.2.7 → 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 (49) hide show
  1. package/.medusa/server/src/admin/index.js +752 -997
  2. package/.medusa/server/src/admin/index.mjs +752 -997
  3. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -1
  4. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js +1 -0
  5. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js.map +1 -1
  6. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
  7. package/.medusa/server/src/api/store/paypal/capture-order/route.js +61 -74
  8. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  9. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  10. package/.medusa/server/src/api/store/paypal/create-order/route.js +3 -24
  11. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  12. package/.medusa/server/src/api/store/paypal/settings/route.d.ts.map +1 -1
  13. package/.medusa/server/src/api/store/paypal/settings/route.js +7 -1
  14. package/.medusa/server/src/api/store/paypal/settings/route.js.map +1 -1
  15. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
  16. package/.medusa/server/src/api/store/paypal-complete/route.js +46 -24
  17. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
  18. package/.medusa/server/src/jobs/paypal-reconcile.d.ts.map +1 -1
  19. package/.medusa/server/src/jobs/paypal-reconcile.js +19 -5
  20. package/.medusa/server/src/jobs/paypal-reconcile.js.map +1 -1
  21. package/.medusa/server/src/modules/paypal/service.d.ts +67 -61
  22. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  23. package/.medusa/server/src/modules/paypal/service.js +34 -4
  24. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  25. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +14 -0
  26. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -0
  27. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +32 -0
  28. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -0
  29. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +9 -9
  30. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
  31. package/.medusa/server/src/modules/paypal/webhook-processor.js +20 -7
  32. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/admin/routes/settings/paypal/additional-settings/page.tsx +226 -346
  35. package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +227 -381
  36. package/src/admin/routes/settings/paypal/audit-logs/page.tsx +127 -131
  37. package/src/admin/routes/settings/paypal/disputes/page.tsx +186 -259
  38. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +599 -557
  39. package/src/admin/routes/settings/paypal/reconciliation-status/page.tsx +120 -165
  40. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +12 -1
  41. package/src/api/store/paypal/capture-order/route.ts +276 -284
  42. package/src/api/store/paypal/create-order/route.ts +2 -32
  43. package/src/api/store/paypal/settings/route.ts +8 -1
  44. package/src/api/store/paypal-complete/route.ts +75 -45
  45. package/src/jobs/paypal-reconcile.ts +21 -6
  46. package/src/modules/paypal/service.ts +39 -4
  47. package/src/modules/paypal/utils/paypal-auth.ts +32 -0
  48. package/src/modules/paypal/webhook-processor.ts +22 -8
  49. package/tsconfig.json +1 -1
@@ -1,46 +1,76 @@
1
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
- import { Pool } from "pg"
3
-
4
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
5
- const { cart_id } = req.body as { cart_id: string; order_id?: string; capture_id?: string }
6
-
7
- if (!cart_id) {
8
- return res.status(400).json({ error: "cart_id is required" })
9
- }
10
-
11
- const pool = new Pool({ connectionString: process.env.DATABASE_URL })
12
- try {
13
- // Only update authorized_at — do NOT touch paypal object
14
- // capture_id and order_id are already correctly saved by capture-order route
15
- const { rows } = await pool.query(
16
- `UPDATE payment_session
17
- SET status = 'authorized',
18
- data = data || jsonb_build_object('authorized_at', $1::text)
19
- WHERE id = (
20
- SELECT ps.id
21
- FROM payment_session ps
22
- JOIN payment_collection pc ON ps.payment_collection_id = pc.id
23
- JOIN cart_payment_collection cpc ON cpc.payment_collection_id = pc.id
24
- WHERE cpc.cart_id = $2
25
- AND ps.provider_id LIKE '%paypal%'
26
- ORDER BY ps.created_at DESC
27
- LIMIT 1
28
- )
29
- RETURNING id, status`,
30
- [new Date().toISOString(), cart_id]
31
- )
32
-
33
- console.log("[paypal-complete] session authorized:", rows)
34
-
35
- if (!rows.length) {
36
- return res.status(400).json({ error: "No PayPal payment session found for cart" })
37
- }
38
-
39
- return res.json({ success: true, session_id: rows[0].id })
40
- } catch (e: any) {
41
- console.error("[paypal-complete] error:", e?.message || e)
42
- return res.status(500).json({ error: e?.message || "Internal error" })
43
- } finally {
44
- await pool.end()
45
- }
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
+ }
46
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
+ }
@@ -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",