@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,113 +1,113 @@
|
|
|
1
|
-
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
-
import { Modules } from "@medusajs/framework/utils"
|
|
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"
|
|
6
|
-
|
|
7
|
-
const STATUS_MAP: Record<string, "pending" | "authorized" | "captured" | "canceled"> = {
|
|
8
|
-
CREATED: "pending",
|
|
9
|
-
APPROVED: "authorized",
|
|
10
|
-
COMPLETED: "captured",
|
|
11
|
-
VOIDED: "canceled",
|
|
12
|
-
CANCELLED: "canceled",
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export default async function paypalReconcile(container: MedusaContainer) {
|
|
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
|
|
20
|
-
|
|
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],
|
|
30
|
-
status: ["pending", "authorized"],
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for (const session of sessions || []) {
|
|
35
|
-
const data = (session?.data || {}) as Record<string, any>
|
|
36
|
-
const paypalData = (data.paypal || {}) as Record<string, any>
|
|
37
|
-
const orderId = String(paypalData.order_id || data.order_id || "")
|
|
38
|
-
if (!orderId) {
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const order = await paypal.getOrderDetails(orderId)
|
|
44
|
-
const status = STATUS_MAP[String(order?.status || "").toUpperCase()]
|
|
45
|
-
if (!status) {
|
|
46
|
-
continue
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const previousStatus = session?.status
|
|
50
|
-
if (previousStatus && previousStatus !== status) {
|
|
51
|
-
await paypal.recordPaymentLog("reconcile_drift", {
|
|
52
|
-
session_id: session.id,
|
|
53
|
-
order_id: orderId,
|
|
54
|
-
previous_status: previousStatus,
|
|
55
|
-
next_status: status,
|
|
56
|
-
})
|
|
57
|
-
await paypal.recordMetric("reconcile_drift")
|
|
58
|
-
await paypal.sendAlert({
|
|
59
|
-
type: "reconcile_drift",
|
|
60
|
-
message: `PayPal reconciliation drift detected for order ${orderId}.`,
|
|
61
|
-
metadata: {
|
|
62
|
-
session_id: session.id,
|
|
63
|
-
previous_status: previousStatus,
|
|
64
|
-
next_status: status,
|
|
65
|
-
},
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// FIX 3d: was paymentSessionService.update(session.id, { ... })
|
|
70
|
-
// updatePaymentSession is the correct method name on the payment module service.
|
|
71
|
-
await paymentModule.updatePaymentSession(session.id, {
|
|
72
|
-
status,
|
|
73
|
-
amount: session.amount,
|
|
74
|
-
data: {
|
|
75
|
-
...(session.data || {}),
|
|
76
|
-
paypal: {
|
|
77
|
-
...((session.data || {}).paypal || {}),
|
|
78
|
-
order,
|
|
79
|
-
reconciliation: {
|
|
80
|
-
last_reconciled_at: new Date().toISOString(),
|
|
81
|
-
last_status: status,
|
|
82
|
-
order_status: order?.status,
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
})
|
|
87
|
-
try {
|
|
88
|
-
await paypal.recordMetric("reconcile_success")
|
|
89
|
-
} catch {
|
|
90
|
-
// ignore metrics failures
|
|
91
|
-
}
|
|
92
|
-
} catch (error) {
|
|
93
|
-
try {
|
|
94
|
-
await paypal.recordMetric("reconcile_failed")
|
|
95
|
-
} catch {
|
|
96
|
-
// ignore metrics failures
|
|
97
|
-
}
|
|
98
|
-
await paypal.sendAlert({
|
|
99
|
-
type: "reconcile_failed",
|
|
100
|
-
message: `PayPal reconciliation failed for order ${orderId}.`,
|
|
101
|
-
metadata: {
|
|
102
|
-
session_id: session.id,
|
|
103
|
-
},
|
|
104
|
-
})
|
|
105
|
-
console.error("[PayPal] reconcile error", error)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export const config = {
|
|
111
|
-
name: "paypal-reconcile",
|
|
112
|
-
schedule: "*/15 * * * *",
|
|
1
|
+
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
+
import { Modules } from "@medusajs/framework/utils"
|
|
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"
|
|
6
|
+
|
|
7
|
+
const STATUS_MAP: Record<string, "pending" | "authorized" | "captured" | "canceled"> = {
|
|
8
|
+
CREATED: "pending",
|
|
9
|
+
APPROVED: "authorized",
|
|
10
|
+
COMPLETED: "captured",
|
|
11
|
+
VOIDED: "canceled",
|
|
12
|
+
CANCELLED: "canceled",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function paypalReconcile(container: MedusaContainer) {
|
|
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
|
|
20
|
+
|
|
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],
|
|
30
|
+
status: ["pending", "authorized"],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
for (const session of sessions || []) {
|
|
35
|
+
const data = (session?.data || {}) as Record<string, any>
|
|
36
|
+
const paypalData = (data.paypal || {}) as Record<string, any>
|
|
37
|
+
const orderId = String(paypalData.order_id || data.order_id || "")
|
|
38
|
+
if (!orderId) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const order = await paypal.getOrderDetails(orderId)
|
|
44
|
+
const status = STATUS_MAP[String(order?.status || "").toUpperCase()]
|
|
45
|
+
if (!status) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const previousStatus = session?.status
|
|
50
|
+
if (previousStatus && previousStatus !== status) {
|
|
51
|
+
await paypal.recordPaymentLog("reconcile_drift", {
|
|
52
|
+
session_id: session.id,
|
|
53
|
+
order_id: orderId,
|
|
54
|
+
previous_status: previousStatus,
|
|
55
|
+
next_status: status,
|
|
56
|
+
})
|
|
57
|
+
await paypal.recordMetric("reconcile_drift")
|
|
58
|
+
await paypal.sendAlert({
|
|
59
|
+
type: "reconcile_drift",
|
|
60
|
+
message: `PayPal reconciliation drift detected for order ${orderId}.`,
|
|
61
|
+
metadata: {
|
|
62
|
+
session_id: session.id,
|
|
63
|
+
previous_status: previousStatus,
|
|
64
|
+
next_status: status,
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// FIX 3d: was paymentSessionService.update(session.id, { ... })
|
|
70
|
+
// updatePaymentSession is the correct method name on the payment module service.
|
|
71
|
+
await paymentModule.updatePaymentSession(session.id, {
|
|
72
|
+
status,
|
|
73
|
+
amount: session.amount,
|
|
74
|
+
data: {
|
|
75
|
+
...(session.data || {}),
|
|
76
|
+
paypal: {
|
|
77
|
+
...((session.data || {}).paypal || {}),
|
|
78
|
+
order,
|
|
79
|
+
reconciliation: {
|
|
80
|
+
last_reconciled_at: new Date().toISOString(),
|
|
81
|
+
last_status: status,
|
|
82
|
+
order_status: order?.status,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
try {
|
|
88
|
+
await paypal.recordMetric("reconcile_success")
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore metrics failures
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
try {
|
|
94
|
+
await paypal.recordMetric("reconcile_failed")
|
|
95
|
+
} catch {
|
|
96
|
+
// ignore metrics failures
|
|
97
|
+
}
|
|
98
|
+
await paypal.sendAlert({
|
|
99
|
+
type: "reconcile_failed",
|
|
100
|
+
message: `PayPal reconciliation failed for order ${orderId}.`,
|
|
101
|
+
metadata: {
|
|
102
|
+
session_id: session.id,
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
console.error("[PayPal] reconcile error", error)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const config = {
|
|
111
|
+
name: "paypal-reconcile",
|
|
112
|
+
schedule: "*/15 * * * *",
|
|
113
113
|
}
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
-
import type PayPalModuleService from "../modules/paypal/service"
|
|
3
|
-
import {
|
|
4
|
-
computeNextRetryAt,
|
|
5
|
-
isAllowedEventType,
|
|
6
|
-
processPayPalWebhookEvent,
|
|
7
|
-
} from "../modules/paypal/webhook-processor"
|
|
8
|
-
|
|
9
|
-
const MAX_WEBHOOK_ATTEMPTS = 5
|
|
10
|
-
|
|
11
|
-
export default async function paypalWebhookRetry(container: MedusaContainer) {
|
|
12
|
-
const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
|
|
13
|
-
const now = Date.now()
|
|
14
|
-
const candidates = await paypal.listPayPalWebhookEvents({ status: "failed" })
|
|
15
|
-
|
|
16
|
-
for (const event of candidates || []) {
|
|
17
|
-
const nextRetryAt = event?.next_retry_at ? new Date(event.next_retry_at).getTime() : null
|
|
18
|
-
if (!nextRetryAt || nextRetryAt > now) {
|
|
19
|
-
continue
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const attemptCount = Number(event.attempt_count || 0) + 1
|
|
23
|
-
if (attemptCount > MAX_WEBHOOK_ATTEMPTS) {
|
|
24
|
-
continue
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
await paypal.updateWebhookEventRecord({
|
|
28
|
-
id: event.id,
|
|
29
|
-
status: "processing",
|
|
30
|
-
attempt_count: attemptCount,
|
|
31
|
-
next_retry_at: null,
|
|
32
|
-
last_error: null,
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const eventType = String(event.event_type || "")
|
|
37
|
-
if (!isAllowedEventType(eventType)) {
|
|
38
|
-
await paypal.updateWebhookEventRecord({
|
|
39
|
-
id: event.id,
|
|
40
|
-
status: "ignored",
|
|
41
|
-
processed_at: new Date(),
|
|
42
|
-
})
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const payload = (event.payload || {}) as Record<string, any>
|
|
47
|
-
const processed = await processPayPalWebhookEvent(container, {
|
|
48
|
-
eventType,
|
|
49
|
-
payload,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
await paypal.updateWebhookEventRecord({
|
|
53
|
-
id: event.id,
|
|
54
|
-
status: "processed",
|
|
55
|
-
processed_at: new Date(),
|
|
56
|
-
resource_id: processed.refundId || processed.captureId || processed.orderId || null,
|
|
57
|
-
})
|
|
58
|
-
try {
|
|
59
|
-
await paypal.recordMetric("webhook_retry_success")
|
|
60
|
-
} catch {
|
|
61
|
-
// ignore metrics failures
|
|
62
|
-
}
|
|
63
|
-
} catch (error: any) {
|
|
64
|
-
const nextRetry = attemptCount >= MAX_WEBHOOK_ATTEMPTS ? null : computeNextRetryAt(attemptCount)
|
|
65
|
-
await paypal.updateWebhookEventRecord({
|
|
66
|
-
id: event.id,
|
|
67
|
-
status: "failed",
|
|
68
|
-
attempt_count: attemptCount,
|
|
69
|
-
next_retry_at: nextRetry,
|
|
70
|
-
last_error: error?.message || String(error),
|
|
71
|
-
})
|
|
72
|
-
try {
|
|
73
|
-
await paypal.recordMetric("webhook_retry_failed")
|
|
74
|
-
} catch {
|
|
75
|
-
// ignore metrics failures
|
|
76
|
-
}
|
|
77
|
-
console.error("[PayPal] webhook retry error", error)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const config = {
|
|
83
|
-
name: "paypal-webhook-retry",
|
|
84
|
-
schedule: "*/10 * * * *",
|
|
85
|
-
}
|
|
1
|
+
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
+
import type PayPalModuleService from "../modules/paypal/service"
|
|
3
|
+
import {
|
|
4
|
+
computeNextRetryAt,
|
|
5
|
+
isAllowedEventType,
|
|
6
|
+
processPayPalWebhookEvent,
|
|
7
|
+
} from "../modules/paypal/webhook-processor"
|
|
8
|
+
|
|
9
|
+
const MAX_WEBHOOK_ATTEMPTS = 5
|
|
10
|
+
|
|
11
|
+
export default async function paypalWebhookRetry(container: MedusaContainer) {
|
|
12
|
+
const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
|
|
13
|
+
const now = Date.now()
|
|
14
|
+
const candidates = await paypal.listPayPalWebhookEvents({ status: "failed" })
|
|
15
|
+
|
|
16
|
+
for (const event of candidates || []) {
|
|
17
|
+
const nextRetryAt = event?.next_retry_at ? new Date(event.next_retry_at).getTime() : null
|
|
18
|
+
if (!nextRetryAt || nextRetryAt > now) {
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const attemptCount = Number(event.attempt_count || 0) + 1
|
|
23
|
+
if (attemptCount > MAX_WEBHOOK_ATTEMPTS) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await paypal.updateWebhookEventRecord({
|
|
28
|
+
id: event.id,
|
|
29
|
+
status: "processing",
|
|
30
|
+
attempt_count: attemptCount,
|
|
31
|
+
next_retry_at: null,
|
|
32
|
+
last_error: null,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const eventType = String(event.event_type || "")
|
|
37
|
+
if (!isAllowedEventType(eventType)) {
|
|
38
|
+
await paypal.updateWebhookEventRecord({
|
|
39
|
+
id: event.id,
|
|
40
|
+
status: "ignored",
|
|
41
|
+
processed_at: new Date(),
|
|
42
|
+
})
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const payload = (event.payload || {}) as Record<string, any>
|
|
47
|
+
const processed = await processPayPalWebhookEvent(container, {
|
|
48
|
+
eventType,
|
|
49
|
+
payload,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await paypal.updateWebhookEventRecord({
|
|
53
|
+
id: event.id,
|
|
54
|
+
status: "processed",
|
|
55
|
+
processed_at: new Date(),
|
|
56
|
+
resource_id: processed.refundId || processed.captureId || processed.orderId || null,
|
|
57
|
+
})
|
|
58
|
+
try {
|
|
59
|
+
await paypal.recordMetric("webhook_retry_success")
|
|
60
|
+
} catch {
|
|
61
|
+
// ignore metrics failures
|
|
62
|
+
}
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
const nextRetry = attemptCount >= MAX_WEBHOOK_ATTEMPTS ? null : computeNextRetryAt(attemptCount)
|
|
65
|
+
await paypal.updateWebhookEventRecord({
|
|
66
|
+
id: event.id,
|
|
67
|
+
status: "failed",
|
|
68
|
+
attempt_count: attemptCount,
|
|
69
|
+
next_retry_at: nextRetry,
|
|
70
|
+
last_error: error?.message || String(error),
|
|
71
|
+
})
|
|
72
|
+
try {
|
|
73
|
+
await paypal.recordMetric("webhook_retry_failed")
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore metrics failures
|
|
76
|
+
}
|
|
77
|
+
console.error("[PayPal] webhook retry error", error)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const config = {
|
|
83
|
+
name: "paypal-webhook-retry",
|
|
84
|
+
schedule: "*/10 * * * *",
|
|
85
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
export class PayPalSellerClient {
|
|
2
|
-
constructor(private opts: { environment: "sandbox" | "live"; accessToken: string }) {}
|
|
3
|
-
|
|
4
|
-
private baseUrl() {
|
|
5
|
-
return this.opts.environment === "live"
|
|
6
|
-
? "https://api-m.paypal.com"
|
|
7
|
-
: "https://api-m.sandbox.paypal.com"
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
private headers(extra?: Record<string, string>) {
|
|
11
|
-
return {
|
|
12
|
-
"Content-Type": "application/json",
|
|
13
|
-
Authorization: `Bearer ${this.opts.accessToken}`,
|
|
14
|
-
...(extra ?? {}),
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async createOrder(body: any) {
|
|
19
|
-
const res = await fetch(`${this.baseUrl()}/v2/checkout/orders`, {
|
|
20
|
-
method: "POST",
|
|
21
|
-
headers: this.headers(),
|
|
22
|
-
body: JSON.stringify(body),
|
|
23
|
-
})
|
|
24
|
-
const json = await res.json().catch(() => ({}))
|
|
25
|
-
if (!res.ok) throw new Error(`PayPal createOrder failed (${res.status}): ${JSON.stringify(json)}`)
|
|
26
|
-
return json
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async getOrder(orderId: string) {
|
|
30
|
-
const res = await fetch(`${this.baseUrl()}/v2/checkout/orders/${orderId}`, {
|
|
31
|
-
method: "GET",
|
|
32
|
-
headers: this.headers(),
|
|
33
|
-
})
|
|
34
|
-
const json = await res.json().catch(() => ({}))
|
|
35
|
-
if (!res.ok) throw new Error(`PayPal getOrder failed (${res.status}): ${JSON.stringify(json)}`)
|
|
36
|
-
return json
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async captureOrder(orderId: string) {
|
|
40
|
-
const res = await fetch(`${this.baseUrl()}/v2/checkout/orders/${orderId}/capture`, {
|
|
41
|
-
method: "POST",
|
|
42
|
-
headers: this.headers(),
|
|
43
|
-
})
|
|
44
|
-
const json = await res.json().catch(() => ({}))
|
|
45
|
-
if (!res.ok) throw new Error(`PayPal captureOrder failed (${res.status}): ${JSON.stringify(json)}`)
|
|
46
|
-
return json
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async refundCapture(captureId: string, body?: any) {
|
|
50
|
-
const res = await fetch(`${this.baseUrl()}/v2/payments/captures/${captureId}/refund`, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: this.headers(),
|
|
53
|
-
body: body ? JSON.stringify(body) : "{}",
|
|
54
|
-
})
|
|
55
|
-
const json = await res.json().catch(() => ({}))
|
|
56
|
-
if (!res.ok) throw new Error(`PayPal refund failed (${res.status}): ${JSON.stringify(json)}`)
|
|
57
|
-
return json
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
export class PayPalSellerClient {
|
|
2
|
+
constructor(private opts: { environment: "sandbox" | "live"; accessToken: string }) {}
|
|
3
|
+
|
|
4
|
+
private baseUrl() {
|
|
5
|
+
return this.opts.environment === "live"
|
|
6
|
+
? "https://api-m.paypal.com"
|
|
7
|
+
: "https://api-m.sandbox.paypal.com"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
private headers(extra?: Record<string, string>) {
|
|
11
|
+
return {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
Authorization: `Bearer ${this.opts.accessToken}`,
|
|
14
|
+
...(extra ?? {}),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async createOrder(body: any) {
|
|
19
|
+
const res = await fetch(`${this.baseUrl()}/v2/checkout/orders`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: this.headers(),
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
})
|
|
24
|
+
const json = await res.json().catch(() => ({}))
|
|
25
|
+
if (!res.ok) throw new Error(`PayPal createOrder failed (${res.status}): ${JSON.stringify(json)}`)
|
|
26
|
+
return json
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getOrder(orderId: string) {
|
|
30
|
+
const res = await fetch(`${this.baseUrl()}/v2/checkout/orders/${orderId}`, {
|
|
31
|
+
method: "GET",
|
|
32
|
+
headers: this.headers(),
|
|
33
|
+
})
|
|
34
|
+
const json = await res.json().catch(() => ({}))
|
|
35
|
+
if (!res.ok) throw new Error(`PayPal getOrder failed (${res.status}): ${JSON.stringify(json)}`)
|
|
36
|
+
return json
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async captureOrder(orderId: string) {
|
|
40
|
+
const res = await fetch(`${this.baseUrl()}/v2/checkout/orders/${orderId}/capture`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: this.headers(),
|
|
43
|
+
})
|
|
44
|
+
const json = await res.json().catch(() => ({}))
|
|
45
|
+
if (!res.ok) throw new Error(`PayPal captureOrder failed (${res.status}): ${JSON.stringify(json)}`)
|
|
46
|
+
return json
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async refundCapture(captureId: string, body?: any) {
|
|
50
|
+
const res = await fetch(`${this.baseUrl()}/v2/payments/captures/${captureId}/refund`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: this.headers(),
|
|
53
|
+
body: body ? JSON.stringify(body) : "{}",
|
|
54
|
+
})
|
|
55
|
+
const json = await res.json().catch(() => ({}))
|
|
56
|
+
if (!res.ok) throw new Error(`PayPal refund failed (${res.status}): ${JSON.stringify(json)}`)
|
|
57
|
+
return json
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Module } from "@medusajs/framework/utils"
|
|
2
|
-
import PayPalModuleService from "./service"
|
|
3
|
-
|
|
4
|
-
export const PAYPAL_MODULE = "paypal_onboarding"
|
|
5
|
-
|
|
6
|
-
export default Module(PAYPAL_MODULE, {
|
|
7
|
-
service: PayPalModuleService,
|
|
8
|
-
})
|
|
1
|
+
import { Module } from "@medusajs/framework/utils"
|
|
2
|
+
import PayPalModuleService from "./service"
|
|
3
|
+
|
|
4
|
+
export const PAYPAL_MODULE = "paypal_onboarding"
|
|
5
|
+
|
|
6
|
+
export default Module(PAYPAL_MODULE, {
|
|
7
|
+
service: PayPalModuleService,
|
|
8
|
+
})
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
|
2
|
-
|
|
3
|
-
export class Migration20260115120000 extends Migration {
|
|
4
|
-
async up(): Promise<void> {
|
|
5
|
-
this.addSql(`
|
|
6
|
-
CREATE TABLE IF NOT EXISTS "paypal_connection" (
|
|
7
|
-
"id" text NOT NULL,
|
|
8
|
-
"environment" text NOT NULL DEFAULT 'sandbox',
|
|
9
|
-
"status" text NOT NULL DEFAULT 'disconnected',
|
|
10
|
-
"shared_id" text NULL,
|
|
11
|
-
"auth_code" text NULL,
|
|
12
|
-
"seller_client_id" text NULL,
|
|
13
|
-
"seller_client_secret" text NULL,
|
|
14
|
-
"app_access_token" text NULL,
|
|
15
|
-
"app_access_token_expires_at" timestamptz NULL,
|
|
16
|
-
"metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
17
|
-
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
18
|
-
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
19
|
-
"deleted_at" timestamptz NULL,
|
|
20
|
-
CONSTRAINT "paypal_connection_pkey" PRIMARY KEY ("id")
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
CREATE INDEX IF NOT EXISTS "idx_paypal_connection_deleted_at"
|
|
24
|
-
ON "paypal_connection" ("deleted_at");
|
|
25
|
-
`)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async down(): Promise<void> {
|
|
29
|
-
this.addSql(`
|
|
30
|
-
DROP TABLE IF EXISTS "paypal_connection" CASCADE;
|
|
31
|
-
`)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
|
2
|
+
|
|
3
|
+
export class Migration20260115120000 extends Migration {
|
|
4
|
+
async up(): Promise<void> {
|
|
5
|
+
this.addSql(`
|
|
6
|
+
CREATE TABLE IF NOT EXISTS "paypal_connection" (
|
|
7
|
+
"id" text NOT NULL,
|
|
8
|
+
"environment" text NOT NULL DEFAULT 'sandbox',
|
|
9
|
+
"status" text NOT NULL DEFAULT 'disconnected',
|
|
10
|
+
"shared_id" text NULL,
|
|
11
|
+
"auth_code" text NULL,
|
|
12
|
+
"seller_client_id" text NULL,
|
|
13
|
+
"seller_client_secret" text NULL,
|
|
14
|
+
"app_access_token" text NULL,
|
|
15
|
+
"app_access_token_expires_at" timestamptz NULL,
|
|
16
|
+
"metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
17
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
18
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
19
|
+
"deleted_at" timestamptz NULL,
|
|
20
|
+
CONSTRAINT "paypal_connection_pkey" PRIMARY KEY ("id")
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS "idx_paypal_connection_deleted_at"
|
|
24
|
+
ON "paypal_connection" ("deleted_at");
|
|
25
|
+
`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async down(): Promise<void> {
|
|
29
|
+
this.addSql(`
|
|
30
|
+
DROP TABLE IF EXISTS "paypal_connection" CASCADE;
|
|
31
|
+
`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
|
2
|
-
|
|
3
|
-
export class Migration20260123090000 extends Migration {
|
|
4
|
-
async up(): Promise<void> {
|
|
5
|
-
this.addSql(`
|
|
6
|
-
CREATE TABLE IF NOT EXISTS "paypal_settings" (
|
|
7
|
-
"id" text NOT NULL,
|
|
8
|
-
"data" jsonb NULL,
|
|
9
|
-
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
10
|
-
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
11
|
-
"deleted_at" timestamptz NULL,
|
|
12
|
-
CONSTRAINT "paypal_settings_pkey" PRIMARY KEY ("id")
|
|
13
|
-
);
|
|
14
|
-
CREATE INDEX IF NOT EXISTS "idx_paypal_settings_deleted_at"
|
|
15
|
-
ON "paypal_settings" ("deleted_at");
|
|
16
|
-
`)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async down(): Promise<void> {
|
|
20
|
-
this.addSql(`DROP TABLE IF EXISTS "paypal_settings" CASCADE;`)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
|
2
|
+
|
|
3
|
+
export class Migration20260123090000 extends Migration {
|
|
4
|
+
async up(): Promise<void> {
|
|
5
|
+
this.addSql(`
|
|
6
|
+
CREATE TABLE IF NOT EXISTS "paypal_settings" (
|
|
7
|
+
"id" text NOT NULL,
|
|
8
|
+
"data" jsonb NULL,
|
|
9
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
10
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
11
|
+
"deleted_at" timestamptz NULL,
|
|
12
|
+
CONSTRAINT "paypal_settings_pkey" PRIMARY KEY ("id")
|
|
13
|
+
);
|
|
14
|
+
CREATE INDEX IF NOT EXISTS "idx_paypal_settings_deleted_at"
|
|
15
|
+
ON "paypal_settings" ("deleted_at");
|
|
16
|
+
`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async down(): Promise<void> {
|
|
20
|
+
this.addSql(`DROP TABLE IF EXISTS "paypal_settings" CASCADE;`)
|
|
21
|
+
}
|
|
22
|
+
}
|