@omega.js/backend 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 (993) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +1053 -0
  3. package/bin/omega +2 -0
  4. package/bin/omega-backend +2 -0
  5. package/bin/omg +2 -0
  6. package/cli.js +3 -0
  7. package/dist/cli/command-table.js +268 -0
  8. package/dist/cli/commands/auth.js +259 -0
  9. package/dist/cli/commands/base-command.js +451 -0
  10. package/dist/cli/commands/build.js +27 -0
  11. package/dist/cli/commands/clean.js +13 -0
  12. package/dist/cli/commands/clear.js +11 -0
  13. package/dist/cli/commands/cwd.js +9 -0
  14. package/dist/cli/commands/deploy.js +205 -0
  15. package/dist/cli/commands/emulator-orphans.js +440 -0
  16. package/dist/cli/commands/emulator.js +1829 -0
  17. package/dist/cli/commands/firebase-init.js +101 -0
  18. package/dist/cli/commands/firestore.js +261 -0
  19. package/dist/cli/commands/indexes.js +51 -0
  20. package/dist/cli/commands/install.js +151 -0
  21. package/dist/cli/commands/logs.js +358 -0
  22. package/dist/cli/commands/mcp.js +41 -0
  23. package/dist/cli/commands/migrate-markers.js +176 -0
  24. package/dist/cli/commands/migrate-rules.js +90 -0
  25. package/dist/cli/commands/migrate.js +59 -0
  26. package/dist/cli/commands/serve.js +208 -0
  27. package/dist/cli/commands/setup-tests/base-test.js +174 -0
  28. package/dist/cli/commands/setup-tests/emulator-config.js +109 -0
  29. package/dist/cli/commands/setup-tests/env-runtime-config-deprecated.js +47 -0
  30. package/dist/cli/commands/setup-tests/firebase-admin.js +49 -0
  31. package/dist/cli/commands/setup-tests/firebase-auth.js +26 -0
  32. package/dist/cli/commands/setup-tests/firebase-cli.js +26 -0
  33. package/dist/cli/commands/setup-tests/firebase-functions.js +49 -0
  34. package/dist/cli/commands/setup-tests/firestore-indexes-file.js +72 -0
  35. package/dist/cli/commands/setup-tests/firestore-indexes-in-json.js +20 -0
  36. package/dist/cli/commands/setup-tests/firestore-indexes-required.js +108 -0
  37. package/dist/cli/commands/setup-tests/firestore-indexes-synced.js +236 -0
  38. package/dist/cli/commands/setup-tests/firestore-rules-file.js +115 -0
  39. package/dist/cli/commands/setup-tests/firestore-rules-in-json.js +49 -0
  40. package/dist/cli/commands/setup-tests/functions-package.js +44 -0
  41. package/dist/cli/commands/setup-tests/gcloud-cli.js +26 -0
  42. package/dist/cli/commands/setup-tests/gitignore.js +122 -0
  43. package/dist/cli/commands/setup-tests/helpers/merge-line-files.js +28 -0
  44. package/dist/cli/commands/setup-tests/helpers/required-indexes.js +109 -0
  45. package/dist/cli/commands/setup-tests/helpers/seed-campaigns.js +246 -0
  46. package/dist/cli/commands/setup-tests/helpers.js +72 -0
  47. package/dist/cli/commands/setup-tests/hosting-folder.js +23 -0
  48. package/dist/cli/commands/setup-tests/hosting-rewrites.js +52 -0
  49. package/dist/cli/commands/setup-tests/index.js +111 -0
  50. package/dist/cli/commands/setup-tests/is-firebase-project.js +21 -0
  51. package/dist/cli/commands/setup-tests/java-installed.js +27 -0
  52. package/dist/cli/commands/setup-tests/legacy-tests-cleanup.js +43 -0
  53. package/dist/cli/commands/setup-tests/marketing-campaigns-seeded.js +230 -0
  54. package/dist/cli/commands/setup-tests/node-version.js +60 -0
  55. package/dist/cli/commands/setup-tests/npm-project-scripts.js +42 -0
  56. package/dist/cli/commands/setup-tests/nvmrc-version.js +36 -0
  57. package/dist/cli/commands/setup-tests/omega-backend.js +42 -0
  58. package/dist/cli/commands/setup-tests/omega-config.js +84 -0
  59. package/dist/cli/commands/setup-tests/project-directories.js +34 -0
  60. package/dist/cli/commands/setup-tests/project-id-consistency.js +201 -0
  61. package/dist/cli/commands/setup-tests/public-html-files.js +22 -0
  62. package/dist/cli/commands/setup-tests/realtime-rules-file.js +69 -0
  63. package/dist/cli/commands/setup-tests/realtime-rules-in-json.js +20 -0
  64. package/dist/cli/commands/setup-tests/remoteconfig-template-file.js +32 -0
  65. package/dist/cli/commands/setup-tests/remoteconfig-template-in-json.js +31 -0
  66. package/dist/cli/commands/setup-tests/service-account.js +86 -0
  67. package/dist/cli/commands/setup-tests/storage-lifecycle-policy.js +81 -0
  68. package/dist/cli/commands/setup-tests/storage-rules-file.js +32 -0
  69. package/dist/cli/commands/setup-tests/storage-rules-in-json.js +20 -0
  70. package/dist/cli/commands/stripe.js +14 -0
  71. package/dist/cli/commands/test-lanes/stripe-live.js +440 -0
  72. package/dist/cli/commands/test.js +976 -0
  73. package/dist/cli/commands/update.js +27 -0
  74. package/dist/cli/commands/version.js +10 -0
  75. package/dist/cli/commands/watch.js +196 -0
  76. package/dist/cli/flags.js +13 -0
  77. package/dist/cli/index.js +173 -0
  78. package/dist/cli/run.js +33 -0
  79. package/dist/cli/utils/attach-log-file.js +4 -0
  80. package/dist/cli/utils/compile-rules.js +1155 -0
  81. package/dist/cli/utils/ensure-target.js +314 -0
  82. package/dist/cli/utils/project-type.js +140 -0
  83. package/dist/cli/utils/public-files.js +49 -0
  84. package/dist/cli/utils/safe-install.js +4 -0
  85. package/dist/cli/utils/spawn-shell.js +94 -0
  86. package/dist/cli/utils/stage-functions.js +338 -0
  87. package/dist/cli/utils/stage-local-packages.js +210 -0
  88. package/dist/cli/utils/target-checks.js +161 -0
  89. package/dist/cli/utils/target.js +24 -0
  90. package/dist/cli/utils/ui.js +292 -0
  91. package/dist/defaults/AGENTS.md +125 -0
  92. package/dist/defaults/CHANGELOG.md +15 -0
  93. package/dist/defaults/CLAUDE.md +1 -0
  94. package/dist/defaults/_.gitignore +65 -0
  95. package/dist/defaults/docs/README.md +17 -0
  96. package/dist/defaults/test/README.md +65 -0
  97. package/dist/defaults/test/_init.js +14 -0
  98. package/dist/defaults/test/helpers/connect-trap.js +72 -0
  99. package/dist/defaults/test/unit/registration.test.js +143 -0
  100. package/dist/defaults/test/unit/rules-posture.test.js +101 -0
  101. package/dist/defaults/test/unit/socket-free.test.js +43 -0
  102. package/dist/manager/events/auth/before-create.js +89 -0
  103. package/dist/manager/events/auth/before-signin.js +76 -0
  104. package/dist/manager/events/auth/on-create.js +112 -0
  105. package/dist/manager/events/auth/on-delete.js +141 -0
  106. package/dist/manager/events/auth/utils.js +93 -0
  107. package/dist/manager/events/cron/daily/blog-auto-publisher.js +287 -0
  108. package/dist/manager/events/cron/daily/data-requests.js +57 -0
  109. package/dist/manager/events/cron/daily/expire-paypal-cancellations.js +191 -0
  110. package/dist/manager/events/cron/daily/marketing-prune.js +486 -0
  111. package/dist/manager/events/cron/daily/reset-usage.js +188 -0
  112. package/dist/manager/events/cron/daily/trial-lapse-sweep.js +355 -0
  113. package/dist/manager/events/cron/daily.js +3 -0
  114. package/dist/manager/events/cron/frequent/abandoned-carts.js +204 -0
  115. package/dist/manager/events/cron/frequent/email-queue.js +71 -0
  116. package/dist/manager/events/cron/frequent/marketing-campaigns.js +463 -0
  117. package/dist/manager/events/cron/frequent/retry-failed-webhooks.js +70 -0
  118. package/dist/manager/events/cron/frequent.js +3 -0
  119. package/dist/manager/events/cron/runner.js +64 -0
  120. package/dist/manager/events/firestore/notifications/on-write.js +123 -0
  121. package/dist/manager/events/firestore/payments-disputes/on-write.js +256 -0
  122. package/dist/manager/events/firestore/payments-disputes/providers/stripe.js +218 -0
  123. package/dist/manager/events/firestore/payments-disputes/providers/test.js +231 -0
  124. package/dist/manager/events/firestore/payments-webhooks/analytics.js +687 -0
  125. package/dist/manager/events/firestore/payments-webhooks/on-write.js +1224 -0
  126. package/dist/manager/events/firestore/payments-webhooks/transitions/index.js +395 -0
  127. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +58 -0
  128. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +28 -0
  129. package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-refunded.js +30 -0
  130. package/dist/manager/events/firestore/payments-webhooks/transitions/send-email.js +74 -0
  131. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-removed.js +14 -0
  132. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +28 -0
  133. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/checkout-declined.js +18 -0
  134. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +67 -0
  135. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +25 -0
  136. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +26 -0
  137. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js +34 -0
  138. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +42 -0
  139. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +33 -0
  140. package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-winback.js +17 -0
  141. package/dist/manager/functions/_legacy/actions/create-post-handler.js +188 -0
  142. package/dist/manager/functions/_legacy/actions/generate-uuid.js +63 -0
  143. package/dist/manager/functions/_legacy/actions/sign-up-handler.js +205 -0
  144. package/dist/manager/functions/_legacy/admin/create-post.js +207 -0
  145. package/dist/manager/functions/_legacy/admin/firestore-write.js +72 -0
  146. package/dist/manager/functions/_legacy/admin/get-stats.js +218 -0
  147. package/dist/manager/functions/_legacy/admin/query.js +198 -0
  148. package/dist/manager/functions/_legacy/admin/send-notification.js +206 -0
  149. package/dist/manager/functions/_legacy/template.js +33 -0
  150. package/dist/manager/functions/_legacy/test/authenticate.js +40 -0
  151. package/dist/manager/functions/_legacy/test/webhook.js +37 -0
  152. package/dist/manager/functions/wrappers/mailchimp/addToList.js +25 -0
  153. package/dist/manager/helpers/analytics.js +498 -0
  154. package/dist/manager/helpers/api-manager.js +313 -0
  155. package/dist/manager/helpers/backend-router.js +37 -0
  156. package/dist/manager/helpers/context/authenticate.js +215 -0
  157. package/dist/manager/helpers/context/client-info.js +91 -0
  158. package/dist/manager/helpers/context/index.js +228 -0
  159. package/dist/manager/helpers/context/logging.js +150 -0
  160. package/dist/manager/helpers/context/parse.js +160 -0
  161. package/dist/manager/helpers/context/respond.js +247 -0
  162. package/dist/manager/helpers/event-middleware.js +109 -0
  163. package/dist/manager/helpers/metadata.js +32 -0
  164. package/dist/manager/helpers/middleware.js +541 -0
  165. package/dist/manager/helpers/redact-secret.js +25 -0
  166. package/dist/manager/helpers/resolved-config.js +34 -0
  167. package/dist/manager/helpers/roles.js +69 -0
  168. package/dist/manager/helpers/safe-compare.js +31 -0
  169. package/dist/manager/helpers/schema-engine.js +314 -0
  170. package/dist/manager/helpers/schema-zod.js +203 -0
  171. package/dist/manager/helpers/settings.js +215 -0
  172. package/dist/manager/helpers/usage.js +541 -0
  173. package/dist/manager/helpers/user.js +57 -0
  174. package/dist/manager/helpers/utilities.js +565 -0
  175. package/dist/manager/index.js +1404 -0
  176. package/dist/manager/libraries/abandoned-cart-config.js +12 -0
  177. package/dist/manager/libraries/ai/index.js +321 -0
  178. package/dist/manager/libraries/ai/prompt.js +85 -0
  179. package/dist/manager/libraries/ai/providers/anthropic-format.js +297 -0
  180. package/dist/manager/libraries/ai/providers/anthropic.js +169 -0
  181. package/dist/manager/libraries/ai/providers/claude-code.js +183 -0
  182. package/dist/manager/libraries/ai/providers/openai.js +1272 -0
  183. package/dist/manager/libraries/ai/providers/test.js +257 -0
  184. package/dist/manager/libraries/ai/tokens.js +54 -0
  185. package/dist/manager/libraries/analytics/conversions.js +478 -0
  186. package/dist/manager/libraries/analytics/match-data.js +573 -0
  187. package/dist/manager/libraries/analytics/signup.js +120 -0
  188. package/dist/manager/libraries/auth-user.js +46 -0
  189. package/dist/manager/libraries/content/feed-parser.js +198 -0
  190. package/dist/manager/libraries/content/ghostii.js +191 -0
  191. package/dist/manager/libraries/content/source-resolver.js +629 -0
  192. package/dist/manager/libraries/email/constants.js +532 -0
  193. package/dist/manager/libraries/email/data/blocked-local-parts.json +55 -0
  194. package/dist/manager/libraries/email/data/blocked-local-patterns.js +14 -0
  195. package/dist/manager/libraries/email/data/corporate-domains.json +23 -0
  196. package/dist/manager/libraries/email/data/custom-disposable-domains.json +58 -0
  197. package/dist/manager/libraries/email/data/disposable-domains.json +8184 -0
  198. package/dist/manager/libraries/email/data/typo-domains.js +83 -0
  199. package/dist/manager/libraries/email/disposable-domains.js +92 -0
  200. package/dist/manager/libraries/email/generators/lib/filter.js +179 -0
  201. package/dist/manager/libraries/email/generators/lib/image-host.js +344 -0
  202. package/dist/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
  203. package/dist/manager/libraries/email/generators/lib/markdown-renderer.js +294 -0
  204. package/dist/manager/libraries/email/generators/lib/mjml-template.js +120 -0
  205. package/dist/manager/libraries/email/generators/lib/structure.js +299 -0
  206. package/dist/manager/libraries/email/generators/lib/svg-illustrator.js +197 -0
  207. package/dist/manager/libraries/email/generators/lib/templates/base.js +229 -0
  208. package/dist/manager/libraries/email/generators/lib/templates/card.js +33 -0
  209. package/dist/manager/libraries/email/generators/lib/templates/classic-schema.js +59 -0
  210. package/dist/manager/libraries/email/generators/lib/templates/clean.js +82 -0
  211. package/dist/manager/libraries/email/generators/lib/templates/editorial/helpers.js +100 -0
  212. package/dist/manager/libraries/email/generators/lib/templates/editorial/index.js +318 -0
  213. package/dist/manager/libraries/email/generators/lib/templates/feedback.js +101 -0
  214. package/dist/manager/libraries/email/generators/lib/templates/field-report/helpers.js +138 -0
  215. package/dist/manager/libraries/email/generators/lib/templates/field-report/index.js +494 -0
  216. package/dist/manager/libraries/email/generators/lib/templates/index.js +47 -0
  217. package/dist/manager/libraries/email/generators/lib/templates/newsletter-shared.js +610 -0
  218. package/dist/manager/libraries/email/generators/lib/templates/order.js +461 -0
  219. package/dist/manager/libraries/email/generators/lib/templates/plain.js +83 -0
  220. package/dist/manager/libraries/email/generators/lib/templates/shared-campaign.js +64 -0
  221. package/dist/manager/libraries/email/generators/newsletter.js +899 -0
  222. package/dist/manager/libraries/email/index.js +146 -0
  223. package/dist/manager/libraries/email/marketing/index.js +754 -0
  224. package/dist/manager/libraries/email/prepare.js +365 -0
  225. package/dist/manager/libraries/email/providers/beehiiv.js +589 -0
  226. package/dist/manager/libraries/email/providers/sendgrid.js +845 -0
  227. package/dist/manager/libraries/email/transactional/index.js +510 -0
  228. package/dist/manager/libraries/email/utm.js +86 -0
  229. package/dist/manager/libraries/email/validation-provider-neverbounce.js +70 -0
  230. package/dist/manager/libraries/email/validation-provider-zerobounce.js +42 -0
  231. package/dist/manager/libraries/email/validation.js +255 -0
  232. package/dist/manager/libraries/env.js +198 -0
  233. package/dist/manager/libraries/infer-contact.js +101 -0
  234. package/dist/manager/libraries/load-provider.js +65 -0
  235. package/dist/manager/libraries/notification.js +256 -0
  236. package/dist/manager/libraries/openai.js +13 -0
  237. package/dist/manager/libraries/payment/discount-codes.js +204 -0
  238. package/dist/manager/libraries/payment/fetch-failure.js +56 -0
  239. package/dist/manager/libraries/payment/license.js +44 -0
  240. package/dist/manager/libraries/payment/order-id.js +57 -0
  241. package/dist/manager/libraries/payment/provider-errors.js +71 -0
  242. package/dist/manager/libraries/payment/providers/chargebee.js +817 -0
  243. package/dist/manager/libraries/payment/providers/coinbase.js +330 -0
  244. package/dist/manager/libraries/payment/providers/paypal.js +987 -0
  245. package/dist/manager/libraries/payment/providers/stripe.js +755 -0
  246. package/dist/manager/libraries/payment/providers/test.js +250 -0
  247. package/dist/manager/libraries/payment/refund-linkage.js +78 -0
  248. package/dist/manager/libraries/payment/refund-policy.js +113 -0
  249. package/dist/manager/libraries/payment/winback.js +55 -0
  250. package/dist/manager/libraries/prompts/infer-contact.md +78 -0
  251. package/dist/manager/libraries/rate-limits.js +17 -0
  252. package/dist/manager/libraries/recaptcha.js +58 -0
  253. package/dist/manager/libraries/user-doc.js +234 -0
  254. package/dist/manager/routes/admin/backup/post.js +106 -0
  255. package/dist/manager/routes/admin/cron/post.js +37 -0
  256. package/dist/manager/routes/admin/database/get.js +35 -0
  257. package/dist/manager/routes/admin/database/post.js +34 -0
  258. package/dist/manager/routes/admin/email/post.js +74 -0
  259. package/dist/manager/routes/admin/firestore/get.js +35 -0
  260. package/dist/manager/routes/admin/firestore/post.js +56 -0
  261. package/dist/manager/routes/admin/firestore/query/post.js +128 -0
  262. package/dist/manager/routes/admin/hook/post.js +104 -0
  263. package/dist/manager/routes/admin/infer-contact/post.js +35 -0
  264. package/dist/manager/routes/admin/notification/post.js +37 -0
  265. package/dist/manager/routes/admin/payment/post.js +56 -0
  266. package/dist/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
  267. package/dist/manager/routes/admin/post/dispatch-deploy.js +36 -0
  268. package/dist/manager/routes/admin/post/post.js +487 -0
  269. package/dist/manager/routes/admin/post/put.js +146 -0
  270. package/dist/manager/routes/admin/post/templates/post.html +14 -0
  271. package/dist/manager/routes/admin/repo/content/post.js +106 -0
  272. package/dist/manager/routes/admin/stats/get.js +222 -0
  273. package/dist/manager/routes/admin/users/disable/post.js +50 -0
  274. package/dist/manager/routes/admin/users/list/get.js +110 -0
  275. package/dist/manager/routes/admin/users/sync/post.js +119 -0
  276. package/dist/manager/routes/brand/get.js +26 -0
  277. package/dist/manager/routes/content/post/get.js +110 -0
  278. package/dist/manager/routes/general/email/post.js +89 -0
  279. package/dist/manager/routes/general/email/templates/general/download-app-link.js +42 -0
  280. package/dist/manager/routes/general/uuid/post.js +31 -0
  281. package/dist/manager/routes/handler/post/post.js +142 -0
  282. package/dist/manager/routes/health/get.js +43 -0
  283. package/dist/manager/routes/index.js +11 -0
  284. package/dist/manager/routes/marketing/campaign/delete.js +45 -0
  285. package/dist/manager/routes/marketing/campaign/get.js +69 -0
  286. package/dist/manager/routes/marketing/campaign/post.js +124 -0
  287. package/dist/manager/routes/marketing/campaign/put.js +86 -0
  288. package/dist/manager/routes/marketing/campaign/utils.js +59 -0
  289. package/dist/manager/routes/marketing/contact/delete.js +100 -0
  290. package/dist/manager/routes/marketing/contact/post.js +149 -0
  291. package/dist/manager/routes/marketing/contact/put.js +37 -0
  292. package/dist/manager/routes/marketing/email-preferences/post.js +275 -0
  293. package/dist/manager/routes/marketing/webhook/forward/post.js +170 -0
  294. package/dist/manager/routes/marketing/webhook/post.js +123 -0
  295. package/dist/manager/routes/marketing/webhook/providers/beehiiv.js +197 -0
  296. package/dist/manager/routes/marketing/webhook/providers/sendgrid.js +194 -0
  297. package/dist/manager/routes/payments/cancel/_is-trialing.js +40 -0
  298. package/dist/manager/routes/payments/cancel/post.js +149 -0
  299. package/dist/manager/routes/payments/cancel/providers/chargebee.js +41 -0
  300. package/dist/manager/routes/payments/cancel/providers/paypal.js +56 -0
  301. package/dist/manager/routes/payments/cancel/providers/stripe.js +33 -0
  302. package/dist/manager/routes/payments/cancel/providers/test.js +118 -0
  303. package/dist/manager/routes/payments/discount/get.js +25 -0
  304. package/dist/manager/routes/payments/dispute-alert/post.js +117 -0
  305. package/dist/manager/routes/payments/dispute-alert/providers/chargeblast.js +55 -0
  306. package/dist/manager/routes/payments/intent/post.js +341 -0
  307. package/dist/manager/routes/payments/intent/providers/chargebee.js +197 -0
  308. package/dist/manager/routes/payments/intent/providers/coinbase.js +94 -0
  309. package/dist/manager/routes/payments/intent/providers/paypal.js +244 -0
  310. package/dist/manager/routes/payments/intent/providers/stripe.js +141 -0
  311. package/dist/manager/routes/payments/intent/providers/test.js +341 -0
  312. package/dist/manager/routes/payments/plan/post.js +147 -0
  313. package/dist/manager/routes/payments/plan/providers/chargebee.js +62 -0
  314. package/dist/manager/routes/payments/plan/providers/paypal.js +62 -0
  315. package/dist/manager/routes/payments/plan/providers/stripe.js +59 -0
  316. package/dist/manager/routes/payments/plan/providers/test.js +113 -0
  317. package/dist/manager/routes/payments/portal/post.js +113 -0
  318. package/dist/manager/routes/payments/portal/providers/chargebee.js +62 -0
  319. package/dist/manager/routes/payments/portal/providers/paypal.js +25 -0
  320. package/dist/manager/routes/payments/portal/providers/stripe.js +62 -0
  321. package/dist/manager/routes/payments/portal/providers/test.js +18 -0
  322. package/dist/manager/routes/payments/refund/post.js +207 -0
  323. package/dist/manager/routes/payments/refund/providers/chargebee.js +147 -0
  324. package/dist/manager/routes/payments/refund/providers/coinbase.js +40 -0
  325. package/dist/manager/routes/payments/refund/providers/paypal.js +239 -0
  326. package/dist/manager/routes/payments/refund/providers/stripe.js +160 -0
  327. package/dist/manager/routes/payments/refund/providers/test.js +204 -0
  328. package/dist/manager/routes/payments/trial-eligibility/get.js +29 -0
  329. package/dist/manager/routes/payments/uncancel/post.js +106 -0
  330. package/dist/manager/routes/payments/uncancel/providers/chargebee.js +30 -0
  331. package/dist/manager/routes/payments/uncancel/providers/paypal.js +25 -0
  332. package/dist/manager/routes/payments/uncancel/providers/stripe.js +27 -0
  333. package/dist/manager/routes/payments/uncancel/providers/test.js +114 -0
  334. package/dist/manager/routes/payments/webhook/post.js +157 -0
  335. package/dist/manager/routes/payments/webhook/providers/chargebee.js +218 -0
  336. package/dist/manager/routes/payments/webhook/providers/coinbase.js +91 -0
  337. package/dist/manager/routes/payments/webhook/providers/paypal.js +214 -0
  338. package/dist/manager/routes/payments/webhook/providers/stripe.js +182 -0
  339. package/dist/manager/routes/payments/webhook/providers/test.js +18 -0
  340. package/dist/manager/routes/payments/winback/post.js +276 -0
  341. package/dist/manager/routes/payments/winback/providers/chargebee.js +24 -0
  342. package/dist/manager/routes/payments/winback/providers/paypal.js +22 -0
  343. package/dist/manager/routes/payments/winback/providers/stripe.js +41 -0
  344. package/dist/manager/routes/payments/winback/providers/test.js +42 -0
  345. package/dist/manager/routes/restart/index.js +37 -0
  346. package/dist/manager/routes/special/electron-client/post.js +59 -0
  347. package/dist/manager/routes/test/authenticate/get.js +11 -0
  348. package/dist/manager/routes/test/health/get.js +8 -0
  349. package/dist/manager/routes/test/index.js +16 -0
  350. package/dist/manager/routes/test/lab/post.js +17 -0
  351. package/dist/manager/routes/test/redirect/get.js +27 -0
  352. package/dist/manager/routes/test/reset-account/post.js +79 -0
  353. package/dist/manager/routes/test/roster/get.js +40 -0
  354. package/dist/manager/routes/test/schema/post.js +20 -0
  355. package/dist/manager/routes/test/usage/post.js +44 -0
  356. package/dist/manager/routes/test/webhook/post.js +21 -0
  357. package/dist/manager/routes/user/api-keys/post.js +51 -0
  358. package/dist/manager/routes/user/connections/_context.js +194 -0
  359. package/dist/manager/routes/user/connections/_grant.js +247 -0
  360. package/dist/manager/routes/user/connections/_lease.js +184 -0
  361. package/dist/manager/routes/user/connections/_providers.js +191 -0
  362. package/dist/manager/routes/user/connections/_state.js +117 -0
  363. package/dist/manager/routes/user/connections/delete.js +55 -0
  364. package/dist/manager/routes/user/connections/get.js +167 -0
  365. package/dist/manager/routes/user/connections/post.js +370 -0
  366. package/dist/manager/routes/user/connections/providers/discord.js +41 -0
  367. package/dist/manager/routes/user/connections/providers/google.js +55 -0
  368. package/dist/manager/routes/user/connections/providers/kick.js +68 -0
  369. package/dist/manager/routes/user/connections/providers/spotify.js +40 -0
  370. package/dist/manager/routes/user/connections/providers/twitch.js +52 -0
  371. package/dist/manager/routes/user/data-request/delete.js +84 -0
  372. package/dist/manager/routes/user/data-request/get.js +229 -0
  373. package/dist/manager/routes/user/data-request/post.js +136 -0
  374. package/dist/manager/routes/user/delete.js +149 -0
  375. package/dist/manager/routes/user/feedback/post.js +81 -0
  376. package/dist/manager/routes/user/get.js +14 -0
  377. package/dist/manager/routes/user/orders/get.js +95 -0
  378. package/dist/manager/routes/user/sessions/delete.js +95 -0
  379. package/dist/manager/routes/user/sessions/get.js +40 -0
  380. package/dist/manager/routes/user/settings/validate/post.js +86 -0
  381. package/dist/manager/routes/user/signup/post.js +662 -0
  382. package/dist/manager/routes/user/subscription/get.js +77 -0
  383. package/dist/manager/routes/user/token/post.js +30 -0
  384. package/dist/manager/routes/verts/delete.js +43 -0
  385. package/dist/manager/routes/verts/get.js +49 -0
  386. package/dist/manager/routes/verts/post.js +61 -0
  387. package/dist/manager/routes/verts/put.js +75 -0
  388. package/dist/manager/routes/verts/redirect/get.js +36 -0
  389. package/dist/manager/routes/verts/serve/get.js +67 -0
  390. package/dist/manager/routes/verts/utils.js +436 -0
  391. package/dist/manager/schemas/admin/backup/post.js +8 -0
  392. package/dist/manager/schemas/admin/cron/post.js +8 -0
  393. package/dist/manager/schemas/admin/database/get.js +5 -0
  394. package/dist/manager/schemas/admin/database/post.js +6 -0
  395. package/dist/manager/schemas/admin/email/post.js +31 -0
  396. package/dist/manager/schemas/admin/firestore/get.js +5 -0
  397. package/dist/manager/schemas/admin/firestore/post.js +8 -0
  398. package/dist/manager/schemas/admin/firestore/query/post.js +5 -0
  399. package/dist/manager/schemas/admin/hook/post.js +8 -0
  400. package/dist/manager/schemas/admin/infer-contact/post.js +6 -0
  401. package/dist/manager/schemas/admin/notification/post.js +23 -0
  402. package/dist/manager/schemas/admin/payment/post.js +8 -0
  403. package/dist/manager/schemas/admin/post/post.js +23 -0
  404. package/dist/manager/schemas/admin/post/put.js +13 -0
  405. package/dist/manager/schemas/admin/repo/content/post.js +10 -0
  406. package/dist/manager/schemas/admin/stats/get.js +5 -0
  407. package/dist/manager/schemas/admin/users/disable/post.js +9 -0
  408. package/dist/manager/schemas/admin/users/list/get.js +10 -0
  409. package/dist/manager/schemas/admin/users/sync/post.js +7 -0
  410. package/dist/manager/schemas/brand/get.js +3 -0
  411. package/dist/manager/schemas/content/post/get.js +8 -0
  412. package/dist/manager/schemas/general/email/post.js +10 -0
  413. package/dist/manager/schemas/general/uuid/post.js +9 -0
  414. package/dist/manager/schemas/handler/post/post.js +13 -0
  415. package/dist/manager/schemas/health/get.js +3 -0
  416. package/dist/manager/schemas/marketing/campaign/delete.js +8 -0
  417. package/dist/manager/schemas/marketing/campaign/get.js +13 -0
  418. package/dist/manager/schemas/marketing/campaign/post.js +43 -0
  419. package/dist/manager/schemas/marketing/campaign/put.js +37 -0
  420. package/dist/manager/schemas/marketing/contact/delete.js +8 -0
  421. package/dist/manager/schemas/marketing/contact/post.js +14 -0
  422. package/dist/manager/schemas/marketing/contact/put.js +8 -0
  423. package/dist/manager/schemas/marketing/email-preferences/post.js +15 -0
  424. package/dist/manager/schemas/marketing/webhook/forward/post.js +8 -0
  425. package/dist/manager/schemas/marketing/webhook/post.js +7 -0
  426. package/dist/manager/schemas/payments/cancel/post.js +15 -0
  427. package/dist/manager/schemas/payments/discount/get.js +8 -0
  428. package/dist/manager/schemas/payments/dispute-alert/post.js +6 -0
  429. package/dist/manager/schemas/payments/intent/post.js +28 -0
  430. package/dist/manager/schemas/payments/plan/post.js +11 -0
  431. package/dist/manager/schemas/payments/portal/post.js +12 -0
  432. package/dist/manager/schemas/payments/refund/post.js +14 -0
  433. package/dist/manager/schemas/payments/trial-eligibility/get.js +7 -0
  434. package/dist/manager/schemas/payments/uncancel/post.js +9 -0
  435. package/dist/manager/schemas/payments/webhook/post.js +6 -0
  436. package/dist/manager/schemas/payments/winback/post.js +12 -0
  437. package/dist/manager/schemas/restart/index.js +5 -0
  438. package/dist/manager/schemas/special/electron-client/post.js +11 -0
  439. package/dist/manager/schemas/test/authenticate/get.js +3 -0
  440. package/dist/manager/schemas/test/health/get.js +3 -0
  441. package/dist/manager/schemas/test/index.js +5 -0
  442. package/dist/manager/schemas/test/lab/post.js +3 -0
  443. package/dist/manager/schemas/test/redirect/get.js +7 -0
  444. package/dist/manager/schemas/test/reset-account/post.js +3 -0
  445. package/dist/manager/schemas/test/roster/get.js +3 -0
  446. package/dist/manager/schemas/test/schema/post.js +70 -0
  447. package/dist/manager/schemas/test/usage/post.js +8 -0
  448. package/dist/manager/schemas/test/webhook/post.js +7 -0
  449. package/dist/manager/schemas/user/api-keys/post.js +6 -0
  450. package/dist/manager/schemas/user/connections/delete.js +8 -0
  451. package/dist/manager/schemas/user/connections/get.js +21 -0
  452. package/dist/manager/schemas/user/connections/post.js +12 -0
  453. package/dist/manager/schemas/user/data-request/delete.js +3 -0
  454. package/dist/manager/schemas/user/data-request/get.js +5 -0
  455. package/dist/manager/schemas/user/data-request/post.js +6 -0
  456. package/dist/manager/schemas/user/delete.js +6 -0
  457. package/dist/manager/schemas/user/feedback/post.js +8 -0
  458. package/dist/manager/schemas/user/get.js +3 -0
  459. package/dist/manager/schemas/user/orders/get.js +6 -0
  460. package/dist/manager/schemas/user/sessions/delete.js +6 -0
  461. package/dist/manager/schemas/user/sessions/get.js +6 -0
  462. package/dist/manager/schemas/user/settings/validate/post.js +8 -0
  463. package/dist/manager/schemas/user/signup/post.js +27 -0
  464. package/dist/manager/schemas/user/subscription/get.js +5 -0
  465. package/dist/manager/schemas/user/token/post.js +5 -0
  466. package/dist/manager/schemas/verts/delete.js +8 -0
  467. package/dist/manager/schemas/verts/get.js +9 -0
  468. package/dist/manager/schemas/verts/post.js +23 -0
  469. package/dist/manager/schemas/verts/put.js +20 -0
  470. package/dist/manager/schemas/verts/redirect/get.js +10 -0
  471. package/dist/manager/schemas/verts/serve/get.js +14 -0
  472. package/dist/manager/server-manager.js +69 -0
  473. package/dist/mcp/client.js +98 -0
  474. package/dist/mcp/handler.js +521 -0
  475. package/dist/mcp/index.js +157 -0
  476. package/dist/mcp/tools.js +555 -0
  477. package/dist/mcp/utils.js +108 -0
  478. package/dist/omega-bin.js +6 -0
  479. package/dist/require.js +3 -0
  480. package/dist/test/fixtures/firebase-project/.firebaserc +5 -0
  481. package/dist/test/fixtures/firebase-project/config/omega.json5 +131 -0
  482. package/dist/test/fixtures/firebase-project/database.rules.json +6 -0
  483. package/dist/test/fixtures/firebase-project/firebase.json +49 -0
  484. package/dist/test/fixtures/firebase-project/firestore.indexes.json +4 -0
  485. package/dist/test/fixtures/firebase-project/package.json +14 -0
  486. package/dist/test/fixtures/firebase-project/src/index.js +11 -0
  487. package/dist/test/fixtures/firebase-project/storage.rules +8 -0
  488. package/dist/test/parse-audit.js +25 -0
  489. package/dist/test/run-tests.js +90 -0
  490. package/dist/test/runner.js +982 -0
  491. package/dist/test/seed.js +242 -0
  492. package/dist/test/test-accounts.js +2660 -0
  493. package/dist/test/utils/assertions.js +189 -0
  494. package/dist/test/utils/email-capture.js +252 -0
  495. package/dist/test/utils/extended-mode-warning.js +11 -0
  496. package/dist/test/utils/firestore-rules-client.js +277 -0
  497. package/dist/test/utils/http-client.js +228 -0
  498. package/dist/test/utils/test-mode-file.js +192 -0
  499. package/dist/utils/merge-line-files.js +6 -0
  500. package/dist/utils/scaffold-defaults.js +71 -0
  501. package/dist/utils/test-lanes.js +62 -0
  502. package/dist/vendor/account/engine.js +182 -0
  503. package/dist/vendor/account/features.js +220 -0
  504. package/dist/vendor/account/index.js +53 -0
  505. package/dist/vendor/account/schema.js +272 -0
  506. package/dist/vendor/account/subscription.js +38 -0
  507. package/dist/vendor/analytics/adapters/ga4.js +26 -0
  508. package/dist/vendor/analytics/adapters/meta.js +26 -0
  509. package/dist/vendor/analytics/adapters/resolve.js +130 -0
  510. package/dist/vendor/analytics/adapters/tiktok.js +27 -0
  511. package/dist/vendor/analytics/catalog.js +908 -0
  512. package/dist/vendor/analytics/identity.js +136 -0
  513. package/dist/vendor/config/company.js +31 -0
  514. package/dist/vendor/config/defaults.js +173 -0
  515. package/dist/vendor/config/demo.js +18 -0
  516. package/dist/vendor/config/desktop-artifacts.js +110 -0
  517. package/dist/vendor/config/edit.js +769 -0
  518. package/dist/vendor/config/env-delivery.js +145 -0
  519. package/dist/vendor/config/env-rules.js +93 -0
  520. package/dist/vendor/config/env-schema.js +1078 -0
  521. package/dist/vendor/config/env.js +445 -0
  522. package/dist/vendor/config/hooks.js +97 -0
  523. package/dist/vendor/config/index.js +237 -0
  524. package/dist/vendor/config/instances.js +208 -0
  525. package/dist/vendor/config/load.js +490 -0
  526. package/dist/vendor/config/merge.js +68 -0
  527. package/dist/vendor/config/order.js +139 -0
  528. package/dist/vendor/config/ports.js +374 -0
  529. package/dist/vendor/config/providers.js +32 -0
  530. package/dist/vendor/config/repo.js +142 -0
  531. package/dist/vendor/config/retired-keys.js +430 -0
  532. package/dist/vendor/config/schema.js +1610 -0
  533. package/dist/vendor/config/secrets.js +50 -0
  534. package/dist/vendor/config/seed.js +34 -0
  535. package/dist/vendor/config/site-global.js +205 -0
  536. package/dist/vendor/config/validate.js +554 -0
  537. package/dist/vendor/config/winback.js +61 -0
  538. package/dist/vendor/devkit/attach-log-file.js +262 -0
  539. package/dist/vendor/devkit/bare-requires.js +153 -0
  540. package/dist/vendor/devkit/command-path.js +46 -0
  541. package/dist/vendor/devkit/defaults-engine.js +372 -0
  542. package/dist/vendor/devkit/deploy-record.js +180 -0
  543. package/dist/vendor/devkit/deploy.js +286 -0
  544. package/dist/vendor/devkit/license.js +155 -0
  545. package/dist/vendor/devkit/local-https.js +360 -0
  546. package/dist/vendor/devkit/local.js +1905 -0
  547. package/dist/vendor/devkit/logger.js +128 -0
  548. package/dist/vendor/devkit/merge-line-files.js +296 -0
  549. package/dist/vendor/devkit/npm-registry.js +52 -0
  550. package/dist/vendor/devkit/omega-bin.js +345 -0
  551. package/dist/vendor/devkit/parse-audit.js +74 -0
  552. package/dist/vendor/devkit/safe-install.js +18 -0
  553. package/dist/vendor/devkit/scaffold-guard.js +96 -0
  554. package/dist/vendor/devkit/stop-signals.js +28 -0
  555. package/dist/vendor/devkit/test/assert.js +120 -0
  556. package/dist/vendor/devkit/test/define-cases.js +104 -0
  557. package/dist/vendor/devkit/test/runner-core.js +554 -0
  558. package/dist/vendor/devkit/test/scope.js +162 -0
  559. package/dist/vendor/devkit/update.js +569 -0
  560. package/dist/vendor/monitoring/core.js +180 -0
  561. package/dist/vendor/monitoring/env.js +49 -0
  562. package/dist/vendor/monitoring/logger.js +39 -0
  563. package/dist/vendor/monitoring/node.js +72 -0
  564. package/docs/admin-post-route.md +57 -0
  565. package/docs/ai-library.md +171 -0
  566. package/docs/architecture.md +62 -0
  567. package/docs/audit.md +70 -0
  568. package/docs/auth-hooks.md +74 -0
  569. package/docs/build-system.md +65 -0
  570. package/docs/cdp-debugging.md +29 -0
  571. package/docs/cli-firestore-auth.md +85 -0
  572. package/docs/cli-logs.md +67 -0
  573. package/docs/cli-output.md +146 -0
  574. package/docs/code-patterns.md +77 -0
  575. package/docs/common-mistakes.md +12 -0
  576. package/docs/common-operations.md +60 -0
  577. package/docs/connections.md +212 -0
  578. package/docs/consent.md +362 -0
  579. package/docs/directory-structure.md +150 -0
  580. package/docs/email-system.md +459 -0
  581. package/docs/environment-detection.md +93 -0
  582. package/docs/file-naming.md +10 -0
  583. package/docs/firestore.md +134 -0
  584. package/docs/ghostii.md +240 -0
  585. package/docs/index.md +299 -0
  586. package/docs/key-files.md +37 -0
  587. package/docs/logging.md +60 -0
  588. package/docs/marketing-campaigns.md +407 -0
  589. package/docs/marketing-fields.md +25 -0
  590. package/docs/mcp.md +222 -0
  591. package/docs/migration.md +129 -0
  592. package/docs/payment-system.md +856 -0
  593. package/docs/paypal-sandbox-qa.md +105 -0
  594. package/docs/response-headers.md +9 -0
  595. package/docs/routes.md +211 -0
  596. package/docs/sanitization.md +71 -0
  597. package/docs/schemas.md +163 -0
  598. package/docs/shared/agent-docs.md +89 -0
  599. package/docs/shared/analytics.md +612 -0
  600. package/docs/shared/brands.md +51 -0
  601. package/docs/shared/breaking-changes.md +497 -0
  602. package/docs/shared/config.md +1387 -0
  603. package/docs/shared/deploys.md +215 -0
  604. package/docs/shared/icons.md +201 -0
  605. package/docs/shared/local-dev.md +147 -0
  606. package/docs/shared/logging.md +202 -0
  607. package/docs/shared/monitoring.md +153 -0
  608. package/docs/shared/publishing.md +183 -0
  609. package/docs/shared/rulings.md +34 -0
  610. package/docs/shared/testing.md +147 -0
  611. package/docs/shared/theming.md +604 -0
  612. package/docs/shared/translation.md +291 -0
  613. package/docs/shared/updates.md +61 -0
  614. package/docs/stripe-webhook-forwarding.md +20 -0
  615. package/docs/test-boot-layer.md +67 -0
  616. package/docs/test-framework.md +583 -0
  617. package/docs/usage-rate-limiting.md +121 -0
  618. package/docs/verts.md +29 -0
  619. package/package.json +143 -0
  620. package/templates/config/omega.json5 +322 -0
  621. package/templates/database.rules.json +82 -0
  622. package/templates/firebase.json +67 -0
  623. package/templates/firestore.framework.rules +191 -0
  624. package/templates/firestore.indexes.json +4 -0
  625. package/templates/firestore.rules +59 -0
  626. package/templates/index.js +12 -0
  627. package/templates/public/404.html +26 -0
  628. package/templates/public/index.html +24 -0
  629. package/templates/remoteconfig.template.json +1 -0
  630. package/templates/storage-lifecycle-config-1-day.json +9 -0
  631. package/templates/storage-lifecycle-config-30-days.json +9 -0
  632. package/templates/storage.rules +11 -0
  633. package/test/_init/accounts-validation.js +58 -0
  634. package/test/ai/tools-live.test.js +171 -0
  635. package/test/analytics/conversion-delivery.test.js +1013 -0
  636. package/test/analytics/match-normalization.test.js +238 -0
  637. package/test/analytics/signup-conversion.test.js +135 -0
  638. package/test/boot/cli-dispatch.test.js +142 -0
  639. package/test/boot/defaults-scaffold.test.js +248 -0
  640. package/test/boot/deploy-staging.test.js +446 -0
  641. package/test/boot/disposable-domains.test.js +154 -0
  642. package/test/boot/emulator-boots.test.js +39 -0
  643. package/test/boot/emulator-port-preflight.test.js +126 -0
  644. package/test/boot/emulator-ready-timeout.test.js +46 -0
  645. package/test/boot/project-id-refresh.test.js +73 -0
  646. package/test/boot/suite-portability.test.js +278 -0
  647. package/test/boot/update-command.test.js +63 -0
  648. package/test/cli/cross-platform.test.js +168 -0
  649. package/test/cli/custom-project-type.test.js +426 -0
  650. package/test/cli/deploy-license-env.test.js +100 -0
  651. package/test/cli/emulator-adoption.test.js +254 -0
  652. package/test/cli/emulator-orphans.test.js +658 -0
  653. package/test/cli/emulator-ownership.test.js +177 -0
  654. package/test/cli/emulator-port-retry.test.js +395 -0
  655. package/test/cli/emulator-shutdown.test.js +788 -0
  656. package/test/cli/emulator-stale-reap.test.js +574 -0
  657. package/test/cli/ensure-target.test.js +325 -0
  658. package/test/cli/flags.test.js +64 -0
  659. package/test/cli/https-trust.test.js +38 -0
  660. package/test/cli/install-update-dispatch.test.js +47 -0
  661. package/test/cli/lane-environments.test.js +159 -0
  662. package/test/cli/marketing-campaigns-seeded.test.js +194 -0
  663. package/test/cli/migrate-bare-requires.test.js +128 -0
  664. package/test/cli/migrate-markers.test.js +530 -0
  665. package/test/cli/required-indexes.test.js +64 -0
  666. package/test/cli/rules-compile.test.js +622 -0
  667. package/test/cli/rules-migration-deferral.test.js +259 -0
  668. package/test/cli/rules-version.test.js +104 -0
  669. package/test/cli/setup-load-files.test.js +66 -0
  670. package/test/cli/setup-offline-mode.test.js +276 -0
  671. package/test/cli/setup-retired.test.js +64 -0
  672. package/test/cli/setup-shared-project-indexes.test.js +262 -0
  673. package/test/cli/setup-tests-requires.test.js +45 -0
  674. package/test/cli/stage-env-compose.test.js +316 -0
  675. package/test/cli/stripe-live-lane.test.js +303 -0
  676. package/test/cli/target.test.js +182 -0
  677. package/test/cli/templates.test.js +66 -0
  678. package/test/cli/test-runner-env.test.js +115 -0
  679. package/test/cli/test-stack-shutdown.test.js +275 -0
  680. package/test/cli/test-target-no-match.test.js +128 -0
  681. package/test/cli/verb-logs.test.js +118 -0
  682. package/test/cli/version-dispatch.test.js +39 -0
  683. package/test/content/blog-generate.test.js +164 -0
  684. package/test/email/campaign-config-fault.test.js +376 -0
  685. package/test/email/campaign-cron-pipeline.test.js +536 -0
  686. package/test/email/campaign-send.test.js +47 -0
  687. package/test/email/consent-lifecycle.test.js +258 -0
  688. package/test/email/content-html-policy.test.js +270 -0
  689. package/test/email/feedback-and-plain-send.test.js +54 -0
  690. package/test/email/fixtures/clean.json +30 -0
  691. package/test/email/fixtures/editorial.json +30 -0
  692. package/test/email/fixtures/field-report.json +53 -0
  693. package/test/email/identity.test.js +321 -0
  694. package/test/email/marketing/consent-gate.test.js +265 -0
  695. package/test/email/marketing/custom-fields-catalog.test.js +259 -0
  696. package/test/email/marketing/prune-per-provider.test.js +762 -0
  697. package/test/email/marketing/remove-log-privacy.test.js +225 -0
  698. package/test/email/marketing-lifecycle.test.js +139 -0
  699. package/test/email/newsletter-generate.test.js +853 -0
  700. package/test/email/newsletter-svg-tokens.test.js +51 -0
  701. package/test/email/newsletter-templates.test.js +492 -0
  702. package/test/email/order-one-time-cta.test.js +97 -0
  703. package/test/email/render-content.test.js +248 -0
  704. package/test/email/safe-url.test.js +0 -0
  705. package/test/email/sanitize-images.test.js +66 -0
  706. package/test/email/send-log-privacy.test.js +107 -0
  707. package/test/email/templates.test.js +288 -0
  708. package/test/email/testing-capture.test.js +260 -0
  709. package/test/email/transactional-send.test.js +36 -0
  710. package/test/email/transactional.test.js +562 -0
  711. package/test/email/unsubscribe-groups.test.js +155 -0
  712. package/test/email/unsubscribe-key.test.js +91 -0
  713. package/test/email/validation-cases.test.js +152 -0
  714. package/test/email/validation.test.js +746 -0
  715. package/test/events/auth-delete-conversion.test.js +138 -0
  716. package/test/events/auth-delete-race.test.js +211 -0
  717. package/test/events/auth-on-create-log.test.js +164 -0
  718. package/test/events/auth-signup-conversion.test.js +141 -0
  719. package/test/events/auth-signup-limit.test.js +142 -0
  720. package/test/events/auth-trigger-log-privacy.test.js +256 -0
  721. package/test/events/cron-job-doc-shape.test.js +101 -0
  722. package/test/events/cron-reset-usage.test.js +116 -0
  723. package/test/events/notification-conversion.test.js +182 -0
  724. package/test/events/payments/_webhook-harness.js +307 -0
  725. package/test/events/payments/abandoned-cart-activity.test.js +135 -0
  726. package/test/events/payments/analytics-payment-events.test.js +945 -0
  727. package/test/events/payments/dispute-email-status.test.js +55 -0
  728. package/test/events/payments/journey-payments-abandoned.test.js +126 -0
  729. package/test/events/payments/journey-payments-cancel-endpoint.test.js +102 -0
  730. package/test/events/payments/journey-payments-cancel-no-order.test.js +76 -0
  731. package/test/events/payments/journey-payments-cancel.test.js +184 -0
  732. package/test/events/payments/journey-payments-decline.test.js +225 -0
  733. package/test/events/payments/journey-payments-discount.test.js +91 -0
  734. package/test/events/payments/journey-payments-dispute.test.js +190 -0
  735. package/test/events/payments/journey-payments-failure.test.js +148 -0
  736. package/test/events/payments/journey-payments-legacy-product.test.js +151 -0
  737. package/test/events/payments/journey-payments-one-time-decline.test.js +127 -0
  738. package/test/events/payments/journey-payments-one-time-failure.test.js +113 -0
  739. package/test/events/payments/journey-payments-one-time-refund.test.js +186 -0
  740. package/test/events/payments/journey-payments-one-time.test.js +175 -0
  741. package/test/events/payments/journey-payments-plan-change.test.js +146 -0
  742. package/test/events/payments/journey-payments-plan-switch-trial.test.js +161 -0
  743. package/test/events/payments/journey-payments-plan-switch.test.js +182 -0
  744. package/test/events/payments/journey-payments-refund-no-order.test.js +76 -0
  745. package/test/events/payments/journey-payments-refund-webhook.test.js +225 -0
  746. package/test/events/payments/journey-payments-suspend.test.js +183 -0
  747. package/test/events/payments/journey-payments-trial-cancel.test.js +132 -0
  748. package/test/events/payments/journey-payments-trial.test.js +173 -0
  749. package/test/events/payments/journey-payments-uid-resolution.test.js +134 -0
  750. package/test/events/payments/journey-payments-uncancel.test.js +157 -0
  751. package/test/events/payments/journey-payments-upgrade.test.js +137 -0
  752. package/test/events/payments/journey-payments-winback-decline.test.js +171 -0
  753. package/test/events/payments/journey-payments-winback.test.js +195 -0
  754. package/test/events/payments/paypal-expiry-cron.test.js +166 -0
  755. package/test/events/payments/purchase-failed-handler.test.js +80 -0
  756. package/test/events/payments/purchase-refunded-handler.test.js +108 -0
  757. package/test/events/payments/test-processor-doc-shape.test.js +162 -0
  758. package/test/events/payments/transition-order-emails.test.js +185 -0
  759. package/test/events/payments/transition-promo-lines.test.js +192 -0
  760. package/test/events/payments/transitions-detect.test.js +767 -0
  761. package/test/events/payments/trial-lapse-sweep-staleness.test.js +173 -0
  762. package/test/events/payments/trial-lapse-sweep.test.js +359 -0
  763. package/test/events/payments/webhook-atomic-writes.test.js +69 -0
  764. package/test/events/payments/webhook-chargebee-unreachable.test.js +106 -0
  765. package/test/events/payments/webhook-discount-clear.test.js +189 -0
  766. package/test/events/payments/webhook-failure-intent.test.js +93 -0
  767. package/test/events/payments/webhook-hosted-page-uid-trust.test.js +212 -0
  768. package/test/events/payments/webhook-ordering.test.js +326 -0
  769. package/test/events/payments/webhook-provider-lookup-trust.test.js +206 -0
  770. package/test/events/payments/webhook-refund-amount-trust.test.js +223 -0
  771. package/test/events/payments/webhook-refund-envelope-shape.test.js +148 -0
  772. package/test/events/payments/webhook-refund-linkage-trust.test.js +404 -0
  773. package/test/events/payments/webhook-refund-transaction-fallback.test.js +184 -0
  774. package/test/events/payments/webhook-refund-without-order.test.js +170 -0
  775. package/test/events/payments/webhook-refusal-reporting.test.js +194 -0
  776. package/test/events/payments/webhook-refused-intent.test.js +142 -0
  777. package/test/events/payments/webhook-retry-sweep.test.js +252 -0
  778. package/test/events/payments/webhook-transition-claim.test.js +389 -0
  779. package/test/events/payments/webhook-uid-trust.test.js +203 -0
  780. package/test/events/payments/webhook-user-without-auth.test.js +161 -0
  781. package/test/fixtures/chargebee/invoice-one-time.json +27 -0
  782. package/test/fixtures/chargebee/subscription-active.json +44 -0
  783. package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
  784. package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
  785. package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
  786. package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
  787. package/test/fixtures/chargebee/subscription-paused.json +42 -0
  788. package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
  789. package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
  790. package/test/fixtures/coinbase/charge-confirmed.json +50 -0
  791. package/test/fixtures/coinbase/charge-failed.json +34 -0
  792. package/test/fixtures/coinbase/charge-pending.json +34 -0
  793. package/test/fixtures/migrate/ported-route.js.txt +11 -0
  794. package/test/fixtures/paypal/capture-completed.json +51 -0
  795. package/test/fixtures/paypal/capture-refunded.json +43 -0
  796. package/test/fixtures/paypal/order-approved.json +62 -0
  797. package/test/fixtures/paypal/order-completed.json +110 -0
  798. package/test/fixtures/paypal/sale-refunded.json +38 -0
  799. package/test/fixtures/paypal/subscription-active.json +76 -0
  800. package/test/fixtures/paypal/subscription-cancelled.json +50 -0
  801. package/test/fixtures/paypal/subscription-suspended.json +65 -0
  802. package/test/fixtures/stripe/checkout-session-completed.json +130 -0
  803. package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
  804. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
  805. package/test/fixtures/stripe/invoice-subscription-payment-succeeded.json +28 -0
  806. package/test/fixtures/stripe/subscription-active.json +161 -0
  807. package/test/fixtures/stripe/subscription-canceled.json +161 -0
  808. package/test/fixtures/stripe/subscription-trialing.json +161 -0
  809. package/test/helpers/_shared-config.js +15 -0
  810. package/test/helpers/ai-request-payload.test.js +619 -0
  811. package/test/helpers/ai-schema-resolve.test.js +125 -0
  812. package/test/helpers/ai-test-provider.test.js +227 -0
  813. package/test/helpers/ai-token-accounting.test.js +150 -0
  814. package/test/helpers/ai-tools-format.test.js +384 -0
  815. package/test/helpers/analytics-no-id-notice.test.js +146 -0
  816. package/test/helpers/analytics-user-data.test.js +194 -0
  817. package/test/helpers/api-manager.test.js +311 -0
  818. package/test/helpers/backend-router.test.js +136 -0
  819. package/test/helpers/content/blog-auto-publisher.test.js +482 -0
  820. package/test/helpers/content/feed-parser.test.js +529 -0
  821. package/test/helpers/content/ghostii-blocks.test.js +135 -0
  822. package/test/helpers/content/ghostii-feed-integration.test.js +405 -0
  823. package/test/helpers/content/ghostii-write-article.test.js +244 -0
  824. package/test/helpers/dev-only-routes.test.js +225 -0
  825. package/test/helpers/env-reader.test.js +362 -0
  826. package/test/helpers/environment.test.js +263 -0
  827. package/test/helpers/event-middleware.test.js +407 -0
  828. package/test/helpers/infer-contact.test.js +157 -0
  829. package/test/helpers/lane-url.test.js +90 -0
  830. package/test/helpers/merge-line-files.test.js +272 -0
  831. package/test/helpers/metadata.test.js +122 -0
  832. package/test/helpers/middleware-request-log.test.js +134 -0
  833. package/test/helpers/middleware-user-log.test.js +124 -0
  834. package/test/helpers/payment/chargebee/parse-webhook.test.js +490 -0
  835. package/test/helpers/payment/chargebee/refund-details.test.js +198 -0
  836. package/test/helpers/payment/chargebee/to-unified-one-time.test.js +148 -0
  837. package/test/helpers/payment/chargebee/to-unified-subscription.test.js +649 -0
  838. package/test/helpers/payment/coinbase/create-intent.test.js +247 -0
  839. package/test/helpers/payment/coinbase/parse-webhook.test.js +195 -0
  840. package/test/helpers/payment/coinbase/refund-unsupported.test.js +141 -0
  841. package/test/helpers/payment/coinbase/to-unified-one-time.test.js +201 -0
  842. package/test/helpers/payment/discount-codes.test.js +141 -0
  843. package/test/helpers/payment/extract-resource.test.js +113 -0
  844. package/test/helpers/payment/fetch-failure.test.js +215 -0
  845. package/test/helpers/payment/license-gate.test.js +108 -0
  846. package/test/helpers/payment/order-id.test.js +99 -0
  847. package/test/helpers/payment/paypal/create-intent.test.js +382 -0
  848. package/test/helpers/payment/paypal/fetch-capture.test.js +123 -0
  849. package/test/helpers/payment/paypal/fetch-sale.test.js +183 -0
  850. package/test/helpers/payment/paypal/parse-webhook.test.js +678 -0
  851. package/test/helpers/payment/paypal/refund-details.test.js +202 -0
  852. package/test/helpers/payment/paypal/resolve-plan-id.test.js +108 -0
  853. package/test/helpers/payment/paypal/switch-plan.test.js +92 -0
  854. package/test/helpers/payment/paypal/to-unified-one-time.test.js +383 -0
  855. package/test/helpers/payment/paypal/to-unified-subscription.test.js +884 -0
  856. package/test/helpers/payment/stripe/fetch-charge.test.js +147 -0
  857. package/test/helpers/payment/stripe/parse-webhook.test.js +448 -0
  858. package/test/helpers/payment/stripe/refund-details.test.js +147 -0
  859. package/test/helpers/payment/stripe/to-unified-one-time.test.js +307 -0
  860. package/test/helpers/payment/stripe/to-unified-subscription.test.js +709 -0
  861. package/test/helpers/persona-domain.test.js +87 -0
  862. package/test/helpers/recaptcha.test.js +166 -0
  863. package/test/helpers/resolved-config.test.js +106 -0
  864. package/test/helpers/response-log-redaction.test.js +262 -0
  865. package/test/helpers/roles.test.js +155 -0
  866. package/test/helpers/route-context-debug-gate.test.js +118 -0
  867. package/test/helpers/route-context-logging.test.js +214 -0
  868. package/test/helpers/route-context.test.js +129 -0
  869. package/test/helpers/safe-compare.test.js +114 -0
  870. package/test/helpers/sanitize.test.js +223 -0
  871. package/test/helpers/schema-engine.test.js +537 -0
  872. package/test/helpers/schema-zod.test.js +554 -0
  873. package/test/helpers/seed-accounts-load.test.js +163 -0
  874. package/test/helpers/seed-google-personas.test.js +114 -0
  875. package/test/helpers/seeded-personas.test.js +823 -0
  876. package/test/helpers/settings.test.js +292 -0
  877. package/test/helpers/setup-engines-pin.test.js +68 -0
  878. package/test/helpers/setup-manifest-sync.test.js +116 -0
  879. package/test/helpers/slugify.test.js +395 -0
  880. package/test/helpers/storage.test.js +195 -0
  881. package/test/helpers/test-banner-latch.test.js +139 -0
  882. package/test/helpers/usage-consume.test.js +460 -0
  883. package/test/helpers/usage-log-privacy.test.js +121 -0
  884. package/test/helpers/user-doc-heal.test.js +540 -0
  885. package/test/helpers/user.test.js +771 -0
  886. package/test/helpers/webhook-forward.test.js +419 -0
  887. package/test/helpers/wipe-auth-project.test.js +143 -0
  888. package/test/mcp/discovery.test.js +52 -0
  889. package/test/mcp/oauth.test.js +160 -0
  890. package/test/mcp/protocol.test.js +278 -0
  891. package/test/mcp/roles.test.js +203 -0
  892. package/test/mcp/utils.test.js +245 -0
  893. package/test/notification/identity.test.js +155 -0
  894. package/test/routes/admin/create-post.test.js +362 -0
  895. package/test/routes/admin/database.test.js +133 -0
  896. package/test/routes/admin/deduplicate-image-alts.test.js +191 -0
  897. package/test/routes/admin/email-content-html.test.js +58 -0
  898. package/test/routes/admin/email-request-log.test.js +86 -0
  899. package/test/routes/admin/email.test.js +116 -0
  900. package/test/routes/admin/firestore-query.test.js +206 -0
  901. package/test/routes/admin/firestore.test.js +129 -0
  902. package/test/routes/admin/infer-contact.test.js +220 -0
  903. package/test/routes/admin/notification.test.js +199 -0
  904. package/test/routes/admin/post-convert-image.test.js +159 -0
  905. package/test/routes/admin/post-deploy-flag.test.js +81 -0
  906. package/test/routes/admin/post-download-error.test.js +90 -0
  907. package/test/routes/admin/post-resize-image.test.js +185 -0
  908. package/test/routes/admin/post.test.js +369 -0
  909. package/test/routes/admin/repo-content.test.js +223 -0
  910. package/test/routes/admin/stats.test.js +114 -0
  911. package/test/routes/admin/users-disable.test.js +63 -0
  912. package/test/routes/admin/users-list.test.js +71 -0
  913. package/test/routes/content/post.test.js +60 -0
  914. package/test/routes/general/uuid.test.js +133 -0
  915. package/test/routes/health.test.js +110 -0
  916. package/test/routes/marketing/campaign.test.js +184 -0
  917. package/test/routes/marketing/contact.test.js +416 -0
  918. package/test/routes/marketing/email-preferences.test.js +293 -0
  919. package/test/routes/marketing/push-send.test.js +34 -0
  920. package/test/routes/marketing/webhook-forward.test.js +63 -0
  921. package/test/routes/marketing/webhook.test.js +641 -0
  922. package/test/routes/payments/_route-harness.js +145 -0
  923. package/test/routes/payments/cancel-provider-errors.test.js +227 -0
  924. package/test/routes/payments/cancel-skip-guards.test.js +126 -0
  925. package/test/routes/payments/cancel-trialing.test.js +428 -0
  926. package/test/routes/payments/cancel.test.js +165 -0
  927. package/test/routes/payments/dedup-race.test.js +186 -0
  928. package/test/routes/payments/discount.test.js +82 -0
  929. package/test/routes/payments/dispute-alert.test.js +324 -0
  930. package/test/routes/payments/intent-discount-amounts.test.js +550 -0
  931. package/test/routes/payments/intent-discount-coupons.test.js +293 -0
  932. package/test/routes/payments/intent-one-time-metadata.test.js +122 -0
  933. package/test/routes/payments/intent-purchaser-guard.test.js +139 -0
  934. package/test/routes/payments/intent-zero-total.test.js +260 -0
  935. package/test/routes/payments/intent.test.js +413 -0
  936. package/test/routes/payments/plan.test.js +367 -0
  937. package/test/routes/payments/portal-return-url.test.js +107 -0
  938. package/test/routes/payments/portal.test.js +94 -0
  939. package/test/routes/payments/refund-one-time.test.js +254 -0
  940. package/test/routes/payments/refund-paypal-proration.test.js +190 -0
  941. package/test/routes/payments/refund.test.js +181 -0
  942. package/test/routes/payments/trial-eligibility.test.js +73 -0
  943. package/test/routes/payments/uncancel.test.js +270 -0
  944. package/test/routes/payments/webhook-stripe-invoice.test.js +129 -0
  945. package/test/routes/payments/webhook-stripe-refund-one-time.test.js +130 -0
  946. package/test/routes/payments/webhook-test-provider.test.js +156 -0
  947. package/test/routes/payments/webhook.test.js +115 -0
  948. package/test/routes/payments/winback-stripe-coupon.test.js +126 -0
  949. package/test/routes/payments/winback.test.js +597 -0
  950. package/test/routes/test/authenticate.test.js +79 -0
  951. package/test/routes/test/redirect.test.js +66 -0
  952. package/test/routes/test/reset-account.test.js +126 -0
  953. package/test/routes/test/roster.test.js +101 -0
  954. package/test/routes/test/schema.test.js +556 -0
  955. package/test/routes/test/usage.test.js +374 -0
  956. package/test/routes/user/api-keys.test.js +158 -0
  957. package/test/routes/user/connections-grant.test.js +759 -0
  958. package/test/routes/user/connections-identity.test.js +260 -0
  959. package/test/routes/user/connections-log-privacy.test.js +206 -0
  960. package/test/routes/user/connections-refresh-lease.test.js +696 -0
  961. package/test/routes/user/connections-return.test.js +261 -0
  962. package/test/routes/user/connections-uid.test.js +516 -0
  963. package/test/routes/user/delete.test.js +138 -0
  964. package/test/routes/user/feedback.test.js +106 -0
  965. package/test/routes/user/orders.test.js +233 -0
  966. package/test/routes/user/sessions.test.js +231 -0
  967. package/test/routes/user/settings-validate.test.js +84 -0
  968. package/test/routes/user/signup-emails.test.js +113 -0
  969. package/test/routes/user/signup-location.test.js +84 -0
  970. package/test/routes/user/signup-log-privacy.test.js +112 -0
  971. package/test/routes/user/signup.test.js +579 -0
  972. package/test/routes/user/subscription.test.js +101 -0
  973. package/test/routes/user/token.test.js +111 -0
  974. package/test/routes/user/user.test.js +158 -0
  975. package/test/routes/verts/cache.test.js +78 -0
  976. package/test/routes/verts/click-destination.test.js +76 -0
  977. package/test/routes/verts/crud.test.js +194 -0
  978. package/test/routes/verts/redirect.test.js +147 -0
  979. package/test/routes/verts/selection.test.js +237 -0
  980. package/test/routes/verts/serve.test.js +185 -0
  981. package/test/routes/verts/unit-document.test.js +117 -0
  982. package/test/rules/_environment.js +66 -0
  983. package/test/rules/brand-merge.test.js +195 -0
  984. package/test/rules/field-helpers.test.js +309 -0
  985. package/test/rules/notifications.test.js +577 -0
  986. package/test/rules/payments-carts.test.js +406 -0
  987. package/test/rules/sessions.test.js +170 -0
  988. package/test/rules/user.test.js +517 -0
  989. package/test/rules/verts.test.js +101 -0
  990. package/test/security/fetch-log-secrets.test.js +124 -0
  991. package/test/security/primitives.test.js +105 -0
  992. package/test/security/repo-pinning.test.js +77 -0
  993. package/test/stripe-live/subscription-lifecycle.test.js +141 -0
@@ -0,0 +1,987 @@
1
+ const powertools = require('node-powertools');
2
+ const fetchFailure = require('../fetch-failure.js');
3
+ const assertRefundLinkage = require('../refund-linkage.js');
4
+ const env = require('../../env.js');
5
+ const assertLicensedPayments = require('../license.js');
6
+
7
+ // Epoch zero timestamps (used as default/empty dates)
8
+ const EPOCH_ZERO = powertools.timestamp(new Date(0), { output: 'string' });
9
+ const EPOCH_ZERO_UNIX = powertools.timestamp(EPOCH_ZERO, { output: 'unix' });
10
+
11
+ // PayPal interval → unified frequency map
12
+ const INTERVAL_TO_FREQUENCY = { YEAR: 'annually', MONTH: 'monthly', WEEK: 'weekly', DAY: 'daily' };
13
+ const FREQUENCY_TO_INTERVAL = { annually: 'YEAR', monthly: 'MONTH', weekly: 'WEEK', daily: 'DAY' };
14
+
15
+ // PayPal API base URLs
16
+ const LIVE_URL = 'https://api-m.paypal.com';
17
+ const SANDBOX_URL = 'https://api-m.sandbox.paypal.com';
18
+
19
+ // Cached access token, expiry, and resolved base URL
20
+ let cachedToken = null;
21
+ let tokenExpiresAt = 0;
22
+ let resolvedBaseUrl = null;
23
+
24
+ /**
25
+ * Try to authenticate against a specific PayPal endpoint
26
+ * @param {string} auth - Base64-encoded client_id:secret
27
+ * @param {string} baseUrl - PayPal API base URL
28
+ * @returns {Promise<object|null>} Token data or null if auth failed
29
+ */
30
+ async function tryAuth(auth, baseUrl) {
31
+ try {
32
+ const response = await fetch(`${baseUrl}/v1/oauth2/token`, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Authorization': `Basic ${auth}`,
36
+ 'Content-Type': 'application/x-www-form-urlencoded',
37
+ },
38
+ body: 'grant_type=client_credentials',
39
+ });
40
+
41
+ if (!response.ok) {
42
+ return null;
43
+ }
44
+
45
+ return await response.json();
46
+ } catch (e) {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * PayPal shared library
53
+ * Provides API helpers, resource fetching, and unified transformations
54
+ */
55
+ const PayPal = {
56
+ /**
57
+ * Initialize or return a PayPal access token
58
+ * Tries both live and sandbox endpoints in parallel on first auth
59
+ * @returns {Promise<string>} Access token
60
+ */
61
+ async init() {
62
+ // A keyless deploy runs no live payments (#320) — before the token, and
63
+ // before the cache below can hand one back
64
+ assertLicensedPayments('PayPal');
65
+
66
+ // Return cached token if still valid (with 60s buffer)
67
+ if (cachedToken && Date.now() < tokenExpiresAt - 60000) {
68
+ return cachedToken;
69
+ }
70
+
71
+ const clientId = env.get('PAYPAL_CLIENT_ID');
72
+ const clientSecret = env.get('PAYPAL_CLIENT_SECRET');
73
+
74
+ if (!clientId || !clientSecret) {
75
+ throw new Error('PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET environment variables are required');
76
+ }
77
+
78
+ const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
79
+
80
+ // First auth — try both endpoints in parallel to detect environment
81
+ if (!resolvedBaseUrl) {
82
+ const [liveResult, sandboxResult] = await Promise.all([
83
+ tryAuth(auth, LIVE_URL),
84
+ tryAuth(auth, SANDBOX_URL),
85
+ ]);
86
+
87
+ if (liveResult) {
88
+ resolvedBaseUrl = LIVE_URL;
89
+ cachedToken = liveResult.access_token;
90
+ tokenExpiresAt = Date.now() + (liveResult.expires_in * 1000);
91
+ return cachedToken;
92
+ }
93
+
94
+ if (sandboxResult) {
95
+ resolvedBaseUrl = SANDBOX_URL;
96
+ cachedToken = sandboxResult.access_token;
97
+ tokenExpiresAt = Date.now() + (sandboxResult.expires_in * 1000);
98
+ return cachedToken;
99
+ }
100
+
101
+ throw new Error('PayPal auth failed on both live and sandbox — check your client ID and secret');
102
+ }
103
+
104
+ // Subsequent auths — use the resolved endpoint
105
+ const result = await tryAuth(auth, resolvedBaseUrl);
106
+
107
+ if (!result) {
108
+ throw new Error(`PayPal auth failed (${resolvedBaseUrl})`);
109
+ }
110
+
111
+ cachedToken = result.access_token;
112
+ tokenExpiresAt = Date.now() + (result.expires_in * 1000);
113
+
114
+ return cachedToken;
115
+ },
116
+
117
+ /**
118
+ * Make an authenticated PayPal API request
119
+ * @param {string} endpoint - API path (e.g., '/v1/billing/subscriptions/I-xxx')
120
+ * @param {object} options - fetch options (method, body, etc.)
121
+ * @returns {Promise<object>} Parsed JSON response
122
+ */
123
+ async request(endpoint, options = {}) {
124
+ const token = await this.init();
125
+
126
+ const response = await fetch(`${resolvedBaseUrl}${endpoint}`, {
127
+ ...options,
128
+ headers: {
129
+ 'Authorization': `Bearer ${token}`,
130
+ 'Content-Type': 'application/json',
131
+ ...options.headers,
132
+ },
133
+ });
134
+
135
+ // 204 No Content
136
+ if (response.status === 204) {
137
+ return {};
138
+ }
139
+
140
+ const data = await response.json();
141
+
142
+ if (!response.ok) {
143
+ const msg = data.message || data.error_description || JSON.stringify(data);
144
+ throw new Error(`PayPal API ${response.status}: ${msg}`);
145
+ }
146
+
147
+ return data;
148
+ },
149
+
150
+ /**
151
+ * Fetch the latest resource from PayPal's API
152
+ * PayPal's answer is the only trusted source — a lookup that fails throws the
153
+ * classified failure ([../fetch-failure.js](../fetch-failure.js)) instead of
154
+ * degrading to the webhook payload
155
+ *
156
+ * For orders: captures the payment first (moves funds), then returns the captured order
157
+ *
158
+ * @param {string} resourceType - 'subscription', 'order', or 'sale'
159
+ * @param {string} resourceId - PayPal resource ID (e.g., 'I-xxx', an order ID, or a sale ID)
160
+ * @param {object} context - Additional context (e.g., { config })
161
+ * @returns {object} Full PayPal resource object
162
+ */
163
+ async fetchResource(resourceType, resourceId, context) {
164
+ try {
165
+ if (resourceType === 'subscription') {
166
+ const sub = await this.request(`/v1/billing/subscriptions/${resourceId}`);
167
+
168
+ // Fetch the plan to get product_id (subscription doesn't include it)
169
+ if (sub.plan_id) {
170
+ try {
171
+ const plan = await this.request(`/v1/billing/plans/${sub.plan_id}`);
172
+ sub._plan = plan;
173
+ } catch (e) {
174
+ // Plan fetch failed — continue without it
175
+ }
176
+ }
177
+
178
+ return sub;
179
+ }
180
+
181
+ if (resourceType === 'order') {
182
+ // Capture the order to move funds, then return the captured state
183
+ const captured = await this.request(`/v2/checkout/orders/${resourceId}/capture`, {
184
+ method: 'POST',
185
+ });
186
+
187
+ return captured;
188
+ }
189
+
190
+ if (resourceType === 'capture') {
191
+ // The v2 twin of the sale branch below. A capture created by this
192
+ // framework's own v2 Orders flow carries our custom_id directly, so
193
+ // there is nothing to fold — one read is the whole answer
194
+ // ([#240](https://github.com/Omega-JS-Stack/omega/issues/240)).
195
+ return await this.request(`/v2/payments/captures/${resourceId}`);
196
+ }
197
+
198
+ if (resourceType === 'sale') {
199
+ // The refund of a one-time purchase names the SALE it reversed. A v1 sale
200
+ // carries the money and the payment behind it, never our custom_id — the
201
+ // parent payment's transaction is the only place uid/orderId/productId
202
+ // live, so fold it onto the sale the way the subscription case folds its
203
+ // plan ([#224](https://github.com/Omega-JS-Stack/omega/issues/224)).
204
+ const sale = await this.request(`/v1/payments/sale/${resourceId}`);
205
+
206
+ // v1 sometimes spells the field `custom` — normalize before deciding a
207
+ // second read is needed, so identifiers already in hand are never dropped
208
+ if (!sale.custom_id && sale.custom) {
209
+ sale.custom_id = sale.custom;
210
+ }
211
+
212
+ if (!sale.custom_id && sale.parent_payment) {
213
+ try {
214
+ const payment = await this.request(`/v1/payments/payment/${sale.parent_payment}`);
215
+ const transaction = payment.transactions?.[0];
216
+ const custom = transaction?.custom_id || transaction?.custom || null;
217
+
218
+ if (custom) {
219
+ sale.custom_id = custom;
220
+ }
221
+ } catch (e) {
222
+ // Parent payment fetch failed — the sale itself is still the live
223
+ // answer, but without a folded custom_id the pipeline cannot name the
224
+ // order this refund hits, so the miss must be visible in the logs.
225
+ const message = `paypal fetchResource(sale/${resourceId}) could not read parent payment ${sale.parent_payment} for identifiers: ${e?.message || e}`;
226
+
227
+ if (context?.ctx?.warn) {
228
+ context.ctx.warn(message);
229
+ } else {
230
+ console.warn(`[@omega.js/backend:payment:paypal] ${message}`);
231
+ }
232
+ }
233
+ }
234
+
235
+ return sale;
236
+ }
237
+
238
+ throw new Error(`Unknown resource type: ${resourceType}`);
239
+ } catch (e) {
240
+ // An order fetch IS the capture, so its failure also means the money never moved
241
+ throw fetchFailure(e, {
242
+ provider: 'paypal',
243
+ fn: 'fetchResource',
244
+ resourceType,
245
+ resourceId,
246
+ consequence: resourceType === 'order'
247
+ ? 'the order was NOT captured, so the funds have NOT moved'
248
+ : null,
249
+ });
250
+ }
251
+ },
252
+
253
+ /**
254
+ * Transform a raw PayPal subscription object into the unified subscription shape
255
+ *
256
+ * @param {object} rawSubscription - Raw PayPal subscription object (with _plan attached)
257
+ * @param {object} options
258
+ * @param {object} options.config - @omega.js/backend config (must contain products array)
259
+ * @param {string} options.eventName - Name of the webhook event
260
+ * @param {string} options.eventId - ID of the webhook event
261
+ * @returns {object} Unified subscription object
262
+ */
263
+ toUnifiedSubscription(rawSubscription, options) {
264
+ options = options || {};
265
+ const config = options.config || {};
266
+
267
+ const status = resolveStatus(rawSubscription);
268
+ const cancellation = resolveCancellation(rawSubscription);
269
+ const trial = resolveTrial(rawSubscription);
270
+ const frequency = resolveFrequency(rawSubscription);
271
+ const product = resolveProduct(rawSubscription, config);
272
+ const expires = resolveExpires(rawSubscription);
273
+ const startDate = resolveStartDate(rawSubscription);
274
+ const price = resolvePrice(product.id, frequency, config);
275
+
276
+ // Parse custom_id for uid and orderId
277
+ const customData = parseCustomId(rawSubscription.custom_id);
278
+
279
+ const now = powertools.timestamp(new Date(), { output: 'string' });
280
+ const nowUNIX = powertools.timestamp(now, { output: 'unix' });
281
+
282
+ return {
283
+ product: product,
284
+ status: status,
285
+ expires: expires,
286
+ trial: trial,
287
+ cancellation: cancellation,
288
+ payment: {
289
+ provider: 'paypal',
290
+ orderId: customData.orderId || null,
291
+ resourceId: rawSubscription.id || null,
292
+ frequency: frequency,
293
+ price: price,
294
+ startDate: startDate,
295
+ updatedBy: {
296
+ event: {
297
+ name: options.eventName || null,
298
+ id: options.eventId || null,
299
+ },
300
+ date: {
301
+ timestamp: now,
302
+ timestampUNIX: nowUNIX,
303
+ },
304
+ },
305
+ },
306
+ };
307
+ },
308
+
309
+ /**
310
+ * Transform a raw PayPal one-time payment resource into a unified shape
311
+ *
312
+ * @param {object} rawResource - Raw PayPal resource (capture, order, etc.)
313
+ * @param {object} options
314
+ * @returns {object} Unified one-time payment object
315
+ */
316
+ toUnifiedOneTime(rawResource, options) {
317
+ options = options || {};
318
+ const config = options.config || {};
319
+
320
+ const now = powertools.timestamp(new Date(), { output: 'string' });
321
+ const nowUNIX = powertools.timestamp(now, { output: 'unix' });
322
+
323
+ // Resolve product from purchase_units custom_id (orders) or top-level custom_id (subscriptions)
324
+ const purchaseCustomId = rawResource.purchase_units?.[0]?.custom_id;
325
+ const customData = parseCustomId(purchaseCustomId || rawResource.custom_id);
326
+ const productId = customData.productId;
327
+ const product = resolveProductOneTime(productId, config);
328
+ const price = resolvePrice(productId, 'once', config);
329
+
330
+ return {
331
+ product: product,
332
+ status: rawResource.status === 'COMPLETED' ? 'completed' : rawResource.status?.toLowerCase() || 'unknown',
333
+ payment: {
334
+ provider: 'paypal',
335
+ orderId: customData.orderId || null,
336
+ resourceId: rawResource.id || null,
337
+ price: price,
338
+ updatedBy: {
339
+ event: {
340
+ name: options.eventName || null,
341
+ id: options.eventId || null,
342
+ },
343
+ date: {
344
+ timestamp: now,
345
+ timestampUNIX: nowUNIX,
346
+ },
347
+ },
348
+ },
349
+ };
350
+ },
351
+
352
+ /**
353
+ * Resolve a PayPal plan ID from product config at runtime
354
+ * Fetches plans for the PayPal product ID and matches by interval + amount +
355
+ * TRIAL-cycle presence — the manager's own matching rule
356
+ *
357
+ * PayPal puts a trial on the PLAN, never on the subscribe call, so a product
358
+ * with trial days carries a TWIN pair per interval: one with the TRIAL cycle
359
+ * and one without. `trial` picks between them, which is the only way a buyer
360
+ * who is not owed a trial gets a plan with no free cycle on it
361
+ * ([#761](https://github.com/Omega-JS-Stack/omega/issues/761)). A trial
362
+ * product whose skip-trial twin has not been minted yet (the manager walk has
363
+ * not run since) throws rather than falling back to the trial twin — the fix
364
+ * is a manage run, never a free period nobody granted.
365
+ *
366
+ * @param {object} product - Product from config
367
+ * @param {string} frequency - 'monthly', 'annually', etc.
368
+ * @param {boolean} [trial=false] - Whether this checkout is taking the trial
369
+ * @returns {Promise<string>} PayPal plan ID
370
+ */
371
+ async resolvePlanId(product, frequency, trial = false) {
372
+ if (product.archived) {
373
+ throw new Error(`Product ${product.id} is archived`);
374
+ }
375
+
376
+ const paypalProductId = product.paypal?.productId;
377
+
378
+ if (!paypalProductId) {
379
+ throw new Error(`No PayPal product ID for ${product.id}`);
380
+ }
381
+
382
+ const expectedAmount = product.prices?.[frequency];
383
+
384
+ if (!expectedAmount) {
385
+ throw new Error(`No price configured for ${product.id}/${frequency}`);
386
+ }
387
+
388
+ // Fetch plans with full details (Prefer header includes billing_cycles in list response)
389
+ // TODO: Paginate — page_size=20 only returns first page. Fine for now (each product has ~2-4 plans in 1:1 model) but will break if a product ever accumulates >20 plans.
390
+ const response = await this.request(`/v1/billing/plans?product_id=${paypalProductId}&page_size=20&total_required=true`, {
391
+ headers: { 'Prefer': 'return=representation' },
392
+ });
393
+ const plans = response.plans || [];
394
+
395
+ // Map frequency to PayPal interval unit
396
+ const intervalUnit = FREQUENCY_TO_INTERVAL[frequency] || 'MONTH';
397
+
398
+ // Find matching active plan by interval + amount + trial-cycle presence
399
+ for (const plan of plans) {
400
+ if (plan.status !== 'ACTIVE') {
401
+ continue;
402
+ }
403
+
404
+ const cycle = plan.billing_cycles?.find(c => c.tenure_type === 'REGULAR');
405
+
406
+ if (!cycle) {
407
+ continue;
408
+ }
409
+
410
+ const planInterval = cycle.frequency?.interval_unit;
411
+ const planAmount = parseFloat(cycle.pricing_scheme?.fixed_price?.value || '0');
412
+ const planHasTrial = !!plan.billing_cycles?.find(c => c.tenure_type === 'TRIAL');
413
+
414
+ if (planInterval === intervalUnit && planAmount === expectedAmount && planHasTrial === !!trial) {
415
+ return plan.id;
416
+ }
417
+ }
418
+
419
+ throw new Error(`No active PayPal plan for ${product.id}/${frequency} at $${expectedAmount} ${trial ? 'with' : 'without'} a trial cycle (product: ${paypalProductId}) — run the manager payment walk to mint it`);
420
+ },
421
+
422
+ /**
423
+ * Extract the resource a PayPal webhook envelope carries
424
+ * Identifiers only — the payload never drives state ([../fetch-failure.js](../fetch-failure.js))
425
+ *
426
+ * @param {object} raw - Raw PayPal webhook payload
427
+ * @returns {object|null}
428
+ */
429
+ extractResource(raw) {
430
+ return raw?.resource || null;
431
+ },
432
+
433
+ /**
434
+ * Extract the internal orderId from a PayPal resource
435
+ * Stripe stores orderId in resource.metadata.orderId, but PayPal stores it in custom_id
436
+ *
437
+ * @param {object} resource - Raw PayPal resource (subscription or order)
438
+ * @returns {string|null}
439
+ */
440
+ getOrderId(resource) {
441
+ const purchaseCustomId = resource.purchase_units?.[0]?.custom_id;
442
+ const customData = parseCustomId(purchaseCustomId || resource.custom_id);
443
+ return customData.orderId || null;
444
+ },
445
+
446
+ /**
447
+ * Extract the UID from a PayPal resource's custom_id
448
+ * Used to resolve UID after fetchResource() for events like PAYMENT.SALE
449
+ * where the initial webhook payload doesn't carry custom_id
450
+ *
451
+ * @param {object} resource - Raw PayPal resource (subscription or order)
452
+ * @returns {string|null}
453
+ */
454
+ getUid(resource) {
455
+ const purchaseCustomId = resource.purchase_units?.[0]?.custom_id;
456
+ const customData = parseCustomId(purchaseCustomId || resource.custom_id);
457
+ return customData?.uid || null;
458
+ },
459
+
460
+ /**
461
+ * What a PayPal refund actually moved, read back from PayPal's own refund record
462
+ *
463
+ * The resource already in hand is no use here: it is the sale or the capture the
464
+ * refund reversed — the ORIGINAL payment, whose amount is the purchase price and
465
+ * not the refund's, which is simply wrong for a partial refund. And the amounts
466
+ * used to come from the webhook envelope, so a payload naming an inflated total
467
+ * wrote that number onto the order and into the customer's refund email
468
+ * ([#510](https://github.com/Omega-JS-Stack/omega/issues/510)).
469
+ *
470
+ * So the refund is fetched by its OWN id — the one the parser keeps beside the
471
+ * resourceId it reassigned to the sale/capture — and only the id comes from the
472
+ * event, the same trust level as the resourceId every lookup here starts from.
473
+ * v2 capture refunds read at `/v2/payments/refunds/{id}`; v1 sale refunds (both
474
+ * the one-time and the subscription path) at `/v1/payments/refund/{id}`.
475
+ *
476
+ * The record that answers has to point BACK at the sale or capture the event
477
+ * named, or it is another order's refund and the event is refused
478
+ * ([../refund-linkage.js](../refund-linkage.js)).
479
+ *
480
+ * @param {object} resource - The resource the refund reversed, as PayPal answered for it (the record it must link back to)
481
+ * @param {object} [options] - { refundId, eventType, resourceType, raw, ctx }
482
+ * @returns {Promise<{ amount: string|null, currency: string, reason: string|null }>}
483
+ */
484
+ async getRefundDetails(resource, options = {}) {
485
+ // A doc stored before the parser threaded the id still names the refund in the
486
+ // envelope it carries — an identifier, read the same way resourceId is
487
+ const refundId = options.refundId || options.raw?.resource?.id || null;
488
+
489
+ if (!refundId) {
490
+ const message = `paypal getRefundDetails(): ${options.eventType || 'refund event'} names no refund id, so PayPal cannot be asked what came back — the refund is recorded with no amount rather than with the payload's`;
491
+
492
+ if (options.ctx?.warn) {
493
+ options.ctx.warn(message);
494
+ } else {
495
+ console.warn(`[@omega.js/backend:payment:paypal] ${message}`);
496
+ }
497
+
498
+ return { amount: null, currency: 'USD', reason: null };
499
+ }
500
+
501
+ const endpoint = options.eventType === 'PAYMENT.CAPTURE.REFUNDED'
502
+ ? `/v2/payments/refunds/${refundId}`
503
+ : `/v1/payments/refund/${refundId}`;
504
+
505
+ let refund;
506
+
507
+ try {
508
+ refund = await this.request(endpoint);
509
+ } catch (e) {
510
+ throw fetchFailure(e, { provider: 'paypal', fn: 'getRefundDetails', resourceType: 'refund', resourceId: refundId });
511
+ }
512
+
513
+ // The refund is a SECOND record, keyed by an id the payload chose: without a
514
+ // link back to the sale or capture this event is about, an unrelated refund
515
+ // from the same account would have booked its amount onto this order
516
+ // ([#532](https://github.com/Omega-JS-Stack/omega/issues/532)). PayPal writes
517
+ // that link both ways round — a v1 refund names its `sale_id`, a v2 refund
518
+ // links `up` to its capture. A refund of a SUBSCRIPTION's sale names the sale,
519
+ // never the billing agreement the event resolved to, so there is nothing to
520
+ // compare and the linkage helper says so out loud.
521
+ assertRefundLinkage({
522
+ provider: 'paypal',
523
+ refundType: 'refund',
524
+ refundId: refundId,
525
+ resourceType: options.resourceType || 'resource',
526
+ resourceId: resource?.id || null,
527
+ expected: resource?.id || null,
528
+ ctx: options.ctx,
529
+ ...refundLink(refund, options.resourceType),
530
+ });
531
+
532
+ return {
533
+ // v1 spells the amount `total`/`currency`, v2 spells it
534
+ // `value`/`currency_code` — a v2 capture refund read only the v1
535
+ // spelling and landed a null amount on the order record and the
536
+ // customer's refund email
537
+ // ([#240](https://github.com/Omega-JS-Stack/omega/issues/240)).
538
+ amount: refund?.amount?.total || refund?.amount?.value || refund?.total_refunded_amount?.value || null,
539
+ currency: refund?.amount?.currency || refund?.amount?.currency_code || 'USD',
540
+ reason: refund?.reason_code || null,
541
+ };
542
+ },
543
+
544
+ /**
545
+ * Build the custom_id string for PayPal subscriptions and orders
546
+ * Format: uid:{uid},orderId:{orderId} or uid:{uid},orderId:{orderId},productId:{productId}
547
+ *
548
+ * @param {string} uid - User's Firebase UID
549
+ * @param {string} orderId - Our internal order ID
550
+ * @param {string} [productId] - Product ID (used for one-time payments)
551
+ * @returns {string}
552
+ */
553
+ buildCustomId(uid, orderId, productId) {
554
+ let customId = `uid:${uid},orderId:${orderId}`;
555
+
556
+ if (productId) {
557
+ customId += `,productId:${productId}`;
558
+ }
559
+
560
+ return customId;
561
+ },
562
+ };
563
+
564
+ /**
565
+ * The back-pointer a PayPal refund record carries to the payment it reversed
566
+ *
567
+ * A v1 refund names its `sale_id`; a v2 refund links `up` to its capture. The
568
+ * refund of a SUBSCRIPTION's sale names the sale, never the billing agreement the
569
+ * event resolved to, so that path has nothing to compare and says so
570
+ * ([refund-linkage.js](../refund-linkage.js)).
571
+ *
572
+ * @param {object} refund - The fetched refund record
573
+ * @param {string} resourceType - The event's resource type ('sale' | 'capture')
574
+ * @returns {{ field: string|null, found: string|null }}
575
+ */
576
+ function refundLink(refund, resourceType) {
577
+ if (resourceType === 'sale') {
578
+ return { field: 'sale_id', found: refund?.sale_id || null };
579
+ }
580
+
581
+ if (resourceType === 'capture') {
582
+ return { field: 'links[rel=up]', found: parseUpLinkId(refund?.links) };
583
+ }
584
+
585
+ return { field: null, found: null };
586
+ }
587
+
588
+ /**
589
+ * The id a v2 record points `up` at, read off its HATEOAS links
590
+ *
591
+ * PayPal names a v2 refund's parent capture with a link whose `rel` is `up`, and
592
+ * the id is that URL's last segment — the only back-pointer a v2 refund carries
593
+ * ([#532](https://github.com/Omega-JS-Stack/omega/issues/532)).
594
+ *
595
+ * @param {Array|undefined} links - The record's HATEOAS links
596
+ * @returns {string|null}
597
+ */
598
+ function parseUpLinkId(links) {
599
+ const up = (links || []).find((link) => link?.rel === 'up');
600
+
601
+ if (!up?.href) {
602
+ return null;
603
+ }
604
+
605
+ return up.href.split('/').filter(Boolean).pop() || null;
606
+ }
607
+
608
+ /**
609
+ * Parse the custom_id string from a PayPal subscription
610
+ * Format: uid:{uid},orderId:{orderId}
611
+ *
612
+ * @param {string} customId - The custom_id string
613
+ * @returns {{ uid: string|null, orderId: string|null, productId: string|null }}
614
+ */
615
+ function parseCustomId(customId) {
616
+ if (!customId) {
617
+ return { uid: null, orderId: null, productId: null };
618
+ }
619
+
620
+ const result = { uid: null, orderId: null, productId: null };
621
+
622
+ for (const part of customId.split(',')) {
623
+ const [key, ...valueParts] = part.split(':');
624
+ const value = valueParts.join(':'); // Handle values that contain colons
625
+
626
+ if (key === 'uid') {
627
+ result.uid = value || null;
628
+ } else if (key === 'orderId') {
629
+ result.orderId = value || null;
630
+ } else if (key === 'productId') {
631
+ result.productId = value || null;
632
+ }
633
+ }
634
+
635
+ return result;
636
+ }
637
+
638
+ /**
639
+ * Is this subscription still INSIDE its free trial?
640
+ *
641
+ * The trial WINDOW is the answer, not the payment record: conversion happens at
642
+ * the trial's end, so a subscription whose trial end is still ahead of it cannot
643
+ * have converted, whatever money has changed hands. A plan carrying a setup fee
644
+ * charges one on day zero, and reading that fee as a paid billing period is what
645
+ * made a cancelled trial look like a paid term with time left on it
646
+ * ([#267](https://github.com/Omega-JS-Stack/omega/issues/267)).
647
+ *
648
+ * A trial with no computable end (no `start_time`) reads FALSE — this decides
649
+ * whether access is revoked, and missing data must never be the thing that
650
+ * revokes it.
651
+ *
652
+ * @param {object} raw - Raw PayPal subscription (with _plan attached)
653
+ * @returns {boolean}
654
+ */
655
+ function isInTrial(raw) {
656
+ const trial = resolveTrial(raw);
657
+
658
+ return !!(trial.claimed && trial.expires.timestampUNIX > Math.floor(Date.now() / 1000));
659
+ }
660
+
661
+ /**
662
+ * Calculate when the current billing period ends based on last payment + interval
663
+ * Used for cancelled subs to determine remaining access time
664
+ *
665
+ * @param {object} raw - Raw PayPal subscription (with _plan attached)
666
+ * @returns {Date|null} Period end date, or null if cannot be calculated
667
+ */
668
+ function calculatePeriodEnd(raw) {
669
+ // A TRIAL has no paid period to serve out, so cancelling one ends access NOW
670
+ // (Ian's ruling, 2026-08-15 — we do not keep serving a trial we know will not
671
+ // convert). This is the ONE place the three cancelled-subscription readers
672
+ // below share, so status, cancellation and expiry can never disagree about it.
673
+ if (isInTrial(raw)) {
674
+ return null;
675
+ }
676
+
677
+ const lastPayment = raw.billing_info?.last_payment?.time;
678
+
679
+ if (!lastPayment) {
680
+ return null;
681
+ }
682
+
683
+ const plan = raw._plan;
684
+ const regularCycle = plan?.billing_cycles?.find(c => c.tenure_type === 'REGULAR');
685
+
686
+ if (!regularCycle) {
687
+ return null;
688
+ }
689
+
690
+ const unit = regularCycle.frequency.interval_unit;
691
+ const count = regularCycle.frequency.interval_count || 1;
692
+ const lastDate = new Date(lastPayment);
693
+
694
+ if (unit === 'YEAR') {
695
+ lastDate.setFullYear(lastDate.getFullYear() + count);
696
+ } else if (unit === 'MONTH') {
697
+ lastDate.setMonth(lastDate.getMonth() + count);
698
+ } else if (unit === 'WEEK') {
699
+ lastDate.setDate(lastDate.getDate() + (count * 7));
700
+ } else if (unit === 'DAY') {
701
+ lastDate.setDate(lastDate.getDate() + count);
702
+ }
703
+
704
+ return lastDate;
705
+ }
706
+
707
+ /**
708
+ * Map PayPal subscription status to unified status
709
+ *
710
+ * | PayPal Status | Unified Status |
711
+ * |------------------|----------------|
712
+ * | ACTIVE | active |
713
+ * | SUSPENDED | suspended |
714
+ * | CANCELLED (period remaining) | active (with cancellation.pending) |
715
+ * | CANCELLED (period ended) | cancelled |
716
+ * | EXPIRED | cancelled |
717
+ * | APPROVAL_PENDING | cancelled |
718
+ * | APPROVED | active |
719
+ */
720
+ function resolveStatus(raw) {
721
+ const status = raw.status;
722
+
723
+ if (status === 'ACTIVE' || status === 'APPROVED') {
724
+ return 'active';
725
+ }
726
+
727
+ if (status === 'SUSPENDED') {
728
+ return 'suspended';
729
+ }
730
+
731
+ // CANCELLED — check if user still has paid time remaining
732
+ if (status === 'CANCELLED') {
733
+ const periodEnd = calculatePeriodEnd(raw);
734
+
735
+ if (periodEnd && periodEnd > new Date()) {
736
+ // User still has access until period end — treat as active with pending cancellation
737
+ return 'active';
738
+ }
739
+
740
+ return 'cancelled';
741
+ }
742
+
743
+ // EXPIRED, APPROVAL_PENDING, or anything else
744
+ return 'cancelled';
745
+ }
746
+
747
+ /**
748
+ * Resolve cancellation state from PayPal subscription
749
+ *
750
+ * PayPal has no cancel_at_period_end like Stripe. When cancelled:
751
+ * - If billing period hasn't ended: pending=true, date=period end (user keeps access)
752
+ * - If billing period has ended: pending=false, date=cancellation time (fully cancelled)
753
+ */
754
+ function resolveCancellation(raw) {
755
+ if (raw.status === 'CANCELLED') {
756
+ const periodEnd = calculatePeriodEnd(raw);
757
+
758
+ // Period still active — pending cancellation (user keeps access until period end)
759
+ if (periodEnd && periodEnd > new Date()) {
760
+ const periodEndStr = powertools.timestamp(periodEnd, { output: 'string' });
761
+
762
+ return {
763
+ pending: true,
764
+ date: {
765
+ timestamp: periodEndStr,
766
+ timestampUNIX: powertools.timestamp(periodEndStr, { output: 'unix' }),
767
+ },
768
+ };
769
+ }
770
+
771
+ // Period has ended — fully cancelled
772
+ const cancelDate = raw.status_update_time
773
+ ? powertools.timestamp(new Date(raw.status_update_time), { output: 'string' })
774
+ : EPOCH_ZERO;
775
+
776
+ return {
777
+ pending: false,
778
+ date: {
779
+ timestamp: cancelDate,
780
+ timestampUNIX: cancelDate !== EPOCH_ZERO
781
+ ? powertools.timestamp(cancelDate, { output: 'unix' })
782
+ : EPOCH_ZERO_UNIX,
783
+ },
784
+ };
785
+ }
786
+
787
+ return {
788
+ pending: false,
789
+ date: {
790
+ timestamp: EPOCH_ZERO,
791
+ timestampUNIX: EPOCH_ZERO_UNIX,
792
+ },
793
+ };
794
+ }
795
+
796
+ /**
797
+ * Resolve trial state from PayPal subscription
798
+ * PayPal trials are represented as billing_cycles with tenure_type === 'TRIAL'
799
+ */
800
+ function resolveTrial(raw) {
801
+ // Check if the plan has a trial cycle
802
+ const plan = raw._plan || {};
803
+ const trialCycle = plan.billing_cycles?.find(c => c.tenure_type === 'TRIAL');
804
+
805
+ if (!trialCycle) {
806
+ return {
807
+ claimed: false,
808
+ expires: { timestamp: EPOCH_ZERO, timestampUNIX: EPOCH_ZERO_UNIX },
809
+ };
810
+ }
811
+
812
+ // PayPal doesn't expose exact trial start/end dates on the subscription
813
+ // We can calculate from start_time + trial duration
814
+ const startTime = raw.start_time ? new Date(raw.start_time) : null;
815
+
816
+ if (!startTime) {
817
+ return {
818
+ claimed: true,
819
+ expires: { timestamp: EPOCH_ZERO, timestampUNIX: EPOCH_ZERO_UNIX },
820
+ };
821
+ }
822
+
823
+ // Calculate trial end based on trial cycle frequency
824
+ const trialFreq = trialCycle.frequency;
825
+ const trialCount = trialCycle.total_cycles || 1;
826
+ const trialEnd = new Date(startTime);
827
+
828
+ if (trialFreq?.interval_unit === 'DAY') {
829
+ trialEnd.setDate(trialEnd.getDate() + (trialFreq.interval_count || 1) * trialCount);
830
+ } else if (trialFreq?.interval_unit === 'MONTH') {
831
+ trialEnd.setMonth(trialEnd.getMonth() + (trialFreq.interval_count || 1) * trialCount);
832
+ }
833
+
834
+ const trialEndStr = powertools.timestamp(trialEnd, { output: 'string' });
835
+
836
+ return {
837
+ claimed: true,
838
+ expires: {
839
+ timestamp: trialEndStr,
840
+ timestampUNIX: powertools.timestamp(trialEndStr, { output: 'unix' }),
841
+ },
842
+ };
843
+ }
844
+
845
+ /**
846
+ * Resolve billing frequency from PayPal subscription
847
+ */
848
+ function resolveFrequency(raw) {
849
+ // Try _plan first (fetched separately)
850
+ const plan = raw._plan || {};
851
+ const regularCycle = plan.billing_cycles?.find(c => c.tenure_type === 'REGULAR');
852
+
853
+ if (regularCycle?.frequency?.interval_unit) {
854
+ return INTERVAL_TO_FREQUENCY[regularCycle.frequency.interval_unit] || null;
855
+ }
856
+
857
+ // Fallback: try inline plan info from ?fields=plan
858
+ const inlinePlan = raw.plan;
859
+ if (inlinePlan?.billing_cycles) {
860
+ const cycle = inlinePlan.billing_cycles.find(c => c.tenure_type === 'REGULAR');
861
+ if (cycle?.frequency?.interval_unit) {
862
+ return INTERVAL_TO_FREQUENCY[cycle.frequency.interval_unit] || null;
863
+ }
864
+ }
865
+
866
+ return null;
867
+ }
868
+
869
+ /**
870
+ * Resolve product by matching the PayPal product ID against config products
871
+ * Uses: sub._plan.product_id → match config product.paypal.productId
872
+ * Also checks paypal.legacyProductIds[] for migrated products
873
+ */
874
+ function resolveProduct(raw, config) {
875
+ // Get PayPal product ID from the plan (attached during fetchResource)
876
+ const paypalProductId = raw._plan?.product_id || null;
877
+
878
+ if (!paypalProductId || !config.payment?.products) {
879
+ return { id: 'basic', name: 'Basic' };
880
+ }
881
+
882
+ for (const product of config.payment.products) {
883
+ // Check current product ID
884
+ if (product.paypal?.productId === paypalProductId) {
885
+ return { id: product.id, name: product.name || product.id };
886
+ }
887
+
888
+ // Check legacy product IDs (for migrated products)
889
+ if (product.paypal?.legacyProductIds?.includes(paypalProductId)) {
890
+ return { id: product.id, name: product.name || product.id };
891
+ }
892
+ }
893
+
894
+ return { id: 'basic', name: 'Basic' };
895
+ }
896
+
897
+ /**
898
+ * Resolve product for one-time payments
899
+ */
900
+ function resolveProductOneTime(productId, config) {
901
+ if (!productId || !config.payment?.products) {
902
+ return { id: productId || 'unknown', name: 'Unknown' };
903
+ }
904
+
905
+ const product = config.payment.products.find(p => p.id === productId);
906
+
907
+ if (!product) {
908
+ return { id: productId, name: productId };
909
+ }
910
+
911
+ return { id: product.id, name: product.name || product.id };
912
+ }
913
+
914
+ /**
915
+ * Resolve subscription expiration from PayPal data
916
+ *
917
+ * For cancelled subs with remaining time, uses calculated period end.
918
+ * For active subs, uses next_billing_time.
919
+ */
920
+ function resolveExpires(raw) {
921
+ // Cancelled subs with remaining time — use calculated period end
922
+ if (raw.status === 'CANCELLED') {
923
+ const periodEnd = calculatePeriodEnd(raw);
924
+
925
+ if (periodEnd && periodEnd > new Date()) {
926
+ const expiresStr = powertools.timestamp(periodEnd, { output: 'string' });
927
+
928
+ return {
929
+ timestamp: expiresStr,
930
+ timestampUNIX: powertools.timestamp(expiresStr, { output: 'unix' }),
931
+ };
932
+ }
933
+ }
934
+
935
+ // Active subs: PayPal's billing_info.next_billing_time is the closest to "period end"
936
+ const nextBilling = raw.billing_info?.next_billing_time;
937
+
938
+ if (!nextBilling) {
939
+ return {
940
+ timestamp: EPOCH_ZERO,
941
+ timestampUNIX: EPOCH_ZERO_UNIX,
942
+ };
943
+ }
944
+
945
+ const expiresDate = powertools.timestamp(new Date(nextBilling), { output: 'string' });
946
+
947
+ return {
948
+ timestamp: expiresDate,
949
+ timestampUNIX: powertools.timestamp(expiresDate, { output: 'unix' }),
950
+ };
951
+ }
952
+
953
+ /**
954
+ * Resolve subscription start date from PayPal data
955
+ */
956
+ function resolveStartDate(raw) {
957
+ const startTime = raw.start_time || raw.create_time;
958
+
959
+ if (!startTime) {
960
+ return {
961
+ timestamp: EPOCH_ZERO,
962
+ timestampUNIX: EPOCH_ZERO_UNIX,
963
+ };
964
+ }
965
+
966
+ const startDate = powertools.timestamp(new Date(startTime), { output: 'string' });
967
+
968
+ return {
969
+ timestamp: startDate,
970
+ timestampUNIX: powertools.timestamp(startDate, { output: 'unix' }),
971
+ };
972
+ }
973
+
974
+ /**
975
+ * Resolve the display price for a product/frequency from config
976
+ */
977
+ function resolvePrice(productId, frequency, config) {
978
+ const product = config.payment?.products?.find(p => p.id === productId);
979
+
980
+ if (!product || !product.prices) {
981
+ return 0;
982
+ }
983
+
984
+ return product.prices[frequency] || 0;
985
+ }
986
+
987
+ module.exports = PayPal;