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