@easypayment/medusa-paypal 0.5.8 → 0.6.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.
- package/.medusa/server/src/admin/index.js +9 -16
- package/.medusa/server/src/admin/index.mjs +9 -16
- package/.medusa/server/src/api/admin/paypal/save-credentials/route.d.ts.map +1 -1
- package/.medusa/server/src/api/admin/paypal/save-credentials/route.js +5 -1
- package/.medusa/server/src/api/admin/paypal/save-credentials/route.js.map +1 -1
- package/.medusa/server/src/modules/paypal/service.d.ts +31 -0
- package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
- package/.medusa/server/src/modules/paypal/service.js +201 -22
- package/.medusa/server/src/modules/paypal/service.js.map +1 -1
- package/package.json +4 -6
- package/src/admin/routes/settings/paypal/connection/page.tsx +747 -754
- package/src/api/admin/paypal/save-credentials/route.ts +22 -14
- package/src/modules/paypal/service.ts +250 -22
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
-
import type PayPalModuleService from "../../../../modules/paypal/service"
|
|
3
|
-
|
|
4
|
-
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
5
|
-
const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
|
|
6
|
-
const body = req.body as {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
1
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
2
|
+
import type PayPalModuleService from "../../../../modules/paypal/service"
|
|
3
|
+
|
|
4
|
+
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
5
|
+
const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
|
|
6
|
+
const body = req.body as {
|
|
7
|
+
clientId?: string
|
|
8
|
+
clientSecret?: string
|
|
9
|
+
environment?: "sandbox" | "live"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!body?.clientId || !body?.clientSecret) {
|
|
13
|
+
return res.status(400).json({ message: "Missing clientId/clientSecret" })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await paypal.saveAndHydrateSellerCredentials({
|
|
17
|
+
clientId: body.clientId,
|
|
18
|
+
clientSecret: body.clientSecret,
|
|
19
|
+
environment: body.environment,
|
|
20
|
+
})
|
|
21
|
+
return res.json({ ok: true })
|
|
22
|
+
}
|
|
@@ -185,7 +185,13 @@ class PayPalModuleService extends MedusaService({
|
|
|
185
185
|
return {
|
|
186
186
|
clientId: creds.client_id || creds.clientId || undefined,
|
|
187
187
|
clientSecret: creds.client_secret || creds.clientSecret || undefined,
|
|
188
|
-
sellerMerchantId:
|
|
188
|
+
sellerMerchantId:
|
|
189
|
+
creds.seller_merchant_id ||
|
|
190
|
+
creds.sellerMerchantId ||
|
|
191
|
+
creds.payer_id ||
|
|
192
|
+
creds.merchant_id ||
|
|
193
|
+
creds.merchantId ||
|
|
194
|
+
undefined,
|
|
189
195
|
sellerEmail: creds.seller_email || creds.sellerEmail || undefined,
|
|
190
196
|
}
|
|
191
197
|
}
|
|
@@ -289,6 +295,155 @@ class PayPalModuleService extends MedusaService({
|
|
|
289
295
|
return json
|
|
290
296
|
}
|
|
291
297
|
|
|
298
|
+
private async getAppAccessTokenForCredentials(
|
|
299
|
+
env: Environment,
|
|
300
|
+
credentials: { clientId: string; clientSecret: string }
|
|
301
|
+
) {
|
|
302
|
+
const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
|
|
303
|
+
const basic = Buffer.from(`${credentials.clientId}:${credentials.clientSecret}`).toString("base64")
|
|
304
|
+
|
|
305
|
+
const body = new URLSearchParams()
|
|
306
|
+
body.set("grant_type", "client_credentials")
|
|
307
|
+
|
|
308
|
+
const res = await fetch(`${baseUrl}/v1/oauth2/token`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: {
|
|
311
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
312
|
+
Authorization: `Basic ${basic}`,
|
|
313
|
+
},
|
|
314
|
+
body,
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const text = await res.text().catch(() => "")
|
|
318
|
+
let json: any = {}
|
|
319
|
+
try {
|
|
320
|
+
json = text ? JSON.parse(text) : {}
|
|
321
|
+
} catch (e: any) {
|
|
322
|
+
console.warn("[PayPal] Failed to parse app token response JSON:", e?.message)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!res.ok) {
|
|
326
|
+
throw new Error(`PayPal client_credentials failed (${res.status}): ${text || JSON.stringify(json)}`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const accessToken = String(json.access_token || "")
|
|
330
|
+
if (!accessToken) {
|
|
331
|
+
throw new Error("PayPal client_credentials succeeded but access_token is missing.")
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { accessToken, tokenPayload: json }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async fetchSellerProfileFromDirectCredentials(
|
|
338
|
+
env: Environment,
|
|
339
|
+
credentials?: { clientId: string; clientSecret: string }
|
|
340
|
+
): Promise<{ sellerMerchantId: string | null; sellerEmail: string | null }> {
|
|
341
|
+
const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
|
|
342
|
+
const partnerMerchantId = await this.getPartnerMerchantId(env)
|
|
343
|
+
let tokenPayload: Record<string, any> | null = null
|
|
344
|
+
|
|
345
|
+
let accessToken = ""
|
|
346
|
+
if (credentials) {
|
|
347
|
+
const tokenResp = await this.getAppAccessTokenForCredentials(env, {
|
|
348
|
+
clientId: credentials.clientId,
|
|
349
|
+
clientSecret: credentials.clientSecret,
|
|
350
|
+
})
|
|
351
|
+
accessToken = tokenResp.accessToken
|
|
352
|
+
tokenPayload = tokenResp.tokenPayload
|
|
353
|
+
} else {
|
|
354
|
+
accessToken = await this.getAppAccessToken()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let sellerEmail = this.extractSellerEmail(tokenPayload || undefined)
|
|
358
|
+
let sellerMerchantId = String(
|
|
359
|
+
tokenPayload?.merchant_id || tokenPayload?.payer_id || tokenPayload?.account_id || ""
|
|
360
|
+
).trim() || null
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const userInfoResp = await fetch(`${baseUrl}/v1/identity/oauth2/userinfo?schema=paypalv1`, {
|
|
364
|
+
method: "GET",
|
|
365
|
+
headers: {
|
|
366
|
+
Authorization: `Bearer ${accessToken}`,
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
Accept: "application/json",
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
if (userInfoResp.ok) {
|
|
372
|
+
const userInfo = await userInfoResp.json().catch(() => ({}))
|
|
373
|
+
sellerEmail = sellerEmail || this.extractSellerEmail(userInfo)
|
|
374
|
+
sellerMerchantId =
|
|
375
|
+
sellerMerchantId ||
|
|
376
|
+
String(userInfo?.merchant_id || userInfo?.payer_id || userInfo?.user_id || userInfo?.sub || "").trim() ||
|
|
377
|
+
null
|
|
378
|
+
}
|
|
379
|
+
} catch (e: any) {
|
|
380
|
+
console.warn("[PayPal] userinfo lookup failed:", e?.message || e)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (partnerMerchantId) {
|
|
384
|
+
try {
|
|
385
|
+
const credsResp = await fetch(
|
|
386
|
+
`${baseUrl}/v1/customer/partners/${encodeURIComponent(
|
|
387
|
+
partnerMerchantId
|
|
388
|
+
)}/merchant-integrations/credentials/`,
|
|
389
|
+
{
|
|
390
|
+
method: "GET",
|
|
391
|
+
headers: {
|
|
392
|
+
Authorization: `Bearer ${accessToken}`,
|
|
393
|
+
"Content-Type": "application/json",
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
if (credsResp.ok) {
|
|
398
|
+
const credsJson = await credsResp.json().catch(() => ({}))
|
|
399
|
+
sellerEmail = sellerEmail || this.extractSellerEmail(credsJson)
|
|
400
|
+
sellerMerchantId = sellerMerchantId || String(credsJson?.merchant_id || "").trim() || null
|
|
401
|
+
}
|
|
402
|
+
} catch (e: any) {
|
|
403
|
+
console.warn("[PayPal] direct credential profile lookup failed:", e?.message || e)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const hydrated = await this.hydrateSellerMetadataFromCredentials(env, {
|
|
408
|
+
accessToken,
|
|
409
|
+
sellerMerchantId,
|
|
410
|
+
sellerEmail,
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
sellerMerchantId: hydrated.sellerMerchantId,
|
|
415
|
+
sellerEmail: hydrated.sellerEmail,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async hydrateSellerMetadataFromCredentials(
|
|
420
|
+
env: Environment,
|
|
421
|
+
input: {
|
|
422
|
+
accessToken?: string
|
|
423
|
+
sellerMerchantId?: string | null
|
|
424
|
+
sellerEmail?: string | null
|
|
425
|
+
metadataCandidates?: any[]
|
|
426
|
+
}
|
|
427
|
+
): Promise<{ sellerMerchantId: string | null; sellerEmail: string | null }> {
|
|
428
|
+
let sellerMerchantId = (input.sellerMerchantId || "").trim() || null
|
|
429
|
+
let sellerEmail = (input.sellerEmail || "").trim() || null
|
|
430
|
+
|
|
431
|
+
if (!sellerEmail && input.metadataCandidates?.length) {
|
|
432
|
+
sellerEmail = this.extractSellerEmail(...input.metadataCandidates)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (sellerMerchantId && !sellerEmail) {
|
|
436
|
+
try {
|
|
437
|
+
const details = await this.fetchMerchantIntegrationDetails(env, sellerMerchantId, input.accessToken)
|
|
438
|
+
sellerEmail = this.extractSellerEmail(details)
|
|
439
|
+
} catch (e: any) {
|
|
440
|
+
console.warn("[PayPal] merchant integration lookup failed:", e?.message || e)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return { sellerMerchantId, sellerEmail }
|
|
445
|
+
}
|
|
446
|
+
|
|
292
447
|
private async syncRowFieldsFromMetadata(row: any, env: Environment) {
|
|
293
448
|
const c = this.getEnvCreds(row, env)
|
|
294
449
|
await this.updatePayPalConnections({
|
|
@@ -641,8 +796,8 @@ class PayPalModuleService extends MedusaService({
|
|
|
641
796
|
)
|
|
642
797
|
}
|
|
643
798
|
|
|
644
|
-
const clientId = String(credJson.client_id || "")
|
|
645
|
-
const clientSecret = String(credJson.client_secret || "")
|
|
799
|
+
const clientId = String(credJson.client_id || credJson.clientId || "")
|
|
800
|
+
const clientSecret = String(credJson.client_secret || credJson.clientSecret || "")
|
|
646
801
|
if (!clientId || !clientSecret) {
|
|
647
802
|
throw new Error(
|
|
648
803
|
`PayPal credentials response missing client_id/client_secret. Keys: ${Object.keys(credJson || {}).join(", ")}`
|
|
@@ -650,14 +805,17 @@ class PayPalModuleService extends MedusaService({
|
|
|
650
805
|
}
|
|
651
806
|
|
|
652
807
|
let sellerEmail = this.extractSellerEmail(credJson, tokenJson)
|
|
653
|
-
let sellerMerchantId =
|
|
808
|
+
let sellerMerchantId =
|
|
809
|
+
String(credJson.payer_id || credJson.merchant_id || tokenJson.payer_id || tokenJson.merchant_id || "").trim() ||
|
|
810
|
+
null
|
|
654
811
|
|
|
655
812
|
if (!sellerEmail) {
|
|
656
813
|
// Use all available merchant/payer ID candidates from the token and credential responses
|
|
657
814
|
const merchantCandidates = [
|
|
815
|
+
String(credJson.payer_id || "").trim(),
|
|
658
816
|
String(credJson.merchant_id || "").trim(),
|
|
659
|
-
String(tokenJson.merchant_id || "").trim(),
|
|
660
817
|
String(tokenJson.payer_id || "").trim(),
|
|
818
|
+
String(tokenJson.merchant_id || "").trim(),
|
|
661
819
|
].filter(Boolean).filter((v, i, arr) => arr.indexOf(v) === i)
|
|
662
820
|
|
|
663
821
|
for (const merchantId of merchantCandidates) {
|
|
@@ -707,16 +865,31 @@ class PayPalModuleService extends MedusaService({
|
|
|
707
865
|
clientSecret: string
|
|
708
866
|
sellerMerchantId?: string | null
|
|
709
867
|
sellerEmail?: string | null
|
|
868
|
+
environment?: Environment
|
|
710
869
|
}) {
|
|
711
870
|
const row = await this.getCurrentRow()
|
|
712
|
-
const
|
|
871
|
+
const currentEnv = await this.getCurrentEnvironment()
|
|
872
|
+
const env = (input.environment || currentEnv) as Environment
|
|
713
873
|
|
|
714
874
|
const encryptedSecret = this.maybeEncryptSecret(input.clientSecret)
|
|
875
|
+
const existingCreds = row ? this.getEnvCreds(row, env) : {}
|
|
876
|
+
const nextSellerMerchantId =
|
|
877
|
+
(input.sellerMerchantId || "").trim() || existingCreds.sellerMerchantId || row?.seller_merchant_id || null
|
|
878
|
+
const nextSellerEmail =
|
|
879
|
+
(input.sellerEmail || "").trim() || existingCreds.sellerEmail || row?.seller_email || null
|
|
880
|
+
|
|
715
881
|
const nextCreds = {
|
|
716
882
|
client_id: input.clientId,
|
|
883
|
+
clientId: input.clientId,
|
|
717
884
|
client_secret: encryptedSecret,
|
|
718
|
-
|
|
719
|
-
|
|
885
|
+
clientSecret: encryptedSecret,
|
|
886
|
+
merchantId: nextSellerMerchantId,
|
|
887
|
+
merchant_id: nextSellerMerchantId,
|
|
888
|
+
payer_id: nextSellerMerchantId,
|
|
889
|
+
seller_merchant_id: nextSellerMerchantId,
|
|
890
|
+
sellerMerchantId: nextSellerMerchantId,
|
|
891
|
+
seller_email: nextSellerEmail,
|
|
892
|
+
sellerEmail: nextSellerEmail,
|
|
720
893
|
}
|
|
721
894
|
|
|
722
895
|
if (!row) {
|
|
@@ -725,8 +898,8 @@ class PayPalModuleService extends MedusaService({
|
|
|
725
898
|
status: "connected",
|
|
726
899
|
seller_client_id: input.clientId,
|
|
727
900
|
seller_client_secret: encryptedSecret,
|
|
728
|
-
seller_merchant_id:
|
|
729
|
-
seller_email:
|
|
901
|
+
seller_merchant_id: nextSellerMerchantId,
|
|
902
|
+
seller_email: nextSellerEmail,
|
|
730
903
|
app_access_token: null,
|
|
731
904
|
app_access_token_expires_at: null,
|
|
732
905
|
metadata: {
|
|
@@ -756,8 +929,8 @@ class PayPalModuleService extends MedusaService({
|
|
|
756
929
|
status: "connected",
|
|
757
930
|
seller_client_id: input.clientId,
|
|
758
931
|
seller_client_secret: encryptedSecret,
|
|
759
|
-
seller_merchant_id:
|
|
760
|
-
seller_email:
|
|
932
|
+
seller_merchant_id: nextSellerMerchantId,
|
|
933
|
+
seller_email: nextSellerEmail,
|
|
761
934
|
app_access_token: null,
|
|
762
935
|
app_access_token_expires_at: null,
|
|
763
936
|
metadata: {
|
|
@@ -774,6 +947,46 @@ class PayPalModuleService extends MedusaService({
|
|
|
774
947
|
return updated
|
|
775
948
|
}
|
|
776
949
|
|
|
950
|
+
async saveAndHydrateSellerCredentials(input: {
|
|
951
|
+
clientId: string
|
|
952
|
+
clientSecret: string
|
|
953
|
+
environment?: Environment
|
|
954
|
+
}) {
|
|
955
|
+
const env = (input.environment || (await this.getCurrentEnvironment())) as Environment
|
|
956
|
+
|
|
957
|
+
await this.saveSellerCredentials({
|
|
958
|
+
clientId: input.clientId,
|
|
959
|
+
clientSecret: input.clientSecret,
|
|
960
|
+
environment: env,
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
const hydrated = await this.fetchSellerProfileFromDirectCredentials(env, {
|
|
965
|
+
clientId: input.clientId,
|
|
966
|
+
clientSecret: input.clientSecret,
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
if (hydrated.sellerEmail || hydrated.sellerMerchantId) {
|
|
970
|
+
await this.saveSellerCredentials({
|
|
971
|
+
clientId: input.clientId,
|
|
972
|
+
clientSecret: input.clientSecret,
|
|
973
|
+
sellerMerchantId: hydrated.sellerMerchantId,
|
|
974
|
+
sellerEmail: hydrated.sellerEmail,
|
|
975
|
+
environment: env,
|
|
976
|
+
})
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const refreshedRow = await this.getCurrentRow()
|
|
980
|
+
if (refreshedRow) {
|
|
981
|
+
await this.syncRowFieldsFromMetadata(refreshedRow, env)
|
|
982
|
+
}
|
|
983
|
+
} catch (e: any) {
|
|
984
|
+
console.warn("[PayPal] saveAndHydrateSellerCredentials lookup failed:", e?.message || e)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return await this.getStatus(env)
|
|
988
|
+
}
|
|
989
|
+
|
|
777
990
|
private async resolveWebhookUrl() {
|
|
778
991
|
const { onboarding } = await this.ensureSettingsDefaults()
|
|
779
992
|
const base = String(onboarding.backend_url || "").replace(/\/$/, "")
|
|
@@ -965,23 +1178,38 @@ class PayPalModuleService extends MedusaService({
|
|
|
965
1178
|
const c = this.getEnvCreds(row, env)
|
|
966
1179
|
const hasCreds = !!(c.clientId && c.clientSecret)
|
|
967
1180
|
let sellerEmail: string | null = c.sellerEmail || row.seller_email || null
|
|
968
|
-
|
|
1181
|
+
let sellerMerchantId: string | null = c.sellerMerchantId || row.seller_merchant_id || null
|
|
1182
|
+
let decryptedSecret: string | null = null
|
|
1183
|
+
if (c.clientSecret) {
|
|
1184
|
+
try {
|
|
1185
|
+
decryptedSecret = this.maybeDecryptSecret(c.clientSecret)
|
|
1186
|
+
} catch (e: any) {
|
|
1187
|
+
console.warn("[PayPal] Unable to decrypt seller client secret for status sync:", e?.message || e)
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
969
1190
|
|
|
970
|
-
if (!sellerEmail && hasCreds
|
|
1191
|
+
if (!sellerEmail && hasCreds) {
|
|
971
1192
|
try {
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
if (fetchedEmail) {
|
|
975
|
-
sellerEmail = fetchedEmail
|
|
1193
|
+
const hydrated = await this.fetchSellerProfileFromDirectCredentials(env)
|
|
1194
|
+
if (hydrated.sellerEmail || hydrated.sellerMerchantId) {
|
|
976
1195
|
await this.saveSellerCredentials({
|
|
977
1196
|
clientId: c.clientId!,
|
|
978
|
-
clientSecret: c.clientSecret!,
|
|
979
|
-
sellerMerchantId,
|
|
980
|
-
sellerEmail,
|
|
1197
|
+
clientSecret: decryptedSecret || c.clientSecret!,
|
|
1198
|
+
sellerMerchantId: hydrated.sellerMerchantId || sellerMerchantId,
|
|
1199
|
+
sellerEmail: hydrated.sellerEmail || sellerEmail,
|
|
1200
|
+
environment: env,
|
|
981
1201
|
})
|
|
1202
|
+
|
|
1203
|
+
const refreshedRow = await this.getCurrentRow()
|
|
1204
|
+
if (refreshedRow) {
|
|
1205
|
+
const refreshedCreds = this.getEnvCreds(refreshedRow, env)
|
|
1206
|
+
sellerEmail = refreshedCreds.sellerEmail || refreshedRow.seller_email || sellerEmail
|
|
1207
|
+
sellerMerchantId =
|
|
1208
|
+
refreshedCreds.sellerMerchantId || refreshedRow.seller_merchant_id || sellerMerchantId
|
|
1209
|
+
}
|
|
982
1210
|
}
|
|
983
1211
|
} catch (e: any) {
|
|
984
|
-
console.warn("[PayPal] status
|
|
1212
|
+
console.warn("[PayPal] status direct credential lookup failed:", e?.message || e)
|
|
985
1213
|
}
|
|
986
1214
|
}
|
|
987
1215
|
|