@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.
- package/.medusa/server/src/admin/index.js +12 -15
- package/.medusa/server/src/admin/index.mjs +12 -15
- package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/capture-order/route.js +1 -11
- package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
- 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 +0 -9
- package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/webhook/route.js +162 -115
- package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
- package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal-complete/route.js +0 -6
- package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
- package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -1
- package/.medusa/server/src/jobs/paypal-webhook-retry.js +97 -43
- package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
- package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.d.ts +6 -0
- package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.js +20 -0
- package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.js.map +1 -0
- package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/service.js +0 -42
- package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/service.d.ts +0 -8
- package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/service.js +6 -114
- package/.medusa/server/src/modules/paypal/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/types/config.d.ts +0 -2
- package/.medusa/server/src/modules/paypal/types/config.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/types/config.js +0 -9
- package/.medusa/server/src/modules/paypal/types/config.js.map +1 -1
- package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +21 -17
- package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/webhook-processor.js +195 -99
- package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
- package/README.md +156 -152
- package/package.json +1 -1
- package/src/admin/routes/settings/paypal/_components/Tabs.tsx +48 -52
- package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +0 -23
- package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +56 -65
- package/src/api/store/paypal/capture-order/route.ts +266 -276
- package/src/api/store/paypal/create-order/route.ts +0 -9
- package/src/api/store/paypal/webhook/route.ts +325 -246
- package/src/api/store/paypal-complete/route.ts +69 -75
- package/src/jobs/paypal-webhook-retry.ts +149 -85
- package/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.ts +17 -0
- package/src/modules/paypal/payment-provider/service.ts +1079 -1121
- package/src/modules/paypal/service.ts +6 -127
- package/src/modules/paypal/types/config.ts +33 -47
- package/src/modules/paypal/webhook-processor.ts +377 -215
- package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.d.ts +0 -3
- package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.d.ts.map +0 -1
- package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.js +0 -9
- package/.medusa/server/src/api/admin/paypal/rotate-credentials/route.js.map +0 -1
- package/.medusa/server/src/jobs/paypal-reconcile.d.ts +0 -7
- package/.medusa/server/src/jobs/paypal-reconcile.d.ts.map +0 -1
- package/.medusa/server/src/jobs/paypal-reconcile.js +0 -109
- package/.medusa/server/src/jobs/paypal-reconcile.js.map +0 -1
- package/.medusa/server/src/modules/paypal/utils/crypto.d.ts +0 -4
- package/.medusa/server/src/modules/paypal/utils/crypto.d.ts.map +0 -1
- package/.medusa/server/src/modules/paypal/utils/crypto.js +0 -47
- package/.medusa/server/src/modules/paypal/utils/crypto.js.map +0 -1
- package/src/api/admin/paypal/rotate-credentials/route.ts +0 -8
- package/src/jobs/paypal-reconcile.ts +0 -113
- package/src/modules/paypal/utils/crypto.ts +0 -51
|
@@ -1,246 +1,325 @@
|
|
|
1
|
-
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
-
import type PayPalModuleService from "../../../../modules/paypal/service"
|
|
3
|
-
import {
|
|
4
|
-
computeNextRetryAt,
|
|
5
|
-
isAllowedEventType,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
await paypal.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
1
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import type PayPalModuleService from "../../../../modules/paypal/service"
|
|
3
|
+
import {
|
|
4
|
+
computeNextRetryAt,
|
|
5
|
+
isAllowedEventType,
|
|
6
|
+
isRetryableError,
|
|
7
|
+
normalizeEventVersion,
|
|
8
|
+
processPayPalWebhookEvent,
|
|
9
|
+
} from "../../../../modules/paypal/webhook-processor"
|
|
10
|
+
|
|
11
|
+
const REPLAY_WINDOW_MINUTES = (() => {
|
|
12
|
+
const v = Number(process.env.PAYPAL_WEBHOOK_REPLAY_WINDOW_MINUTES)
|
|
13
|
+
return Number.isFinite(v) && v > 0 ? v : 60
|
|
14
|
+
})()
|
|
15
|
+
|
|
16
|
+
function getHeader(
|
|
17
|
+
headers: Record<string, string | string[] | undefined>,
|
|
18
|
+
name: string
|
|
19
|
+
): string | undefined {
|
|
20
|
+
const direct = headers[name]
|
|
21
|
+
if (Array.isArray(direct)) return direct[0]
|
|
22
|
+
if (typeof direct === "string") return direct
|
|
23
|
+
const lower = name.toLowerCase()
|
|
24
|
+
const key = Object.keys(headers).find((h) => h.toLowerCase() === lower)
|
|
25
|
+
if (!key) return undefined
|
|
26
|
+
const val = headers[key]
|
|
27
|
+
return Array.isArray(val) ? val[0] : val
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ValidationFail {
|
|
31
|
+
ok: false
|
|
32
|
+
status: number
|
|
33
|
+
message: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ValidationPass {
|
|
37
|
+
ok: true
|
|
38
|
+
eventId: string
|
|
39
|
+
eventType: string
|
|
40
|
+
transmissionId: string | null
|
|
41
|
+
transmissionTime: Date | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateRequest(req: MedusaRequest): ValidationFail | ValidationPass {
|
|
45
|
+
const payload = (req.body || {}) as Record<string, any>
|
|
46
|
+
const eventId = String(payload?.id || payload?.event_id || "").trim()
|
|
47
|
+
const eventType = String(payload?.event_type || payload?.eventType || "").trim()
|
|
48
|
+
|
|
49
|
+
if (!eventId || !eventType) {
|
|
50
|
+
return { ok: false, status: 400, message: "Missing required fields: id and event_type" }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const transmissionTimeHeader = getHeader(req.headers, "paypal-transmission-time")
|
|
54
|
+
if (!transmissionTimeHeader) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
status: 400,
|
|
58
|
+
message: "Missing required header: paypal-transmission-time",
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const transmissionMs = Date.parse(transmissionTimeHeader)
|
|
63
|
+
if (!Number.isFinite(transmissionMs)) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
status: 400,
|
|
67
|
+
message: "Invalid paypal-transmission-time header value",
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ageMs = Math.abs(Date.now() - transmissionMs)
|
|
72
|
+
if (ageMs > REPLAY_WINDOW_MINUTES * 60 * 1000) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
status: 400,
|
|
76
|
+
message: `Webhook rejected: outside ${REPLAY_WINDOW_MINUTES}-minute replay window`,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
eventId,
|
|
83
|
+
eventType,
|
|
84
|
+
transmissionId: getHeader(req.headers, "paypal-transmission-id") || null,
|
|
85
|
+
transmissionTime: new Date(transmissionMs),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveWebhookId(
|
|
90
|
+
environment: string,
|
|
91
|
+
settings: Record<string, unknown>
|
|
92
|
+
): string | undefined {
|
|
93
|
+
const ids = (settings?.webhook_ids || {}) as Record<string, string | undefined>
|
|
94
|
+
if (environment === "live") {
|
|
95
|
+
return (
|
|
96
|
+
ids.live ||
|
|
97
|
+
(settings?.webhook_id_live as string) ||
|
|
98
|
+
process.env.PAYPAL_WEBHOOK_ID_LIVE
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
return (
|
|
102
|
+
ids.sandbox ||
|
|
103
|
+
(settings?.webhook_id_sandbox as string) ||
|
|
104
|
+
process.env.PAYPAL_WEBHOOK_ID_SANDBOX
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function verifyWebhookSignature(
|
|
109
|
+
paypal: PayPalModuleService,
|
|
110
|
+
environment: string,
|
|
111
|
+
body: Record<string, unknown>,
|
|
112
|
+
headers: Record<string, string | string[] | undefined>
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const settings = await paypal.getSettings().catch(() => ({ data: {} }))
|
|
115
|
+
const webhookId = resolveWebhookId(
|
|
116
|
+
environment,
|
|
117
|
+
(settings?.data as Record<string, unknown>) || {}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if (!webhookId) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`PayPal webhook ID not configured for environment "${environment}". Set PAYPAL_WEBHOOK_ID_${environment.toUpperCase()} or configure it in admin settings.`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const base =
|
|
127
|
+
environment === "live"
|
|
128
|
+
? "https://api-m.paypal.com"
|
|
129
|
+
: "https://api-m.sandbox.paypal.com"
|
|
130
|
+
|
|
131
|
+
const accessToken = await paypal.getAppAccessToken()
|
|
132
|
+
|
|
133
|
+
const verifyPayload = {
|
|
134
|
+
auth_algo: getHeader(headers, "paypal-auth-algo"),
|
|
135
|
+
cert_url: getHeader(headers, "paypal-cert-url"),
|
|
136
|
+
transmission_id: getHeader(headers, "paypal-transmission-id"),
|
|
137
|
+
transmission_sig: getHeader(headers, "paypal-transmission-sig"),
|
|
138
|
+
transmission_time: getHeader(headers, "paypal-transmission-time"),
|
|
139
|
+
webhook_id: webhookId,
|
|
140
|
+
webhook_event: body,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const missing = Object.entries(verifyPayload)
|
|
144
|
+
.filter(([k, v]) => k !== "webhook_id" && k !== "webhook_event" && !v)
|
|
145
|
+
.map(([k]) => k)
|
|
146
|
+
|
|
147
|
+
if (missing.length > 0) {
|
|
148
|
+
throw new Error(`Missing required PayPal webhook headers: ${missing.join(", ")}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const resp = await fetch(`${base}/v1/notifications/verify-webhook-signature`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${accessToken}`,
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify(verifyPayload),
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const json = await resp.json().catch(() => ({}))
|
|
161
|
+
const debugId = resp.headers.get("paypal-debug-id") || json?.debug_id
|
|
162
|
+
|
|
163
|
+
if (!resp.ok) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`PayPal signature verification API error (${resp.status}): ${JSON.stringify(json)}` +
|
|
166
|
+
(debugId ? ` debug_id=${debugId}` : "")
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
if (json?.verification_status !== "VERIFIED") {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`PayPal webhook signature not verified. Status: ${json?.verification_status}` +
|
|
172
|
+
(debugId ? ` debug_id=${debugId}` : "")
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
178
|
+
const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
|
|
179
|
+
|
|
180
|
+
const validation = validateRequest(req)
|
|
181
|
+
if (!validation.ok) {
|
|
182
|
+
console.warn("[PayPal] webhook: validation failed:", validation.message)
|
|
183
|
+
return res.status(validation.status).json({ message: validation.message })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { eventId, eventType, transmissionId, transmissionTime } = validation
|
|
187
|
+
const payload = (req.body || {}) as Record<string, any>
|
|
188
|
+
|
|
189
|
+
if (transmissionId) {
|
|
190
|
+
try {
|
|
191
|
+
const existing = await paypal.listPayPalWebhookEvents({ transmission_id: transmissionId })
|
|
192
|
+
if ((existing || []).length > 0) {
|
|
193
|
+
console.info("[PayPal] webhook: duplicate transmission_id", {
|
|
194
|
+
transmissionId,
|
|
195
|
+
eventId,
|
|
196
|
+
})
|
|
197
|
+
return res.json({ ok: true, duplicate: true })
|
|
198
|
+
}
|
|
199
|
+
} catch (e: any) {
|
|
200
|
+
console.warn("[PayPal] webhook: transmission_id dedup check failed:", e?.message)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const creds = await paypal.getActiveCredentials()
|
|
206
|
+
await verifyWebhookSignature(paypal, creds.environment, payload, req.headers)
|
|
207
|
+
} catch (e: any) {
|
|
208
|
+
console.error("[PayPal] webhook: signature verification failed:", e?.message)
|
|
209
|
+
return res
|
|
210
|
+
.status(401)
|
|
211
|
+
.json({ message: e?.message || "Webhook signature verification failed" })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const eventVersion = normalizeEventVersion(payload)
|
|
215
|
+
let recordId: string | null = null
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const recordResult = await paypal.createWebhookEventRecord({
|
|
219
|
+
event_id: eventId,
|
|
220
|
+
event_type: eventType,
|
|
221
|
+
payload,
|
|
222
|
+
event_version: eventVersion,
|
|
223
|
+
transmission_id: transmissionId,
|
|
224
|
+
transmission_time: transmissionTime,
|
|
225
|
+
status: "processing",
|
|
226
|
+
attempt_count: 1,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
if (!recordResult.created) {
|
|
230
|
+
console.info("[PayPal] webhook: duplicate event_id", { eventId, eventType })
|
|
231
|
+
return res.json({ ok: true, duplicate: true })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
recordId = recordResult.event?.id ?? null
|
|
235
|
+
} catch (e: any) {
|
|
236
|
+
console.error("[PayPal] webhook: failed to create DB record:", e?.message)
|
|
237
|
+
return res.status(500).json({ message: "Failed to record webhook event" })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!isAllowedEventType(eventType)) {
|
|
241
|
+
console.info("[PayPal] webhook: unsupported event type, ignoring", { eventType })
|
|
242
|
+
await paypal.recordAuditEvent("webhook_unsupported_event", {
|
|
243
|
+
event_id: eventId,
|
|
244
|
+
event_type: eventType,
|
|
245
|
+
})
|
|
246
|
+
if (recordId) {
|
|
247
|
+
await paypal
|
|
248
|
+
.updateWebhookEventRecord({
|
|
249
|
+
id: recordId,
|
|
250
|
+
status: "ignored",
|
|
251
|
+
processed_at: new Date(),
|
|
252
|
+
})
|
|
253
|
+
.catch(() => {})
|
|
254
|
+
}
|
|
255
|
+
return res.json({ ok: true, ignored: true })
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const processed = await processPayPalWebhookEvent(req.scope, { eventType, payload })
|
|
260
|
+
|
|
261
|
+
if (recordId) {
|
|
262
|
+
await paypal
|
|
263
|
+
.updateWebhookEventRecord({
|
|
264
|
+
id: recordId,
|
|
265
|
+
status: "processed",
|
|
266
|
+
processed_at: new Date(),
|
|
267
|
+
resource_id:
|
|
268
|
+
processed.refundId || processed.captureId || processed.orderId || null,
|
|
269
|
+
})
|
|
270
|
+
.catch(() => {})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.info("[PayPal] webhook: processed", {
|
|
274
|
+
event_id: eventId,
|
|
275
|
+
event_type: eventType,
|
|
276
|
+
order_id: processed.orderId,
|
|
277
|
+
capture_id: processed.captureId,
|
|
278
|
+
refund_id: processed.refundId,
|
|
279
|
+
cart_id: processed.cartId,
|
|
280
|
+
session_updated: processed.sessionUpdated,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
await paypal.recordMetric("webhook_success").catch(() => {})
|
|
284
|
+
return res.json({ ok: true })
|
|
285
|
+
} catch (e: any) {
|
|
286
|
+
console.error("[PayPal] webhook: processing failed", {
|
|
287
|
+
event_id: eventId,
|
|
288
|
+
event_type: eventType,
|
|
289
|
+
error: e?.message,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const retryable = isRetryableError(e)
|
|
293
|
+
const nextStatus = retryable ? "failed" : "dead_letter"
|
|
294
|
+
|
|
295
|
+
if (recordId) {
|
|
296
|
+
await paypal
|
|
297
|
+
.updateWebhookEventRecord({
|
|
298
|
+
id: recordId,
|
|
299
|
+
status: nextStatus,
|
|
300
|
+
attempt_count: 1,
|
|
301
|
+
next_retry_at: retryable ? computeNextRetryAt(1) : null,
|
|
302
|
+
last_error: e?.message || String(e),
|
|
303
|
+
})
|
|
304
|
+
.catch(() => {})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
await paypal
|
|
308
|
+
.recordAuditEvent("webhook_processing_failed", {
|
|
309
|
+
event_id: eventId,
|
|
310
|
+
event_type: eventType,
|
|
311
|
+
retryable,
|
|
312
|
+
message: e?.message || String(e),
|
|
313
|
+
})
|
|
314
|
+
.catch(() => {})
|
|
315
|
+
|
|
316
|
+
await paypal.recordMetric("webhook_failed").catch(() => {})
|
|
317
|
+
|
|
318
|
+
if (!retryable) {
|
|
319
|
+
return res.status(200).json({ ok: false, message: e?.message })
|
|
320
|
+
}
|
|
321
|
+
return res
|
|
322
|
+
.status(500)
|
|
323
|
+
.json({ message: e?.message || "PayPal webhook processing error" })
|
|
324
|
+
}
|
|
325
|
+
}
|