@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,270 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
+ import { randomUUID } from "crypto"
3
+ import type PayPalModuleService from "../../../../modules/paypal/service"
4
+ import { isPayPalProviderId } from "../../../../modules/paypal/utils/provider-ids"
5
+
6
+ type Body = {
7
+ cart_id: string
8
+ order_id: string
9
+ }
10
+
11
+ async function getPayPalApiBase(environment: string) {
12
+ return environment === "live"
13
+ ? "https://api-m.paypal.com"
14
+ : "https://api-m.sandbox.paypal.com"
15
+ }
16
+
17
+ async function getPayPalAccessToken(opts: {
18
+ environment: string
19
+ client_id: string
20
+ client_secret: string
21
+ }) {
22
+ const base = await getPayPalApiBase(opts.environment)
23
+ const auth = Buffer.from(`${opts.client_id}:${opts.client_secret}`).toString("base64")
24
+
25
+ const resp = await fetch(`${base}/v1/oauth2/token`, {
26
+ method: "POST",
27
+ headers: {
28
+ Authorization: `Basic ${auth}`,
29
+ "Content-Type": "application/x-www-form-urlencoded",
30
+ },
31
+ body: "grant_type=client_credentials",
32
+ })
33
+
34
+ const text = await resp.text()
35
+ if (!resp.ok) {
36
+ throw new Error(`PayPal token error (${resp.status}): ${text}`)
37
+ }
38
+
39
+ const json = JSON.parse(text)
40
+ return { accessToken: String(json.access_token), base }
41
+ }
42
+
43
+ function resolveIdempotencyKey(req: MedusaRequest, suffix: string, fallback: string) {
44
+ const header =
45
+ req.headers["idempotency-key"] ||
46
+ req.headers["Idempotency-Key"] ||
47
+ req.headers["x-idempotency-key"] ||
48
+ req.headers["X-Idempotency-Key"]
49
+ const key = Array.isArray(header) ? header[0] : header
50
+ if (key && String(key).trim()) {
51
+ return `${String(key).trim()}-${suffix}`
52
+ }
53
+ return fallback || `pp-${suffix}-${randomUUID()}`
54
+ }
55
+
56
+ async function attachPayPalCaptureToSession(
57
+ req: MedusaRequest,
58
+ cartId: string,
59
+ orderId: string,
60
+ capture: any
61
+ ) {
62
+ try {
63
+ const paymentCollectionService = req.scope.resolve("payment_collection") as any
64
+ const paymentSessionService = req.scope.resolve("payment_session") as any
65
+
66
+ const pc = await paymentCollectionService.retrieveByCartId(cartId).catch(() => null)
67
+ if (!pc?.id) {
68
+ return
69
+ }
70
+
71
+ const sessions = await paymentSessionService.list({ payment_collection_id: pc.id })
72
+ const paypalSession = sessions?.find((s: any) => isPayPalProviderId(s.provider_id))
73
+ if (!paypalSession) {
74
+ return
75
+ }
76
+
77
+ await paymentSessionService.update(paypalSession.id, {
78
+ status: "captured",
79
+ data: {
80
+ ...(paypalSession.data || {}),
81
+ paypal: {
82
+ ...((paypalSession.data || {}).paypal || {}),
83
+ order_id: orderId,
84
+ capture_id: capture?.id || capture?.purchase_units?.[0]?.payments?.captures?.[0]?.id,
85
+ capture,
86
+ },
87
+ },
88
+ })
89
+ } catch {
90
+ // ignore
91
+ }
92
+ }
93
+
94
+ async function attachPayPalAuthorizationToSession(
95
+ req: MedusaRequest,
96
+ cartId: string,
97
+ orderId: string,
98
+ authorization: any
99
+ ) {
100
+ try {
101
+ const paymentCollectionService = req.scope.resolve("payment_collection") as any
102
+ const paymentSessionService = req.scope.resolve("payment_session") as any
103
+
104
+ const pc = await paymentCollectionService.retrieveByCartId(cartId).catch(() => null)
105
+ if (!pc?.id) {
106
+ return
107
+ }
108
+
109
+ const sessions = await paymentSessionService.list({ payment_collection_id: pc.id })
110
+ const paypalSession = sessions?.find((s: any) => isPayPalProviderId(s.provider_id))
111
+ if (!paypalSession) {
112
+ return
113
+ }
114
+
115
+ await paymentSessionService.update(paypalSession.id, {
116
+ status: "authorized",
117
+ data: {
118
+ ...(paypalSession.data || {}),
119
+ paypal: {
120
+ ...((paypalSession.data || {}).paypal || {}),
121
+ order_id: orderId,
122
+ authorization_id:
123
+ authorization?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id,
124
+ authorization,
125
+ },
126
+ },
127
+ })
128
+ } catch {
129
+ // ignore
130
+ }
131
+ }
132
+ async function getExistingCapture(
133
+ req: MedusaRequest,
134
+ cartId: string,
135
+ orderId: string
136
+ ) {
137
+ try {
138
+ const paymentCollectionService = req.scope.resolve("payment_collection") as any
139
+ const paymentSessionService = req.scope.resolve("payment_session") as any
140
+
141
+ const pc = await paymentCollectionService.retrieveByCartId(cartId).catch(() => null)
142
+ if (!pc?.id) {
143
+ return null
144
+ }
145
+
146
+ const sessions = await paymentSessionService.list({ payment_collection_id: pc.id })
147
+ const paypalSession = sessions?.find((s: any) => isPayPalProviderId(s.provider_id))
148
+ if (!paypalSession) {
149
+ return null
150
+ }
151
+
152
+ const paypalData = (paypalSession.data || {}).paypal || {}
153
+ const existingOrderId = String(paypalData.order_id || "")
154
+ if (existingOrderId && existingOrderId !== orderId) {
155
+ return null
156
+ }
157
+ if (paypalData.capture) {
158
+ return paypalData.capture
159
+ }
160
+ if (paypalData.capture_id) {
161
+ return { id: paypalData.capture_id }
162
+ }
163
+ return null
164
+ } catch {
165
+ return null
166
+ }
167
+ }
168
+
169
+
170
+ export async function POST(req: MedusaRequest, res: MedusaResponse) {
171
+ const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
172
+ let debugId: string | null = null
173
+
174
+ try {
175
+ const body = (req.body || {}) as Body
176
+ const cartId = body.cart_id
177
+ const orderId = body.order_id
178
+
179
+ if (!cartId || !orderId) {
180
+ return res.status(400).json({ message: "cart_id and order_id are required" })
181
+ }
182
+
183
+ const existingCapture = await getExistingCapture(req, cartId, orderId)
184
+ if (existingCapture) {
185
+ return res.json({ capture: existingCapture })
186
+ }
187
+
188
+ const creds = await paypal.getActiveCredentials()
189
+ const { accessToken, base } = await getPayPalAccessToken(creds)
190
+ const settings = await paypal.getSettings().catch(() => ({}))
191
+ const data =
192
+ settings && typeof settings === "object" && "data" in settings
193
+ ? ((settings as { data?: Record<string, any> }).data ?? {})
194
+ : {}
195
+ const additionalSettings = (data.additional_settings || {}) as Record<string, any>
196
+ const paymentAction =
197
+ typeof additionalSettings.paymentAction === "string"
198
+ ? additionalSettings.paymentAction
199
+ : "capture"
200
+
201
+ const requestId = resolveIdempotencyKey(req, "capture-order", `pp-capture-${orderId}`)
202
+ const endpoint =
203
+ paymentAction === "authorize"
204
+ ? `${base}/v2/checkout/orders/${orderId}/authorize`
205
+ : `${base}/v2/checkout/orders/${orderId}/capture`
206
+
207
+ const ppResp = await fetch(endpoint, {
208
+ method: "POST",
209
+ headers: {
210
+ Authorization: `Bearer ${accessToken}`,
211
+ "Content-Type": "application/json",
212
+ "PayPal-Request-Id": requestId,
213
+ },
214
+ })
215
+
216
+ const ppText = await ppResp.text()
217
+ debugId = ppResp.headers.get("paypal-debug-id")
218
+ if (!ppResp.ok) {
219
+ throw new Error(
220
+ `PayPal capture error (${ppResp.status}): ${ppText}${
221
+ debugId ? ` debug_id=${debugId}` : ""
222
+ }`
223
+ )
224
+ }
225
+
226
+ const payload = JSON.parse(ppText)
227
+ if (paymentAction === "authorize") {
228
+ await attachPayPalAuthorizationToSession(req, cartId, orderId, payload)
229
+ } else {
230
+ await attachPayPalCaptureToSession(req, cartId, orderId, payload)
231
+ }
232
+
233
+ console.info("[PayPal] capture-order", {
234
+ cart_id: cartId,
235
+ order_id: orderId,
236
+ request_id: requestId,
237
+ debug_id: ppResp.headers.get("paypal-debug-id"),
238
+ capture_id: payload?.id,
239
+ })
240
+ try {
241
+ await paypal.recordMetric(
242
+ paymentAction === "authorize" ? "authorize_order_success" : "capture_order_success"
243
+ )
244
+ } catch {
245
+ // ignore metrics failures
246
+ }
247
+
248
+
249
+ // Clear storefront cart cookie (httpOnly) so user gets a fresh cart after successful payment
250
+ // Note: works when storefront and backend share the same cookie domain (e.g. localhost)
251
+ res.setHeader("Set-Cookie", "_medusa_cart_id=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Strict")
252
+ return paymentAction === "authorize"
253
+ ? res.json({ authorization: payload })
254
+ : res.json({ capture: payload })
255
+ } catch (e: any) {
256
+ try {
257
+ const body = (req.body || {}) as Body
258
+ await paypal.recordAuditEvent("capture_order_failed", {
259
+ cart_id: body.cart_id,
260
+ order_id: body.order_id,
261
+ debug_id: debugId,
262
+ message: e?.message || String(e),
263
+ })
264
+ await paypal.recordMetric("capture_order_failed")
265
+ } catch {
266
+ // ignore audit logging failures
267
+ }
268
+ return res.status(500).json({ message: e?.message || "Failed to capture PayPal order" })
269
+ }
270
+ }
@@ -0,0 +1,59 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
+ import type PayPalModuleService from "../../../../modules/paypal/service"
3
+ import {
4
+ getPayPalCurrencyCompatibility,
5
+ getPayPalSupportedCurrencies,
6
+ normalizeCurrencyCode,
7
+ } from "../../../../modules/paypal/utils/currencies"
8
+
9
+ export async function GET(req: MedusaRequest, res: MedusaResponse) {
10
+ const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
11
+
12
+ try {
13
+ const creds = await paypal.getActiveCredentials()
14
+ const apiDetails = await paypal.getApiDetails().catch(() => null)
15
+
16
+ // CardFields/PaymentFields require a client token on the script tag.
17
+ // Generate it server-side and return it with config.
18
+ const client_token = await paypal.generateClientToken({ locale: "en_US" }).catch(() => "")
19
+
20
+ const cartId = (req.query?.cart_id as string) || ""
21
+ const query = req.scope.resolve("query")
22
+
23
+ let currency = normalizeCurrencyCode(
24
+ apiDetails?.apiDetails?.currency_code || process.env.PAYPAL_CURRENCY || "USD"
25
+ )
26
+ if (cartId) {
27
+ const { data: carts } = await query.graph({
28
+ entity: "cart",
29
+ fields: ["id", "currency_code", "region.currency_code"],
30
+ filters: { id: cartId },
31
+ })
32
+
33
+ const cart = carts?.[0]
34
+ if (cart) {
35
+ currency = normalizeCurrencyCode(
36
+ cart.region?.currency_code || cart.currency_code || currency
37
+ )
38
+ }
39
+ }
40
+
41
+ const compatibility = getPayPalCurrencyCompatibility({
42
+ currencyCode: currency,
43
+ paypalCurrencyOverride:
44
+ apiDetails?.apiDetails?.currency_code || process.env.PAYPAL_CURRENCY,
45
+ })
46
+
47
+ return res.json({
48
+ environment: creds.environment,
49
+ client_id: creds.client_id,
50
+ currency: compatibility.currency,
51
+ currency_supported: compatibility.supported,
52
+ currency_errors: compatibility.errors,
53
+ supported_currencies: getPayPalSupportedCurrencies(),
54
+ client_token,
55
+ })
56
+ } catch (e: any) {
57
+ return res.status(500).json({ message: e?.message || "Failed to load PayPal config" })
58
+ }
59
+ }
@@ -0,0 +1,374 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
+ import { randomUUID } from "crypto"
3
+ import { formatAmountForPayPal } from "../../../../modules/paypal/utils/amounts"
4
+ import {
5
+ assertPayPalCurrencySupported,
6
+ normalizeCurrencyCode,
7
+ } from "../../../../modules/paypal/utils/currencies"
8
+ import type PayPalModuleService from "../../../../modules/paypal/service"
9
+ import { isPayPalProviderId } from "../../../../modules/paypal/utils/provider-ids"
10
+
11
+ type Body = {
12
+ cart_id: string
13
+ }
14
+
15
+ async function getPayPalApiBase(environment: string) {
16
+ return environment === "live"
17
+ ? "https://api-m.paypal.com"
18
+ : "https://api-m.sandbox.paypal.com"
19
+ }
20
+
21
+ async function getPayPalAccessToken(opts: {
22
+ environment: string
23
+ client_id: string
24
+ client_secret: string
25
+ }) {
26
+ const base = await getPayPalApiBase(opts.environment)
27
+ const auth = Buffer.from(`${opts.client_id}:${opts.client_secret}`).toString("base64")
28
+
29
+ const resp = await fetch(`${base}/v1/oauth2/token`, {
30
+ method: "POST",
31
+ headers: {
32
+ Authorization: `Basic ${auth}`,
33
+ "Content-Type": "application/x-www-form-urlencoded",
34
+ },
35
+ body: "grant_type=client_credentials",
36
+ })
37
+
38
+ const text = await resp.text()
39
+ if (!resp.ok) {
40
+ throw new Error(`PayPal token error (${resp.status}): ${text}`)
41
+ }
42
+
43
+ const json = JSON.parse(text)
44
+ return { accessToken: String(json.access_token), base }
45
+ }
46
+
47
+ function resolveIdempotencyKey(req: MedusaRequest, suffix: string, fallback: string) {
48
+ const header =
49
+ req.headers["idempotency-key"] ||
50
+ req.headers["Idempotency-Key"] ||
51
+ req.headers["x-idempotency-key"] ||
52
+ req.headers["X-Idempotency-Key"]
53
+ const key = Array.isArray(header) ? header[0] : header
54
+ if (key && String(key).trim()) {
55
+ return `${String(key).trim()}-${suffix}`
56
+ }
57
+ return fallback || `pp-${suffix}-${randomUUID()}`
58
+ }
59
+
60
+ async function attachPayPalOrderToSession(
61
+ req: MedusaRequest,
62
+ cartId: string,
63
+ orderId: string
64
+ ) {
65
+ try {
66
+ const paymentCollectionService = req.scope.resolve("payment_collection") as any
67
+ const paymentSessionService = req.scope.resolve("payment_session") as any
68
+
69
+ const pc = await paymentCollectionService.retrieveByCartId(cartId).catch(() => null)
70
+ if (!pc?.id) {
71
+ return
72
+ }
73
+
74
+ const sessions = await paymentSessionService.list({ payment_collection_id: pc.id })
75
+ const paypalSession = sessions?.find((s: any) => isPayPalProviderId(s.provider_id))
76
+ if (!paypalSession) {
77
+ return
78
+ }
79
+
80
+ await paymentSessionService.update(paypalSession.id, {
81
+ data: {
82
+ ...(paypalSession.data || {}),
83
+ paypal: {
84
+ ...((paypalSession.data || {}).paypal || {}),
85
+ order_id: orderId,
86
+ },
87
+ },
88
+ })
89
+ } catch {
90
+ // ignore best-effort session update
91
+ }
92
+ }
93
+
94
+ async function getExistingPayPalOrderId(req: MedusaRequest, cartId: string) {
95
+ try {
96
+ const paymentCollectionService = req.scope.resolve("payment_collection") as any
97
+ const paymentSessionService = req.scope.resolve("payment_session") as any
98
+
99
+ const pc = await paymentCollectionService.retrieveByCartId(cartId).catch(() => null)
100
+ if (!pc?.id) {
101
+ return null
102
+ }
103
+
104
+ const sessions = await paymentSessionService.list({ payment_collection_id: pc.id })
105
+ const paypalSession = sessions?.find((s: any) => isPayPalProviderId(s.provider_id))
106
+ if (!paypalSession) {
107
+ return null
108
+ }
109
+
110
+ const paypalData = (paypalSession.data || {}).paypal || {}
111
+ return paypalData.order_id ? String(paypalData.order_id) : null
112
+ } catch {
113
+ return null
114
+ }
115
+ }
116
+
117
+ function resolveReturnUrl(req: MedusaRequest) {
118
+ const configured = process.env.STOREFRONT_URL || process.env.STORE_URL
119
+ if (!configured) {
120
+ return undefined
121
+ }
122
+ return `${configured.replace(/\/$/, "")}/checkout`
123
+ }
124
+
125
+ function resolveCancelUrl(req: MedusaRequest) {
126
+ const configured = process.env.STOREFRONT_URL || process.env.STORE_URL
127
+ if (!configured) {
128
+ return undefined
129
+ }
130
+ return `${configured.replace(/\/$/, "")}/cart`
131
+ }
132
+
133
+ export async function POST(req: MedusaRequest, res: MedusaResponse) {
134
+ const paypal = req.scope.resolve<PayPalModuleService>("paypal_onboarding")
135
+ let debugId: string | null = null
136
+
137
+ try {
138
+ const body = (req.body || {}) as Body
139
+ const cartId = body.cart_id
140
+
141
+ if (!cartId) {
142
+ return res.status(400).json({ message: "cart_id is required" })
143
+ }
144
+
145
+ const existingOrderId = await getExistingPayPalOrderId(req, cartId)
146
+ if (existingOrderId) {
147
+ return res.json({ id: existingOrderId })
148
+ }
149
+
150
+ /**
151
+ * ✅ Medusa v2 cart retrieval via Query Graph
152
+ */
153
+ const query = req.scope.resolve("query")
154
+
155
+ const { data } = await query.graph({
156
+ entity: "cart",
157
+ fields: [
158
+ "id",
159
+ "total",
160
+ "subtotal",
161
+ "shipping_total",
162
+ "tax_total",
163
+ "discount_total",
164
+ "gift_card_total",
165
+ "currency_code",
166
+ "region.currency_code",
167
+ ],
168
+ filters: { id: cartId },
169
+ })
170
+
171
+ const cart = (data?.[0] as any) || null
172
+
173
+ if (!cart) {
174
+ return res.status(404).json({ message: "Cart not found" })
175
+ }
176
+
177
+ const creds = await paypal.getActiveCredentials()
178
+
179
+ type PayPalSettingsResponse = {
180
+ data?: { additional_settings?: Record<string, unknown>; api_details?: Record<string, unknown> }
181
+ }
182
+ const settings = await paypal
183
+ .getSettings()
184
+ .catch((): PayPalSettingsResponse => ({}))
185
+ const additionalSettings = settings.data?.additional_settings || {}
186
+ const apiDetails = settings.data?.api_details || {}
187
+ const configuredCurrency =
188
+ typeof apiDetails.currency_code === "string"
189
+ ? normalizeCurrencyCode(apiDetails.currency_code)
190
+ : normalizeCurrencyCode(process.env.PAYPAL_CURRENCY || "USD")
191
+
192
+ const currency = normalizeCurrencyCode(
193
+ cart.region?.currency_code || cart.currency_code || configuredCurrency
194
+ )
195
+ assertPayPalCurrencySupported({
196
+ currencyCode: currency,
197
+ paypalCurrencyOverride: configuredCurrency,
198
+ })
199
+
200
+ const totalMinor = Number(cart.total || 0)
201
+ const value = formatAmountForPayPal(totalMinor, currency)
202
+
203
+ const paymentActionRaw =
204
+ typeof additionalSettings.paymentAction === "string"
205
+ ? additionalSettings.paymentAction
206
+ : "capture"
207
+ const paymentAction = paymentActionRaw === "authorize" ? "AUTHORIZE" : "CAPTURE"
208
+ const brandName =
209
+ typeof additionalSettings.brandName === "string"
210
+ ? additionalSettings.brandName
211
+ : undefined
212
+ const landingPageRaw =
213
+ typeof additionalSettings.landingPage === "string"
214
+ ? additionalSettings.landingPage
215
+ : undefined
216
+ const landingPage =
217
+ landingPageRaw === "login"
218
+ ? "LOGIN"
219
+ : landingPageRaw === "billing"
220
+ ? "BILLING"
221
+ : landingPageRaw === "no_preference"
222
+ ? "NO_PREFERENCE"
223
+ : undefined
224
+ const skipReview =
225
+ typeof additionalSettings.skipOrderReviewPage === "boolean"
226
+ ? additionalSettings.skipOrderReviewPage
227
+ : undefined
228
+ const requireInstantPayment =
229
+ typeof additionalSettings.requireInstantPayment === "boolean"
230
+ ? additionalSettings.requireInstantPayment
231
+ : undefined
232
+ const invoicePrefix =
233
+ typeof additionalSettings.invoicePrefix === "string"
234
+ ? additionalSettings.invoicePrefix
235
+ : ""
236
+ const invoiceId = `${invoicePrefix}${cart.id}`.trim() || cart.id
237
+ const returnUrl =
238
+ (typeof apiDetails.storefront_url === "string" && apiDetails.storefront_url.trim()
239
+ ? `${apiDetails.storefront_url.replace(/\/$/, "")}/checkout`
240
+ : resolveReturnUrl(req))
241
+ const cancelUrl =
242
+ (typeof apiDetails.storefront_url === "string" && apiDetails.storefront_url.trim()
243
+ ? `${apiDetails.storefront_url.replace(/\/$/, "")}/cart`
244
+ : resolveCancelUrl(req))
245
+
246
+ const applicationContext: Record<string, any> = {
247
+ ...(brandName ? { brand_name: brandName } : {}),
248
+ ...(landingPage ? { landing_page: landingPage } : {}),
249
+ ...(typeof skipReview === "boolean"
250
+ ? { user_action: skipReview ? "PAY_NOW" : "CONTINUE" }
251
+ : {}),
252
+ ...(requireInstantPayment ? { payment_method_preference: "IMMEDIATE_PAYMENT_REQUIRED" } : {}),
253
+ ...(returnUrl ? { return_url: returnUrl } : {}),
254
+ ...(cancelUrl ? { cancel_url: cancelUrl } : {}),
255
+ }
256
+
257
+ const breakdown: Record<string, any> = {}
258
+ const subtotalMinor = Number(cart.subtotal || 0)
259
+ const shippingMinor = Number(cart.shipping_total || 0)
260
+ const taxMinor = Number(cart.tax_total || 0)
261
+ const discountMinor = Number(cart.discount_total || 0)
262
+ const giftCardMinor = Number(cart.gift_card_total || 0)
263
+ const adjustmentMinor = Math.max(
264
+ 0,
265
+ subtotalMinor + shippingMinor + taxMinor - totalMinor
266
+ )
267
+ if (subtotalMinor > 0) {
268
+ breakdown.item_total = {
269
+ currency_code: currency,
270
+ value: formatAmountForPayPal(subtotalMinor, currency),
271
+ }
272
+ }
273
+ if (shippingMinor > 0) {
274
+ breakdown.shipping = {
275
+ currency_code: currency,
276
+ value: formatAmountForPayPal(shippingMinor, currency),
277
+ }
278
+ }
279
+ if (taxMinor > 0) {
280
+ breakdown.tax_total = {
281
+ currency_code: currency,
282
+ value: formatAmountForPayPal(taxMinor, currency),
283
+ }
284
+ }
285
+ const discountValue = Math.max(0, discountMinor + giftCardMinor, adjustmentMinor)
286
+ if (discountValue > 0) {
287
+ breakdown.discount = {
288
+ currency_code: currency,
289
+ value: formatAmountForPayPal(discountValue, currency),
290
+ }
291
+ }
292
+
293
+ const { accessToken, base } = await getPayPalAccessToken({
294
+ environment: creds.environment,
295
+ client_id: creds.client_id,
296
+ client_secret: creds.client_secret,
297
+ })
298
+
299
+ const requestId = resolveIdempotencyKey(req, "create-order", `pp-create-${cart.id}`)
300
+
301
+ const ppResp = await fetch(`${base}/v2/checkout/orders`, {
302
+ method: "POST",
303
+ headers: {
304
+ Authorization: `Bearer ${accessToken}`,
305
+ "Content-Type": "application/json",
306
+ "PayPal-Request-Id": requestId,
307
+ },
308
+ body: JSON.stringify({
309
+ intent: paymentAction,
310
+ purchase_units: [
311
+ {
312
+ reference_id: cart.id,
313
+ invoice_id: invoiceId,
314
+ amount: {
315
+ currency_code: currency,
316
+ value,
317
+ ...(Object.keys(breakdown).length > 0 ? { breakdown } : {}),
318
+ },
319
+ },
320
+ ],
321
+ custom_id: cart.id,
322
+ ...(Object.keys(applicationContext).length > 0
323
+ ? { application_context: applicationContext }
324
+ : {}),
325
+ }),
326
+ })
327
+
328
+ const ppText = await ppResp.text()
329
+ debugId = ppResp.headers.get("paypal-debug-id")
330
+ if (!ppResp.ok) {
331
+ throw new Error(
332
+ `PayPal create order error (${ppResp.status}): ${ppText}${
333
+ debugId ? ` debug_id=${debugId}` : ""
334
+ }`
335
+ )
336
+ }
337
+
338
+ const order = JSON.parse(ppText)
339
+
340
+ await attachPayPalOrderToSession(req, cart.id, order.id)
341
+
342
+ console.info("[PayPal] create-order", {
343
+ cart_id: cart.id,
344
+ order_id: order.id,
345
+ request_id: requestId,
346
+ debug_id: ppResp.headers.get("paypal-debug-id"),
347
+ })
348
+ try {
349
+ await paypal.recordMetric("create_order_success")
350
+ } catch {
351
+ // ignore metrics failures
352
+ }
353
+ return res.json({ id: order.id })
354
+ } catch (e: any) {
355
+ try {
356
+ const body = (req.body || {}) as Body
357
+ await paypal.recordAuditEvent("create_order_failed", {
358
+ cart_id: body.cart_id,
359
+ debug_id: debugId,
360
+ message: e?.message || String(e),
361
+ })
362
+ await paypal.recordMetric("create_order_failed")
363
+ } catch {
364
+ // ignore audit logging failures
365
+ }
366
+ const message = e?.message || "Failed to create PayPal order"
367
+ const status = message.includes("PayPal does not support currency")
368
+ ? 400
369
+ : message.includes("PayPal is configured for")
370
+ ? 400
371
+ : 500
372
+ return res.status(status).json({ message })
373
+ }
374
+ }