@easypayment/medusa-paypal 0.6.2 → 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 -159
- 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,76 +1,70 @@
|
|
|
1
|
-
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
-
import type { IPaymentModuleService } from "@medusajs/framework/types"
|
|
3
|
-
import { Modules } from "@medusajs/framework/utils"
|
|
4
|
-
import type PayPalModuleService from "../../../modules/paypal/service"
|
|
5
|
-
|
|
6
|
-
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
7
|
-
const { cart_id } = req.body as { cart_id: string }
|
|
8
|
-
|
|
9
|
-
if (!cart_id) {
|
|
10
|
-
return res.status(400).json({ error: "cart_id is required" })
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"payment_collection.payment_sessions.
|
|
39
|
-
"payment_collection.payment_sessions.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
console.log(`[paypal-complete] session ${sessionStatus}:`, session.id)
|
|
71
|
-
return res.json({ success: true, session_id: session.id })
|
|
72
|
-
} catch (e: any) {
|
|
73
|
-
console.error("[paypal-complete] error:", e?.message || e)
|
|
74
|
-
return res.status(500).json({ error: e?.message || "Internal error" })
|
|
75
|
-
}
|
|
1
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import type { IPaymentModuleService } from "@medusajs/framework/types"
|
|
3
|
+
import { Modules } from "@medusajs/framework/utils"
|
|
4
|
+
import type PayPalModuleService from "../../../modules/paypal/service"
|
|
5
|
+
|
|
6
|
+
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
7
|
+
const { cart_id } = req.body as { cart_id: string }
|
|
8
|
+
|
|
9
|
+
if (!cart_id) {
|
|
10
|
+
return res.status(400).json({ error: "cart_id is required" })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
|
|
15
|
+
const settings = await paypal.getSettings().catch(() => ({}))
|
|
16
|
+
const settingsData =
|
|
17
|
+
settings && typeof settings === "object" && "data" in settings
|
|
18
|
+
? ((settings as { data?: Record<string, any> }).data ?? {})
|
|
19
|
+
: {}
|
|
20
|
+
const additionalSettings = (settingsData.additional_settings || {}) as Record<string, any>
|
|
21
|
+
const paymentAction =
|
|
22
|
+
typeof additionalSettings.paymentAction === "string"
|
|
23
|
+
? additionalSettings.paymentAction
|
|
24
|
+
: "capture"
|
|
25
|
+
|
|
26
|
+
const sessionStatus = paymentAction === "authorize" ? "authorized" : "captured"
|
|
27
|
+
const timestampKey = paymentAction === "authorize" ? "authorized_at" : "captured_at"
|
|
28
|
+
|
|
29
|
+
const query = req.scope.resolve("query")
|
|
30
|
+
const { data: carts } = await query.graph({
|
|
31
|
+
entity: "cart",
|
|
32
|
+
fields: [
|
|
33
|
+
"id",
|
|
34
|
+
"payment_collection.payment_sessions.id",
|
|
35
|
+
"payment_collection.payment_sessions.data",
|
|
36
|
+
"payment_collection.payment_sessions.provider_id",
|
|
37
|
+
"payment_collection.payment_sessions.created_at",
|
|
38
|
+
"payment_collection.payment_sessions.amount",
|
|
39
|
+
"payment_collection.payment_sessions.currency_code",
|
|
40
|
+
],
|
|
41
|
+
filters: { id: cart_id },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const sessions = carts?.[0]?.payment_collection?.payment_sessions || []
|
|
45
|
+
const session = sessions
|
|
46
|
+
.filter((s: any) => String(s.provider_id || "").includes("paypal"))
|
|
47
|
+
.sort(
|
|
48
|
+
(a: any, b: any) =>
|
|
49
|
+
new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
|
|
50
|
+
)[0]
|
|
51
|
+
|
|
52
|
+
if (!session) {
|
|
53
|
+
return res.status(400).json({ error: "No PayPal payment session found for cart" })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const paymentModule = req.scope.resolve(Modules.PAYMENT) as IPaymentModuleService
|
|
57
|
+
await (paymentModule as any).updatePaymentSession({
|
|
58
|
+
id: session.id,
|
|
59
|
+
data: { ...(session.data || {}), [timestampKey]: new Date().toISOString() },
|
|
60
|
+
status: sessionStatus as any,
|
|
61
|
+
amount: session.amount,
|
|
62
|
+
currency_code: session.currency_code,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return res.json({ success: true, session_id: session.id })
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
console.error("[paypal-complete] error:", e?.message || e)
|
|
68
|
+
return res.status(500).json({ error: e?.message || "Internal error" })
|
|
69
|
+
}
|
|
76
70
|
}
|
|
@@ -1,85 +1,149 @@
|
|
|
1
|
-
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
-
import type PayPalModuleService from "../modules/paypal/service"
|
|
3
|
-
import {
|
|
4
|
-
computeNextRetryAt,
|
|
5
|
-
isAllowedEventType,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export default async function paypalWebhookRetry(container: MedusaContainer) {
|
|
12
|
-
const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
|
|
13
|
-
const now = Date.now()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
+
import type PayPalModuleService from "../modules/paypal/service"
|
|
3
|
+
import {
|
|
4
|
+
computeNextRetryAt,
|
|
5
|
+
isAllowedEventType,
|
|
6
|
+
isRetryableError,
|
|
7
|
+
MAX_WEBHOOK_ATTEMPTS,
|
|
8
|
+
processPayPalWebhookEvent,
|
|
9
|
+
} from "../modules/paypal/webhook-processor"
|
|
10
|
+
|
|
11
|
+
export default async function paypalWebhookRetry(container: MedusaContainer) {
|
|
12
|
+
const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
|
|
13
|
+
const now = Date.now()
|
|
14
|
+
|
|
15
|
+
const candidates = await paypal.listPayPalWebhookEvents({ status: "failed" })
|
|
16
|
+
if (!candidates?.length) return
|
|
17
|
+
|
|
18
|
+
console.info(
|
|
19
|
+
`[PayPal] webhook-retry: evaluating ${candidates.length} failed event(s)`
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
for (const event of candidates) {
|
|
23
|
+
const nextRetryAt = event?.next_retry_at
|
|
24
|
+
? new Date(event.next_retry_at).getTime()
|
|
25
|
+
: null
|
|
26
|
+
if (!nextRetryAt || nextRetryAt > now) continue
|
|
27
|
+
|
|
28
|
+
const attemptCount = Number(event.attempt_count || 0)
|
|
29
|
+
|
|
30
|
+
if (attemptCount >= MAX_WEBHOOK_ATTEMPTS) {
|
|
31
|
+
await paypal
|
|
32
|
+
.updateWebhookEventRecord({
|
|
33
|
+
id: event.id,
|
|
34
|
+
status: "dead_letter",
|
|
35
|
+
next_retry_at: null,
|
|
36
|
+
last_error: `Exceeded max attempts (${MAX_WEBHOOK_ATTEMPTS})`,
|
|
37
|
+
})
|
|
38
|
+
.catch(() => {})
|
|
39
|
+
console.warn("[PayPal] webhook-retry: dead-lettered (max attempts)", {
|
|
40
|
+
id: event.id,
|
|
41
|
+
event_type: event.event_type,
|
|
42
|
+
attempts: attemptCount,
|
|
43
|
+
})
|
|
44
|
+
await paypal.recordMetric("webhook_dead_letter").catch(() => {})
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await paypal
|
|
49
|
+
.updateWebhookEventRecord({
|
|
50
|
+
id: event.id,
|
|
51
|
+
status: "processing",
|
|
52
|
+
attempt_count: attemptCount + 1,
|
|
53
|
+
next_retry_at: null,
|
|
54
|
+
last_error: null,
|
|
55
|
+
})
|
|
56
|
+
.catch(() => {})
|
|
57
|
+
|
|
58
|
+
const eventType = String(event.event_type || "")
|
|
59
|
+
|
|
60
|
+
if (!isAllowedEventType(eventType)) {
|
|
61
|
+
await paypal
|
|
62
|
+
.updateWebhookEventRecord({
|
|
63
|
+
id: event.id,
|
|
64
|
+
status: "ignored",
|
|
65
|
+
processed_at: new Date(),
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {})
|
|
68
|
+
console.info("[PayPal] webhook-retry: ignored unsupported event type", {
|
|
69
|
+
id: event.id,
|
|
70
|
+
event_type: eventType,
|
|
71
|
+
})
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const payload = (event.payload || {}) as Record<string, any>
|
|
77
|
+
const processed = await processPayPalWebhookEvent(container, { eventType, payload })
|
|
78
|
+
|
|
79
|
+
await paypal
|
|
80
|
+
.updateWebhookEventRecord({
|
|
81
|
+
id: event.id,
|
|
82
|
+
status: "processed",
|
|
83
|
+
processed_at: new Date(),
|
|
84
|
+
resource_id:
|
|
85
|
+
processed.refundId || processed.captureId || processed.orderId || null,
|
|
86
|
+
})
|
|
87
|
+
.catch(() => {})
|
|
88
|
+
|
|
89
|
+
console.info("[PayPal] webhook-retry: processed successfully", {
|
|
90
|
+
id: event.id,
|
|
91
|
+
event_type: eventType,
|
|
92
|
+
attempt: attemptCount + 1,
|
|
93
|
+
order_id: processed.orderId,
|
|
94
|
+
capture_id: processed.captureId,
|
|
95
|
+
cart_id: processed.cartId,
|
|
96
|
+
session_updated: processed.sessionUpdated,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
await paypal.recordMetric("webhook_retry_success").catch(() => {})
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
const retryable = isRetryableError(error)
|
|
102
|
+
const nextAttempt = attemptCount + 1
|
|
103
|
+
|
|
104
|
+
if (!retryable || nextAttempt >= MAX_WEBHOOK_ATTEMPTS) {
|
|
105
|
+
await paypal
|
|
106
|
+
.updateWebhookEventRecord({
|
|
107
|
+
id: event.id,
|
|
108
|
+
status: "dead_letter",
|
|
109
|
+
attempt_count: nextAttempt,
|
|
110
|
+
next_retry_at: null,
|
|
111
|
+
last_error: error?.message || String(error),
|
|
112
|
+
})
|
|
113
|
+
.catch(() => {})
|
|
114
|
+
console.error("[PayPal] webhook-retry: dead-lettered after error", {
|
|
115
|
+
id: event.id,
|
|
116
|
+
event_type: eventType,
|
|
117
|
+
attempt: nextAttempt,
|
|
118
|
+
retryable,
|
|
119
|
+
error: error?.message,
|
|
120
|
+
})
|
|
121
|
+
await paypal.recordMetric("webhook_dead_letter").catch(() => {})
|
|
122
|
+
} else {
|
|
123
|
+
const nextRetry = computeNextRetryAt(nextAttempt)
|
|
124
|
+
await paypal
|
|
125
|
+
.updateWebhookEventRecord({
|
|
126
|
+
id: event.id,
|
|
127
|
+
status: "failed",
|
|
128
|
+
attempt_count: nextAttempt,
|
|
129
|
+
next_retry_at: nextRetry,
|
|
130
|
+
last_error: error?.message || String(error),
|
|
131
|
+
})
|
|
132
|
+
.catch(() => {})
|
|
133
|
+
console.warn("[PayPal] webhook-retry: scheduled retry", {
|
|
134
|
+
id: event.id,
|
|
135
|
+
event_type: eventType,
|
|
136
|
+
attempt: nextAttempt,
|
|
137
|
+
next_retry_at: nextRetry?.toISOString(),
|
|
138
|
+
error: error?.message,
|
|
139
|
+
})
|
|
140
|
+
await paypal.recordMetric("webhook_retry_failed").catch(() => {})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const config = {
|
|
147
|
+
name: "paypal-webhook-retry",
|
|
148
|
+
schedule: "*/10 * * * *",
|
|
149
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
|
2
|
+
|
|
3
|
+
export class Migration20270201000000 extends Migration {
|
|
4
|
+
async up(): Promise<void> {
|
|
5
|
+
this.addSql(`
|
|
6
|
+
CREATE INDEX IF NOT EXISTS "idx_paypal_webhook_event_status_retry"
|
|
7
|
+
ON "paypal_webhook_event" ("status", "next_retry_at")
|
|
8
|
+
WHERE "status" = 'failed';
|
|
9
|
+
`)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async down(): Promise<void> {
|
|
13
|
+
this.addSql(`
|
|
14
|
+
DROP INDEX IF EXISTS "idx_paypal_webhook_event_status_retry";
|
|
15
|
+
`)
|
|
16
|
+
}
|
|
17
|
+
}
|