@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.
Files changed (70) hide show
  1. package/.medusa/server/src/admin/index.js +7 -7
  2. package/.medusa/server/src/admin/index.mjs +7 -7
  3. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  4. package/.medusa/server/src/api/store/paypal/create-order/route.js +62 -139
  5. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  6. package/.medusa/server/src/modules/paypal/migrations/20260115120000_create_paypal_connection.js +22 -22
  7. package/.medusa/server/src/modules/paypal/migrations/20260123090000_create_paypal_settings.js +11 -11
  8. package/.medusa/server/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.js +18 -18
  9. package/.medusa/server/src/modules/paypal/migrations/20260401090000_create_paypal_metric.js +16 -16
  10. package/.medusa/server/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.js +20 -20
  11. package/.medusa/server/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.js +14 -14
  12. package/.medusa/server/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.js +15 -15
  13. package/README.md +142 -142
  14. package/package.json +75 -75
  15. package/src/admin/index.ts +7 -7
  16. package/src/admin/routes/settings/paypal/_components/Tabs.tsx +52 -52
  17. package/src/admin/routes/settings/paypal/_components/Toast.tsx +51 -51
  18. package/src/admin/routes/settings/paypal/additional-settings/page.tsx +200 -200
  19. package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +183 -183
  20. package/src/admin/routes/settings/paypal/apple-pay/page.tsx +5 -5
  21. package/src/admin/routes/settings/paypal/connection/page.tsx +754 -754
  22. package/src/admin/routes/settings/paypal/google-pay/page.tsx +5 -5
  23. package/src/admin/routes/settings/paypal/pay-later-messaging/page.tsx +5 -5
  24. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +376 -376
  25. package/src/api/admin/payment-collections/[id]/payment-sessions/route.ts +24 -24
  26. package/src/api/admin/paypal/disconnect/route.ts +8 -8
  27. package/src/api/admin/paypal/environment/route.ts +25 -25
  28. package/src/api/admin/paypal/onboard-complete/route.ts +44 -44
  29. package/src/api/admin/paypal/onboarding-link/route.ts +45 -45
  30. package/src/api/admin/paypal/onboarding-status/route.ts +18 -18
  31. package/src/api/admin/paypal/rotate-credentials/route.ts +8 -8
  32. package/src/api/admin/paypal/save-credentials/route.ts +14 -14
  33. package/src/api/admin/paypal/settings/route.ts +14 -14
  34. package/src/api/admin/paypal/status/route.ts +12 -12
  35. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +65 -65
  36. package/src/api/store/paypal/capture-order/route.ts +276 -276
  37. package/src/api/store/paypal/config/route.ts +102 -102
  38. package/src/api/store/paypal/create-order/route.ts +77 -176
  39. package/src/api/store/paypal/settings/route.ts +19 -19
  40. package/src/api/store/paypal/webhook/route.ts +246 -246
  41. package/src/api/store/paypal-complete/route.ts +75 -75
  42. package/src/jobs/paypal-reconcile.ts +112 -112
  43. package/src/jobs/paypal-webhook-retry.ts +85 -85
  44. package/src/modules/paypal/clients/paypal-seller.client.ts +59 -59
  45. package/src/modules/paypal/index.ts +8 -8
  46. package/src/modules/paypal/migrations/20260115120000_create_paypal_connection.ts +33 -33
  47. package/src/modules/paypal/migrations/20260123090000_create_paypal_settings.ts +22 -22
  48. package/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.ts +29 -29
  49. package/src/modules/paypal/migrations/20260401090000_create_paypal_metric.ts +27 -27
  50. package/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.ts +31 -31
  51. package/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.ts +25 -25
  52. package/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.ts +26 -26
  53. package/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.ts +11 -11
  54. package/src/modules/paypal/models/paypal_connection.ts +21 -21
  55. package/src/modules/paypal/models/paypal_metric.ts +9 -9
  56. package/src/modules/paypal/models/paypal_settings.ts +8 -8
  57. package/src/modules/paypal/models/paypal_webhook_event.ts +19 -19
  58. package/src/modules/paypal/payment-provider/README.md +22 -22
  59. package/src/modules/paypal/payment-provider/card-service.ts +760 -760
  60. package/src/modules/paypal/payment-provider/index.ts +19 -19
  61. package/src/modules/paypal/payment-provider/service.ts +1121 -1121
  62. package/src/modules/paypal/payment-provider/webhook-utils.ts +88 -88
  63. package/src/modules/paypal/service.ts +1247 -1247
  64. package/src/modules/paypal/types/config.ts +47 -47
  65. package/src/modules/paypal/utils/amounts.ts +41 -41
  66. package/src/modules/paypal/utils/crypto.ts +51 -51
  67. package/src/modules/paypal/utils/currencies.ts +84 -84
  68. package/src/modules/paypal/utils/paypal-auth.ts +32 -32
  69. package/src/modules/paypal/utils/provider-ids.ts +15 -15
  70. 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
+ }