@easypayment/medusa-payment-paypal 0.9.14 → 0.9.16
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 -12
- package/.medusa/server/src/admin/index.mjs +12 -12
- package/.medusa/server/src/admin/routes/settings/paypal/connection/page.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 +27 -12
- 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 +83 -56
- 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 +2 -1
- 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 +37 -32
- 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 +42 -1
- package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
- package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js +5 -4
- package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.js +13 -10
- package/.medusa/server/src/modules/paypal/payment-provider/card-service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/service.js +10 -9
- package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts +12 -0
- package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js +12 -0
- package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js.map +1 -1
- package/.medusa/server/src/modules/paypal/service.d.ts +10 -1
- package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/service.js +71 -21
- package/.medusa/server/src/modules/paypal/service.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/amounts.d.ts +9 -1
- package/.medusa/server/src/modules/paypal/utils/amounts.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/amounts.js +17 -5
- package/.medusa/server/src/modules/paypal/utils/amounts.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +0 -8
- package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +0 -22
- package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -1
- package/.medusa/server/src/modules/paypal/utils/paypal-fetch.d.ts +16 -0
- package/.medusa/server/src/modules/paypal/utils/paypal-fetch.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/paypal-fetch.js +31 -0
- package/.medusa/server/src/modules/paypal/utils/paypal-fetch.js.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts +14 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.d.ts.map +1 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.js +91 -0
- package/.medusa/server/src/modules/paypal/utils/secret-crypto.js.map +1 -0
- package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/webhook-processor.js +29 -13
- package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
- package/.medusa/server/src/subscribers/paypal-order-invoice.d.ts.map +1 -1
- package/.medusa/server/src/subscribers/paypal-order-invoice.js +5 -3
- package/.medusa/server/src/subscribers/paypal-order-invoice.js.map +1 -1
- package/README.md +12 -0
- package/package.json +5 -2
- package/src/admin/routes/settings/paypal/connection/page.tsx +1 -1
- package/src/api/store/paypal/capture-order/route.ts +39 -17
- package/src/api/store/paypal/create-order/route.ts +106 -64
- package/src/api/store/paypal/webhook/route.ts +2 -1
- package/src/api/store/paypal-complete/route.ts +58 -44
- package/src/jobs/paypal-webhook-retry.ts +51 -1
- package/src/modules/paypal/clients/paypal-seller.client.ts +6 -4
- package/src/modules/paypal/payment-provider/card-service.ts +13 -10
- package/src/modules/paypal/payment-provider/service.ts +10 -9
- package/src/modules/paypal/payment-provider/webhook-utils.ts +12 -0
- package/src/modules/paypal/service.ts +76 -21
- package/src/modules/paypal/utils/amounts.ts +17 -8
- package/src/modules/paypal/utils/paypal-auth.ts +0 -27
- package/src/modules/paypal/utils/paypal-fetch.ts +30 -0
- package/src/modules/paypal/utils/secret-crypto.ts +94 -0
- package/src/modules/paypal/webhook-processor.ts +32 -15
- package/src/subscribers/paypal-order-invoice.ts +6 -4
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
-
import type { IPaymentModuleService } from "@medusajs/framework/types"
|
|
3
|
-
import { Modules } from "@medusajs/framework/utils"
|
|
2
|
+
import type { IPaymentModuleService, Logger } from "@medusajs/framework/types"
|
|
3
|
+
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
|
4
4
|
import { completeCartWorkflow } from "@medusajs/core-flows"
|
|
5
|
-
import
|
|
5
|
+
import { randomUUID } from "crypto"
|
|
6
|
+
import { isPayPalProviderId } from "../../../modules/paypal/utils/provider-ids"
|
|
6
7
|
|
|
7
8
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
9
|
+
const logger = req.scope.resolve<Logger>(ContainerRegistrationKeys.LOGGER)
|
|
10
|
+
const requestId = randomUUID()
|
|
8
11
|
const { cart_id } = req.body as { cart_id: string }
|
|
9
12
|
|
|
10
13
|
if (!cart_id || typeof cart_id !== "string") {
|
|
@@ -40,7 +43,7 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
|
40
43
|
|
|
41
44
|
const sessions = cart?.payment_collection?.payment_sessions || []
|
|
42
45
|
const session = sessions
|
|
43
|
-
.filter((s: any) =>
|
|
46
|
+
.filter((s: any) => isPayPalProviderId(s.provider_id))
|
|
44
47
|
.sort(
|
|
45
48
|
(a: any, b: any) =>
|
|
46
49
|
new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
|
|
@@ -52,46 +55,27 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
|
52
55
|
|
|
53
56
|
const paymentModule = req.scope.resolve(Modules.PAYMENT) as IPaymentModuleService
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const liveData = (liveSession?.data || {}) as Record<string, any>
|
|
61
|
-
|
|
62
|
-
const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
|
|
63
|
-
const settings = await paypal.getSettings().catch(() => ({}))
|
|
64
|
-
const settingsData =
|
|
65
|
-
settings && typeof settings === "object" && "data" in settings
|
|
66
|
-
? ((settings as { data?: Record<string, any> }).data ?? {})
|
|
67
|
-
: {}
|
|
68
|
-
const additionalSettings = (settingsData.additional_settings || {}) as Record<string, any>
|
|
69
|
-
const paymentAction =
|
|
70
|
-
typeof additionalSettings.paymentAction === "string"
|
|
71
|
-
? additionalSettings.paymentAction
|
|
72
|
-
: "capture"
|
|
73
|
-
|
|
74
|
-
const timestampKey = paymentAction === "authorize" ? "authorized_at" : "captured_at"
|
|
75
|
-
|
|
76
|
-
if (!liveData[timestampKey]) {
|
|
77
|
-
await (paymentModule as any).updatePaymentSession({
|
|
78
|
-
id: session.id,
|
|
79
|
-
data: {
|
|
80
|
-
...liveData,
|
|
81
|
-
[timestampKey]: new Date().toISOString(),
|
|
82
|
-
},
|
|
83
|
-
amount: liveSession?.amount ?? session.amount,
|
|
84
|
-
currency_code: liveSession?.currency_code ?? session.currency_code,
|
|
85
|
-
})
|
|
86
|
-
}
|
|
58
|
+
// Do NOT fabricate captured_at/authorized_at here. The authoritative
|
|
59
|
+
// timestamp and status are set by the provider's authorizePayment (invoked
|
|
60
|
+
// via authorizePaymentSession below) only after it verifies the capture /
|
|
61
|
+
// authorization against PayPal. Stamping it ourselves would mark an unpaid
|
|
62
|
+
// session as paid.
|
|
87
63
|
|
|
88
64
|
const currentStatus = String(session.status || "")
|
|
89
65
|
if (currentStatus !== "authorized") {
|
|
90
66
|
try {
|
|
91
67
|
await (paymentModule as any).authorizePaymentSession(session.id, {})
|
|
92
|
-
|
|
68
|
+
logger.info(
|
|
69
|
+
`[paypal] paypal-complete authorizePaymentSession succeeded (request_id=${requestId}, session_id=${session.id})`
|
|
70
|
+
)
|
|
93
71
|
} catch (e: any) {
|
|
94
|
-
|
|
72
|
+
// Authorization is the gate for completion; if it fails, the
|
|
73
|
+
// completeCartWorkflow below fails too and we report that honestly.
|
|
74
|
+
logger.warn(
|
|
75
|
+
`[paypal] paypal-complete authorizePaymentSession failed (request_id=${requestId}, session_id=${session.id}): ${
|
|
76
|
+
e?.message ?? String(e)
|
|
77
|
+
}`
|
|
78
|
+
)
|
|
95
79
|
}
|
|
96
80
|
}
|
|
97
81
|
|
|
@@ -99,14 +83,44 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
|
99
83
|
const { result } = await completeCartWorkflow(req.scope).run({
|
|
100
84
|
input: { id: cart_id },
|
|
101
85
|
})
|
|
102
|
-
|
|
103
|
-
|
|
86
|
+
logger.info(
|
|
87
|
+
`[paypal] paypal-complete cart completed (request_id=${requestId}, cart_id=${cart_id}, order_id=${result?.id})`
|
|
88
|
+
)
|
|
89
|
+
return res.json({ success: true, cart_id, order_id: result?.id, request_id: requestId })
|
|
104
90
|
} catch (e: any) {
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
// The cart may have been completed by a concurrent request between our
|
|
92
|
+
// initial check and now — treat that as success, not an error.
|
|
93
|
+
const recheck = await query
|
|
94
|
+
.graph({ entity: "cart", fields: ["id", "completed_at"], filters: { id: cart_id } })
|
|
95
|
+
.catch(() => ({ data: [] as any[] }))
|
|
96
|
+
if (recheck?.data?.[0]?.completed_at) {
|
|
97
|
+
return res.json({ success: true, cart_id, already_completed: true, request_id: requestId })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logger.error(
|
|
101
|
+
`[paypal] paypal-complete completeCartWorkflow failed (request_id=${requestId}, cart_id=${cart_id}): ${
|
|
102
|
+
e?.message ?? String(e)
|
|
103
|
+
}`,
|
|
104
|
+
e instanceof Error ? e : undefined
|
|
105
|
+
)
|
|
106
|
+
// The payment may have been captured at PayPal but the order was not
|
|
107
|
+
// finalized. Report failure (not a false success) so the storefront can
|
|
108
|
+
// surface a "payment taken, contact support" state.
|
|
109
|
+
return res.status(500).json({
|
|
110
|
+
success: false,
|
|
111
|
+
cart_id,
|
|
112
|
+
session_id: session.id,
|
|
113
|
+
message: "Payment was processed but the order could not be finalized.",
|
|
114
|
+
request_id: requestId,
|
|
115
|
+
})
|
|
107
116
|
}
|
|
108
117
|
} catch (e: any) {
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
logger.error(
|
|
119
|
+
`[paypal] paypal-complete failed (request_id=${requestId}, cart_id=${cart_id}): ${
|
|
120
|
+
e?.message ?? String(e)
|
|
121
|
+
}`,
|
|
122
|
+
e instanceof Error ? e : undefined
|
|
123
|
+
)
|
|
124
|
+
return res.status(500).json({ message: "Internal error", request_id: requestId })
|
|
111
125
|
}
|
|
112
126
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
|
2
3
|
import type PayPalModuleService from "../modules/paypal/service"
|
|
3
4
|
import {
|
|
4
5
|
computeNextRetryAt,
|
|
@@ -8,11 +9,60 @@ import {
|
|
|
8
9
|
processPayPalWebhookEvent,
|
|
9
10
|
} from "../modules/paypal/webhook-processor"
|
|
10
11
|
|
|
12
|
+
// Bound how many failed events one run drains so the cron can't load an
|
|
13
|
+
// unbounded backlog into memory.
|
|
14
|
+
const RETRY_BATCH_SIZE = 100
|
|
15
|
+
|
|
16
|
+
// Stable key for the Postgres advisory lock that serializes this cron across
|
|
17
|
+
// instances. pg_try_advisory_xact_lock auto-releases at transaction end, so
|
|
18
|
+
// there is no unlock to pair (and no risk of a leaked lock).
|
|
19
|
+
const RETRY_ADVISORY_LOCK_KEY = 838907812
|
|
20
|
+
|
|
11
21
|
export default async function paypalWebhookRetry(container: MedusaContainer) {
|
|
22
|
+
// Run the sweep under a Postgres advisory lock so that with multiple server
|
|
23
|
+
// instances running this cron, only one drains the queue per cycle (each
|
|
24
|
+
// event is processed at most once). Falls back to running directly if the
|
|
25
|
+
// raw connection isn't resolvable, so the job degrades gracefully rather
|
|
26
|
+
// than breaking.
|
|
27
|
+
let pg: any = null
|
|
28
|
+
try {
|
|
29
|
+
pg = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
|
|
30
|
+
} catch {
|
|
31
|
+
pg = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!pg?.transaction) {
|
|
35
|
+
await runRetrySweep(container)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await pg.transaction(async (trx: any) => {
|
|
41
|
+
const res = await trx.raw(
|
|
42
|
+
"SELECT pg_try_advisory_xact_lock(?) AS locked",
|
|
43
|
+
[RETRY_ADVISORY_LOCK_KEY]
|
|
44
|
+
)
|
|
45
|
+
const locked = res?.rows?.[0]?.locked ?? res?.[0]?.locked ?? false
|
|
46
|
+
if (!locked) {
|
|
47
|
+
console.info("[PayPal] webhook-retry: another instance holds the lock, skipping")
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
await runRetrySweep(container)
|
|
51
|
+
})
|
|
52
|
+
} catch (e: any) {
|
|
53
|
+
console.warn("[PayPal] webhook-retry: advisory lock path failed, running unlocked:", e?.message)
|
|
54
|
+
await runRetrySweep(container)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runRetrySweep(container: MedusaContainer) {
|
|
12
59
|
const paypal = container.resolve<PayPalModuleService>("paypal_onboarding")
|
|
13
60
|
const now = Date.now()
|
|
14
61
|
|
|
15
|
-
const candidates = await paypal.listPayPalWebhookEvents(
|
|
62
|
+
const candidates = await paypal.listPayPalWebhookEvents(
|
|
63
|
+
{ status: "failed" },
|
|
64
|
+
{ take: RETRY_BATCH_SIZE, order: { next_retry_at: "ASC" } }
|
|
65
|
+
)
|
|
16
66
|
if (!candidates?.length) return
|
|
17
67
|
|
|
18
68
|
console.info(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { paypalFetch } from "../utils/paypal-fetch"
|
|
2
|
+
|
|
1
3
|
export class PayPalSellerClient {
|
|
2
4
|
private static readonly BN_CODE = "MBJTechnolabs_SI_SPB"
|
|
3
5
|
|
|
@@ -19,7 +21,7 @@ export class PayPalSellerClient {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
async createOrder(body: any) {
|
|
22
|
-
const res = await
|
|
24
|
+
const res = await paypalFetch(`${this.baseUrl()}/v2/checkout/orders`, {
|
|
23
25
|
method: "POST",
|
|
24
26
|
headers: this.headers(),
|
|
25
27
|
body: JSON.stringify(body),
|
|
@@ -30,7 +32,7 @@ export class PayPalSellerClient {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
async getOrder(orderId: string) {
|
|
33
|
-
const res = await
|
|
35
|
+
const res = await paypalFetch(`${this.baseUrl()}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
|
|
34
36
|
method: "GET",
|
|
35
37
|
headers: this.headers(),
|
|
36
38
|
})
|
|
@@ -40,7 +42,7 @@ export class PayPalSellerClient {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
async captureOrder(orderId: string) {
|
|
43
|
-
const res = await
|
|
45
|
+
const res = await paypalFetch(`${this.baseUrl()}/v2/checkout/orders/${encodeURIComponent(orderId)}/capture`, {
|
|
44
46
|
method: "POST",
|
|
45
47
|
headers: this.headers(),
|
|
46
48
|
})
|
|
@@ -50,7 +52,7 @@ export class PayPalSellerClient {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
async refundCapture(captureId: string, body?: any) {
|
|
53
|
-
const res = await
|
|
55
|
+
const res = await paypalFetch(`${this.baseUrl()}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
|
|
54
56
|
method: "POST",
|
|
55
57
|
headers: this.headers(),
|
|
56
58
|
body: body ? JSON.stringify(body) : "{}",
|
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
} from "@medusajs/framework/types"
|
|
27
27
|
import { getPayPalWebhookActionAndData } from "./webhook-utils"
|
|
28
28
|
import { formatAmountForPayPal } from "../utils/amounts"
|
|
29
|
+
import { paypalFetch } from "../utils/paypal-fetch"
|
|
29
30
|
import {
|
|
30
31
|
assertPayPalCurrencySupported,
|
|
31
32
|
normalizeCurrencyCode,
|
|
@@ -110,7 +111,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
110
111
|
|
|
111
112
|
private async getOrderDetails(orderId: string) {
|
|
112
113
|
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
113
|
-
const resp = await
|
|
114
|
+
const resp = await paypalFetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
|
|
114
115
|
method: "GET",
|
|
115
116
|
headers: {
|
|
116
117
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -350,7 +351,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
350
351
|
: {}),
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
const ppResp = await
|
|
354
|
+
const ppResp = await paypalFetch(`${base}/v2/checkout/orders`, {
|
|
354
355
|
method: "POST",
|
|
355
356
|
headers: {
|
|
356
357
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -404,7 +405,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
404
405
|
},
|
|
405
406
|
}
|
|
406
407
|
} else {
|
|
407
|
-
const authorizeResp = await
|
|
408
|
+
const authorizeResp = await paypalFetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/authorize`, {
|
|
408
409
|
method: "POST",
|
|
409
410
|
headers: {
|
|
410
411
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -495,7 +496,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
495
496
|
order?.intent || paypalData.order?.intent || data.intent || ""
|
|
496
497
|
).toUpperCase()
|
|
497
498
|
if (!authorizationId && resolvedIntent === "AUTHORIZE") {
|
|
498
|
-
const authorizeResp = await
|
|
499
|
+
const authorizeResp = await paypalFetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/authorize`, {
|
|
499
500
|
method: "POST",
|
|
500
501
|
headers: {
|
|
501
502
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -544,7 +545,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
544
545
|
? `${base}/v2/payments/authorizations/${encodeURIComponent(authorizationId)}/capture`
|
|
545
546
|
: `${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/capture`
|
|
546
547
|
|
|
547
|
-
const ppResp = await
|
|
548
|
+
const ppResp = await paypalFetch(captureUrl, {
|
|
548
549
|
method: "POST",
|
|
549
550
|
headers: {
|
|
550
551
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -611,7 +612,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
611
612
|
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
612
613
|
const requestId = this.getIdempotencyKey(_input, `void-${authorizationId}`)
|
|
613
614
|
|
|
614
|
-
const resp = await
|
|
615
|
+
const resp = await paypalFetch(
|
|
615
616
|
`${base}/v2/payments/authorizations/${encodeURIComponent(authorizationId)}/void`,
|
|
616
617
|
{
|
|
617
618
|
method: "POST",
|
|
@@ -635,9 +636,9 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
635
636
|
}
|
|
636
637
|
} else if (captureId) {
|
|
637
638
|
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
638
|
-
const requestId = this.getIdempotencyKey(_input, `refund-${captureId}`)
|
|
639
|
+
const requestId = this.getIdempotencyKey(_input, `cancel-refund-${captureId}`)
|
|
639
640
|
|
|
640
|
-
const resp = await
|
|
641
|
+
const resp = await paypalFetch(
|
|
641
642
|
`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`,
|
|
642
643
|
{
|
|
643
644
|
method: "POST",
|
|
@@ -710,7 +711,9 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
710
711
|
}
|
|
711
712
|
|
|
712
713
|
const requestId = this.getIdempotencyKey(_input, `refund-${captureId}`)
|
|
713
|
-
|
|
714
|
+
// Use the refund amount Medusa passes (top-level input), not the session
|
|
715
|
+
// amount in `data` — otherwise a partial refund would refund the full order.
|
|
716
|
+
const amount = Number(_input.amount ?? 0)
|
|
714
717
|
const currencyOverride = await this.resolveCurrencyOverride()
|
|
715
718
|
const currencyCode = normalizeCurrencyCode(
|
|
716
719
|
data.currency_code || currencyOverride || "EUR"
|
|
@@ -726,7 +729,7 @@ class PayPalAdvancedCardProvider extends AbstractPaymentProvider<Options> {
|
|
|
726
729
|
}
|
|
727
730
|
: {}
|
|
728
731
|
|
|
729
|
-
const resp = await
|
|
732
|
+
const resp = await paypalFetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
|
|
730
733
|
method: "POST",
|
|
731
734
|
headers: {
|
|
732
735
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../utils/currencies"
|
|
32
32
|
import type PayPalModuleService from "../service"
|
|
33
33
|
import { getPayPalWebhookActionAndData } from "./webhook-utils"
|
|
34
|
+
import { paypalFetch } from "../utils/paypal-fetch"
|
|
34
35
|
|
|
35
36
|
type Options = {}
|
|
36
37
|
|
|
@@ -110,7 +111,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
|
110
111
|
|
|
111
112
|
private async getOrderDetails(orderId: string) {
|
|
112
113
|
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
113
|
-
const resp = await
|
|
114
|
+
const resp = await paypalFetch(`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}`, {
|
|
114
115
|
method: "GET",
|
|
115
116
|
headers: {
|
|
116
117
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -395,7 +396,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
|
395
396
|
data.session_id || data.cart_id || data.payment_collection_id || undefined,
|
|
396
397
|
}
|
|
397
398
|
|
|
398
|
-
const ppResp = await
|
|
399
|
+
const ppResp = await paypalFetch(`${base}/v2/checkout/orders`, {
|
|
399
400
|
method: "POST",
|
|
400
401
|
headers: {
|
|
401
402
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -430,7 +431,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
|
430
431
|
if (existingAuthorization) {
|
|
431
432
|
authorization = order
|
|
432
433
|
} else {
|
|
433
|
-
const authorizeResp = await
|
|
434
|
+
const authorizeResp = await paypalFetch(
|
|
434
435
|
`${base}/v2/checkout/orders/${encodeURIComponent(newOrderId)}/authorize`,
|
|
435
436
|
{
|
|
436
437
|
method: "POST",
|
|
@@ -613,7 +614,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
|
613
614
|
).toUpperCase()
|
|
614
615
|
|
|
615
616
|
if (!authorizationId && resolvedIntent === "AUTHORIZE") {
|
|
616
|
-
const authorizeResp = await
|
|
617
|
+
const authorizeResp = await paypalFetch(
|
|
617
618
|
`${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/authorize`,
|
|
618
619
|
{
|
|
619
620
|
method: "POST",
|
|
@@ -668,7 +669,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
|
668
669
|
? `${base}/v2/payments/authorizations/${encodeURIComponent(authorizationId)}/capture`
|
|
669
670
|
: `${base}/v2/checkout/orders/${encodeURIComponent(orderId)}/capture`
|
|
670
671
|
|
|
671
|
-
const ppResp = await
|
|
672
|
+
const ppResp = await paypalFetch(captureUrl, {
|
|
672
673
|
method: "POST",
|
|
673
674
|
headers: {
|
|
674
675
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -781,7 +782,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
|
781
782
|
refundPayload.note_to_payer = refundReason
|
|
782
783
|
}
|
|
783
784
|
|
|
784
|
-
const ppResp = await
|
|
785
|
+
const ppResp = await paypalFetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
|
|
785
786
|
method: "POST",
|
|
786
787
|
headers: {
|
|
787
788
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -871,7 +872,7 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
|
871
872
|
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
872
873
|
const requestId = this.getIdempotencyKey(input, `void-${authorizationId}`)
|
|
873
874
|
|
|
874
|
-
const resp = await
|
|
875
|
+
const resp = await paypalFetch(
|
|
875
876
|
`${base}/v2/payments/authorizations/${encodeURIComponent(authorizationId)}/void`,
|
|
876
877
|
{
|
|
877
878
|
method: "POST",
|
|
@@ -901,9 +902,9 @@ class PayPalPaymentProvider extends AbstractPaymentProvider<Options> {
|
|
|
901
902
|
})
|
|
902
903
|
} else if (captureId) {
|
|
903
904
|
const { accessToken, base } = await this.getPayPalAccessToken()
|
|
904
|
-
const requestId = this.getIdempotencyKey(input, `refund-${captureId}`)
|
|
905
|
+
const requestId = this.getIdempotencyKey(input, `cancel-refund-${captureId}`)
|
|
905
906
|
|
|
906
|
-
const resp = await
|
|
907
|
+
const resp = await paypalFetch(`${base}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
|
|
907
908
|
method: "POST",
|
|
908
909
|
headers: {
|
|
909
910
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -66,6 +66,18 @@ function resolveEventType(payload: ProviderWebhookPayload["payload"]) {
|
|
|
66
66
|
return raw?.event_type || raw?.eventType
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Maps a PayPal webhook payload to the generic Medusa payment-provider webhook
|
|
71
|
+
* result (the `getWebhookActionAndData` hook on AbstractPaymentProvider).
|
|
72
|
+
*
|
|
73
|
+
* NOTE: the authoritative webhook handling for this plugin is the dedicated
|
|
74
|
+
* `/store/paypal/webhook` route (signature verification, replay window,
|
|
75
|
+
* dedup, dead-letter retry). This helper exists only for the framework's
|
|
76
|
+
* generic `/hooks/payment/:provider` path. The `session_id` returned here is
|
|
77
|
+
* derived from the PayPal resource `custom_id`, which this plugin sets to the
|
|
78
|
+
* cart id (see create-order) — so consumers must resolve the cart, not treat
|
|
79
|
+
* it as a payment-session id directly.
|
|
80
|
+
*/
|
|
69
81
|
export function getPayPalWebhookActionAndData(
|
|
70
82
|
payload: ProviderWebhookPayload["payload"]
|
|
71
83
|
): WebhookActionResult {
|