@easypayment/medusa-paypal 0.6.3 → 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.
Files changed (68) hide show
  1. package/.medusa/server/src/admin/index.js +12 -15
  2. package/.medusa/server/src/admin/index.mjs +12 -15
  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.map +1 -1
  5. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
  6. package/.medusa/server/src/api/store/paypal/capture-order/route.js +1 -11
  7. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  8. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  9. package/.medusa/server/src/api/store/paypal/create-order/route.js +0 -9
  10. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  11. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
  12. package/.medusa/server/src/api/store/paypal/webhook/route.js +162 -115
  13. package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
  14. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
  15. package/.medusa/server/src/api/store/paypal-complete/route.js +0 -6
  16. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
  17. package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -1
  18. package/.medusa/server/src/jobs/paypal-webhook-retry.js +97 -43
  19. package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
  20. package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.d.ts +6 -0
  21. package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.d.ts.map +1 -0
  22. package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.js +20 -0
  23. package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.js.map +1 -0
  24. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
  25. package/.medusa/server/src/modules/paypal/payment-provider/service.js +0 -42
  26. package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
  27. package/.medusa/server/src/modules/paypal/service.d.ts +0 -8
  28. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  29. package/.medusa/server/src/modules/paypal/service.js +6 -114
  30. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  31. package/.medusa/server/src/modules/paypal/types/config.d.ts +0 -2
  32. package/.medusa/server/src/modules/paypal/types/config.d.ts.map +1 -1
  33. package/.medusa/server/src/modules/paypal/types/config.js +0 -9
  34. package/.medusa/server/src/modules/paypal/types/config.js.map +1 -1
  35. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +21 -17
  36. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
  37. package/.medusa/server/src/modules/paypal/webhook-processor.js +195 -99
  38. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
  39. package/README.md +156 -152
  40. package/package.json +1 -1
  41. package/src/admin/routes/settings/paypal/_components/Tabs.tsx +48 -52
  42. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +0 -23
  43. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +56 -65
  44. package/src/api/store/paypal/capture-order/route.ts +266 -276
  45. package/src/api/store/paypal/create-order/route.ts +0 -9
  46. package/src/api/store/paypal/webhook/route.ts +325 -246
  47. package/src/api/store/paypal-complete/route.ts +69 -75
  48. package/src/jobs/paypal-webhook-retry.ts +149 -85
  49. package/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.ts +17 -0
  50. package/src/modules/paypal/payment-provider/service.ts +1079 -1121
  51. package/src/modules/paypal/service.ts +6 -127
  52. package/src/modules/paypal/types/config.ts +33 -47
  53. package/src/modules/paypal/webhook-processor.ts +377 -215
  54. package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.d.ts +0 -3
  55. package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.d.ts.map +0 -1
  56. package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.js +0 -9
  57. package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.js.map +0 -1
  58. package/.medusa/server/src/jobs/paypal-reconcile.d.ts +0 -7
  59. package/.medusa/server/src/jobs/paypal-reconcile.d.ts.map +0 -1
  60. package/.medusa/server/src/jobs/paypal-reconcile.js +0 -109
  61. package/.medusa/server/src/jobs/paypal-reconcile.js.map +0 -1
  62. package/.medusa/server/src/modules/paypal/utils/crypto.d.ts +0 -4
  63. package/.medusa/server/src/modules/paypal/utils/crypto.d.ts.map +0 -1
  64. package/.medusa/server/src/modules/paypal/utils/crypto.js +0 -47
  65. package/.medusa/server/src/modules/paypal/utils/crypto.js.map +0 -1
  66. package/src/api/admin/paypal/rotate-credentials/route.ts +0 -8
  67. package/src/jobs/paypal-reconcile.ts +0 -113
  68. package/src/modules/paypal/utils/crypto.ts +0 -51
@@ -1,215 +1,377 @@
1
- import type { MedusaContainer } from "@medusajs/framework/types"
2
- import { Modules } from "@medusajs/framework/utils"
3
- import type PayPalModuleService from "./service"
4
- import { isPayPalProviderId } from "./utils/provider-ids"
5
- import { PAYPAL_MODULE } from "./index"
6
-
7
- export const EVENT_STATUS_MAP: Record<string, "authorized" | "captured" | "canceled" | "error"> = {
8
- "CHECKOUT.ORDER.APPROVED": "authorized",
9
- "CHECKOUT.ORDER.CANCELLED": "canceled",
10
- "PAYMENT.CAPTURE.COMPLETED": "captured",
11
- "PAYMENT.CAPTURE.DENIED": "error",
12
- "PAYMENT.CAPTURE.REFUNDED": "canceled",
13
- "PAYMENT.CAPTURE.REVERSED": "canceled",
14
- "PAYMENT.AUTHORIZATION.CREATED": "authorized",
15
- "PAYMENT.AUTHORIZATION.VOIDED": "canceled",
16
- "PAYMENT.AUTHORIZATION.DENIED": "error",
17
- "PAYMENT.REFUND.COMPLETED": "canceled",
18
- "PAYMENT.REFUND.DENIED": "error",
19
- }
20
-
21
- export const REQUIRED_EVENT_PREFIXES = [
22
- "PAYMENT.CAPTURE.",
23
- "CHECKOUT.ORDER.",
24
- "PAYMENT.AUTHORIZATION.",
25
- "PAYMENT.REFUND.",
26
- ]
27
-
28
- export function isAllowedEventType(eventType: string) {
29
- return REQUIRED_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix))
30
- }
31
-
32
- export function normalizeResource(payload: Record<string, any>) {
33
- const resource = payload?.resource
34
- if (!resource) {
35
- return {}
36
- }
37
- if (typeof resource === "string") {
38
- try {
39
- return JSON.parse(resource)
40
- } catch {
41
- return {}
42
- }
43
- }
44
- return resource
45
- }
46
-
47
- export function normalizeEventVersion(payload: Record<string, any>) {
48
- const raw =
49
- payload?.event_version ??
50
- payload?.resource_version ??
51
- payload?.resource?.resource_version ??
52
- payload?.resource?.version ??
53
- null
54
-
55
- if (!raw) {
56
- return null
57
- }
58
-
59
- return String(raw).trim().replace(/^v/i, "")
60
- }
61
-
62
- export function extractIdentifiers(resource: any) {
63
- const relatedIds = resource?.supplementary_data?.related_ids || {}
64
- const orderId =
65
- relatedIds?.order_id ||
66
- resource?.purchase_units?.[0]?.custom_id ||
67
- resource?.custom_id ||
68
- resource?.purchase_units?.[0]?.reference_id
69
- const captureId =
70
- relatedIds?.capture_id ||
71
- resource?.id ||
72
- resource?.purchase_units?.[0]?.payments?.captures?.[0]?.id
73
- const refundId =
74
- relatedIds?.refund_id ||
75
- resource?.id ||
76
- resource?.purchase_units?.[0]?.payments?.refunds?.[0]?.id
77
- const cartId =
78
- resource?.custom_id ||
79
- resource?.purchase_units?.[0]?.custom_id ||
80
- resource?.purchase_units?.[0]?.reference_id
81
-
82
- return { orderId, captureId, refundId, cartId, resource }
83
- }
84
-
85
- function mergeRefunds(existing: any[], incoming: any[]) {
86
- const seen = new Set<string>()
87
- const merged: any[] = []
88
-
89
- for (const refund of [...existing, ...incoming]) {
90
- const id = refund?.id ? String(refund.id) : ""
91
- if (id && seen.has(id)) {
92
- continue
93
- }
94
- if (id) {
95
- seen.add(id)
96
- }
97
- merged.push(refund)
98
- }
99
-
100
- return merged
101
- }
102
-
103
- async function updatePaymentSession(
104
- container: MedusaContainer,
105
- cartId: string,
106
- status: string,
107
- data: Record<string, unknown>
108
- ) {
109
- // FIX 4b: was container.resolve("payment_collection") as any
110
- // and container.resolve("payment_session") as any
111
- // Modules.PAYMENT is the official typed constant that resolves the Medusa payment module.
112
- // It gives access to both payment collections and payment sessions through one service.
113
- const paymentModule = container.resolve(Modules.PAYMENT) as any
114
-
115
- const pc = await paymentModule.retrievePaymentCollectionByCartId?.(cartId).catch(() => null)
116
- ?? await paymentModule.listPaymentCollections({ cart_id: cartId })
117
- .then((r: any[]) => r?.[0] ?? null)
118
- .catch(() => null)
119
-
120
- if (!pc?.id) {
121
- return
122
- }
123
-
124
- const sessions = await paymentModule.listPaymentSessions({ payment_collection_id: pc.id })
125
- const paypalSession = sessions?.find((s: any) => isPayPalProviderId(s.provider_id))
126
- if (!paypalSession) {
127
- return
128
- }
129
-
130
- const existingData = (paypalSession.data || {}) as Record<string, any>
131
- const existingPaypal = (existingData.paypal || {}) as Record<string, any>
132
- const existingRefunds = Array.isArray(existingPaypal.refunds) ? existingPaypal.refunds : []
133
- const incomingRefunds = Array.isArray(data.refunds) ? data.refunds : null
134
- const nextRefunds = incomingRefunds
135
- ? mergeRefunds(existingRefunds, incomingRefunds)
136
- : existingRefunds
137
-
138
- await paymentModule.updatePaymentSession(paypalSession.id, {
139
- status,
140
- data: {
141
- ...existingData,
142
- paypal: {
143
- ...existingPaypal,
144
- ...data,
145
- refunds: nextRefunds,
146
- },
147
- },
148
- })
149
- }
150
-
151
- export function computeNextRetryAt(attemptCount: number) {
152
- const scheduleMinutes = [5, 15, 30, 60, 120]
153
- const delayMinutes = scheduleMinutes[Math.min(attemptCount - 1, scheduleMinutes.length - 1)]
154
- if (!delayMinutes || attemptCount <= 0) {
155
- return null
156
- }
157
- return new Date(Date.now() + delayMinutes * 60 * 1000)
158
- }
159
-
160
- export async function processPayPalWebhookEvent(
161
- container: MedusaContainer,
162
- input: {
163
- eventType: string
164
- payload: Record<string, any>
165
- }
166
- ) {
167
- // FIX 4c: was container.resolve<PayPalModuleService>("paypal_onboarding")
168
- // PAYPAL_MODULE is the exported constant from ./index.ts — value is "paypal_onboarding"
169
- // Using the constant means if the key ever changes, this file updates automatically.
170
- const paypal = container.resolve<PayPalModuleService>(PAYPAL_MODULE)
171
- const resource = normalizeResource(input.payload)
172
- const { orderId, captureId, refundId, cartId } = extractIdentifiers(resource)
173
- const refundReason =
174
- String(resource?.note_to_payer || resource?.reason || resource?.seller_note || "").trim() ||
175
- undefined
176
- const refundReasonCode =
177
- String(resource?.reason_code || resource?.reasonCode || "").trim() || undefined
178
-
179
- const status = EVENT_STATUS_MAP[input.eventType]
180
- if (status && cartId) {
181
- const refundEntry =
182
- refundId || resource?.status
183
- ? [
184
- {
185
- id: refundId,
186
- status: resource?.status,
187
- reason: refundReason,
188
- reason_code: refundReasonCode,
189
- amount: (resource as any)?.amount,
190
- raw: resource,
191
- },
192
- ]
193
- : null
194
-
195
- await updatePaymentSession(container, cartId, status, {
196
- order_id: orderId,
197
- capture_id: captureId,
198
- refund_id: refundId,
199
- refund_status: resource?.status,
200
- refund_reason: refundReason,
201
- refund_reason_code: refundReasonCode,
202
- refunds: refundEntry || undefined,
203
- webhook_event_type: input.eventType,
204
- webhook_resource: resource,
205
- })
206
- }
207
-
208
- return {
209
- orderId,
210
- captureId,
211
- refundId,
212
- cartId,
213
- resource,
214
- }
215
- }
1
+ import type { MedusaContainer } from "@medusajs/framework/types"
2
+ import { Modules } from "@medusajs/framework/utils"
3
+ import { isPayPalProviderId } from "./utils/provider-ids"
4
+
5
+ // ─── Event Medusa status mapping ───────────────────────────────────────────
6
+
7
+ export const EVENT_STATUS_MAP: Record<
8
+ string,
9
+ "authorized" | "captured" | "canceled" | "error"
10
+ > = {
11
+ "CHECKOUT.ORDER.APPROVED": "authorized",
12
+ "CHECKOUT.ORDER.CANCELLED": "canceled",
13
+ "PAYMENT.CAPTURE.COMPLETED": "captured",
14
+ "PAYMENT.CAPTURE.DENIED": "error",
15
+ "PAYMENT.CAPTURE.PENDING": "authorized",
16
+ "PAYMENT.CAPTURE.REFUNDED": "canceled",
17
+ "PAYMENT.CAPTURE.REVERSED": "canceled",
18
+ "PAYMENT.AUTHORIZATION.CREATED": "authorized",
19
+ "PAYMENT.AUTHORIZATION.VOIDED": "canceled",
20
+ "PAYMENT.AUTHORIZATION.DENIED": "error",
21
+ "PAYMENT.AUTHORIZATION.EXPIRED": "canceled",
22
+ "PAYMENT.REFUND.COMPLETED": "canceled",
23
+ "PAYMENT.REFUND.DENIED": "error",
24
+ }
25
+
26
+ // ─── Status transition guard ──────────────────────────────────────────────────
27
+ // Only allow forward/meaningful moves.
28
+ // Prevents a late-arriving webhook from downgrading an already-captured payment.
29
+
30
+ const ALLOWED_TRANSITIONS: Record<string, Set<string>> = {
31
+ pending: new Set(["authorized", "captured", "canceled", "error"]),
32
+ authorized: new Set(["captured", "canceled", "error"]),
33
+ captured: new Set(["canceled"]),
34
+ canceled: new Set([]),
35
+ error: new Set(["authorized", "captured", "canceled"]),
36
+ }
37
+
38
+ export function isTransitionAllowed(from: string, to: string): boolean {
39
+ return ALLOWED_TRANSITIONS[from]?.has(to) ?? false
40
+ }
41
+
42
+ // ─── Event type helpers ───────────────────────────────────────────────────────
43
+
44
+ export const SUPPORTED_EVENT_PREFIXES = [
45
+ "PAYMENT.CAPTURE.",
46
+ "CHECKOUT.ORDER.",
47
+ "PAYMENT.AUTHORIZATION.",
48
+ "PAYMENT.REFUND.",
49
+ ]
50
+
51
+ export function isAllowedEventType(eventType: string): boolean {
52
+ return SUPPORTED_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix))
53
+ }
54
+
55
+ // ─── Error classification ─────────────────────────────────────────────────────
56
+ // Non-retryable: event is permanently unprocessable (wrong cart, missing session).
57
+ // Retryable: transient failure (DB down, network error) — worth trying again.
58
+
59
+ const NON_RETRYABLE_PATTERNS = [
60
+ "payment collection not found",
61
+ "no paypal session",
62
+ "session not found",
63
+ "cart not found",
64
+ "no payment collection",
65
+ ]
66
+
67
+ export function isRetryableError(error: unknown): boolean {
68
+ const message = String(
69
+ error instanceof Error ? error.message : error ?? ""
70
+ ).toLowerCase()
71
+ return !NON_RETRYABLE_PATTERNS.some((p) => message.includes(p))
72
+ }
73
+
74
+ // ─── Retry schedule ───────────────────────────────────────────────────────────
75
+
76
+ const RETRY_SCHEDULE_MINUTES = [2, 10, 30, 60, 120]
77
+ export const MAX_WEBHOOK_ATTEMPTS = RETRY_SCHEDULE_MINUTES.length + 1
78
+
79
+ export function computeNextRetryAt(attemptCount: number): Date | null {
80
+ const idx = attemptCount - 1
81
+ const delayMinutes = RETRY_SCHEDULE_MINUTES[idx]
82
+ if (delayMinutes === undefined || attemptCount <= 0) return null
83
+ return new Date(Date.now() + delayMinutes * 60 * 1000)
84
+ }
85
+
86
+ // ─── Payload normalisation ────────────────────────────────────────────────────
87
+
88
+ export function normalizeResource(payload: Record<string, any>): Record<string, any> {
89
+ const resource = payload?.resource
90
+ if (!resource) return {}
91
+ if (typeof resource === "string") {
92
+ try {
93
+ return JSON.parse(resource)
94
+ } catch {
95
+ return {}
96
+ }
97
+ }
98
+ return resource as Record<string, any>
99
+ }
100
+
101
+ export function normalizeEventVersion(payload: Record<string, any>): string | null {
102
+ const raw =
103
+ payload?.event_version ??
104
+ payload?.resource_version ??
105
+ payload?.resource?.resource_version ??
106
+ payload?.resource?.version ??
107
+ null
108
+ if (!raw) return null
109
+ return String(raw).trim().replace(/^v/i, "")
110
+ }
111
+
112
+ // ─── Identifier extraction ────────────────────────────────────────────────────
113
+
114
+ export interface ExtractedIdentifiers {
115
+ orderId: string | null
116
+ captureId: string | null
117
+ refundId: string | null
118
+ cartId: string | null
119
+ }
120
+
121
+ export function extractIdentifiers(
122
+ resource: Record<string, any>,
123
+ eventType: string
124
+ ): ExtractedIdentifiers {
125
+ const related = resource?.supplementary_data?.related_ids || {}
126
+ const isOrder = eventType.startsWith("CHECKOUT.ORDER.")
127
+ const isCapture = eventType.startsWith("PAYMENT.CAPTURE.")
128
+ const isAuthorization = eventType.startsWith("PAYMENT.AUTHORIZATION.")
129
+ const isRefund = eventType.startsWith("PAYMENT.REFUND.")
130
+
131
+ let orderId: string | null = null
132
+ let captureId: string | null = null
133
+ let refundId: string | null = null
134
+ let cartId: string | null = null
135
+
136
+ if (isOrder) {
137
+ orderId = String(resource?.id || "").trim() || null
138
+ cartId =
139
+ String(
140
+ resource?.purchase_units?.[0]?.custom_id || resource?.custom_id || ""
141
+ ).trim() || null
142
+ captureId =
143
+ String(
144
+ resource?.purchase_units?.[0]?.payments?.captures?.[0]?.id || ""
145
+ ).trim() || null
146
+ } else if (isCapture) {
147
+ captureId = String(resource?.id || "").trim() || null
148
+ orderId = String(related?.order_id || "").trim() || null
149
+ cartId = String(resource?.custom_id || "").trim() || null
150
+ } else if (isAuthorization) {
151
+ orderId = String(related?.order_id || "").trim() || null
152
+ cartId = String(resource?.custom_id || "").trim() || null
153
+ } else if (isRefund) {
154
+ refundId = String(resource?.id || "").trim() || null
155
+ orderId = String(related?.order_id || "").trim() || null
156
+ captureId = String(related?.capture_id || "").trim() || null
157
+ cartId = null
158
+ }
159
+
160
+ return { orderId, captureId, refundId, cartId }
161
+ }
162
+
163
+ // ─── Session lookup ───────────────────────────────────────────────────────────
164
+
165
+ interface ResolvedSession {
166
+ sessionId: string
167
+ sessionData: Record<string, any>
168
+ sessionStatus: string
169
+ collectionId: string
170
+ }
171
+
172
+ async function findPayPalSession(
173
+ container: MedusaContainer,
174
+ cartId: string
175
+ ): Promise<ResolvedSession | null> {
176
+ const paymentModule = container.resolve(Modules.PAYMENT) as any
177
+
178
+ let collections: any[]
179
+ try {
180
+ collections = await paymentModule.listPaymentCollections(
181
+ { cart_id: [cartId] },
182
+ { take: 1 }
183
+ )
184
+ } catch (e: any) {
185
+ throw new Error(`payment collection not found for cart ${cartId}: ${e?.message}`)
186
+ }
187
+
188
+ const collection = collections?.[0]
189
+ if (!collection?.id) {
190
+ throw new Error(`payment collection not found for cart ${cartId}`)
191
+ }
192
+
193
+ const sessions = await paymentModule.listPaymentSessions({
194
+ payment_collection_id: collection.id,
195
+ })
196
+
197
+ const paypalSession = (sessions || [])
198
+ .filter((s: any) => isPayPalProviderId(s.provider_id))
199
+ .sort(
200
+ (a: any, b: any) =>
201
+ new Date(b.created_at || 0).getTime() -
202
+ new Date(a.created_at || 0).getTime()
203
+ )[0]
204
+
205
+ if (!paypalSession) {
206
+ throw new Error(
207
+ `no paypal session found in collection ${collection.id} for cart ${cartId}`
208
+ )
209
+ }
210
+
211
+ return {
212
+ sessionId: paypalSession.id,
213
+ sessionData: (paypalSession.data || {}) as Record<string, any>,
214
+ sessionStatus: String(paypalSession.status || "pending"),
215
+ collectionId: collection.id,
216
+ }
217
+ }
218
+
219
+ // ─── Session update ───────────────────────────────────────────────────────────
220
+
221
+ function mergeRefunds(existing: any[], incoming: any[]): any[] {
222
+ const seen = new Set<string>()
223
+ const merged: any[] = []
224
+ for (const refund of [...existing, ...incoming]) {
225
+ const id = String(refund?.id || "")
226
+ if (id && seen.has(id)) continue
227
+ if (id) seen.add(id)
228
+ merged.push(refund)
229
+ }
230
+ return merged
231
+ }
232
+
233
+ async function applyStatusToSession(
234
+ container: MedusaContainer,
235
+ resolved: ResolvedSession,
236
+ status: string,
237
+ patch: Record<string, unknown>
238
+ ): Promise<void> {
239
+ const paymentModule = container.resolve(Modules.PAYMENT) as any
240
+
241
+ if (!isTransitionAllowed(resolved.sessionStatus, status)) {
242
+ console.info(
243
+ `[PayPal] webhook: skipping disallowed transition ${resolved.sessionStatus} → ${status} for session ${resolved.sessionId}`
244
+ )
245
+ return
246
+ }
247
+
248
+ const existingPaypal = (resolved.sessionData.paypal || {}) as Record<string, any>
249
+ const existingRefunds = Array.isArray(existingPaypal.refunds)
250
+ ? existingPaypal.refunds
251
+ : []
252
+ const incomingRefunds = Array.isArray(patch.refunds)
253
+ ? (patch.refunds as any[])
254
+ : null
255
+ const nextRefunds = incomingRefunds
256
+ ? mergeRefunds(existingRefunds, incomingRefunds)
257
+ : existingRefunds
258
+
259
+ await paymentModule.updatePaymentSession({
260
+ id: resolved.sessionId,
261
+ status,
262
+ data: {
263
+ ...resolved.sessionData,
264
+ paypal: {
265
+ ...existingPaypal,
266
+ ...patch,
267
+ refunds: nextRefunds,
268
+ },
269
+ },
270
+ })
271
+ }
272
+
273
+ // ─── Main event processor ─────────────────────────────────────────────────────
274
+
275
+ export interface ProcessResult {
276
+ orderId: string | null
277
+ captureId: string | null
278
+ refundId: string | null
279
+ cartId: string | null
280
+ sessionUpdated: boolean
281
+ }
282
+
283
+ export async function processPayPalWebhookEvent(
284
+ container: MedusaContainer,
285
+ input: {
286
+ eventType: string
287
+ payload: Record<string, any>
288
+ }
289
+ ): Promise<ProcessResult> {
290
+ const resource = normalizeResource(input.payload)
291
+ const { orderId, captureId, refundId, cartId: rawCartId } = extractIdentifiers(
292
+ resource,
293
+ input.eventType
294
+ )
295
+
296
+ const refundReason =
297
+ String(
298
+ resource?.note_to_payer || resource?.reason || resource?.seller_note || ""
299
+ ).trim() || undefined
300
+ const refundReasonCode =
301
+ String(resource?.reason_code || resource?.reasonCode || "").trim() ||
302
+ undefined
303
+
304
+ const targetStatus = EVENT_STATUS_MAP[input.eventType]
305
+ if (!targetStatus) {
306
+ return { orderId, captureId, refundId, cartId: rawCartId, sessionUpdated: false }
307
+ }
308
+
309
+ let cartId = rawCartId
310
+
311
+ if (!cartId) {
312
+ try {
313
+ const paymentModule = container.resolve(Modules.PAYMENT) as any
314
+ const allSessions = await paymentModule.listPaymentSessions({
315
+ provider_id: ["pp_paypal_paypal", "pp_paypal_card_paypal_card"],
316
+ })
317
+ const matchedSession = (allSessions || []).find((s: any) => {
318
+ const pp = ((s.data || {}) as Record<string, any>).paypal || {}
319
+ if (orderId && pp.order_id === orderId) return true
320
+ if (captureId && pp.capture_id === captureId) return true
321
+ return false
322
+ })
323
+ if (matchedSession?.payment_collection_id) {
324
+ const colls = await paymentModule.listPaymentCollections(
325
+ { id: [matchedSession.payment_collection_id] },
326
+ { take: 1 }
327
+ )
328
+ cartId = String(colls?.[0]?.cart_id || "").trim() || null
329
+ }
330
+ } catch (e: any) {
331
+ console.warn(
332
+ `[PayPal] webhook: cartId fallback lookup failed for ${input.eventType}:`,
333
+ e?.message
334
+ )
335
+ }
336
+ }
337
+
338
+ let sessionUpdated = false
339
+
340
+ if (cartId) {
341
+ const resolved = await findPayPalSession(container, cartId)
342
+ if (resolved) {
343
+ const refundEntry = refundId
344
+ ? [
345
+ {
346
+ id: refundId,
347
+ status: resource?.status,
348
+ reason: refundReason,
349
+ reason_code: refundReasonCode,
350
+ amount: resource?.amount,
351
+ raw: resource,
352
+ },
353
+ ]
354
+ : null
355
+
356
+ await applyStatusToSession(container, resolved, targetStatus, {
357
+ order_id: orderId,
358
+ capture_id: captureId ?? resolved.sessionData.paypal?.capture_id ?? undefined,
359
+ refund_id: refundId,
360
+ refund_status: refundId ? resource?.status : undefined,
361
+ refund_reason: refundReason,
362
+ refund_reason_code: refundReasonCode,
363
+ ...(refundEntry ? { refunds: refundEntry } : {}),
364
+ webhook_event_type: input.eventType,
365
+ last_webhook_at: new Date().toISOString(),
366
+ })
367
+ sessionUpdated = true
368
+ }
369
+ } else {
370
+ console.warn(
371
+ `[PayPal] webhook: could not resolve cartId for event ${input.eventType}`,
372
+ { orderId, captureId, refundId }
373
+ )
374
+ }
375
+
376
+ return { orderId, captureId, refundId, cartId, sessionUpdated }
377
+ }
@@ -1,3 +0,0 @@
1
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
- export declare function POST(req: MedusaRequest, res: MedusaResponse): Promise<MedusaResponse>;
3
- //# sourceMappingURL=route.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/api/admin/paypal/rotate-credentials/route.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAG7E,wBAAsB,IAAI,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,2BAIjE"}
@@ -1,9 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.POST = POST;
4
- async function POST(req, res) {
5
- const paypal = req.scope.resolve("paypal_onboarding");
6
- const result = await paypal.rotateCredentialEncryptionKey();
7
- return res.json(result);
8
- }
9
- //# sourceMappingURL=route.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../../../../../src/api/admin/paypal/rotate-credentials/route.ts"],"names":[],"mappings":";;AAGA,oBAIC;AAJM,KAAK,UAAU,IAAI,CAAC,GAAkB,EAAE,GAAmB;IAChE,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAsB,mBAAmB,CAAC,CAAA;IAC1E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,6BAA6B,EAAE,CAAA;IAC3D,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AACzB,CAAC"}
@@ -1,7 +0,0 @@
1
- import type { MedusaContainer } from "@medusajs/framework/types";
2
- export default function paypalReconcile(container: MedusaContainer): Promise<void>;
3
- export declare const config: {
4
- name: string;
5
- schedule: string;
6
- };
7
- //# sourceMappingURL=paypal-reconcile.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"paypal-reconcile.d.ts","sourceRoot":"","sources":["../../../../src/jobs/paypal-reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAchE,wBAA8B,eAAe,CAAC,SAAS,EAAE,eAAe,iBA6FvE;AAED,eAAO,MAAM,MAAM;;;CAGlB,CAAA"}