@easypayment/medusa-payment-paypal 0.7.4

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.
Files changed (268) hide show
  1. package/.medusa/server/src/admin/index.js +1267 -0
  2. package/.medusa/server/src/admin/index.mjs +1268 -0
  3. package/.medusa/server/src/api/admin/payment-collections/[id]/payment-sessions/route.d.ts +3 -0
  4. package/.medusa/server/src/api/admin/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -0
  5. package/.medusa/server/src/api/admin/payment-collections/[id]/payment-sessions/route.js +18 -0
  6. package/.medusa/server/src/api/admin/payment-collections/[id]/payment-sessions/route.js.map +1 -0
  7. package/.medusa/server/src/api/admin/paypal/disconnect/route.d.ts +3 -0
  8. package/.medusa/server/src/api/admin/paypal/disconnect/route.d.ts.map +1 -0
  9. package/.medusa/server/src/api/admin/paypal/disconnect/route.js +9 -0
  10. package/.medusa/server/src/api/admin/paypal/disconnect/route.js.map +1 -0
  11. package/.medusa/server/src/api/admin/paypal/environment/route.d.ts +4 -0
  12. package/.medusa/server/src/api/admin/paypal/environment/route.d.ts.map +1 -0
  13. package/.medusa/server/src/api/admin/paypal/environment/route.js +23 -0
  14. package/.medusa/server/src/api/admin/paypal/environment/route.js.map +1 -0
  15. package/.medusa/server/src/api/admin/paypal/onboard-complete/route.d.ts +4 -0
  16. package/.medusa/server/src/api/admin/paypal/onboard-complete/route.d.ts.map +1 -0
  17. package/.medusa/server/src/api/admin/paypal/onboard-complete/route.js +31 -0
  18. package/.medusa/server/src/api/admin/paypal/onboard-complete/route.js.map +1 -0
  19. package/.medusa/server/src/api/admin/paypal/onboarding-link/route.d.ts +4 -0
  20. package/.medusa/server/src/api/admin/paypal/onboarding-link/route.d.ts.map +1 -0
  21. package/.medusa/server/src/api/admin/paypal/onboarding-link/route.js +35 -0
  22. package/.medusa/server/src/api/admin/paypal/onboarding-link/route.js.map +1 -0
  23. package/.medusa/server/src/api/admin/paypal/onboarding-status/route.d.ts +3 -0
  24. package/.medusa/server/src/api/admin/paypal/onboarding-status/route.d.ts.map +1 -0
  25. package/.medusa/server/src/api/admin/paypal/onboarding-status/route.js +19 -0
  26. package/.medusa/server/src/api/admin/paypal/onboarding-status/route.js.map +1 -0
  27. package/.medusa/server/src/api/admin/paypal/save-credentials/route.d.ts +3 -0
  28. package/.medusa/server/src/api/admin/paypal/save-credentials/route.d.ts.map +1 -0
  29. package/.medusa/server/src/api/admin/paypal/save-credentials/route.js +17 -0
  30. package/.medusa/server/src/api/admin/paypal/save-credentials/route.js.map +1 -0
  31. package/.medusa/server/src/api/admin/paypal/settings/route.d.ts +4 -0
  32. package/.medusa/server/src/api/admin/paypal/settings/route.d.ts.map +1 -0
  33. package/.medusa/server/src/api/admin/paypal/settings/route.js +14 -0
  34. package/.medusa/server/src/api/admin/paypal/settings/route.js.map +1 -0
  35. package/.medusa/server/src/api/admin/paypal/status/route.d.ts +3 -0
  36. package/.medusa/server/src/api/admin/paypal/status/route.d.ts.map +1 -0
  37. package/.medusa/server/src/api/admin/paypal/status/route.js +11 -0
  38. package/.medusa/server/src/api/admin/paypal/status/route.js.map +1 -0
  39. package/.medusa/server/src/api/middlewares.d.ts +3 -0
  40. package/.medusa/server/src/api/middlewares.d.ts.map +1 -0
  41. package/.medusa/server/src/api/middlewares.js +16 -0
  42. package/.medusa/server/src/api/middlewares.js.map +1 -0
  43. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts +3 -0
  44. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -0
  45. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js +43 -0
  46. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js.map +1 -0
  47. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts +3 -0
  48. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -0
  49. package/.medusa/server/src/api/store/paypal/capture-order/route.js +204 -0
  50. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -0
  51. package/.medusa/server/src/api/store/paypal/config/route.d.ts +3 -0
  52. package/.medusa/server/src/api/store/paypal/config/route.d.ts.map +1 -0
  53. package/.medusa/server/src/api/store/paypal/config/route.js +77 -0
  54. package/.medusa/server/src/api/store/paypal/config/route.js.map +1 -0
  55. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts +3 -0
  56. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -0
  57. package/.medusa/server/src/api/store/paypal/create-order/route.js +361 -0
  58. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -0
  59. package/.medusa/server/src/api/store/paypal/settings/route.d.ts +3 -0
  60. package/.medusa/server/src/api/store/paypal/settings/route.d.ts.map +1 -0
  61. package/.medusa/server/src/api/store/paypal/settings/route.js +20 -0
  62. package/.medusa/server/src/api/store/paypal/settings/route.js.map +1 -0
  63. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts +3 -0
  64. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -0
  65. package/.medusa/server/src/api/store/paypal/webhook/route.js +250 -0
  66. package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -0
  67. package/.medusa/server/src/api/store/paypal-complete/route.d.ts +3 -0
  68. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -0
  69. package/.medusa/server/src/api/store/paypal-complete/route.js +76 -0
  70. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -0
  71. package/.medusa/server/src/index.d.ts +2 -0
  72. package/.medusa/server/src/index.d.ts.map +1 -0
  73. package/.medusa/server/src/index.js +9 -0
  74. package/.medusa/server/src/index.js.map +1 -0
  75. package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts +7 -0
  76. package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -0
  77. package/.medusa/server/src/jobs/paypal-webhook-retry.js +132 -0
  78. package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -0
  79. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.d.ts +15 -0
  80. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.d.ts.map +1 -0
  81. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js +67 -0
  82. package/.medusa/server/src/modules/paypal/clients/paypal-seller.client.js.map +1 -0
  83. package/.medusa/server/src/modules/paypal/index.d.ts +64 -0
  84. package/.medusa/server/src/modules/paypal/index.d.ts.map +1 -0
  85. package/.medusa/server/src/modules/paypal/index.js +13 -0
  86. package/.medusa/server/src/modules/paypal/index.js.map +1 -0
  87. package/.medusa/server/src/modules/paypal/migrations/20260115120000_create_paypal_connection.d.ts +6 -0
  88. package/.medusa/server/src/modules/paypal/migrations/20260115120000_create_paypal_connection.d.ts.map +1 -0
  89. package/.medusa/server/src/modules/paypal/migrations/20260115120000_create_paypal_connection.js +36 -0
  90. package/.medusa/server/src/modules/paypal/migrations/20260115120000_create_paypal_connection.js.map +1 -0
  91. package/.medusa/server/src/modules/paypal/migrations/20260123090000_create_paypal_settings.d.ts +6 -0
  92. package/.medusa/server/src/modules/paypal/migrations/20260123090000_create_paypal_settings.d.ts.map +1 -0
  93. package/.medusa/server/src/modules/paypal/migrations/20260123090000_create_paypal_settings.js +25 -0
  94. package/.medusa/server/src/modules/paypal/migrations/20260123090000_create_paypal_settings.js.map +1 -0
  95. package/.medusa/server/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.d.ts +6 -0
  96. package/.medusa/server/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.d.ts.map +1 -0
  97. package/.medusa/server/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.js +32 -0
  98. package/.medusa/server/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.js.map +1 -0
  99. package/.medusa/server/src/modules/paypal/migrations/20260401090000_create_paypal_metric.d.ts +6 -0
  100. package/.medusa/server/src/modules/paypal/migrations/20260401090000_create_paypal_metric.d.ts.map +1 -0
  101. package/.medusa/server/src/modules/paypal/migrations/20260401090000_create_paypal_metric.js +30 -0
  102. package/.medusa/server/src/modules/paypal/migrations/20260401090000_create_paypal_metric.js.map +1 -0
  103. package/.medusa/server/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.d.ts +6 -0
  104. package/.medusa/server/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.d.ts.map +1 -0
  105. package/.medusa/server/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.js +34 -0
  106. package/.medusa/server/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.js.map +1 -0
  107. package/.medusa/server/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.d.ts +6 -0
  108. package/.medusa/server/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.d.ts.map +1 -0
  109. package/.medusa/server/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.js +28 -0
  110. package/.medusa/server/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.js.map +1 -0
  111. package/.medusa/server/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.d.ts +6 -0
  112. package/.medusa/server/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.d.ts.map +1 -0
  113. package/.medusa/server/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.js +29 -0
  114. package/.medusa/server/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.js.map +1 -0
  115. package/.medusa/server/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.d.ts +6 -0
  116. package/.medusa/server/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.d.ts.map +1 -0
  117. package/.medusa/server/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.js +14 -0
  118. package/.medusa/server/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.js.map +1 -0
  119. package/.medusa/server/src/modules/paypal/migrations/20270102090000_add_paypal_seller_email.d.ts +6 -0
  120. package/.medusa/server/src/modules/paypal/migrations/20270102090000_add_paypal_seller_email.d.ts.map +1 -0
  121. package/.medusa/server/src/modules/paypal/migrations/20270102090000_add_paypal_seller_email.js +14 -0
  122. package/.medusa/server/src/modules/paypal/migrations/20270102090000_add_paypal_seller_email.js.map +1 -0
  123. package/.medusa/server/src/modules/paypal/migrations/20270103090000_add_paypal_seller_merchant_id.d.ts +6 -0
  124. package/.medusa/server/src/modules/paypal/migrations/20270103090000_add_paypal_seller_merchant_id.d.ts.map +1 -0
  125. package/.medusa/server/src/modules/paypal/migrations/20270103090000_add_paypal_seller_merchant_id.js +14 -0
  126. package/.medusa/server/src/modules/paypal/migrations/20270103090000_add_paypal_seller_merchant_id.js.map +1 -0
  127. package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.d.ts +6 -0
  128. package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.d.ts.map +1 -0
  129. package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.js +20 -0
  130. package/.medusa/server/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.js.map +1 -0
  131. package/.medusa/server/src/modules/paypal/models/paypal_connection.d.ts +16 -0
  132. package/.medusa/server/src/modules/paypal/models/paypal_connection.d.ts.map +1 -0
  133. package/.medusa/server/src/modules/paypal/models/paypal_connection.js +19 -0
  134. package/.medusa/server/src/modules/paypal/models/paypal_connection.js.map +1 -0
  135. package/.medusa/server/src/modules/paypal/models/paypal_metric.d.ts +7 -0
  136. package/.medusa/server/src/modules/paypal/models/paypal_metric.d.ts.map +1 -0
  137. package/.medusa/server/src/modules/paypal/models/paypal_metric.js +10 -0
  138. package/.medusa/server/src/modules/paypal/models/paypal_metric.js.map +1 -0
  139. package/.medusa/server/src/modules/paypal/models/paypal_settings.d.ts +6 -0
  140. package/.medusa/server/src/modules/paypal/models/paypal_settings.d.ts.map +1 -0
  141. package/.medusa/server/src/modules/paypal/models/paypal_settings.js +9 -0
  142. package/.medusa/server/src/modules/paypal/models/paypal_settings.js.map +1 -0
  143. package/.medusa/server/src/modules/paypal/models/paypal_webhook_event.d.ts +17 -0
  144. package/.medusa/server/src/modules/paypal/models/paypal_webhook_event.d.ts.map +1 -0
  145. package/.medusa/server/src/modules/paypal/models/paypal_webhook_event.js +20 -0
  146. package/.medusa/server/src/modules/paypal/models/paypal_webhook_event.js.map +1 -0
  147. package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts +32 -0
  148. package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts.map +1 -0
  149. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js +625 -0
  150. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js.map +1 -0
  151. package/.medusa/server/src/modules/paypal/payment-provider/index.d.ts +6 -0
  152. package/.medusa/server/src/modules/paypal/payment-provider/index.d.ts.map +1 -0
  153. package/.medusa/server/src/modules/paypal/payment-provider/index.js +18 -0
  154. package/.medusa/server/src/modules/paypal/payment-provider/index.js.map +1 -0
  155. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts +39 -0
  156. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -0
  157. package/.medusa/server/src/modules/paypal/payment-provider/service.js +869 -0
  158. package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -0
  159. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts +3 -0
  160. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.d.ts.map +1 -0
  161. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js +74 -0
  162. package/.medusa/server/src/modules/paypal/payment-provider/webhook-utils.js.map +1 -0
  163. package/.medusa/server/src/modules/paypal/service.d.ts +266 -0
  164. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -0
  165. package/.medusa/server/src/modules/paypal/service.js +1149 -0
  166. package/.medusa/server/src/modules/paypal/service.js.map +1 -0
  167. package/.medusa/server/src/modules/paypal/types/config.d.ts +12 -0
  168. package/.medusa/server/src/modules/paypal/types/config.d.ts.map +1 -0
  169. package/.medusa/server/src/modules/paypal/types/config.js +24 -0
  170. package/.medusa/server/src/modules/paypal/types/config.js.map +1 -0
  171. package/.medusa/server/src/modules/paypal/utils/amounts.d.ts +3 -0
  172. package/.medusa/server/src/modules/paypal/utils/amounts.d.ts.map +1 -0
  173. package/.medusa/server/src/modules/paypal/utils/amounts.js +40 -0
  174. package/.medusa/server/src/modules/paypal/utils/amounts.js.map +1 -0
  175. package/.medusa/server/src/modules/paypal/utils/currencies.d.ts +19 -0
  176. package/.medusa/server/src/modules/paypal/utils/currencies.d.ts.map +1 -0
  177. package/.medusa/server/src/modules/paypal/utils/currencies.js +69 -0
  178. package/.medusa/server/src/modules/paypal/utils/currencies.js.map +1 -0
  179. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +10 -0
  180. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -0
  181. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +30 -0
  182. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -0
  183. package/.medusa/server/src/modules/paypal/utils/provider-ids.d.ts +5 -0
  184. package/.medusa/server/src/modules/paypal/utils/provider-ids.d.ts.map +1 -0
  185. package/.medusa/server/src/modules/paypal/utils/provider-ids.js +16 -0
  186. package/.medusa/server/src/modules/paypal/utils/provider-ids.js.map +1 -0
  187. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +29 -0
  188. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -0
  189. package/.medusa/server/src/modules/paypal/webhook-processor.js +267 -0
  190. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -0
  191. package/.medusa/server/src/providers/paypal/index.d.ts +5 -0
  192. package/.medusa/server/src/providers/paypal/index.d.ts.map +1 -0
  193. package/.medusa/server/src/providers/paypal/index.js +10 -0
  194. package/.medusa/server/src/providers/paypal/index.js.map +1 -0
  195. package/.medusa/server/src/providers/paypal_card/index.d.ts +5 -0
  196. package/.medusa/server/src/providers/paypal_card/index.d.ts.map +1 -0
  197. package/.medusa/server/src/providers/paypal_card/index.js +10 -0
  198. package/.medusa/server/src/providers/paypal_card/index.js.map +1 -0
  199. package/.medusa/server/src/subscribers/paypal-order-invoice.d.ts +6 -0
  200. package/.medusa/server/src/subscribers/paypal-order-invoice.d.ts.map +1 -0
  201. package/.medusa/server/src/subscribers/paypal-order-invoice.js +124 -0
  202. package/.medusa/server/src/subscribers/paypal-order-invoice.js.map +1 -0
  203. package/LICENSE +21 -0
  204. package/README.md +172 -0
  205. package/package.json +80 -0
  206. package/src/admin/index.ts +7 -0
  207. package/src/admin/routes/settings/paypal/_components/Tabs.tsx +48 -0
  208. package/src/admin/routes/settings/paypal/_components/Toast.tsx +51 -0
  209. package/src/admin/routes/settings/paypal/additional-settings/page.tsx +199 -0
  210. package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +183 -0
  211. package/src/admin/routes/settings/paypal/apple-pay/page.tsx +5 -0
  212. package/src/admin/routes/settings/paypal/connection/page.tsx +699 -0
  213. package/src/admin/routes/settings/paypal/google-pay/page.tsx +5 -0
  214. package/src/admin/routes/settings/paypal/page.tsx +13 -0
  215. package/src/admin/routes/settings/paypal/pay-later-messaging/page.tsx +5 -0
  216. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +351 -0
  217. package/src/api/admin/payment-collections/[id]/payment-sessions/route.ts +24 -0
  218. package/src/api/admin/paypal/disconnect/route.ts +8 -0
  219. package/src/api/admin/paypal/environment/route.ts +25 -0
  220. package/src/api/admin/paypal/onboard-complete/route.ts +34 -0
  221. package/src/api/admin/paypal/onboarding-link/route.ts +45 -0
  222. package/src/api/admin/paypal/onboarding-status/route.ts +17 -0
  223. package/src/api/admin/paypal/save-credentials/route.ts +22 -0
  224. package/src/api/admin/paypal/settings/route.ts +14 -0
  225. package/src/api/admin/paypal/status/route.ts +12 -0
  226. package/src/api/middlewares.ts +14 -0
  227. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +56 -0
  228. package/src/api/store/paypal/capture-order/route.ts +265 -0
  229. package/src/api/store/paypal/config/route.ts +100 -0
  230. package/src/api/store/paypal/create-order/route.ts +449 -0
  231. package/src/api/store/paypal/settings/route.ts +19 -0
  232. package/src/api/store/paypal/webhook/route.ts +325 -0
  233. package/src/api/store/paypal-complete/route.ts +95 -0
  234. package/src/index.ts +1 -0
  235. package/src/jobs/paypal-webhook-retry.ts +149 -0
  236. package/src/modules/paypal/clients/paypal-seller.client.ts +62 -0
  237. package/src/modules/paypal/index.ts +8 -0
  238. package/src/modules/paypal/migrations/20260115120000_create_paypal_connection.ts +33 -0
  239. package/src/modules/paypal/migrations/20260123090000_create_paypal_settings.ts +22 -0
  240. package/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.ts +29 -0
  241. package/src/modules/paypal/migrations/20260401090000_create_paypal_metric.ts +27 -0
  242. package/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.ts +31 -0
  243. package/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.ts +25 -0
  244. package/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.ts +26 -0
  245. package/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.ts +11 -0
  246. package/src/modules/paypal/migrations/20270102090000_add_paypal_seller_email.ts +11 -0
  247. package/src/modules/paypal/migrations/20270103090000_add_paypal_seller_merchant_id.ts +11 -0
  248. package/src/modules/paypal/migrations/20270201000000_add_webhook_dead_letter.ts +17 -0
  249. package/src/modules/paypal/models/paypal_connection.ts +23 -0
  250. package/src/modules/paypal/models/paypal_metric.ts +9 -0
  251. package/src/modules/paypal/models/paypal_settings.ts +8 -0
  252. package/src/modules/paypal/models/paypal_webhook_event.ts +19 -0
  253. package/src/modules/paypal/payment-provider/README.md +22 -0
  254. package/src/modules/paypal/payment-provider/card-service.ts +767 -0
  255. package/src/modules/paypal/payment-provider/index.ts +15 -0
  256. package/src/modules/paypal/payment-provider/service.ts +1039 -0
  257. package/src/modules/paypal/payment-provider/webhook-utils.ts +88 -0
  258. package/src/modules/paypal/service.ts +1416 -0
  259. package/src/modules/paypal/types/config.ts +33 -0
  260. package/src/modules/paypal/utils/amounts.ts +41 -0
  261. package/src/modules/paypal/utils/currencies.ts +84 -0
  262. package/src/modules/paypal/utils/paypal-auth.ts +31 -0
  263. package/src/modules/paypal/utils/provider-ids.ts +15 -0
  264. package/src/modules/paypal/webhook-processor.ts +363 -0
  265. package/src/providers/paypal/index.ts +8 -0
  266. package/src/providers/paypal_card/index.ts +8 -0
  267. package/src/subscribers/paypal-order-invoice.ts +171 -0
  268. package/tsconfig.json +31 -0
@@ -0,0 +1,1416 @@
1
+ import { MedusaService } from "@medusajs/framework/utils"
2
+ import PayPalConnection from "./models/paypal_connection"
3
+ import PayPalMetric from "./models/paypal_metric"
4
+ import PayPalSettings from "./models/paypal_settings"
5
+ import PayPalWebhookEvent from "./models/paypal_webhook_event"
6
+ import { getPayPalConfig } from "./types/config"
7
+ import { normalizeCurrencyCode } from "./utils/currencies"
8
+
9
+ type Environment = "sandbox" | "live"
10
+
11
+ type Status =
12
+ | "disconnected"
13
+ | "pending"
14
+ | "pending_credentials"
15
+ | "connected"
16
+ | "revoked"
17
+
18
+ class PayPalModuleService extends MedusaService({
19
+ PayPalConnection,
20
+ PayPalMetric,
21
+ PayPalSettings,
22
+ PayPalWebhookEvent,
23
+ }) {
24
+ protected cfg = getPayPalConfig()
25
+
26
+ private get bnCode(): string {
27
+ return this.cfg.bnCode || "MBJTechnolabs_SI_SPB"
28
+ }
29
+
30
+ private async getSettingsData() {
31
+ const settings = await this.getSettings()
32
+ return (settings?.data || {}) as Record<string, any>
33
+ }
34
+
35
+ private async ensureSettingsDefaults() {
36
+ const data = await this.getSettingsData()
37
+ const onboarding = { ...(data.onboarding_config || {}) } as Record<string, any>
38
+ const apiDetails = { ...(data.api_details || {}) } as Record<string, any>
39
+ let changed = false
40
+
41
+ if (!onboarding.partner_service_url) {
42
+ onboarding.partner_service_url = this.cfg.partnerServiceUrl
43
+ changed = true
44
+ }
45
+ if (!onboarding.partner_js_url) {
46
+ onboarding.partner_js_url = this.cfg.partnerJsUrl
47
+ changed = true
48
+ }
49
+ if (!onboarding.backend_url) {
50
+ onboarding.backend_url = this.cfg.backendUrl
51
+ changed = true
52
+ }
53
+ if (!onboarding.seller_nonce) {
54
+ onboarding.seller_nonce = this.cfg.sellerNonce
55
+ changed = true
56
+ }
57
+ if (!onboarding.bn_code && this.cfg.bnCode) {
58
+ onboarding.bn_code = this.cfg.bnCode
59
+ changed = true
60
+ }
61
+ if (!onboarding.partner_merchant_id_sandbox) {
62
+ onboarding.partner_merchant_id_sandbox = this.cfg.partnerMerchantIdSandbox
63
+ changed = true
64
+ }
65
+ if (!onboarding.partner_merchant_id_live) {
66
+ onboarding.partner_merchant_id_live = this.cfg.partnerMerchantIdLive
67
+ changed = true
68
+ }
69
+
70
+ if (!apiDetails.currency_code) {
71
+ const raw = (process.env.PAYPAL_CURRENCY || "").trim()
72
+ apiDetails.currency_code = raw ? normalizeCurrencyCode(raw) : "EUR"
73
+ changed = true
74
+ }
75
+ if (!apiDetails.storefront_url) {
76
+ const storeUrl = process.env.STOREFRONT_URL || process.env.STORE_URL
77
+ if (storeUrl) {
78
+ apiDetails.storefront_url = storeUrl
79
+ changed = true
80
+ }
81
+ }
82
+
83
+ if (changed) {
84
+ await this.saveSettings({
85
+ onboarding_config: onboarding,
86
+ api_details: apiDetails,
87
+ })
88
+ }
89
+
90
+ return { onboarding, apiDetails }
91
+ }
92
+
93
+ async getApiDetails() {
94
+ const { onboarding, apiDetails } = await this.ensureSettingsDefaults()
95
+ return {
96
+ onboarding,
97
+ apiDetails,
98
+ }
99
+ }
100
+
101
+ private getAlertWebhookUrls() {
102
+ return (this.cfg.alertWebhookUrls || []).map((url) => url.trim()).filter(Boolean)
103
+ }
104
+
105
+ private async getPartnerMerchantId(env: Environment) {
106
+ const { onboarding } = await this.ensureSettingsDefaults()
107
+ return env === "live" ? onboarding.partner_merchant_id_live : onboarding.partner_merchant_id_sandbox
108
+ }
109
+
110
+ private async getCurrentRow(): Promise<any | null> {
111
+ const rows = await this.listPayPalConnections({})
112
+ return rows?.[0] ?? null
113
+ }
114
+
115
+ private async getCurrentEnvironment(): Promise<Environment> {
116
+ try {
117
+ const row = await this.getCurrentRow()
118
+ const env = (row?.environment as Environment) || "live"
119
+ return env === "sandbox" ? "sandbox" : "live"
120
+ } catch {
121
+ return "live"
122
+ }
123
+ }
124
+
125
+ private getEnvCreds(
126
+ row: any,
127
+ env: Environment
128
+ ): { clientId?: string; clientSecret?: string; sellerMerchantId?: string; sellerEmail?: string } {
129
+ const meta = (row?.metadata || {}) as any
130
+ const creds = meta?.credentials?.[env] || {}
131
+ return {
132
+ clientId: creds.client_id || creds.clientId || undefined,
133
+ clientSecret: creds.client_secret || creds.clientSecret || undefined,
134
+ sellerMerchantId:
135
+ creds.seller_merchant_id ||
136
+ creds.sellerMerchantId ||
137
+ creds.payer_id ||
138
+ creds.merchant_id ||
139
+ creds.merchantId ||
140
+ undefined,
141
+ sellerEmail: creds.seller_email || creds.sellerEmail || undefined,
142
+ }
143
+ }
144
+
145
+ private extractSellerEmail(...candidates: any[]): string | null {
146
+ const queue = [...candidates]
147
+
148
+ while (queue.length > 0) {
149
+ const value = queue.shift()
150
+ if (!value) {
151
+ continue
152
+ }
153
+
154
+ if (typeof value === "string") {
155
+ const trimmed = value.trim()
156
+ if (trimmed && trimmed.includes("@")) {
157
+ return trimmed
158
+ }
159
+ continue
160
+ }
161
+
162
+ if (Array.isArray(value)) {
163
+ queue.push(...value)
164
+ continue
165
+ }
166
+
167
+ if (typeof value === "object") {
168
+ const obj = value as Record<string, any>
169
+ const prioritized = [
170
+ obj.email,
171
+ obj.primary_email,
172
+ obj.merchant_email,
173
+ obj.email_address,
174
+ obj.account_email,
175
+ obj.contact_email,
176
+ obj.value,
177
+ obj.address,
178
+ ]
179
+ queue.push(...prioritized)
180
+
181
+ for (const [k, v] of Object.entries(obj)) {
182
+ const key = String(k).toLowerCase()
183
+ if (key.includes("email") || key.includes("address")) {
184
+ queue.push(v)
185
+ }
186
+ }
187
+
188
+ queue.push(...Object.values(obj))
189
+ }
190
+ }
191
+
192
+ return null
193
+ }
194
+
195
+ private async fetchMerchantIntegrationDetails(
196
+ env: Environment,
197
+ merchantId: string,
198
+ accessTokenOverride?: string
199
+ ) {
200
+ const partnerMerchantId = await this.getPartnerMerchantId(env)
201
+ if (!partnerMerchantId) {
202
+ throw new Error("Missing PayPal partner merchant id configuration.")
203
+ }
204
+
205
+ const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
206
+ const accessToken = accessTokenOverride ?? await this.getAppAccessToken()
207
+
208
+ const resp = await fetch(
209
+ `${baseUrl}/v1/customer/partners/${encodeURIComponent(
210
+ partnerMerchantId
211
+ )}/merchant-integrations/${encodeURIComponent(merchantId)}`,
212
+ {
213
+ method: "GET",
214
+ headers: {
215
+ "Content-Type": "application/json",
216
+ Authorization: `Bearer ${accessToken}`,
217
+ "PayPal-Partner-Attribution-Id": this.bnCode,
218
+ },
219
+ }
220
+ )
221
+
222
+ const text = await resp.text().catch(() => "")
223
+ let json: any = {}
224
+ try {
225
+ json = text ? JSON.parse(text) : {}
226
+ } catch (e: any) {
227
+ console.warn("[PayPal] Failed to parse response JSON — using empty object:", e?.message)
228
+ }
229
+
230
+ if (!resp.ok) {
231
+ throw new Error(
232
+ `PayPal merchant integration lookup failed (${resp.status}): ${text || JSON.stringify(json)}`
233
+ )
234
+ }
235
+
236
+ return json
237
+ }
238
+
239
+ private async getAppAccessTokenForCredentials(
240
+ env: Environment,
241
+ credentials: { clientId: string; clientSecret: string }
242
+ ) {
243
+ const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
244
+ const basic = Buffer.from(`${credentials.clientId}:${credentials.clientSecret}`).toString("base64")
245
+
246
+ const body = new URLSearchParams()
247
+ body.set("grant_type", "client_credentials")
248
+
249
+ const res = await fetch(`${baseUrl}/v1/oauth2/token`, {
250
+ method: "POST",
251
+ headers: {
252
+ "Content-Type": "application/x-www-form-urlencoded",
253
+ Authorization: `Basic ${basic}`,
254
+ "PayPal-Partner-Attribution-Id": this.bnCode,
255
+ },
256
+ body,
257
+ })
258
+
259
+ const text = await res.text().catch(() => "")
260
+ let json: any = {}
261
+ try {
262
+ json = text ? JSON.parse(text) : {}
263
+ } catch (e: any) {
264
+ console.warn("[PayPal] Failed to parse app token response JSON:", e?.message)
265
+ }
266
+
267
+ if (!res.ok) {
268
+ throw new Error(`PayPal client_credentials failed (${res.status}): ${text || JSON.stringify(json)}`)
269
+ }
270
+
271
+ const accessToken = String(json.access_token || "")
272
+ if (!accessToken) {
273
+ throw new Error("PayPal client_credentials succeeded but access_token is missing.")
274
+ }
275
+
276
+ return { accessToken, tokenPayload: json }
277
+ }
278
+
279
+ private async fetchSellerProfileFromDirectCredentials(
280
+ env: Environment,
281
+ credentials?: { clientId: string; clientSecret: string }
282
+ ): Promise<{ sellerMerchantId: string | null; sellerEmail: string | null }> {
283
+ const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
284
+ const partnerMerchantId = await this.getPartnerMerchantId(env)
285
+ let tokenPayload: Record<string, any> | null = null
286
+
287
+ let accessToken = ""
288
+ if (credentials) {
289
+ const tokenResp = await this.getAppAccessTokenForCredentials(env, {
290
+ clientId: credentials.clientId,
291
+ clientSecret: credentials.clientSecret,
292
+ })
293
+ accessToken = tokenResp.accessToken
294
+ tokenPayload = tokenResp.tokenPayload
295
+ } else {
296
+ accessToken = await this.getAppAccessToken()
297
+ }
298
+
299
+ let sellerEmail = this.extractSellerEmail(tokenPayload || undefined)
300
+ let sellerMerchantId = String(
301
+ tokenPayload?.merchant_id || tokenPayload?.payer_id || tokenPayload?.account_id || ""
302
+ ).trim() || null
303
+
304
+ try {
305
+ const userInfoResp = await fetch(`${baseUrl}/v1/identity/oauth2/userinfo?schema=paypalv1`, {
306
+ method: "GET",
307
+ headers: {
308
+ Authorization: `Bearer ${accessToken}`,
309
+ "Content-Type": "application/json",
310
+ Accept: "application/json",
311
+ "PayPal-Partner-Attribution-Id": this.bnCode,
312
+ },
313
+ })
314
+ if (userInfoResp.ok) {
315
+ const userInfo = await userInfoResp.json().catch(() => ({}))
316
+ sellerEmail = sellerEmail || this.extractSellerEmail(userInfo)
317
+ sellerMerchantId =
318
+ sellerMerchantId ||
319
+ String(userInfo?.merchant_id || userInfo?.payer_id || userInfo?.user_id || userInfo?.sub || "").trim() ||
320
+ null
321
+ }
322
+ } catch (e: any) {
323
+ console.warn("[PayPal] userinfo lookup failed:", e?.message || e)
324
+ }
325
+
326
+ if (partnerMerchantId) {
327
+ try {
328
+ const credsResp = await fetch(
329
+ `${baseUrl}/v1/customer/partners/${encodeURIComponent(
330
+ partnerMerchantId
331
+ )}/merchant-integrations/credentials/`,
332
+ {
333
+ method: "GET",
334
+ headers: {
335
+ Authorization: `Bearer ${accessToken}`,
336
+ "Content-Type": "application/json",
337
+ "PayPal-Partner-Attribution-Id": this.bnCode,
338
+ },
339
+ }
340
+ )
341
+ if (credsResp.ok) {
342
+ const credsJson = await credsResp.json().catch(() => ({}))
343
+ sellerEmail = sellerEmail || this.extractSellerEmail(credsJson)
344
+ sellerMerchantId = sellerMerchantId || String(credsJson?.merchant_id || "").trim() || null
345
+ }
346
+ } catch (e: any) {
347
+ console.warn("[PayPal] direct credential profile lookup failed:", e?.message || e)
348
+ }
349
+ }
350
+
351
+ const hydrated = await this.hydrateSellerMetadataFromCredentials(env, {
352
+ accessToken,
353
+ sellerMerchantId,
354
+ sellerEmail,
355
+ })
356
+
357
+ return {
358
+ sellerMerchantId: hydrated.sellerMerchantId,
359
+ sellerEmail: hydrated.sellerEmail,
360
+ }
361
+ }
362
+
363
+ private async hydrateSellerMetadataFromCredentials(
364
+ env: Environment,
365
+ input: {
366
+ accessToken?: string
367
+ sellerMerchantId?: string | null
368
+ sellerEmail?: string | null
369
+ metadataCandidates?: any[]
370
+ }
371
+ ): Promise<{ sellerMerchantId: string | null; sellerEmail: string | null }> {
372
+ let sellerMerchantId = (input.sellerMerchantId || "").trim() || null
373
+ let sellerEmail = (input.sellerEmail || "").trim() || null
374
+
375
+ if (!sellerEmail && input.metadataCandidates?.length) {
376
+ sellerEmail = this.extractSellerEmail(...input.metadataCandidates)
377
+ }
378
+
379
+ if (sellerMerchantId && !sellerEmail) {
380
+ try {
381
+ const details = await this.fetchMerchantIntegrationDetails(env, sellerMerchantId, input.accessToken)
382
+ sellerEmail = this.extractSellerEmail(details)
383
+ } catch (e: any) {
384
+ console.warn("[PayPal] merchant integration lookup failed:", e?.message || e)
385
+ }
386
+ }
387
+
388
+ return { sellerMerchantId, sellerEmail }
389
+ }
390
+
391
+ private async syncRowFieldsFromMetadata(row: any, env: Environment) {
392
+ const c = this.getEnvCreds(row, env)
393
+ await this.updatePayPalConnections({
394
+ id: row.id,
395
+ status: c.clientId && c.clientSecret ? "connected" : "disconnected",
396
+ seller_client_id: c.clientId || null,
397
+ seller_client_secret: c.clientSecret || null,
398
+ seller_merchant_id: c.sellerMerchantId || null,
399
+ seller_email: c.sellerEmail || null,
400
+ metadata: {
401
+ ...(row.metadata || {}),
402
+ active_environment: env,
403
+ },
404
+ })
405
+ }
406
+
407
+ async setEnvironment(env: Environment) {
408
+ const nextEnv: Environment = env === "sandbox" ? "sandbox" : "live"
409
+ const row = await this.getCurrentRow()
410
+ const previousEnv = (row?.environment as Environment) || "live"
411
+
412
+ if (!row) {
413
+ const created = await this.createPayPalConnections({
414
+ environment: nextEnv,
415
+ status: "disconnected",
416
+ shared_id: null,
417
+ auth_code: null,
418
+ seller_client_id: null,
419
+ seller_client_secret: null,
420
+ seller_merchant_id: null,
421
+ seller_email: null,
422
+ app_access_token: null,
423
+ app_access_token_expires_at: null,
424
+ metadata: { credentials: {}, active_environment: nextEnv },
425
+ })
426
+ await this.recordAuditEvent("environment_switched", {
427
+ previous_environment: previousEnv,
428
+ environment: nextEnv,
429
+ })
430
+ return created
431
+ }
432
+
433
+ await this.updatePayPalConnections({
434
+ id: row.id,
435
+ environment: nextEnv,
436
+ app_access_token: null,
437
+ app_access_token_expires_at: null,
438
+ metadata: {
439
+ ...(row.metadata || {}),
440
+ active_environment: nextEnv,
441
+ },
442
+ })
443
+
444
+ const updated = await this.getCurrentRow()
445
+ if (updated) {
446
+ await this.syncRowFieldsFromMetadata(updated, nextEnv)
447
+ }
448
+
449
+ await this.recordAuditEvent("environment_switched", {
450
+ previous_environment: previousEnv,
451
+ environment: nextEnv,
452
+ })
453
+ return await this.getCurrentRow()
454
+ }
455
+
456
+ async createOnboardingLink(input?: { email?: string; products?: string[] }) {
457
+ const { onboarding } = await this.ensureSettingsDefaults()
458
+ const return_url = `${String(onboarding.backend_url || "").replace(/\/$/, "")}/admin/paypal/onboard-complete`
459
+ const env = await this.getCurrentEnvironment()
460
+ const partner_merchant_id = await this.getPartnerMerchantId(env)
461
+
462
+ const email = (input?.email || "").trim()
463
+
464
+ if (!partner_merchant_id) {
465
+ throw new Error("Missing PAYPAL_PARTNER_MERCHANT_ID_* env for current environment")
466
+ }
467
+
468
+ const form = new URLSearchParams()
469
+ if (email) {
470
+ form.set("email", email)
471
+ }
472
+ form.set("sandbox", env === "live" ? "no" : "yes")
473
+ form.set("return_url", return_url)
474
+ form.set("return_url_description", "Return to your shop.")
475
+ form.set("partner_merchant_id", partner_merchant_id)
476
+ form.set("from", "medusa")
477
+
478
+ const products = input?.products?.length ? input.products : ["PPCP"]
479
+
480
+ products.forEach((p, i) => {
481
+ form.append(`products[${i}]`, p)
482
+ form.append("products[]", p)
483
+ })
484
+
485
+ const res = await fetch(onboarding.partner_service_url, {
486
+ method: "POST",
487
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
488
+ body: form.toString(),
489
+ })
490
+
491
+ const text = await res.text().catch(() => "")
492
+ if (!res.ok) {
493
+ throw new Error(`Onboarding service failed (${res.status}): ${text}`)
494
+ }
495
+
496
+ const trimmed = text.trim()
497
+
498
+ if (trimmed.startsWith("http")) {
499
+ return { onboarding_url: trimmed, return_url }
500
+ }
501
+
502
+ let json: any
503
+ try {
504
+ json = JSON.parse(trimmed)
505
+ } catch {
506
+ throw new Error(`Invalid onboarding link response (not JSON / URL): ${trimmed.slice(0, 200)}`)
507
+ }
508
+
509
+ if (json?.body) {
510
+ const inner = typeof json.body === "string" ? json.body.trim() : json.body
511
+
512
+ if (typeof inner === "string" && inner.startsWith("http")) {
513
+ return { onboarding_url: inner, return_url }
514
+ }
515
+
516
+ try {
517
+ json = typeof inner === "string" ? JSON.parse(inner) : inner
518
+ } catch {
519
+ throw new Error(
520
+ `Onboarding wrapper JSON 'body' is not valid JSON / URL: ${
521
+ typeof inner === "string" ? inner.slice(0, 200) : "[object]"
522
+ }`
523
+ )
524
+ }
525
+ }
526
+
527
+ if (json?.name && json?.message && (json?.debug_id || json?.details || json?.links)) {
528
+ const debug = json.debug_id ? ` debug_id=${json.debug_id}` : ""
529
+ const details = Array.isArray(json.details)
530
+ ? json.details
531
+ .slice(0, 3)
532
+ .map((d: any) => {
533
+ const issue = d?.issue ? String(d.issue) : ""
534
+ const desc = d?.description ? String(d.description) : ""
535
+ const field = d?.field ? String(d.field) : ""
536
+ return [issue, desc, field].filter(Boolean).join(" | ")
537
+ })
538
+ .filter(Boolean)
539
+ .join("; ")
540
+ : ""
541
+
542
+ throw new Error(`PayPal onboarding error: ${json.name}: ${json.message}.${debug}${details ? ` Details: ${details}` : ""}`)
543
+ }
544
+
545
+ if (json?.onboarding_url && String(json.onboarding_url).startsWith("http")) {
546
+ return { onboarding_url: String(json.onboarding_url), return_url }
547
+ }
548
+
549
+ const links = Array.isArray(json?.links) ? json.links : null
550
+ if (links) {
551
+ const action = links.find(
552
+ (l: any) => l?.rel === "action_url" || l?.rel === "actionUrl" || l?.rel === "action-url"
553
+ )
554
+ const href = action?.href ? String(action.href) : null
555
+ if (href && href.startsWith("http")) {
556
+ return { onboarding_url: href, return_url }
557
+ }
558
+ }
559
+
560
+ throw new Error(
561
+ `Onboarding JSON missing action_url link. Keys: ${Object.keys(json || {}).join(", ")}`
562
+ )
563
+ }
564
+
565
+ async startOnboarding() {
566
+ const row = await this.getCurrentRow()
567
+ const env = await this.getCurrentEnvironment()
568
+
569
+ if (row) {
570
+ await this.updatePayPalConnections({ id: row.id, status: "pending" })
571
+ return
572
+ }
573
+
574
+ await this.createPayPalConnections({
575
+ environment: env,
576
+ status: "pending",
577
+ metadata: {},
578
+ })
579
+ }
580
+
581
+ async saveOnboardCallback(input: { authCode: string; sharedId: string }) {
582
+ const row = await this.getCurrentRow()
583
+ const env = await this.getCurrentEnvironment()
584
+
585
+ if (!row) {
586
+ return await this.createPayPalConnections({
587
+ environment: env,
588
+ status: "pending_credentials",
589
+ auth_code: input.authCode,
590
+ shared_id: input.sharedId,
591
+ metadata: {},
592
+ })
593
+ }
594
+
595
+ return await this.updatePayPalConnections({
596
+ id: row.id,
597
+ status: "pending_credentials",
598
+ auth_code: input.authCode,
599
+ shared_id: input.sharedId,
600
+ })
601
+ }
602
+
603
+ async exchangeAndSaveSellerCredentials(input: {
604
+ authCode: string
605
+ sharedId: string
606
+ env?: "sandbox" | "live"
607
+ }) {
608
+ await this.saveOnboardCallback({ authCode: input.authCode, sharedId: input.sharedId })
609
+
610
+ const env = (input.env || (await this.getCurrentEnvironment())) as Environment
611
+ const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
612
+
613
+ const { onboarding } = await this.ensureSettingsDefaults()
614
+ const sellerNonce = (onboarding.seller_nonce || "").trim()
615
+ if (!sellerNonce) {
616
+ throw new Error("PayPal seller nonce is not configured. Set PAYPAL_SELLER_NONCE.")
617
+ }
618
+
619
+ const tokenBody = new URLSearchParams()
620
+ tokenBody.set("grant_type", "authorization_code")
621
+ tokenBody.set("code", input.authCode)
622
+ tokenBody.set("code_verifier", sellerNonce)
623
+
624
+ const basic = Buffer.from(`${input.sharedId}:`).toString("base64")
625
+
626
+ const tokenRes = await fetch(`${baseUrl}/v1/oauth2/token`, {
627
+ method: "POST",
628
+ headers: {
629
+ "Content-Type": "application/x-www-form-urlencoded",
630
+ Authorization: `Basic ${basic}`,
631
+ "PayPal-Partner-Attribution-Id": this.bnCode,
632
+ },
633
+ body: tokenBody,
634
+ })
635
+
636
+ const tokenText = await tokenRes.text().catch(() => "")
637
+ let tokenJson: any = {}
638
+ try {
639
+ tokenJson = tokenText ? JSON.parse(tokenText) : {}
640
+ } catch (e: any) {
641
+ console.warn("[PayPal] Failed to parse token response JSON:", e?.message)
642
+ }
643
+
644
+ if (!tokenRes.ok) {
645
+ throw new Error(
646
+ `PayPal authorization_code token exchange failed (${tokenRes.status}): ${tokenText || JSON.stringify(tokenJson)}`
647
+ )
648
+ }
649
+
650
+ const sellerAccessToken = String(tokenJson.access_token || "")
651
+ if (!sellerAccessToken) {
652
+ throw new Error("PayPal token exchange succeeded but access_token is missing.")
653
+ }
654
+
655
+ const partnerMerchantId = await this.getPartnerMerchantId(env)
656
+ if (!partnerMerchantId) {
657
+ throw new Error("Missing PayPal partner merchant id configuration.")
658
+ }
659
+
660
+ const credRes = await fetch(
661
+ `${baseUrl}/v1/customer/partners/${encodeURIComponent(partnerMerchantId)}/merchant-integrations/credentials/`,
662
+ {
663
+ method: "GET",
664
+ headers: {
665
+ "Content-Type": "application/json",
666
+ Authorization: `Bearer ${sellerAccessToken}`,
667
+ "PayPal-Partner-Attribution-Id": this.bnCode,
668
+ },
669
+ }
670
+ )
671
+
672
+ const credText = await credRes.text().catch(() => "")
673
+ let credJson: any = {}
674
+ try {
675
+ credJson = credText ? JSON.parse(credText) : {}
676
+ } catch (e: any) {
677
+ console.warn("[PayPal] Failed to parse token response JSON:", e?.message)
678
+ }
679
+
680
+ if (!credRes.ok) {
681
+ throw new Error(
682
+ `PayPal credentials fetch failed (${credRes.status}): ${credText || JSON.stringify(credJson)}`
683
+ )
684
+ }
685
+
686
+ const clientId = String(credJson.client_id || credJson.clientId || "")
687
+ const clientSecret = String(credJson.client_secret || credJson.clientSecret || "")
688
+ if (!clientId || !clientSecret) {
689
+ throw new Error(
690
+ `PayPal credentials response missing client_id/client_secret. Keys: ${Object.keys(credJson || {}).join(", ")}`
691
+ )
692
+ }
693
+
694
+ let sellerEmail = this.extractSellerEmail(credJson, tokenJson)
695
+ let sellerMerchantId =
696
+ String(credJson.payer_id || credJson.merchant_id || tokenJson.payer_id || tokenJson.merchant_id || "").trim() ||
697
+ null
698
+
699
+ if (!sellerEmail) {
700
+ const merchantCandidates = [
701
+ String(credJson.payer_id || "").trim(),
702
+ String(credJson.merchant_id || "").trim(),
703
+ String(tokenJson.payer_id || "").trim(),
704
+ String(tokenJson.merchant_id || "").trim(),
705
+ ].filter(Boolean).filter((v, i, arr) => arr.indexOf(v) === i)
706
+
707
+ for (const merchantId of merchantCandidates) {
708
+ try {
709
+ const merchantDetails = await this.fetchMerchantIntegrationDetails(env, merchantId, sellerAccessToken)
710
+ sellerMerchantId = sellerMerchantId || merchantId
711
+ sellerEmail = this.extractSellerEmail(merchantDetails)
712
+ if (sellerEmail) {
713
+ break
714
+ }
715
+ } catch (e: any) {
716
+ console.warn(`[PayPal] Merchant integration lookup for ${merchantId} failed:`, e?.message || e)
717
+ }
718
+ }
719
+ }
720
+
721
+ await this.saveSellerCredentials({
722
+ clientId,
723
+ clientSecret,
724
+ sellerMerchantId,
725
+ sellerEmail,
726
+ })
727
+
728
+ if (!sellerEmail && sellerMerchantId) {
729
+ try {
730
+ const appAccessToken = await this.getAppAccessToken()
731
+ const details = await this.fetchMerchantIntegrationDetails(env, sellerMerchantId, appAccessToken)
732
+ sellerEmail = this.extractSellerEmail(details)
733
+ if (sellerEmail) {
734
+ await this.saveSellerCredentials({ clientId, clientSecret, sellerMerchantId, sellerEmail })
735
+ }
736
+ } catch (e: any) {
737
+ console.warn("[PayPal] Post-save merchant_id email lookup failed:", e?.message || e)
738
+ }
739
+ }
740
+ }
741
+
742
+ async saveSellerCredentials(input: {
743
+ clientId: string
744
+ clientSecret: string
745
+ sellerMerchantId?: string | null
746
+ sellerEmail?: string | null
747
+ environment?: Environment
748
+ }) {
749
+ const row = await this.getCurrentRow()
750
+ const currentEnv = await this.getCurrentEnvironment()
751
+ const env = (input.environment || currentEnv) as Environment
752
+
753
+ const existingCreds = row ? this.getEnvCreds(row, env) : {}
754
+ const nextSellerMerchantId =
755
+ (input.sellerMerchantId || "").trim() || existingCreds.sellerMerchantId || row?.seller_merchant_id || null
756
+ const nextSellerEmail =
757
+ (input.sellerEmail || "").trim() || existingCreds.sellerEmail || row?.seller_email || null
758
+
759
+ const nextCreds = {
760
+ client_id: input.clientId,
761
+ clientId: input.clientId,
762
+ client_secret: input.clientSecret,
763
+ clientSecret: input.clientSecret,
764
+ merchantId: nextSellerMerchantId,
765
+ merchant_id: nextSellerMerchantId,
766
+ payer_id: nextSellerMerchantId,
767
+ seller_merchant_id: nextSellerMerchantId,
768
+ sellerMerchantId: nextSellerMerchantId,
769
+ seller_email: nextSellerEmail,
770
+ sellerEmail: nextSellerEmail,
771
+ }
772
+
773
+ if (!row) {
774
+ const created = await this.createPayPalConnections({
775
+ environment: env,
776
+ status: "connected",
777
+ seller_client_id: input.clientId,
778
+ seller_client_secret: input.clientSecret,
779
+ seller_merchant_id: nextSellerMerchantId,
780
+ seller_email: nextSellerEmail,
781
+ app_access_token: null,
782
+ app_access_token_expires_at: null,
783
+ metadata: {
784
+ credentials: {
785
+ [env]: nextCreds,
786
+ },
787
+ active_environment: env,
788
+ },
789
+ })
790
+ await this.recordAuditEvent("credentials_saved", {
791
+ environment: env,
792
+ client_id: input.clientId,
793
+ })
794
+ await this.ensureWebhookRegistration()
795
+ return created
796
+ }
797
+
798
+ const meta = (row.metadata || {}) as any
799
+ const creds = { ...(meta.credentials || {}) }
800
+ creds[env] = {
801
+ ...(creds[env] || {}),
802
+ ...nextCreds,
803
+ }
804
+
805
+ const updated = await this.updatePayPalConnections({
806
+ id: row.id,
807
+ status: "connected",
808
+ seller_client_id: input.clientId,
809
+ seller_client_secret: input.clientSecret,
810
+ seller_merchant_id: nextSellerMerchantId,
811
+ seller_email: nextSellerEmail,
812
+ app_access_token: null,
813
+ app_access_token_expires_at: null,
814
+ metadata: {
815
+ ...(row.metadata || {}),
816
+ credentials: creds,
817
+ active_environment: env,
818
+ },
819
+ })
820
+ await this.recordAuditEvent("credentials_saved", {
821
+ environment: env,
822
+ client_id: input.clientId,
823
+ })
824
+ await this.ensureWebhookRegistration()
825
+ return updated
826
+ }
827
+
828
+ async saveAndHydrateSellerCredentials(input: {
829
+ clientId: string
830
+ clientSecret: string
831
+ environment?: Environment
832
+ }) {
833
+ const env = (input.environment || (await this.getCurrentEnvironment())) as Environment
834
+
835
+ await this.saveSellerCredentials({
836
+ clientId: input.clientId,
837
+ clientSecret: input.clientSecret,
838
+ environment: env,
839
+ })
840
+
841
+ try {
842
+ const hydrated = await this.fetchSellerProfileFromDirectCredentials(env, {
843
+ clientId: input.clientId,
844
+ clientSecret: input.clientSecret,
845
+ })
846
+
847
+ if (hydrated.sellerEmail || hydrated.sellerMerchantId) {
848
+ await this.saveSellerCredentials({
849
+ clientId: input.clientId,
850
+ clientSecret: input.clientSecret,
851
+ sellerMerchantId: hydrated.sellerMerchantId,
852
+ sellerEmail: hydrated.sellerEmail,
853
+ environment: env,
854
+ })
855
+ }
856
+
857
+ const refreshedRow = await this.getCurrentRow()
858
+ if (refreshedRow) {
859
+ await this.syncRowFieldsFromMetadata(refreshedRow, env)
860
+ }
861
+ } catch (e: any) {
862
+ console.warn("[PayPal] saveAndHydrateSellerCredentials lookup failed:", e?.message || e)
863
+ }
864
+
865
+ return await this.getStatus(env)
866
+ }
867
+
868
+ private async resolveWebhookUrl() {
869
+ const { onboarding } = await this.ensureSettingsDefaults()
870
+ const base = String(onboarding.backend_url || "").replace(/\/$/, "")
871
+ if (!base) {
872
+ throw new Error("PayPal backend URL is not configured.")
873
+ }
874
+ return `${base}/store/paypal/webhook`
875
+ }
876
+
877
+ private isLocalWebhookUrl(url: string) {
878
+ try {
879
+ const parsed = new URL(url)
880
+ return ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)
881
+ } catch {
882
+ return false
883
+ }
884
+ }
885
+
886
+ private async ensureWebhookRegistration() {
887
+ const env = await this.getCurrentEnvironment()
888
+ const { apiDetails } = await this.ensureSettingsDefaults()
889
+ const webhookIds = { ...(apiDetails.webhook_ids || {}) } as Record<string, string>
890
+
891
+ if (webhookIds[env]) {
892
+ return webhookIds[env]
893
+ }
894
+
895
+ const accessToken = await this.getAppAccessToken()
896
+ const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
897
+ const webhookUrl = await this.resolveWebhookUrl()
898
+
899
+ if (this.isLocalWebhookUrl(webhookUrl)) {
900
+ await this.recordAuditEvent("webhook_skipped_localhost", {
901
+ environment: env,
902
+ webhook_url: webhookUrl,
903
+ })
904
+ return webhookIds[env] || ""
905
+ }
906
+
907
+ const listResp = await fetch(`${baseUrl}/v1/notifications/webhooks`, {
908
+ headers: {
909
+ Authorization: `Bearer ${accessToken}`,
910
+ "Content-Type": "application/json",
911
+ "PayPal-Partner-Attribution-Id": this.bnCode,
912
+ },
913
+ })
914
+
915
+ const listJson = await listResp.json().catch(() => ({}))
916
+ if (!listResp.ok) {
917
+ throw new Error(`PayPal webhook list failed (${listResp.status}): ${JSON.stringify(listJson)}`)
918
+ }
919
+
920
+ const existing = Array.isArray(listJson?.webhooks)
921
+ ? listJson.webhooks.find((hook: any) => hook?.url === webhookUrl)
922
+ : null
923
+
924
+ let webhookId = existing?.id ? String(existing.id) : ""
925
+
926
+ if (!webhookId) {
927
+ const createResp = await fetch(`${baseUrl}/v1/notifications/webhooks`, {
928
+ method: "POST",
929
+ headers: {
930
+ Authorization: `Bearer ${accessToken}`,
931
+ "Content-Type": "application/json",
932
+ "PayPal-Partner-Attribution-Id": this.bnCode,
933
+ },
934
+ body: JSON.stringify({
935
+ url: webhookUrl,
936
+ event_types: [
937
+ { name: "CHECKOUT.ORDER.APPROVED" },
938
+ { name: "CHECKOUT.ORDER.CANCELLED" },
939
+ { name: "PAYMENT.CAPTURE.COMPLETED" },
940
+ { name: "PAYMENT.CAPTURE.DENIED" },
941
+ { name: "PAYMENT.CAPTURE.REFUNDED" },
942
+ { name: "PAYMENT.CAPTURE.REVERSED" },
943
+ { name: "PAYMENT.AUTHORIZATION.CREATED" },
944
+ { name: "PAYMENT.AUTHORIZATION.VOIDED" },
945
+ { name: "PAYMENT.AUTHORIZATION.DENIED" },
946
+ { name: "PAYMENT.REFUND.COMPLETED" },
947
+ { name: "PAYMENT.REFUND.DENIED" },
948
+ ],
949
+ }),
950
+ })
951
+
952
+ const createJson = await createResp.json().catch(() => ({}))
953
+ if (!createResp.ok) {
954
+ throw new Error(
955
+ `PayPal webhook create failed (${createResp.status}): ${JSON.stringify(createJson)}`
956
+ )
957
+ }
958
+
959
+ webhookId = String(createJson?.id || "")
960
+ }
961
+
962
+ if (!webhookId) {
963
+ throw new Error("PayPal webhook registration did not return an id")
964
+ }
965
+
966
+ const nextWebhookIds = { ...webhookIds, [env]: webhookId }
967
+ await this.saveSettings({
968
+ api_details: {
969
+ ...apiDetails,
970
+ webhook_ids: nextWebhookIds,
971
+ },
972
+ })
973
+
974
+ await this.recordAuditEvent("webhook_registered", {
975
+ environment: env,
976
+ webhook_id: webhookId,
977
+ webhook_url: webhookUrl,
978
+ })
979
+
980
+ return webhookId
981
+ }
982
+
983
+ private maskValue(value?: string | null, visibleChars = 4) {
984
+ if (!value) return null
985
+ const trimmed = String(value)
986
+ if (trimmed.length <= visibleChars) {
987
+ return "•".repeat(trimmed.length)
988
+ }
989
+ return `${"•".repeat(Math.max(0, trimmed.length - visibleChars))}${trimmed.slice(
990
+ -visibleChars
991
+ )}`
992
+ }
993
+
994
+ async getStatus(envOverride?: Environment) {
995
+ const row = await this.getCurrentRow()
996
+ const env = envOverride ?? (await this.getCurrentEnvironment())
997
+
998
+ if (!row) {
999
+ return { environment: env, status: "disconnected" as Status, seller_client_id_present: false }
1000
+ }
1001
+
1002
+ const c = this.getEnvCreds(row, env)
1003
+ const hasCreds = !!(c.clientId && c.clientSecret)
1004
+ let sellerEmail: string | null = c.sellerEmail || row.seller_email || null
1005
+ let sellerMerchantId: string | null = c.sellerMerchantId || row.seller_merchant_id || null
1006
+
1007
+ if (!sellerEmail && hasCreds) {
1008
+ try {
1009
+ const hydrated = await this.fetchSellerProfileFromDirectCredentials(env)
1010
+ if (hydrated.sellerEmail || hydrated.sellerMerchantId) {
1011
+ await this.saveSellerCredentials({
1012
+ clientId: c.clientId!,
1013
+ clientSecret: c.clientSecret!,
1014
+ sellerMerchantId: hydrated.sellerMerchantId || sellerMerchantId,
1015
+ sellerEmail: hydrated.sellerEmail || sellerEmail,
1016
+ environment: env,
1017
+ })
1018
+
1019
+ const refreshedRow = await this.getCurrentRow()
1020
+ if (refreshedRow) {
1021
+ const refreshedCreds = this.getEnvCreds(refreshedRow, env)
1022
+ sellerEmail = refreshedCreds.sellerEmail || refreshedRow.seller_email || sellerEmail
1023
+ sellerMerchantId =
1024
+ refreshedCreds.sellerMerchantId || refreshedRow.seller_merchant_id || sellerMerchantId
1025
+ }
1026
+ }
1027
+ } catch (e: any) {
1028
+ console.warn("[PayPal] status direct credential lookup failed:", e?.message || e)
1029
+ }
1030
+ }
1031
+
1032
+ return {
1033
+ environment: env,
1034
+ status: (hasCreds ? "connected" : "disconnected") as Status,
1035
+ shared_id: row.shared_id ?? null,
1036
+ auth_code: row.auth_code ? "***stored***" : null,
1037
+ seller_client_id_present: hasCreds,
1038
+ seller_client_id_masked: this.maskValue(c.clientId),
1039
+ seller_client_secret_masked: c.clientSecret ? "••••••••" : null,
1040
+ seller_merchant_id: sellerMerchantId,
1041
+ seller_email: sellerEmail,
1042
+ updated_at: (row.updated_at as any)?.toISOString?.() ?? null,
1043
+ }
1044
+ }
1045
+
1046
+ async disconnect() {
1047
+ const row = await this.getCurrentRow()
1048
+ if (!row) return
1049
+ const env = await this.getCurrentEnvironment()
1050
+
1051
+ const meta = (row.metadata || {}) as any
1052
+ const creds = { ...(meta.credentials || {}) }
1053
+ delete creds[env]
1054
+
1055
+ const hasAnyCreds = Object.values(creds).some((v: any) => {
1056
+ return v && typeof v === "object" && (v as any).client_id && (v as any).client_secret
1057
+ })
1058
+
1059
+ await this.updatePayPalConnections({
1060
+ id: row.id,
1061
+ status: hasAnyCreds ? "connected" : "disconnected",
1062
+ shared_id: null,
1063
+ auth_code: null,
1064
+ seller_client_id: null,
1065
+ seller_client_secret: null,
1066
+ seller_merchant_id: null,
1067
+ seller_email: null,
1068
+ app_access_token: null,
1069
+ app_access_token_expires_at: null,
1070
+ metadata: {
1071
+ ...(row.metadata || {}),
1072
+ credentials: creds,
1073
+ active_environment: env,
1074
+ },
1075
+ })
1076
+ const updated = await this.getCurrentRow()
1077
+ if (updated) {
1078
+ await this.syncRowFieldsFromMetadata(updated, env)
1079
+ }
1080
+ await this.recordAuditEvent("disconnected", { environment: env })
1081
+ }
1082
+
1083
+ async getAppAccessToken(): Promise<string> {
1084
+ const row = await this.getCurrentRow()
1085
+ const env = await this.getCurrentEnvironment()
1086
+ const creds = await this.getActiveCredentials()
1087
+
1088
+ if (!row) {
1089
+ throw new Error("PayPal connection row not found. Please complete onboarding.")
1090
+ }
1091
+
1092
+ const expiresAt = row.app_access_token_expires_at ? new Date(row.app_access_token_expires_at as any) : null
1093
+ if (row.app_access_token && expiresAt) {
1094
+ const msLeft = expiresAt.getTime() - Date.now()
1095
+ if (msLeft > 2 * 60 * 1000) return row.app_access_token
1096
+ }
1097
+
1098
+ const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
1099
+ const basic = Buffer.from(`${creds.client_id}:${creds.client_secret}`).toString("base64")
1100
+
1101
+ const body = new URLSearchParams()
1102
+ body.set("grant_type", "client_credentials")
1103
+
1104
+ const res = await fetch(`${baseUrl}/v1/oauth2/token`, {
1105
+ method: "POST",
1106
+ headers: {
1107
+ "Content-Type": "application/x-www-form-urlencoded",
1108
+ Authorization: `Basic ${basic}`,
1109
+ "PayPal-Partner-Attribution-Id": this.bnCode,
1110
+ },
1111
+ body,
1112
+ })
1113
+
1114
+ const json = await res.json().catch(() => ({}))
1115
+ if (!res.ok) throw new Error(`PayPal client_credentials failed (${res.status}): ${JSON.stringify(json)}`)
1116
+
1117
+ const accessToken = String(json.access_token)
1118
+ const expiresIn = Number(json.expires_in || 3600)
1119
+ const newExpiresAt = new Date(Date.now() + expiresIn * 1000)
1120
+
1121
+ await this.updatePayPalConnections({
1122
+ id: row.id,
1123
+ app_access_token: accessToken,
1124
+ app_access_token_expires_at: newExpiresAt as any,
1125
+ })
1126
+
1127
+ return accessToken
1128
+ }
1129
+
1130
+ async generateClientToken(opts?: { locale?: string }): Promise<string> {
1131
+ const env = await this.getCurrentEnvironment()
1132
+ const baseUrl = env === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
1133
+
1134
+ const accessToken = await this.getAppAccessToken()
1135
+
1136
+ const res = await fetch(`${baseUrl}/v1/identity/generate-token`, {
1137
+ method: "POST",
1138
+ headers: {
1139
+ Authorization: `Bearer ${accessToken}`,
1140
+ "Content-Type": "application/json",
1141
+ Accept: "application/json",
1142
+ "PayPal-Partner-Attribution-Id": this.bnCode,
1143
+ ...(opts?.locale ? { "Accept-Language": opts.locale } : {}),
1144
+ },
1145
+ })
1146
+
1147
+ const json = await res.json().catch(() => ({}))
1148
+ if (!res.ok) {
1149
+ throw new Error(`PayPal generate-token failed (${res.status}): ${JSON.stringify(json)}`)
1150
+ }
1151
+
1152
+ const token = String((json as any)?.client_token || "")
1153
+ if (!token) {
1154
+ throw new Error("PayPal client_token is missing in generate-token response")
1155
+ }
1156
+
1157
+ return token
1158
+ }
1159
+
1160
+ async getSettings() {
1161
+ const rows = await this.listPayPalSettings({})
1162
+ const row = rows?.[0]
1163
+ return { data: (row?.data || {}) as Record<string, any> }
1164
+ }
1165
+
1166
+ private deepMerge(
1167
+ target: Record<string, any>,
1168
+ source: Record<string, any>
1169
+ ): Record<string, any> {
1170
+ const result = { ...target }
1171
+ for (const key of Object.keys(source)) {
1172
+ const sv = source[key]
1173
+ const tv = target[key]
1174
+ if (
1175
+ sv !== null &&
1176
+ typeof sv === "object" &&
1177
+ !Array.isArray(sv) &&
1178
+ tv !== null &&
1179
+ typeof tv === "object" &&
1180
+ !Array.isArray(tv)
1181
+ ) {
1182
+ result[key] = this.deepMerge(tv, sv)
1183
+ } else {
1184
+ result[key] = sv
1185
+ }
1186
+ }
1187
+ return result
1188
+ }
1189
+
1190
+ async saveSettings(patch: Record<string, any>) {
1191
+ const rows = await this.listPayPalSettings({})
1192
+ const row = rows?.[0]
1193
+ const current = (row?.data || {}) as Record<string, any>
1194
+
1195
+ const next = this.deepMerge(current, patch)
1196
+
1197
+ if (!row) {
1198
+ const created = await this.createPayPalSettings({ data: next })
1199
+ return { data: (created.data || {}) as Record<string, any> }
1200
+ }
1201
+
1202
+ await this.updatePayPalSettings({ id: row.id, data: next })
1203
+ return { data: next }
1204
+ }
1205
+
1206
+ async getActiveCredentials() {
1207
+ const row = await this.getCurrentRow()
1208
+ const env = await this.getCurrentEnvironment()
1209
+
1210
+ if (!row) {
1211
+ throw new Error("PayPal connection row not found. Please complete onboarding.")
1212
+ }
1213
+
1214
+ const c = this.getEnvCreds(row, env)
1215
+ const clientSecret = c.clientSecret || ""
1216
+
1217
+ if (!c.clientId || !clientSecret) {
1218
+ throw new Error(
1219
+ `PayPal credentials missing for environment "${env}". Please save credentials.`
1220
+ )
1221
+ }
1222
+
1223
+ return {
1224
+ environment: env,
1225
+ client_id: c.clientId,
1226
+ client_secret: clientSecret,
1227
+ }
1228
+ }
1229
+
1230
+ async getOrderDetails(orderId: string) {
1231
+ if (!orderId) {
1232
+ throw new Error("PayPal orderId is required")
1233
+ }
1234
+
1235
+ const creds = await this.getActiveCredentials()
1236
+ const base =
1237
+ creds.environment === "live"
1238
+ ? "https://api-m.paypal.com"
1239
+ : "https://api-m.sandbox.paypal.com"
1240
+ const auth = Buffer.from(`${creds.client_id}:${creds.client_secret}`).toString("base64")
1241
+
1242
+ const tokenResp = await fetch(`${base}/v1/oauth2/token`, {
1243
+ method: "POST",
1244
+ headers: {
1245
+ Authorization: `Basic ${auth}`,
1246
+ "Content-Type": "application/x-www-form-urlencoded",
1247
+ "PayPal-Partner-Attribution-Id": this.bnCode,
1248
+ },
1249
+ body: "grant_type=client_credentials",
1250
+ })
1251
+
1252
+ const tokenText = await tokenResp.text()
1253
+ if (!tokenResp.ok) {
1254
+ throw new Error(`PayPal token error (${tokenResp.status}): ${tokenText}`)
1255
+ }
1256
+
1257
+ const tokenJson = JSON.parse(tokenText)
1258
+ const accessToken = String(tokenJson.access_token)
1259
+
1260
+ const resp = await fetch(`${base}/v2/checkout/orders/${orderId}`, {
1261
+ method: "GET",
1262
+ headers: {
1263
+ Authorization: `Bearer ${accessToken}`,
1264
+ "Content-Type": "application/json",
1265
+ "PayPal-Partner-Attribution-Id": this.bnCode,
1266
+ },
1267
+ })
1268
+
1269
+ const text = await resp.text()
1270
+ if (!resp.ok) {
1271
+ throw new Error(`PayPal get order error (${resp.status}): ${text}`)
1272
+ }
1273
+
1274
+ return JSON.parse(text)
1275
+ }
1276
+
1277
+ async createWebhookEventRecord(input: {
1278
+ event_id: string
1279
+ event_type: string
1280
+ resource_id?: string | null
1281
+ payload?: Record<string, unknown>
1282
+ event_version?: string | null
1283
+ transmission_id?: string | null
1284
+ transmission_time?: Date | null
1285
+ status?: string
1286
+ attempt_count?: number
1287
+ }) {
1288
+ try {
1289
+ const created = await this.createPayPalWebhookEvents({
1290
+ event_id: input.event_id,
1291
+ event_type: input.event_type,
1292
+ resource_id: input.resource_id ?? null,
1293
+ payload: input.payload ?? {},
1294
+ event_version: input.event_version ?? null,
1295
+ transmission_id: input.transmission_id ?? null,
1296
+ transmission_time: input.transmission_time ?? null,
1297
+ status: input.status ?? "pending",
1298
+ attempt_count: input.attempt_count ?? 0,
1299
+ next_retry_at: null,
1300
+ processed_at: null,
1301
+ last_error: null,
1302
+ })
1303
+ return { created: true, event: created }
1304
+ } catch (error: any) {
1305
+ const message = String(error?.message || "")
1306
+ if (message.includes("paypal_webhook_event_event_id_unique") || message.includes("unique")) {
1307
+ const existing = await this.listPayPalWebhookEvents({ event_id: input.event_id })
1308
+ return { created: false, event: existing?.[0] ?? null }
1309
+ }
1310
+ throw error
1311
+ }
1312
+ }
1313
+
1314
+ async updateWebhookEventRecord(input: {
1315
+ id: string
1316
+ status?: string
1317
+ attempt_count?: number
1318
+ next_retry_at?: Date | null
1319
+ processed_at?: Date | null
1320
+ last_error?: string | null
1321
+ resource_id?: string | null
1322
+ }) {
1323
+ return await this.updatePayPalWebhookEvents({
1324
+ id: input.id,
1325
+ status: input.status,
1326
+ attempt_count: input.attempt_count,
1327
+ next_retry_at: input.next_retry_at ?? null,
1328
+ processed_at: input.processed_at ?? null,
1329
+ last_error: input.last_error ?? null,
1330
+ resource_id: input.resource_id ?? null,
1331
+ })
1332
+ }
1333
+
1334
+ async recordAuditEvent(_eventType: string, _metadata?: Record<string, unknown>) {
1335
+ return null
1336
+ }
1337
+
1338
+ async recordMetric(name: string, metadata?: Record<string, unknown>) {
1339
+ const existing = await this.listPayPalMetrics({ name })
1340
+ const row = existing?.[0]
1341
+ const current = (row?.data || {}) as Record<string, any>
1342
+ const next = {
1343
+ ...current,
1344
+ ...(metadata || {}),
1345
+ count: Number(current.count || 0) + 1,
1346
+ last_recorded_at: new Date().toISOString(),
1347
+ }
1348
+
1349
+ if (!row) {
1350
+ return await this.createPayPalMetrics({
1351
+ name,
1352
+ data: next,
1353
+ })
1354
+ }
1355
+
1356
+ return await this.updatePayPalMetrics({
1357
+ id: row.id,
1358
+ name,
1359
+ data: next,
1360
+ })
1361
+ }
1362
+
1363
+ async recordPaymentLog(eventType: string, metadata?: Record<string, unknown>) {
1364
+ return await this.recordAuditEvent(`payment_${eventType}`, metadata)
1365
+ }
1366
+
1367
+ async sendAlert(input: {
1368
+ type: string
1369
+ message: string
1370
+ metadata?: Record<string, unknown>
1371
+ }) {
1372
+ const urls = this.getAlertWebhookUrls()
1373
+ if (urls.length === 0) {
1374
+ return
1375
+ }
1376
+
1377
+ const payload = {
1378
+ type: input.type,
1379
+ message: input.message,
1380
+ metadata: input.metadata ?? {},
1381
+ source: "paypal",
1382
+ timestamp: new Date().toISOString(),
1383
+ }
1384
+
1385
+ await Promise.all(
1386
+ urls.map(async (url) => {
1387
+ try {
1388
+ const resp = await fetch(url, {
1389
+ method: "POST",
1390
+ headers: {
1391
+ "Content-Type": "application/json",
1392
+ },
1393
+ body: JSON.stringify(payload),
1394
+ })
1395
+ if (!resp.ok) {
1396
+ const text = await resp.text().catch(() => "")
1397
+ await this.recordAuditEvent("alert_failed", {
1398
+ url,
1399
+ status: resp.status,
1400
+ response: text,
1401
+ })
1402
+ } else {
1403
+ await this.recordAuditEvent("alert_sent", { url, type: input.type })
1404
+ }
1405
+ } catch (error: any) {
1406
+ await this.recordAuditEvent("alert_failed", {
1407
+ url,
1408
+ message: error?.message,
1409
+ })
1410
+ }
1411
+ })
1412
+ )
1413
+ }
1414
+ }
1415
+
1416
+ export default PayPalModuleService