@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.
@@ -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
  }
@@ -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 = 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
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 env = await this.getCurrentEnvironment()
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
- seller_merchant_id: (input.sellerMerchantId || "").trim() || null,
719
- 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,
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: nextCreds.seller_merchant_id,
729
- seller_email: nextCreds.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: nextCreds.seller_merchant_id,
760
- seller_email: nextCreds.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
- const sellerMerchantId: string | null = c.sellerMerchantId || row.seller_merchant_id || 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
+ }
969
1190
 
970
- if (!sellerEmail && hasCreds && sellerMerchantId) {
1191
+ if (!sellerEmail && hasCreds) {
971
1192
  try {
972
- const details = await this.fetchMerchantIntegrationDetails(env, sellerMerchantId)
973
- const fetchedEmail = this.extractSellerEmail(details)
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 merchant_id email lookup failed:", e?.message || e)
1212
+ console.warn("[PayPal] status direct credential lookup failed:", e?.message || e)
985
1213
  }
986
1214
  }
987
1215