@easypayment/medusa-paypal 0.6.8 → 0.7.0

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.
@@ -1,115 +1,172 @@
1
- import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
2
- import type PayPalModuleService from "../modules/paypal/service"
3
- import { getPayPalAccessToken } from "../modules/paypal/utils/paypal-auth"
4
- import { isPayPalProviderId } from "../modules/paypal/utils/provider-ids"
5
-
6
- export default async function paypalOrderInvoiceHandler({
7
- event,
8
- container,
9
- }: SubscriberArgs<{ id: string }>) {
10
- const orderId = event?.data?.id
11
- if (!orderId) return
12
-
13
- try {
14
- const query = container.resolve("query") as any
15
- const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
16
-
17
- // Fetch the order with payment session data
18
- const { data: orders } = await query.graph({
19
- entity: "order",
20
- fields: [
21
- "id",
22
- "display_id",
23
- "payment_collections.payment_sessions.id",
24
- "payment_collections.payment_sessions.data",
25
- "payment_collections.payment_sessions.provider_id",
26
- "payment_collections.payment_sessions.status",
27
- "payment_collections.payment_sessions.created_at",
28
- ],
29
- filters: { id: orderId },
30
- })
31
-
32
- const order = orders?.[0]
33
- if (!order) return
34
-
35
- // Find the PayPal session
36
- const sessions = (order.payment_collections || []).flatMap(
37
- (pc: any) => pc.payment_sessions || []
38
- )
39
-
40
- const paypalSession = sessions
41
- .filter((s: any) => isPayPalProviderId(s.provider_id))
42
- .sort(
43
- (a: any, b: any) =>
44
- new Date(b.created_at || 0).getTime() -
45
- new Date(a.created_at || 0).getTime()
46
- )[0]
47
-
48
- if (!paypalSession) {
49
- console.info("[PayPal] invoice subscriber: no PayPal session found for order", orderId)
50
- return
51
- }
52
-
53
- const paypalData = ((paypalSession.data || {}).paypal || {}) as Record<string, any>
54
- const paypalOrderId = String(paypalData.order_id || "")
55
- if (!paypalOrderId) {
56
- console.info("[PayPal] invoice subscriber: no PayPal order_id in session for order", orderId)
57
- return
58
- }
59
-
60
- // Get invoice prefix from settings
61
- const settings = await paypal.getSettings().catch(() => ({}))
62
- const settingsData =
63
- settings && typeof settings === "object" && "data" in settings
64
- ? ((settings as { data?: Record<string, any> }).data ?? {})
65
- : {}
66
- const additionalSettings = (settingsData.additional_settings || {}) as Record<string, any>
67
- const invoicePrefix =
68
- typeof additionalSettings.invoicePrefix === "string"
69
- ? additionalSettings.invoicePrefix
70
- : ""
71
-
72
- // Build industry-standard invoice ID: prefix + order display_id
73
- // e.g. "WC-140" or "ORD-140"
74
- const displayId = String(order.display_id || "")
75
- const invoiceId = `${invoicePrefix}${displayId}`.trim()
76
- if (!invoiceId) return
77
-
78
- // Patch the PayPal order with the correct invoice_id
79
- const creds = await paypal.getActiveCredentials()
80
- const { accessToken, base } = await getPayPalAccessToken(creds)
81
-
82
- const patchResp = await fetch(`${base}/v2/checkout/orders/${paypalOrderId}`, {
83
- method: "PATCH",
84
- headers: {
85
- Authorization: `Bearer ${accessToken}`,
86
- "Content-Type": "application/json",
87
- },
88
- body: JSON.stringify([
89
- {
90
- op: "replace",
91
- path: "/purchase_units/@reference_id=='default'/invoice_id",
92
- value: invoiceId,
93
- },
94
- ]),
95
- })
96
-
97
- if (patchResp.ok || patchResp.status === 204) {
98
- console.info(
99
- `[PayPal] invoice_id updated to "${invoiceId}" for PayPal order ${paypalOrderId} (Medusa order #${displayId})`
100
- )
101
- } else {
102
- const text = await patchResp.text().catch(() => "")
103
- console.warn(
104
- `[PayPal] invoice_id patch failed (${patchResp.status}) for PayPal order ${paypalOrderId}: ${text}`
105
- )
106
- }
107
- } catch (e: any) {
108
- // Non-fatal — never block order placement
109
- console.warn("[PayPal] paypalOrderInvoiceHandler error:", e?.message || e)
110
- }
111
- }
112
-
113
- export const config: SubscriberConfig = {
114
- event: "order.placed",
115
- }
1
+ import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
2
+ import type PayPalModuleService from "../modules/paypal/service"
3
+ import { getPayPalAccessToken } from "../modules/paypal/utils/paypal-auth"
4
+ import { isPayPalProviderId } from "../modules/paypal/utils/provider-ids"
5
+
6
+ export default async function paypalOrderInvoiceHandler({
7
+ event,
8
+ container,
9
+ }: SubscriberArgs<{ id: string }>) {
10
+ const orderId = event?.data?.id
11
+ if (!orderId) return
12
+
13
+ try {
14
+ const query = container.resolve("query") as any
15
+ const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
16
+
17
+ // Fetch the Medusa order with payment session data
18
+ const { data: orders } = await query.graph({
19
+ entity: "order",
20
+ fields: [
21
+ "id",
22
+ "display_id",
23
+ "payment_collections.payment_sessions.id",
24
+ "payment_collections.payment_sessions.data",
25
+ "payment_collections.payment_sessions.provider_id",
26
+ "payment_collections.payment_sessions.status",
27
+ "payment_collections.payment_sessions.created_at",
28
+ ],
29
+ filters: { id: orderId },
30
+ })
31
+
32
+ const order = orders?.[0]
33
+ if (!order) return
34
+
35
+ // Find the PayPal payment session
36
+ const sessions = (order.payment_collections || []).flatMap(
37
+ (pc: any) => pc.payment_sessions || []
38
+ )
39
+ const paypalSession = sessions
40
+ .filter((s: any) => isPayPalProviderId(s.provider_id))
41
+ .sort(
42
+ (a: any, b: any) =>
43
+ new Date(b.created_at || 0).getTime() -
44
+ new Date(a.created_at || 0).getTime()
45
+ )[0]
46
+
47
+ if (!paypalSession) {
48
+ console.info(
49
+ "[PayPal] invoice subscriber: no PayPal session for order",
50
+ orderId
51
+ )
52
+ return
53
+ }
54
+
55
+ const paypalData = (
56
+ (paypalSession.data || {}).paypal || {}
57
+ ) as Record<string, any>
58
+ const paypalOrderId = String(paypalData.order_id || "")
59
+
60
+ if (!paypalOrderId) {
61
+ console.info(
62
+ "[PayPal] invoice subscriber: no order_id in session for order",
63
+ orderId
64
+ )
65
+ return
66
+ }
67
+
68
+ // Build invoice_id: invoicePrefix + display_id (industry standard)
69
+ const settings = await paypal.getSettings().catch(() => ({}))
70
+ const settingsData =
71
+ settings && typeof settings === "object" && "data" in settings
72
+ ? ((settings as { data?: Record<string, any> }).data ?? {})
73
+ : {}
74
+ const additionalSettings = (
75
+ settingsData.additional_settings || {}
76
+ ) as Record<string, any>
77
+ const invoicePrefix =
78
+ typeof additionalSettings.invoicePrefix === "string"
79
+ ? additionalSettings.invoicePrefix
80
+ : ""
81
+ const displayId = String(order.display_id || "")
82
+ const invoiceId = `${invoicePrefix}${displayId}`.trim()
83
+
84
+ if (!invoiceId) return
85
+
86
+ // Get PayPal access token
87
+ const creds = await paypal.getActiveCredentials()
88
+ const { accessToken, base } = await getPayPalAccessToken(creds)
89
+
90
+ // ── HIGH-VOLUME GUARD ──────────────────────────────────────────────
91
+ // Check if invoice_id is already correct before sending a PATCH.
92
+ // Prevents redundant API calls and duplicate-key errors at scale.
93
+ try {
94
+ const currentResp = await fetch(
95
+ `${base}/v2/checkout/orders/${paypalOrderId}`,
96
+ { headers: { Authorization: `Bearer ${accessToken}` } }
97
+ )
98
+ if (currentResp.ok) {
99
+ const currentOrder = await currentResp.json().catch(() => ({}))
100
+ const currentInvoiceId =
101
+ currentOrder?.purchase_units?.[0]?.invoice_id || ""
102
+ if (currentInvoiceId === invoiceId) {
103
+ console.info(
104
+ `[PayPal] invoice_id already "${invoiceId}" skipping PATCH`
105
+ )
106
+ return
107
+ }
108
+ }
109
+ } catch (e: any) {
110
+ // Non-fatal — proceed with PATCH anyway
111
+ console.warn("[PayPal] pre-PATCH order fetch failed:", e?.message)
112
+ }
113
+ // ──────────────────────────────────────────────────────────────────
114
+
115
+ // Try two PATCH paths for robustness:
116
+ // Path 1: standard "default" reference_id (new orders after this fix)
117
+ // Path 2: array index fallback (orders created before this fix)
118
+ const patchPaths = [
119
+ "/purchase_units/@reference_id=='default'/invoice_id",
120
+ "/purchase_units/0/invoice_id",
121
+ ]
122
+
123
+ let patched = false
124
+ for (const path of patchPaths) {
125
+ const patchResp = await fetch(
126
+ `${base}/v2/checkout/orders/${paypalOrderId}`,
127
+ {
128
+ method: "PATCH",
129
+ headers: {
130
+ Authorization: `Bearer ${accessToken}`,
131
+ "Content-Type": "application/json",
132
+ },
133
+ body: JSON.stringify([{ op: "replace", path, value: invoiceId }]),
134
+ }
135
+ )
136
+
137
+ if (patchResp.ok || patchResp.status === 204) {
138
+ console.info(
139
+ `[PayPal] invoice_id set to "${invoiceId}"`,
140
+ `(PayPal order ${paypalOrderId} / Medusa #${displayId})`,
141
+ `via path: ${path}`
142
+ )
143
+ patched = true
144
+ break
145
+ }
146
+
147
+ const errText = await patchResp.text().catch(() => "")
148
+ console.warn("[PayPal] invoice_id PATCH attempt failed", {
149
+ status: patchResp.status,
150
+ path,
151
+ paypalOrderId,
152
+ errText,
153
+ })
154
+ }
155
+
156
+ if (!patched) {
157
+ // Hard error log — failures must never be silently swallowed at scale
158
+ console.error(
159
+ "[PayPal] CRITICAL: invoice_id PATCH failed for ALL paths.",
160
+ { paypalOrderId, invoiceId, displayId }
161
+ )
162
+ }
163
+
164
+ } catch (e: any) {
165
+ // Non-fatal — never block order placement
166
+ console.warn("[PayPal] paypalOrderInvoiceHandler error:", e?.message || e)
167
+ }
168
+ }
169
+
170
+ export const config: SubscriberConfig = {
171
+ event: "order.placed",
172
+ }