@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.
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 -159
  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,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
- // Step 1 — read paymentAction from DB settings (same pattern as capture-order)
15
- const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
16
- const settings = await paypal.getSettings().catch(() => ({}))
17
- const settingsData =
18
- settings && typeof settings === "object" && "data" in settings
19
- ? ((settings as { data?: Record<string, any> }).data ?? {})
20
- : {}
21
- const additionalSettings = (settingsData.additional_settings || {}) as Record<string, any>
22
- const paymentAction =
23
- typeof additionalSettings.paymentAction === "string"
24
- ? additionalSettings.paymentAction
25
- : "capture"
26
-
27
- // "authorize" mode session status = "authorized"
28
- // "capture" mode → session status = "captured"
29
- const sessionStatus = paymentAction === "authorize" ? "authorized" : "captured"
30
- const timestampKey = paymentAction === "authorize" ? "authorized_at" : "captured_at"
31
-
32
- // Step 2 — find the PayPal session for this cart
33
- const query = req.scope.resolve("query")
34
- const { data: carts } = await query.graph({
35
- entity: "cart",
36
- fields: [
37
- "id",
38
- "payment_collection.payment_sessions.id",
39
- "payment_collection.payment_sessions.data",
40
- "payment_collection.payment_sessions.provider_id",
41
- "payment_collection.payment_sessions.created_at",
42
- "payment_collection.payment_sessions.amount",
43
- "payment_collection.payment_sessions.currency_code",
44
- ],
45
- filters: { id: cart_id },
46
- })
47
-
48
- const sessions = carts?.[0]?.payment_collection?.payment_sessions || []
49
- const session = sessions
50
- .filter((s: any) => String(s.provider_id || "").includes("paypal"))
51
- .sort(
52
- (a: any, b: any) =>
53
- new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
54
- )[0]
55
-
56
- if (!session) {
57
- return res.status(400).json({ error: "No PayPal payment session found for cart" })
58
- }
59
-
60
- // Step 3 — update session with correct status + amount
61
- const paymentModule = req.scope.resolve(Modules.PAYMENT) as IPaymentModuleService
62
- await (paymentModule as any).updatePaymentSession({
63
- id: session.id,
64
- data: { ...(session.data || {}), [timestampKey]: new Date().toISOString() },
65
- status: sessionStatus as any,
66
- amount: session.amount,
67
- currency_code: session.currency_code,
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
- 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
+ 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
+ }