@easypayment/medusa-paypal 0.5.7 → 0.5.9

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,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 { clientId?: string; clientSecret?: string }
7
-
8
- if (!body?.clientId || !body?.clientSecret) {
9
- return res.status(400).json({ message: "Missing clientId/clientSecret" })
10
- }
11
-
12
- await paypal.saveSellerCredentials({ clientId: body.clientId, clientSecret: body.clientSecret })
13
- return res.json({ ok: true })
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: creds.seller_merchant_id || creds.sellerMerchantId || undefined,
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
  }
@@ -221,15 +227,23 @@ class PayPalModuleService extends MedusaService({
221
227
  obj.email_address,
222
228
  obj.account_email,
223
229
  obj.contact_email,
230
+ obj.value,
231
+ obj.address,
224
232
  ]
225
233
  queue.push(...prioritized)
226
234
 
227
235
  for (const [k, v] of Object.entries(obj)) {
228
236
  const key = String(k).toLowerCase()
229
- if (key.includes("email")) {
237
+ if (key.includes("email") || key.includes("address")) {
230
238
  queue.push(v)
231
239
  }
232
240
  }
241
+
242
+ // Fallback: traverse nested values so shapes like
243
+ // { email_address: { value: "seller@example.com" } }
244
+ // or { email: { address: "seller@example.com" } }
245
+ // are still detected.
246
+ queue.push(...Object.values(obj))
233
247
  }
234
248
  }
235
249
 
@@ -281,6 +295,155 @@ class PayPalModuleService extends MedusaService({
281
295
  return json
282
296
  }
283
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
+
284
447
  private async syncRowFieldsFromMetadata(row: any, env: Environment) {
285
448
  const c = this.getEnvCreds(row, env)
286
449
  await this.updatePayPalConnections({
@@ -633,8 +796,8 @@ class PayPalModuleService extends MedusaService({
633
796
  )
634
797
  }
635
798
 
636
- const clientId = String(credJson.client_id || "")
637
- const clientSecret = String(credJson.client_secret || "")
799
+ const clientId = String(credJson.client_id || credJson.clientId || "")
800
+ const clientSecret = String(credJson.client_secret || credJson.clientSecret || "")
638
801
  if (!clientId || !clientSecret) {
639
802
  throw new Error(
640
803
  `PayPal credentials response missing client_id/client_secret. Keys: ${Object.keys(credJson || {}).join(", ")}`
@@ -642,14 +805,17 @@ class PayPalModuleService extends MedusaService({
642
805
  }
643
806
 
644
807
  let sellerEmail = this.extractSellerEmail(credJson, tokenJson)
645
- let sellerMerchantId = String(credJson.merchant_id || tokenJson.merchant_id || tokenJson.payer_id || "").trim() || null
808
+ let sellerMerchantId =
809
+ String(credJson.payer_id || credJson.merchant_id || tokenJson.payer_id || tokenJson.merchant_id || "").trim() ||
810
+ null
646
811
 
647
812
  if (!sellerEmail) {
648
813
  // Use all available merchant/payer ID candidates from the token and credential responses
649
814
  const merchantCandidates = [
815
+ String(credJson.payer_id || "").trim(),
650
816
  String(credJson.merchant_id || "").trim(),
651
- String(tokenJson.merchant_id || "").trim(),
652
817
  String(tokenJson.payer_id || "").trim(),
818
+ String(tokenJson.merchant_id || "").trim(),
653
819
  ].filter(Boolean).filter((v, i, arr) => arr.indexOf(v) === i)
654
820
 
655
821
  for (const merchantId of merchantCandidates) {
@@ -675,44 +841,19 @@ class PayPalModuleService extends MedusaService({
675
841
  sellerEmail,
676
842
  })
677
843
 
678
- // 5) Post-save email lookup via tracking_id (sharedId).
679
- // PayPal's partner list endpoint accepts tracking_id and returns the full
680
- // merchant record including primary_email works even when merchant_id
681
- // and payer_id are absent from token/credential responses (common in sandbox).
682
- if (!sellerEmail && input.sharedId) {
844
+ // 5) Post-save email lookup via merchant_id.
845
+ // Some partner accounts do not have permission to query by tracking_id,
846
+ // so we only rely on merchant integration detail lookup.
847
+ if (!sellerEmail && sellerMerchantId) {
683
848
  try {
684
849
  const appAccessToken = await this.getAppAccessToken()
685
- const { onboarding: onboardingCfg } = await this.ensureSettingsDefaults()
686
- const trackingRes = await fetch(
687
- `${baseUrl}/v1/customer/partners/${encodeURIComponent(partnerMerchantId)}/merchant-integrations?tracking_id=${encodeURIComponent(input.sharedId)}`,
688
- {
689
- method: "GET",
690
- headers: {
691
- "Content-Type": "application/json",
692
- Authorization: `Bearer ${appAccessToken}`,
693
- ...(onboardingCfg.bn_code ? { "PayPal-Partner-Attribution-Id": onboardingCfg.bn_code } : {}),
694
- },
695
- }
696
- )
697
- if (trackingRes.ok) {
698
- const trackingJson = await trackingRes.json().catch(() => ({}))
699
- sellerEmail = this.extractSellerEmail(trackingJson)
700
- const foundMerchantId = String(trackingJson.merchant_id || "").trim()
701
- if (foundMerchantId) {
702
- sellerMerchantId = sellerMerchantId || foundMerchantId
703
- }
704
- if (!sellerEmail && foundMerchantId) {
705
- const details = await this.fetchMerchantIntegrationDetails(env, foundMerchantId, appAccessToken)
706
- sellerEmail = this.extractSellerEmail(details)
707
- }
708
- if (sellerEmail || sellerMerchantId) {
709
- await this.saveSellerCredentials({ clientId, clientSecret, sellerMerchantId, sellerEmail })
710
- }
711
- } else {
712
- console.warn(`[PayPal] tracking_id lookup failed (${trackingRes.status})`)
850
+ const details = await this.fetchMerchantIntegrationDetails(env, sellerMerchantId, appAccessToken)
851
+ sellerEmail = this.extractSellerEmail(details)
852
+ if (sellerEmail) {
853
+ await this.saveSellerCredentials({ clientId, clientSecret, sellerMerchantId, sellerEmail })
713
854
  }
714
855
  } catch (e: any) {
715
- console.warn("[PayPal] Post-save tracking_id email lookup failed:", e?.message || e)
856
+ console.warn("[PayPal] Post-save merchant_id email lookup failed:", e?.message || e)
716
857
  }
717
858
  }
718
859
 
@@ -724,16 +865,31 @@ class PayPalModuleService extends MedusaService({
724
865
  clientSecret: string
725
866
  sellerMerchantId?: string | null
726
867
  sellerEmail?: string | null
868
+ environment?: Environment
727
869
  }) {
728
870
  const row = await this.getCurrentRow()
729
- const env = await this.getCurrentEnvironment()
871
+ const currentEnv = await this.getCurrentEnvironment()
872
+ const env = (input.environment || currentEnv) as Environment
730
873
 
731
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
+
732
881
  const nextCreds = {
733
882
  client_id: input.clientId,
883
+ clientId: input.clientId,
734
884
  client_secret: encryptedSecret,
735
- seller_merchant_id: (input.sellerMerchantId || "").trim() || null,
736
- seller_email: (input.sellerEmail || "").trim() || null,
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,
737
893
  }
738
894
 
739
895
  if (!row) {
@@ -742,8 +898,8 @@ class PayPalModuleService extends MedusaService({
742
898
  status: "connected",
743
899
  seller_client_id: input.clientId,
744
900
  seller_client_secret: encryptedSecret,
745
- seller_merchant_id: nextCreds.seller_merchant_id,
746
- seller_email: nextCreds.seller_email,
901
+ seller_merchant_id: nextSellerMerchantId,
902
+ seller_email: nextSellerEmail,
747
903
  app_access_token: null,
748
904
  app_access_token_expires_at: null,
749
905
  metadata: {
@@ -773,8 +929,8 @@ class PayPalModuleService extends MedusaService({
773
929
  status: "connected",
774
930
  seller_client_id: input.clientId,
775
931
  seller_client_secret: encryptedSecret,
776
- seller_merchant_id: nextCreds.seller_merchant_id,
777
- seller_email: nextCreds.seller_email,
932
+ seller_merchant_id: nextSellerMerchantId,
933
+ seller_email: nextSellerEmail,
778
934
  app_access_token: null,
779
935
  app_access_token_expires_at: null,
780
936
  metadata: {
@@ -791,6 +947,46 @@ class PayPalModuleService extends MedusaService({
791
947
  return updated
792
948
  }
793
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
+
794
990
  private async resolveWebhookUrl() {
795
991
  const { onboarding } = await this.ensureSettingsDefaults()
796
992
  const base = String(onboarding.backend_url || "").replace(/\/$/, "")
@@ -981,7 +1177,41 @@ class PayPalModuleService extends MedusaService({
981
1177
 
982
1178
  const c = this.getEnvCreds(row, env)
983
1179
  const hasCreds = !!(c.clientId && c.clientSecret)
984
- const sellerEmail: string | null = c.sellerEmail || row.seller_email || null
1180
+ let sellerEmail: string | null = c.sellerEmail || row.seller_email || null
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
+ }
1190
+
1191
+ if (!sellerEmail && hasCreds) {
1192
+ try {
1193
+ const hydrated = await this.fetchSellerProfileFromDirectCredentials(env)
1194
+ if (hydrated.sellerEmail || hydrated.sellerMerchantId) {
1195
+ await this.saveSellerCredentials({
1196
+ clientId: c.clientId!,
1197
+ clientSecret: decryptedSecret || c.clientSecret!,
1198
+ sellerMerchantId: hydrated.sellerMerchantId || sellerMerchantId,
1199
+ sellerEmail: hydrated.sellerEmail || sellerEmail,
1200
+ environment: env,
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
+ }
1210
+ }
1211
+ } catch (e: any) {
1212
+ console.warn("[PayPal] status direct credential lookup failed:", e?.message || e)
1213
+ }
1214
+ }
985
1215
 
986
1216
  return {
987
1217
  environment: env,
@@ -991,6 +1221,7 @@ class PayPalModuleService extends MedusaService({
991
1221
  seller_client_id_present: hasCreds,
992
1222
  seller_client_id_masked: this.maskValue(c.clientId),
993
1223
  seller_client_secret_masked: c.clientSecret ? "••••••••" : null,
1224
+ seller_merchant_id: sellerMerchantId,
994
1225
  seller_email: sellerEmail,
995
1226
  updated_at: (row.updated_at as any)?.toISOString?.() ?? null,
996
1227
  }