@easypayment/medusa-paypal 0.4.7 → 0.4.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.
- package/.medusa/server/src/admin/index.js +7 -7
- package/.medusa/server/src/admin/index.mjs +7 -7
- package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/create-order/route.js +62 -139
- package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
- package/.medusa/server/src/modules/paypal/migrations/20260115120000_create_paypal_connection.js +22 -22
- package/.medusa/server/src/modules/paypal/migrations/20260123090000_create_paypal_settings.js +11 -11
- package/.medusa/server/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.js +18 -18
- package/.medusa/server/src/modules/paypal/migrations/20260401090000_create_paypal_metric.js +16 -16
- package/.medusa/server/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.js +20 -20
- package/.medusa/server/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.js +14 -14
- package/.medusa/server/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.js +15 -15
- package/README.md +142 -142
- package/package.json +75 -75
- package/src/admin/index.ts +7 -7
- package/src/admin/routes/settings/paypal/_components/Tabs.tsx +52 -52
- package/src/admin/routes/settings/paypal/_components/Toast.tsx +51 -51
- package/src/admin/routes/settings/paypal/additional-settings/page.tsx +200 -200
- package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +183 -183
- package/src/admin/routes/settings/paypal/apple-pay/page.tsx +5 -5
- package/src/admin/routes/settings/paypal/connection/page.tsx +754 -754
- package/src/admin/routes/settings/paypal/google-pay/page.tsx +5 -5
- package/src/admin/routes/settings/paypal/pay-later-messaging/page.tsx +5 -5
- package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +376 -376
- package/src/api/admin/payment-collections/[id]/payment-sessions/route.ts +24 -24
- package/src/api/admin/paypal/disconnect/route.ts +8 -8
- package/src/api/admin/paypal/environment/route.ts +25 -25
- package/src/api/admin/paypal/onboard-complete/route.ts +44 -44
- package/src/api/admin/paypal/onboarding-link/route.ts +45 -45
- package/src/api/admin/paypal/onboarding-status/route.ts +18 -18
- package/src/api/admin/paypal/rotate-credentials/route.ts +8 -8
- package/src/api/admin/paypal/save-credentials/route.ts +14 -14
- package/src/api/admin/paypal/settings/route.ts +14 -14
- package/src/api/admin/paypal/status/route.ts +12 -12
- package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +65 -65
- package/src/api/store/paypal/capture-order/route.ts +276 -276
- package/src/api/store/paypal/config/route.ts +102 -102
- package/src/api/store/paypal/create-order/route.ts +77 -176
- package/src/api/store/paypal/settings/route.ts +19 -19
- package/src/api/store/paypal/webhook/route.ts +246 -246
- package/src/api/store/paypal-complete/route.ts +75 -75
- package/src/jobs/paypal-reconcile.ts +112 -112
- package/src/jobs/paypal-webhook-retry.ts +85 -85
- package/src/modules/paypal/clients/paypal-seller.client.ts +59 -59
- package/src/modules/paypal/index.ts +8 -8
- package/src/modules/paypal/migrations/20260115120000_create_paypal_connection.ts +33 -33
- package/src/modules/paypal/migrations/20260123090000_create_paypal_settings.ts +22 -22
- package/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.ts +29 -29
- package/src/modules/paypal/migrations/20260401090000_create_paypal_metric.ts +27 -27
- package/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.ts +31 -31
- package/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.ts +25 -25
- package/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.ts +26 -26
- package/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.ts +11 -11
- package/src/modules/paypal/models/paypal_connection.ts +21 -21
- package/src/modules/paypal/models/paypal_metric.ts +9 -9
- package/src/modules/paypal/models/paypal_settings.ts +8 -8
- package/src/modules/paypal/models/paypal_webhook_event.ts +19 -19
- package/src/modules/paypal/payment-provider/README.md +22 -22
- package/src/modules/paypal/payment-provider/card-service.ts +760 -760
- package/src/modules/paypal/payment-provider/index.ts +19 -19
- package/src/modules/paypal/payment-provider/service.ts +1121 -1121
- package/src/modules/paypal/payment-provider/webhook-utils.ts +88 -88
- package/src/modules/paypal/service.ts +1247 -1247
- package/src/modules/paypal/types/config.ts +47 -47
- package/src/modules/paypal/utils/amounts.ts +41 -41
- package/src/modules/paypal/utils/crypto.ts +51 -51
- package/src/modules/paypal/utils/currencies.ts +84 -84
- package/src/modules/paypal/utils/paypal-auth.ts +32 -32
- package/src/modules/paypal/utils/provider-ids.ts +15 -15
- package/src/modules/paypal/webhook-processor.ts +215 -215
|
@@ -1,215 +1,215 @@
|
|
|
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 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
|
+
}
|