@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,1224 @@
1
+ const path = require('path');
2
+ const powertools = require('node-powertools');
3
+ const transitions = require('./transitions/index.js');
4
+ const { trackPayment, isTrialConversion } = require('./analytics.js');
5
+ const loadProvider = require('../../../libraries/load-provider.js');
6
+ const { hasAuthUser } = require('../../../libraries/auth-user.js');
7
+ const User = require('../../../helpers/user.js');
8
+
9
+ /**
10
+ * Firestore trigger: payments-webhooks/{eventId} onWrite
11
+ *
12
+ * Processes pending webhook events:
13
+ * 1. Loads the provider library
14
+ * 2. Fetches the resource from the provider API — the ONLY trusted source, never
15
+ * the object the webhook payload carried (a lookup that cannot be answered
16
+ * refuses or defers, it never processes)
17
+ * 3. Branches on event.category to transform + write:
18
+ * - subscription → toUnifiedSubscription → users/{uid}.subscription + payments-orders/{orderId}
19
+ * - one-time → toUnifiedOneTime → payments-orders/{orderId}
20
+ * 4. Detects state transitions and dispatches handler files (non-blocking)
21
+ * 5. Marks the webhook as completed
22
+ */
23
+ module.exports = async ({ ctx, change, context }) => {
24
+ const Manager = ctx.Manager;
25
+ const admin = Manager.libraries.admin;
26
+
27
+ const dataAfter = change.after.data();
28
+
29
+ // Short-circuit: deleted doc or non-pending status
30
+ if (!dataAfter || dataAfter.status !== 'pending') {
31
+ return;
32
+ }
33
+
34
+ const eventId = context.params.eventId;
35
+ const webhookRef = admin.firestore().doc(`payments-webhooks/${eventId}`);
36
+
37
+ // A doc that already completed once and is pending again is a REPROCESS (a
38
+ // redelivery, or a doc put back to pending) — the transitions that fire on the
39
+ // event type alone must not dispatch their email a second time.
40
+ const previouslyCompleted = change.before.data()?.status === 'completed';
41
+
42
+ // Set status to processing
43
+ await webhookRef.set({ status: 'processing' }, { merge: true });
44
+
45
+ // Hoisted so orderId is available in catch block for audit trail
46
+ let orderId = null;
47
+ let passThruOrderId = null;
48
+ let library = null;
49
+
50
+ // Whether this event was REFUSED — a terminal decision, taken before anything is
51
+ // written. The failure path reads it: a refusal that could not even record itself
52
+ // must not fall back to writing a doc the PAYLOAD named
53
+ // ([#535](https://github.com/Omega-JS-Stack/omega/issues/535)).
54
+ let refused = false;
55
+
56
+ // The transition claim this run WON, once it has one — hoisted because the claim
57
+ // outlives the block that took it. It is settled at the end of the run: 'done'
58
+ // when the pipeline got all the way through, 'failed' beside the webhook doc's
59
+ // own failure stamp, so the cron's retry of this same event id can retake it
60
+ // ([#665](https://github.com/Omega-JS-Stack/omega/issues/665)).
61
+ const claimTicket = { orderId: null, transitionName: null };
62
+
63
+ try {
64
+ const provider = dataAfter.provider;
65
+ // What the event's own payload claimed about whose subscription this is. It
66
+ // is a CLAIM, not the answer: once the lookup succeeds, the provider's record
67
+ // is what steers the write and this is only what that is checked against
68
+ // ([#509](https://github.com/Omega-JS-Stack/omega/issues/509)).
69
+ const payloadUid = dataAfter.owner;
70
+ let uid = payloadUid;
71
+ const raw = dataAfter.raw;
72
+ const eventType = dataAfter.event?.type;
73
+ const category = dataAfter.event?.category;
74
+ const resourceType = dataAfter.event?.resourceType;
75
+ const resourceId = dataAfter.event?.resourceId;
76
+ // The refund's OWN id, which the parser keeps beside the resource the refund
77
+ // reversed — an identifier, and the key its details are looked up by
78
+ // ([#510](https://github.com/Omega-JS-Stack/omega/issues/510)).
79
+ const refundId = dataAfter.event?.refundId || null;
80
+ // THIS charge's own id, where the event names one — the id one charge is
81
+ // reported to the platforms under
82
+ // ([#656](https://github.com/Omega-JS-Stack/omega/issues/656)).
83
+ const chargeId = dataAfter.event?.chargeId || null;
84
+
85
+ ctx.log(`Processing webhook ${eventId}: provider=${provider}, eventType=${eventType}, category=${category}, resourceType=${resourceType}, resourceId=${resourceId}, uid=${uid || 'null'}`);
86
+
87
+ // Validate category
88
+ if (!category) {
89
+ throw new Error(`Webhook event has no category — cannot process`);
90
+ }
91
+
92
+ // Load the shared library for this provider
93
+ try {
94
+ library = loadProvider(path.join(__dirname, '../../../libraries/payment/providers'), provider);
95
+ } catch (e) {
96
+ throw new Error(`Unknown provider library: ${provider}`);
97
+ }
98
+
99
+ // Fetch the resource from the provider API. Its answer is the ONLY thing this
100
+ // pipeline acts on: the object the webhook carried is whatever the caller
101
+ // posted, and letting it stand in for a lookup that failed let unverified data
102
+ // drive real subscription state and real conversions
103
+ // ([#506](https://github.com/Omega-JS-Stack/omega/issues/506)). So a failed
104
+ // lookup produces no resource at all — only which KIND of failure it was.
105
+ let resource;
106
+
107
+ // What a refund actually moved. It rides in the same block because it is the
108
+ // same kind of thing: an amount read out of the envelope is an amount the
109
+ // caller chose, and it was landing on the order record, the customer's refund
110
+ // email and the refund conversion
111
+ // ([#510](https://github.com/Omega-JS-Stack/omega/issues/510)). The provider
112
+ // answers for it too — from the resource already in hand, or from a lookup of
113
+ // the refund's own record — and a lookup that fails is classified by the same
114
+ // seam as the one above.
115
+ const isRefund = transitions.REFUND_EVENTS.includes(eventType);
116
+ let refundDetails = null;
117
+
118
+ try {
119
+ resource = await library.fetchResource(resourceType, resourceId, { admin, ctx, eventType, raw, config: Manager.config });
120
+
121
+ if (isRefund && library.getRefundDetails) {
122
+ refundDetails = await library.getRefundDetails(resource, { raw, refundId, eventType, resourceType, ctx });
123
+ }
124
+ } catch (e) {
125
+ // The provider answered, and the record it answered with belongs to another
126
+ // order: the refund lookup's key came from the payload, and an unrelated
127
+ // refund id from the same merchant account imported another customer's
128
+ // numbers onto this one ([#532](https://github.com/Omega-JS-Stack/omega/issues/532)).
129
+ // No retry can relate two unrelated records, so it is refused and
130
+ // acknowledged — the same terminal shape as every other decision here.
131
+ if (e.refundNotLinked) {
132
+ refused = true;
133
+
134
+ await acknowledgeRefusal(webhookRef, refuseRefundNotLinked({ ctx, error: e, eventId, eventType, provider, uid }), { ctx, eventId, eventType, provider, uid });
135
+
136
+ return;
137
+ }
138
+
139
+ // Unreachable is not "gone": the answer exists and this attempt could not read
140
+ // it, so the event DEFERS — the throw marks the doc failed and the retry sweep
141
+ // (events/cron/frequent/retry-failed-webhooks.js) is the reconciliation path.
142
+ if (!e.notFound) {
143
+ throw e;
144
+ }
145
+
146
+ // The provider affirmatively does not have it. Nothing will ever make this
147
+ // event processable, so it is refused and acknowledged rather than redelivered
148
+ // forever — the same terminal-decision shape as the refusals below.
149
+ //
150
+ // The failure names the lookup that actually missed, which is not always the
151
+ // event's own resource: a refund's details come from a lookup of their own
152
+ // ([#510](https://github.com/Omega-JS-Stack/omega/issues/510)).
153
+ refused = true;
154
+
155
+ await acknowledgeRefusal(webhookRef, refuseResourceNotFound({
156
+ ctx,
157
+ error: e,
158
+ eventId,
159
+ eventType,
160
+ provider,
161
+ resourceType: e.resourceType || resourceType,
162
+ resourceId: e.resourceId || resourceId,
163
+ uid,
164
+ }), { ctx, eventId, eventType, provider, uid });
165
+
166
+ return;
167
+ }
168
+
169
+ // v2 resources spell the field `status`, v1 sale resources spell it `state` —
170
+ // reading only the first reported `status=unknown` on a fetch that plainly
171
+ // succeeded, which is exactly the confusion this line exists to prevent
172
+ // ([#347](https://github.com/Omega-JS-Stack/omega/issues/347)).
173
+ const resourceStatus = resource.status ?? resource.state ?? 'unknown';
174
+ ctx.log(`Fetched resource: type=${resourceType}, id=${resourceId}, status=${resourceStatus}, source=provider API`);
175
+
176
+ // The lookup answered, so the record it answered with is what says who this
177
+ // event belongs to. Every provider's resource carries the uid we put on it —
178
+ // Stripe `metadata.uid`, PayPal `custom_id`, Chargebee `meta_data`/`cf_uid` —
179
+ // and reading the payload's uid instead let an event naming a REAL resource
180
+ // steer that resource's write onto whatever uid the caller typed
181
+ // ([#509](https://github.com/Omega-JS-Stack/omega/issues/509)). It also covers
182
+ // what this block was originally for: events like PAYMENT.SALE, whose payload
183
+ // carries no custom_id at all while the parent subscription does.
184
+ const providerUid = library.getUid ? library.getUid(resource) : null;
185
+
186
+ // Chargebee hosted-page checkouts don't forward subscription[meta_data] to the
187
+ // subscription, so the record answers no uid at all — but the hosted page's
188
+ // pass_thru_content holds ours, and it is a record of the PROVIDER's, not the
189
+ // caller's. It used to be consulted only when the payload claimed nothing
190
+ // (`!uid`), i.e. never in the one case where a claim needed checking: a forged
191
+ // uid on a not-yet-backfilled subscription steered unchecked
192
+ // ([#533](https://github.com/Omega-JS-Stack/omega/issues/533)).
193
+ let resolvedFromPassThru = false;
194
+
195
+ if (providerUid) {
196
+ // The payload's claim is only ever a cross-check now, and a claim that
197
+ // contradicts the provider is refused outright — not silently corrected to
198
+ // the provider's uid, because an event that lies about its owner has
199
+ // nothing left in it worth acting on.
200
+ if (payloadUid && payloadUid !== providerUid) {
201
+ refused = true;
202
+
203
+ await acknowledgeRefusal(webhookRef, refuseUidMismatch({ ctx, eventId, eventType, provider, resourceType, resourceId, payloadUid, providerUid, source: 'record' }), { ctx, eventId, eventType, provider, uid });
204
+
205
+ return;
206
+ }
207
+
208
+ if (uid !== providerUid) {
209
+ uid = providerUid;
210
+ ctx.log(`UID resolved from fetched resource: uid=${uid}, provider=${provider}, resourceType=${resourceType}`);
211
+
212
+ // Update the webhook doc with the resolved UID so it's persisted for debugging
213
+ await webhookRef.set({ owner: uid }, { merge: true });
214
+ }
215
+ } else {
216
+ const passThruResult = library.resolveUidFromHostedPage
217
+ ? await library.resolveUidFromHostedPage(resourceId, ctx)
218
+ : null;
219
+
220
+ if (passThruResult?.uid) {
221
+ // The hosted page answered, so it is the record that steers — and a payload
222
+ // that contradicts it earns the same refusal a contradicted record does.
223
+ if (payloadUid && payloadUid !== passThruResult.uid) {
224
+ refused = true;
225
+
226
+ await acknowledgeRefusal(webhookRef, refuseUidMismatch({ ctx, eventId, eventType, provider, resourceType, resourceId, payloadUid, providerUid: passThruResult.uid, source: 'hosted-page' }), { ctx, eventId, eventType, provider, uid });
227
+
228
+ return;
229
+ }
230
+
231
+ uid = passThruResult.uid;
232
+ passThruOrderId = passThruResult.orderId || null;
233
+ resolvedFromPassThru = true;
234
+ ctx.log(`UID resolved from hosted page pass_thru_content: uid=${uid}, orderId=${passThruOrderId}, resourceId=${resourceId}`);
235
+
236
+ await webhookRef.set({ owner: uid }, { merge: true });
237
+ } else if (payloadUid) {
238
+ // Nothing left to check against: the record carries no uid, and no other
239
+ // record of this provider's answered either — the hosted-page scan covers
240
+ // only the last 25, so a miss honestly means "not in the window", never
241
+ // "not this uid", and refusing on it would break every older hosted-page
242
+ // checkout. The payload steers, which is the pre-#509 behavior — so the
243
+ // line that says so is the only warning a human gets that this write was
244
+ // never cross-checked.
245
+ ctx.warn(`UID FALLBACK: ${eventType} (${provider}) — ${resourceType} ${resourceId} carries no uid metadata and no other ${provider} record answered for it, so the event's own payload steers this write unchecked (uid=${payloadUid}, event=${eventId})`);
246
+ }
247
+ }
248
+
249
+ // Validate UID — must have one by now
250
+ if (!uid) {
251
+ throw new Error(`Webhook event has no UID — could not extract from webhook parse, fetched ${resourceType} resource, or hosted page pass_thru_content`);
252
+ }
253
+
254
+ // Backfill: if UID was resolved from pass_thru_content, set meta_data on the subscription
255
+ // so future webhooks (renewals, cancellations) can resolve the UID directly
256
+ if (resolvedFromPassThru && resourceType === 'subscription' && library.setMetaData) {
257
+ library.setMetaData(resource, { uid, orderId: passThruOrderId })
258
+ .then(() => ctx.log(`Backfilled meta_data on subscription ${resourceId} + customer: uid=${uid}, orderId=${passThruOrderId}`))
259
+ .catch((e) => ctx.error(`Failed to backfill meta_data on ${resourceType} ${resourceId}: ${e.message}`));
260
+ }
261
+
262
+ // Build timestamps
263
+ const now = powertools.timestamp(new Date(), { output: 'string' });
264
+ const nowUNIX = powertools.timestamp(now, { output: 'unix' });
265
+ const webhookReceivedUNIX = dataAfter.metadata?.created?.timestampUNIX || nowUNIX;
266
+
267
+ // Extract orderId from resource (provider-agnostic)
268
+ // Falls back to pass_thru_content orderId when meta_data wasn't available on the resource
269
+ orderId = library.getOrderId(resource) || passThruOrderId;
270
+
271
+ // Process the payment event (subscription or one-time)
272
+ if (category !== 'subscription' && category !== 'one-time') {
273
+ throw new Error(`Unknown event category: ${category}`);
274
+ }
275
+
276
+ const { transition, refusal } = await processPaymentEvent({ category, library, resource, resourceType, uid, provider, eventType, eventId, resourceId, chargeId, orderId, now, nowUNIX, webhookReceivedUNIX, previouslyCompleted, ctx, isRefund, refundDetails, claimTicket });
277
+
278
+ // Mark webhook as completed (include transition name + any refusal for auditing/testing).
279
+ // Both are written on EVERY pass, so a reprocess that now finds its order clears
280
+ // the refusal it was stamped with rather than leaving a stale flag behind.
281
+ await webhookRef.set({
282
+ status: 'completed',
283
+ owner: uid,
284
+ orderId: orderId,
285
+ transition: transition,
286
+ refusal: refusal,
287
+ metadata: {
288
+ completed: {
289
+ timestamp: now,
290
+ timestampUNIX: nowUNIX,
291
+ },
292
+ },
293
+ }, { merge: true });
294
+
295
+ ctx.log(`Webhook ${eventId} completed`);
296
+
297
+ // The transition is finished: its handler, its analytics fire and its marketing
298
+ // sync are all away. Last, so a throw anywhere above still settles as `failed`.
299
+ await transitions.settleClaim({ admin, claim: claimTicket, status: 'done' });
300
+ } catch (e) {
301
+ ctx.error(`Webhook ${eventId} failed: ${e.message}`, e);
302
+
303
+ const now = powertools.timestamp(new Date(), { output: 'string' });
304
+ const nowUNIX = powertools.timestamp(now, { output: 'unix' });
305
+
306
+ // Mark as failed with error message, counting the attempt — the frequent cron's
307
+ // sweep re-flips a failed doc to pending until this count reaches its ceiling
308
+ // (events/cron/frequent/retry-failed-webhooks.js)
309
+ //
310
+ // Unless the failure is PERMANENT: a malformed envelope is a programmer/parser
311
+ // error, not a provider that could not be reached, and deferring it burned the
312
+ // whole ladder before dead-lettering something the first attempt already knew
313
+ // was unprocessable ([#536](https://github.com/Omega-JS-Stack/omega/issues/536)).
314
+ // Dead-lettered where it stands, so the sweep leaves it for the human it needs.
315
+ await webhookRef.set({
316
+ status: 'failed',
317
+ error: e.message || String(e),
318
+ retryCount: (dataAfter.retryCount || 0) + 1,
319
+ ...(e.permanent ? { deadLetter: true } : {}),
320
+ }, { merge: true });
321
+
322
+ // And release the claim the same way: the sweep re-runs this event id, and a
323
+ // claim left standing would refuse the retry that is meant to finish the job.
324
+ await transitions.settleClaim({ admin, claim: claimTicket, status: 'failed' });
325
+
326
+ if (e.permanent) {
327
+ ctx.error(`PERMANENT FAILURE: webhook ${eventId} (provider=${dataAfter.provider}, event=${dataAfter.event?.type || 'unknown'}) cannot be processed by any retry — dead-lettered on its first attempt: ${e.message}`);
328
+ }
329
+
330
+ // A REFUSED event is already terminal: the decision was made, and the only thing
331
+ // that failed is the stamp recording it. The payload-derived intent write below
332
+ // is the one doc a refused forgery could still reach — named by the caller, not
333
+ // by anything the provider confirmed — so a refusal writes nothing else at all,
334
+ // and the retry sweep re-lands the stamp on the next pass
335
+ // ([#535](https://github.com/Omega-JS-Stack/omega/issues/535)).
336
+ if (refused) {
337
+ ctx.warn(`Webhook ${eventId} was REFUSED and its refusal stamp did not land (${e.message}) — nothing else is written for a refused event, and the retry sweep re-lands the stamp`);
338
+
339
+ return;
340
+ }
341
+
342
+ // A throw before the orderId was resolved (a fetchResource failure, an
343
+ // unresolvable UID) would otherwise leave the intent pending forever — resolve
344
+ // it from what IS available: the webhook doc, or the raw payload the provider
345
+ // library can read an orderId out of.
346
+ if (!orderId) {
347
+ orderId = resolveOrderIdAfterFailure({ dataAfter, library, ctx }) || passThruOrderId;
348
+
349
+ if (orderId) {
350
+ ctx.log(`Resolved orderId ${orderId} for failed webhook ${eventId} from the webhook payload`);
351
+ } else {
352
+ ctx.warn(`Webhook ${eventId} failed with no resolvable orderId — its payments-intents doc (if any) stays pending and needs manual reconciliation`);
353
+ }
354
+ }
355
+
356
+ // Mark intent as failed if we resolved the orderId before the error
357
+ if (orderId) {
358
+ await admin.firestore().doc(`payments-intents/${orderId}`).set({
359
+ status: 'failed',
360
+ error: e.message || String(e),
361
+ metadata: {
362
+ completed: {
363
+ timestamp: now,
364
+ timestampUNIX: nowUNIX,
365
+ },
366
+ },
367
+ }, { merge: true });
368
+ }
369
+ }
370
+ };
371
+
372
+ /**
373
+ * Acknowledge an event the pipeline decided not to act on
374
+ *
375
+ * A refusal is a DECISION, not a failure: the doc completes so the provider stops
376
+ * redelivering an event nothing here will ever act on, the retry ladder is left
377
+ * untouched, and the stamp on the event's own doc — which already carries the
378
+ * payload as delivered (`raw`) — is what a human reconciles from. The one shape
379
+ * every terminal refusal taken before processing writes.
380
+ *
381
+ * It is also where the family REPORTS itself
382
+ * ([#550](https://github.com/Omega-JS-Stack/omega/issues/550)): a refusal is an
383
+ * attack signal, and a stamp plus a log line is only ever read by someone
384
+ * already looking. One capture at the shared seam, so every reason reports
385
+ * exactly once and a new one inherits it for free — never a capture per catch.
386
+ *
387
+ * @param {object} webhookRef - The event's own Firestore document reference
388
+ * @param {object} refusal - The stamp from the refuse*() that decided it
389
+ * @param {object} event - What the report names the refusal by
390
+ * @param {object} event.ctx - Assistant instance
391
+ * @param {string} event.eventId - The webhook doc id (the provider's event id)
392
+ * @param {string} event.eventType - The provider's event name
393
+ * @param {string} event.provider - The provider that sent it
394
+ * @param {string|null} event.uid - The owner the event resolved to, if any
395
+ */
396
+ async function acknowledgeRefusal(webhookRef, refusal, { ctx, eventId, eventType, provider, uid }) {
397
+ const refusedAt = powertools.timestamp(new Date(), { output: 'string' });
398
+
399
+ reportRefusal(ctx, {
400
+ message: `Payment webhook refused: ${refusal.reason} (${eventType} from ${provider}, event=${eventId})`,
401
+ refusal: refusal,
402
+ eventId: eventId,
403
+ eventType: eventType,
404
+ provider: provider,
405
+ uid: uid,
406
+ });
407
+
408
+ await webhookRef.set({
409
+ status: 'completed',
410
+ transition: null,
411
+ refusal: refusal,
412
+ metadata: {
413
+ completed: {
414
+ timestamp: refusedAt,
415
+ timestampUNIX: powertools.timestamp(refusedAt, { output: 'unix' }),
416
+ },
417
+ },
418
+ }, { merge: true });
419
+ }
420
+
421
+ /**
422
+ * Report a refusal to the error reporter, as a warning
423
+ *
424
+ * The ONE place this pipeline assembles a capture. `libraries.sentry` is the
425
+ * backend's one capture handle (helpers/context/respond.js reads the same one)
426
+ * and is null whenever no DSN is configured, so the optional chain IS the no-op.
427
+ * WARNING, not an exception: nothing here failed — the pipeline made a decision,
428
+ * correctly.
429
+ *
430
+ * IDS ONLY, per the scrub rules ([docs/shared/monitoring.md]): the refusal stamp
431
+ * the refuse*() built is what rides, plus the event's own identifiers. The
432
+ * payload never does, and no email is assembled at all, so there is nothing for
433
+ * the scrub to take out.
434
+ *
435
+ * Reported BEFORE the stamp is written, deliberately: a refusal whose stamp
436
+ * could not land ([#535](https://github.com/Omega-JS-Stack/omega/issues/535)) is
437
+ * more alarming than one that did, not less.
438
+ *
439
+ * @param {object} ctx - Assistant instance
440
+ * @param {object} options
441
+ * @param {string} options.message - What the dashboard shows as the title
442
+ * @param {object} options.refusal - The stamp from the refuse*() that decided it
443
+ * @param {string} options.eventId - The webhook doc id (the provider's event id)
444
+ * @param {string} options.eventType - The provider's event name
445
+ * @param {string} options.provider - The provider that sent it
446
+ * @param {string|null} options.uid - The owner the event resolved to, if any
447
+ * @param {object} [options.extra] - Ids beyond the stamp's own, if the refusal has any
448
+ */
449
+ function reportRefusal(ctx, { message, refusal, eventId, eventType, provider, uid, extra = {} }) {
450
+ ctx.Manager.libraries.sentry?.captureMessage?.(message, {
451
+ level: 'warning',
452
+ // Searchable on the dashboard: the reason groups the family, the provider
453
+ // says who sent it, and the event id is what a human looks the delivery up
454
+ // by in the provider's own dashboard.
455
+ tags: {
456
+ refusal: refusal.reason,
457
+ provider: provider,
458
+ eventId: eventId,
459
+ },
460
+ ...(uid ? { user: { id: uid } } : {}),
461
+ extra: {
462
+ ...refusal,
463
+ eventId: eventId,
464
+ eventType: eventType,
465
+ provider: provider,
466
+ uid: uid || null,
467
+ ...extra,
468
+ },
469
+ });
470
+ }
471
+
472
+ /**
473
+ * Read the resource out of a webhook envelope
474
+ *
475
+ * Every provider nests it somewhere else — Stripe at data.object, Chargebee at
476
+ * content.<type>, PayPal at resource — so each library names its own shape.
477
+ *
478
+ * IDENTIFIERS ONLY. What comes back is the caller's own object, so it may name
479
+ * which order a failed event belongs to and nothing more; the state a resource is
480
+ * IN comes from the provider's lookup, never from here
481
+ * ([#506](https://github.com/Omega-JS-Stack/omega/issues/506)).
482
+ *
483
+ * The Stripe-shaped default below serves the null-library case only (the library
484
+ * failed to load); every provider library, test included, exports extractResource().
485
+ *
486
+ * @param {object|null} library - Provider library (null when loading it was what failed)
487
+ * @param {object} raw - The raw webhook payload the provider sent
488
+ * @returns {object|null}
489
+ */
490
+ function extractRawResource(library, raw) {
491
+ if (library?.extractResource) {
492
+ return library.extractResource(raw);
493
+ }
494
+
495
+ return raw?.data?.object || null;
496
+ }
497
+
498
+ /**
499
+ * Resolve the orderId for a webhook that threw before the happy path resolved one
500
+ *
501
+ * @param {object} options
502
+ * @param {object} options.dataAfter - The webhook doc's data
503
+ * @param {object|null} options.library - Provider library (null when loading it was what failed)
504
+ * @param {object} options.ctx - Assistant instance
505
+ * @returns {string|null}
506
+ */
507
+ function resolveOrderIdAfterFailure({ dataAfter, library, ctx }) {
508
+ // An earlier pass may already have stamped it on the doc
509
+ if (dataAfter.orderId) {
510
+ return dataAfter.orderId;
511
+ }
512
+
513
+ const rawObject = extractRawResource(library, dataAfter.raw);
514
+
515
+ if (!library?.getOrderId || !rawObject) {
516
+ return null;
517
+ }
518
+
519
+ // The same extraction the happy path uses, over the webhook payload instead of
520
+ // the fetched resource
521
+ try {
522
+ return library.getOrderId(rawObject) || null;
523
+ } catch (e) {
524
+ ctx.error(`resolveOrderIdAfterFailure(): ${dataAfter.provider} getOrderId() threw on the webhook payload: ${e.message}`, e);
525
+ return null;
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Process a payment event (subscription or one-time)
531
+ * 1. Staleness check
532
+ * 2. Read user doc (for transition detection)
533
+ * 3. Transform raw resource → unified object
534
+ * 4. Build order object
535
+ * 5. Detect and dispatch transition handlers (non-blocking)
536
+ * 6. Track analytics (non-blocking)
537
+ * 7. Write to Firestore in ONE batch (user doc for subscriptions + payments-orders + payments-intents)
538
+ *
539
+ * @returns {Promise<{ transition: string|null, refusal: object|null }>} What the
540
+ * caller stamps back on the event doc: the transition detected, and the refusal
541
+ * when the pipeline declined to act on the event at all.
542
+ */
543
+ async function processPaymentEvent({ category, library, resource, resourceType, uid, provider, eventType, eventId, resourceId, chargeId, orderId, now, nowUNIX, webhookReceivedUNIX, previouslyCompleted, ctx, isRefund, refundDetails, claimTicket }) {
544
+ const Manager = ctx.Manager;
545
+ const admin = Manager.libraries.admin;
546
+ const isSubscription = category === 'subscription';
547
+
548
+ // Staleness check: skip if a newer webhook already wrote to this order
549
+ let existingOrder = null;
550
+
551
+ if (orderId) {
552
+ const existingDoc = await admin.firestore().doc(`payments-orders/${orderId}`).get();
553
+ if (existingDoc.exists) {
554
+ existingOrder = existingDoc.data();
555
+ const existingUpdatedUNIX = existingOrder.metadata?.updated?.timestampUNIX || 0;
556
+ if (webhookReceivedUNIX < existingUpdatedUNIX) {
557
+ ctx.log(`Stale webhook ${eventId}: received=${webhookReceivedUNIX}, existing updated=${existingUpdatedUNIX}, skipping`);
558
+ return { transition: null, refusal: null };
559
+ }
560
+ }
561
+ } else {
562
+ // The guard keys on the order — without one, an out-of-order delivery cannot be
563
+ // detected at all. Processing continues, but the unguarded window is visible.
564
+ ctx.warn(`Webhook ${eventId} has no orderId (provider=${provider}, ${resourceType} ${resourceId}) — the staleness guard cannot run, so an out-of-order delivery for this resource would be applied as-is`);
565
+ }
566
+
567
+ // A refund UPDATES a purchase — it can never DEFINE one. With no order record
568
+ // behind it, the refund event used to be read as a fresh purchase definition:
569
+ // payments-orders/{orderId} was minted with `unified.status: 'completed'` and the
570
+ // REFUND's id as the resource, so a reversal was booked as revenue while the
571
+ // trail said one-time/purchase-refunded. Reachable whenever the purchase write is
572
+ // missing — a lost or failed purchase webhook, a webhook registered after the
573
+ // sale, or PayPal delivering REFUNDED before the capture.
574
+ //
575
+ // So the pipeline refuses, loudly, and writes nothing else: the refusal is stamped
576
+ // on the event's own doc for a human to reconcile from, which surfaces the lost
577
+ // purchase webhook instead of letting it masquerade as revenue
578
+ // ([#335](https://github.com/Omega-JS-Stack/omega/issues/335)).
579
+ if (!isSubscription && isRefund && !existingOrder?.unified) {
580
+ return {
581
+ transition: null,
582
+ refusal: refuseRefundWithoutOrder({ ctx, resource, refundDetails, eventId, eventType, provider, resourceType, resourceId, uid, orderId }),
583
+ };
584
+ }
585
+
586
+ // Read current user doc (needed for transition detection + handler context)
587
+ const userDoc = await admin.firestore().doc(`users/${uid}`).get();
588
+ const userData = userDoc.exists ? userDoc.data() : {};
589
+ const before = isSubscription ? (userData.subscription || null) : null;
590
+
591
+ ctx.log(`User doc for ${uid}: exists=${userDoc.exists}, email=${userData?.auth?.email || 'null'}, name=${userData?.personal?.name?.first || 'null'}, subscription=${userData?.subscription?.product?.id || 'null'}`);
592
+
593
+ // A user doc is born at SIGNUP, never at a payment event. A uid with no auth
594
+ // user in this project is not this project's customer at all: a QA checkout run
595
+ // against the emulator with real test-mode keys delivers its webhooks to the
596
+ // DEPLOYED backend (the emulator has no webhook path), and the event minted a
597
+ // LIVE users/{uid} holding nothing but a subscription block
598
+ // ([#399](https://github.com/Omega-JS-Stack/omega/issues/399)).
599
+ //
600
+ // So an absent user doc has to prove the uid before anything is written under
601
+ // it. A doc that already exists takes no auth lookup and no new behavior — an
602
+ // update or a delete creates nothing, so it is left exactly as it was.
603
+ if (!userDoc.exists && !(await hasAuthUser(admin, uid))) {
604
+ return {
605
+ transition: null,
606
+ refusal: refuseUserWithoutAuth({ ctx, eventId, eventType, provider, uid, orderId }),
607
+ };
608
+ }
609
+
610
+ // Auto-fill user name from payment provider if not already set
611
+ if (!userData?.personal?.name?.first) {
612
+ const customerName = extractCustomerName(resource, resourceType);
613
+ if (customerName?.first) {
614
+ await admin.firestore().doc(`users/${uid}`).set({
615
+ personal: { name: customerName },
616
+ }, { merge: true });
617
+ ctx.log(`Auto-filled user name from ${resourceType}: ${customerName.first} ${customerName.last || ''}`);
618
+ }
619
+ }
620
+
621
+ // Transform raw resource → unified object
622
+ const transformOptions = { config: Manager.config, eventName: eventType, eventId: eventId };
623
+ let unified = isSubscription
624
+ ? library.toUnifiedSubscription(resource, transformOptions)
625
+ : library.toUnifiedOneTime(resource, transformOptions);
626
+
627
+ // Override: immediately suspend on payment denial
628
+ // Providers keep the sub active while retrying, but we revoke access right away.
629
+ // If the retry succeeds (e.g. PAYMENT.SALE.COMPLETED), it will restore active status.
630
+ // PayPal: PAYMENT.SALE.DENIED, Stripe: invoice.payment_failed, Chargebee: payment_failed
631
+ const PAYMENT_DENIED_EVENTS = ['PAYMENT.SALE.DENIED', 'invoice.payment_failed', 'payment_failed'];
632
+ if (isSubscription && PAYMENT_DENIED_EVENTS.includes(eventType) && unified.status === 'active') {
633
+ ctx.log(`Overriding status to suspended: ${eventType} received but provider still says active`);
634
+ unified.status = 'suspended';
635
+ }
636
+
637
+ // A refund UPDATES a purchase record — it does not redefine the purchase.
638
+ //
639
+ // A one-time refund's resource is the bare charge that moved the money back: it
640
+ // names no product and no price, so re-deriving the order from it degraded a
641
+ // completed purchase to product=unknown and price=0, and replaced the checkout
642
+ // resourceId with the charge id. Merge instead — the purchase stays exactly what
643
+ // it was, and only the refund outcome is written
644
+ // ([#212](https://github.com/Omega-JS-Stack/omega/issues/212)). A one-time refund
645
+ // with no purchase to merge onto never reaches here — the guard above refused it.
646
+ const isOneTimeRefund = !isSubscription && isRefund;
647
+
648
+ if (isOneTimeRefund) {
649
+ unified = applyRefundToPurchase(existingOrder.unified, unified, { refundDetails, now, nowUNIX });
650
+ }
651
+
652
+ ctx.log(`Unified ${category}: product=${unified.product.id}, status=${unified.status}`, unified);
653
+
654
+ // Read checkout context from payments-intents (attribution, trackingConsent, request, discount, supplemental)
655
+ let intentData = {};
656
+ if (orderId) {
657
+ const intentDoc = await admin.firestore().doc(`payments-intents/${orderId}`).get();
658
+ intentData = intentDoc.exists ? intentDoc.data() : {};
659
+ }
660
+
661
+ // Build the order object (single source of truth for handlers + Firestore)
662
+ const order = {
663
+ id: orderId,
664
+ type: category,
665
+ owner: uid,
666
+ productId: unified.product.id,
667
+ provider: provider,
668
+ // A refund event names the charge that reversed the payment, never the
669
+ // checkout resource the purchase was made through — keep the purchase's own
670
+ resourceId: isOneTimeRefund ? (existingOrder.resourceId || resourceId) : resourceId,
671
+ unified: unified,
672
+ attribution: intentData.attribution || {},
673
+ trackingConsent: intentData.trackingConsent || null,
674
+ // The requester's IP + user agent, captured when the intent was created —
675
+ // what Meta and TikTok match a server conversion to the browsing session on
676
+ // ([#385](https://github.com/Omega-JS-Stack/omega/issues/385)). A webhook's
677
+ // own request is the PROVIDER's, never the customer's, so this can only
678
+ // come from the intent.
679
+ request: intentData.request || null,
680
+ discount: intentData.discount || null,
681
+ supplemental: intentData.supplemental || {},
682
+ metadata: {
683
+ created: {
684
+ timestamp: now,
685
+ timestampUNIX: nowUNIX,
686
+ },
687
+ updated: {
688
+ timestamp: now,
689
+ timestampUNIX: nowUNIX,
690
+ },
691
+ updatedBy: {
692
+ event: {
693
+ name: eventType,
694
+ id: eventId,
695
+ },
696
+ },
697
+ },
698
+ };
699
+
700
+ // Detect and dispatch transition (non-blocking)
701
+ const shouldRunHandlers = !ctx.isTesting() || process.env.TEST_EXTENDED_MODE;
702
+ const transitionName = transitions.detectTransition(category, before, unified, eventType, { previouslyCompleted });
703
+
704
+ if (!transitionName && previouslyCompleted && isRefund) {
705
+ ctx.log(`Transition suppressed (idempotency): webhook ${eventId} already completed once, so its refund email was already sent`);
706
+ }
707
+
708
+ // The event that LOST the claim below, once there is one. Everything the
709
+ // transition drives reads it: the handler, the analytics fire, and the marketing
710
+ // sync were all three duplicated by the racing pair, so all three are gated on it
711
+ // ([#665](https://github.com/Omega-JS-Stack/omega/issues/665)).
712
+ let suppressedBy = null;
713
+
714
+ if (transitionName) {
715
+ ctx.log(`Transition detected: ${category}/${transitionName} (before.status=${before?.status || 'null'}, after.status=${unified.status})`);
716
+
717
+ // Claim it before anything is dispatched. Detection is a diff against the
718
+ // user doc, so two webhooks for the same checkout in flight at once BOTH
719
+ // detect the same transition — the claim is what makes only one of them act
720
+ // on it ([#665](https://github.com/Omega-JS-Stack/omega/issues/665)). Without
721
+ // an order there is nothing to claim on, exactly as the staleness guard above
722
+ // has nothing to compare against, and the dispatch goes out unguarded.
723
+ const claim = orderId
724
+ ? await transitions.claim({ admin, orderId, transitionName, eventId, now, nowUNIX })
725
+ : { claimed: true, eventId: eventId };
726
+
727
+ if (!claim.claimed) {
728
+ suppressedBy = claim.eventId;
729
+ ctx.log(`Transition suppressed (claimed by ${suppressedBy}): ${category}/${transitionName} on payments-orders/${orderId}`);
730
+ } else {
731
+ // Hand the claim to the trigger, which settles it once the run ends. Only the
732
+ // run that TOOK a claim may settle it, so a loser hands over nothing.
733
+ claimTicket.orderId = orderId;
734
+ claimTicket.transitionName = transitionName;
735
+
736
+ if (shouldRunHandlers) {
737
+ transitions.dispatch(transitionName, category, {
738
+ before, after: unified, order, uid, userDoc: userData, ctx, refundDetails,
739
+ });
740
+ } else {
741
+ ctx.log(`Transition handler skipped (testing mode): ${category}/${transitionName}`);
742
+ }
743
+ }
744
+ }
745
+
746
+ // Track payment analytics (non-blocking)
747
+ // Fires independently of transitions — renewals have no transition but still need tracking.
748
+ // `before` rides along for the same reason the transition detector reads it: a plan change
749
+ // needs the plan it came from, and a trial's outcome is only legible against the prior term
750
+ // ([#407](https://github.com/Omega-JS-Stack/omega/issues/407)).
751
+ //
752
+ // A run that lost the claim tracks NOTHING. The resolver is keyed on the
753
+ // transition, so the duplicate fired the same conversion twice — GA4 counted two
754
+ // `trial_start`s, deduplicating neither a custom event nor a second id
755
+ // ([#665](https://github.com/Omega-JS-Stack/omega/issues/665)). Nulling the
756
+ // transition instead would be worse than doing nothing: the resolver's `!transitionName`
757
+ // branches would read the duplicate as a RENEWAL and fire `subscription_payment`.
758
+ if (suppressedBy) {
759
+ ctx.log(`Payment analytics suppressed (claimed by ${suppressedBy}): ${category}/${transitionName} was already tracked`);
760
+ } else if (shouldRunHandlers) {
761
+ trackPayment({ category, transitionName, eventType, unified, order, userDoc: userData, refundDetails, before, uid, provider, chargeId, ctx });
762
+ } else {
763
+ ctx.log(`Payment analytics skipped (testing mode): ${category}/${transitionName || 'no transition'}`);
764
+ }
765
+
766
+ // A persisted discount belongs to the subscription it was applied to, and to
767
+ // that one only ([#333]). The unified object carries no discount key at all,
768
+ // so the merge below would preserve the node forever: a customer who churned
769
+ // and resubscribed kept a spent claim on the new subscription, where the
770
+ // billing card reads `source: 'winback'` as "already claimed" and silently
771
+ // never pitches the save offer again, one the backend would grant.
772
+ //
773
+ // So the claim's stamp is compared against the subscription THIS event is
774
+ // about, and a mismatch clears the node: a new subscription is a clean slate.
775
+ // A node with no stamp was written before the stamp existed and nothing can
776
+ // prove it belongs to an older subscription, so it is read as riding the one
777
+ // it is found on (no live discount is ever taken away on a guess) and stamped
778
+ // there, so it clears on the next resubscribe like any other.
779
+ //
780
+ // The gate needs the RESOURCE to be a subscription, not just the category: a
781
+ // subscription-category event can ride an invoice or sale resource (Stripe
782
+ // charge.refunded, PayPal raw-payload fallback), whose id would never match
783
+ // the stamp and would wrongly clear a live discount.
784
+ const claimedDiscount = isSubscription && resourceType === 'subscription' && before?.discount?.valid === true ? before.discount : null;
785
+ const liveResourceId = unified.payment?.resourceId || null;
786
+ let discountWrite = null;
787
+
788
+ if (claimedDiscount && liveResourceId) {
789
+ if (!claimedDiscount.resourceId) {
790
+ discountWrite = { resourceId: liveResourceId };
791
+ ctx.log(`Adopting users/${uid}.subscription.discount onto ${liveResourceId}: the claim carries no subscription stamp, so it is the one it is riding`);
792
+ } else if (claimedDiscount.resourceId !== liveResourceId) {
793
+ discountWrite = User.EMPTY_DISCOUNT;
794
+ ctx.log(`Clearing users/${uid}.subscription.discount: claimed on ${claimedDiscount.resourceId}, this event is for ${liveResourceId}, so the discount ended with the subscription it was applied to`);
795
+ }
796
+ }
797
+
798
+ // The three writes this event produces — the user's subscription, the order and
799
+ // the intent — land TOGETHER. As separate awaits, anything that threw between
800
+ // them left the state split: a user paid with no order behind it, or an order
801
+ // whose intent still says pending. The batch makes it all-or-nothing.
802
+ const batch = admin.firestore().batch();
803
+
804
+ // Write unified subscription to user doc (subscriptions only)
805
+ //
806
+ // A charge that CONVERTED the trial stamps the outcome on its way past. The
807
+ // provider payloads carry `trial.claimed` — "this subscription HAD a trial", true
808
+ // for its whole life — and nothing else says the first paid charge happened, so
809
+ // without the stamp the next renewal reads exactly like the conversion did
810
+ // ([#697](https://github.com/Omega-JS-Stack/omega/issues/697)). It is STATE, not
811
+ // reporting: it is written whether or not the analytics fire ran, and it is the
812
+ // same field and the same value the trial-lapse sweep stamps when no webhook ever
813
+ // announces the outcome, so the two paths cannot disagree about a trial's fate.
814
+ if (isSubscription) {
815
+ const subscriptionWrite = { ...unified };
816
+
817
+ if (discountWrite) {
818
+ subscriptionWrite.discount = discountWrite;
819
+ }
820
+
821
+ if (isTrialConversion({ transitionName, eventType, unified, before })) {
822
+ subscriptionWrite.trial = { ...unified.trial, outcome: 'converted' };
823
+ ctx.log(`Trial converted: stamping users/${uid}.subscription.trial.outcome=converted (the first paid charge on ${unified.payment?.resourceId})`);
824
+ }
825
+
826
+ batch.set(admin.firestore().doc(`users/${uid}`), {
827
+ subscription: subscriptionWrite,
828
+ }, { merge: true });
829
+ }
830
+
831
+ if (orderId) {
832
+ const orderRef = admin.firestore().doc(`payments-orders/${orderId}`);
833
+ const orderSnap = await orderRef.get();
834
+
835
+ // Initialize requests on first creation only (avoid overwriting cancel/refund
836
+ // data set by endpoints). Keyed on the field rather than the document: the
837
+ // transition claim above writes `transitions` onto this same order first, so on
838
+ // a brand-new order the document now EXISTS by the time the writes are built —
839
+ // and a plain existence test would have left every new order without its
840
+ // requests node ([#665](https://github.com/Omega-JS-Stack/omega/issues/665)).
841
+ if (!orderSnap.data()?.requests) {
842
+ order.requests = {
843
+ cancellation: null,
844
+ refund: null,
845
+ };
846
+ }
847
+
848
+ if (orderSnap.exists) {
849
+ // Preserve original created timestamp on subsequent webhook events
850
+ order.metadata.created = orderSnap.data().metadata?.created || order.metadata.created;
851
+ }
852
+
853
+ // Write to payments-orders/{orderId}
854
+ batch.set(orderRef, order, { merge: true });
855
+
856
+ // Update payments-intents/{orderId} status to match webhook outcome
857
+ batch.set(admin.firestore().doc(`payments-intents/${orderId}`), {
858
+ status: 'completed',
859
+ metadata: {
860
+ completed: {
861
+ timestamp: now,
862
+ timestampUNIX: nowUNIX,
863
+ },
864
+ },
865
+ }, { merge: true });
866
+ }
867
+
868
+ await batch.commit();
869
+
870
+ if (isSubscription) {
871
+ ctx.log(`Updated users/${uid}.subscription: status=${unified.status}, product=${unified.product.id}`);
872
+
873
+ // Sync marketing contact with updated subscription data (non-blocking).
874
+ // Suppressed on a lost claim for the same reason the analytics fire is: the
875
+ // winner syncs this exact subscription state, so the duplicate only re-sent the
876
+ // same contact ([#665](https://github.com/Omega-JS-Stack/omega/issues/665)).
877
+ if (suppressedBy) {
878
+ ctx.log(`Marketing sync suppressed (claimed by ${suppressedBy}): the subscription was already synced`);
879
+ } else if (shouldRunHandlers) {
880
+ const email = Manager.Email(ctx);
881
+ const updatedUserDoc = { ...userData, subscription: unified };
882
+ email.sync(updatedUserDoc)
883
+ .then((r) => ctx.log('Marketing sync after payment:', r))
884
+ .catch((e) => ctx.error('Marketing sync after payment failed:', e));
885
+ } else {
886
+ ctx.log('Marketing sync skipped (testing mode)');
887
+ }
888
+ }
889
+
890
+ if (orderId) {
891
+ ctx.log(`Updated payments-orders/${orderId}: type=${category}, uid=${uid}, eventType=${eventType}`);
892
+ ctx.log(`Updated payments-intents/${orderId}: status=completed`);
893
+ }
894
+
895
+ // Mark abandoned cart as completed (non-blocking, fire-and-forget)
896
+ const { COLLECTION } = require('../../../libraries/abandoned-cart-config.js');
897
+ admin.firestore().doc(`${COLLECTION}/${uid}`).set({
898
+ status: 'completed',
899
+ metadata: {
900
+ updated: {
901
+ timestamp: now,
902
+ timestampUNIX: nowUNIX,
903
+ },
904
+ },
905
+ }, { merge: true })
906
+ .then(() => ctx.log(`Updated ${COLLECTION}/${uid}: status=completed`))
907
+ .catch((e) => {
908
+ // Ignore not-found — cart may not exist for this user
909
+ if (e.code !== 5) {
910
+ ctx.error(`Failed to update ${COLLECTION}/${uid}: ${e.message}`);
911
+ }
912
+ });
913
+
914
+ return { transition: transitionName, refusal: null };
915
+ }
916
+
917
+ /**
918
+ * Record a refund on the unified purchase it reversed
919
+ *
920
+ * The purchase — product, price, the resource it was bought through — is kept
921
+ * exactly as the completed purchase wrote it. Only the outcome changes: the
922
+ * status, the refund itself, and which event last touched the record.
923
+ *
924
+ * @param {object} purchase - The unified one-time object already on the order
925
+ * @param {object} derived - The unified object derived from the refund's own resource
926
+ * @param {object} options
927
+ * @param {object|null} options.refundDetails - The library's { amount, currency, reason }
928
+ * @param {string} options.now - Timestamp string
929
+ * @param {number} options.nowUNIX - Timestamp seconds
930
+ * @returns {object} Unified one-time object
931
+ */
932
+ function applyRefundToPurchase(purchase, derived, { refundDetails, now, nowUNIX }) {
933
+ return {
934
+ ...purchase,
935
+ status: 'refunded',
936
+ payment: {
937
+ ...purchase.payment,
938
+ refund: {
939
+ amount: refundDetails?.amount || null,
940
+ currency: refundDetails?.currency || 'USD',
941
+ reason: refundDetails?.reason || null,
942
+ date: {
943
+ timestamp: now,
944
+ timestampUNIX: nowUNIX,
945
+ },
946
+ },
947
+ updatedBy: derived.payment?.updatedBy || purchase.payment?.updatedBy || null,
948
+ },
949
+ };
950
+ }
951
+
952
+ /**
953
+ * Refuse a refund that has no purchase behind it, loudly, and say so on the record
954
+ *
955
+ * The books are the point: nothing is written to payments-orders or
956
+ * payments-intents, so no revenue is invented. What the reconciler needs is stamped
957
+ * on the event's OWN doc, which already carries the refund payload as delivered
958
+ * (`raw`), the owner and the order it named: the reason it was refused, and the id
959
+ * of the capture the refund reversed — the only pointer back to the purchase whose
960
+ * webhook never landed ([#335](https://github.com/Omega-JS-Stack/omega/issues/335)).
961
+ *
962
+ * @param {object} options
963
+ * @param {object} options.ctx - Assistant instance
964
+ * @param {object} options.resource - The refund resource fetched from the provider
965
+ * @param {object|null} options.refundDetails - The library's { amount, currency, reason }
966
+ * @param {string} options.eventId - The webhook doc id (the provider's event id)
967
+ * @param {string} options.eventType - The provider's event name
968
+ * @param {string} options.provider - The provider that sent it
969
+ * @param {string} options.resourceType - The parsed event's resource type
970
+ * @param {string} options.resourceId - The refund's own id
971
+ * @param {string} options.uid - The owner the event resolved to
972
+ * @param {string|null} options.orderId - The order the refund named, which does not exist
973
+ * @returns {{ reason: string, captureId: string|null }} The refusal stamp for the event doc
974
+ */
975
+ function refuseRefundWithoutOrder({ ctx, resource, refundDetails, eventId, eventType, provider, resourceType, resourceId, uid, orderId }) {
976
+ const captureId = extractParentResourceId(resource);
977
+
978
+ ctx.error(`REFUND WITHOUT ORDER: ${eventType} (${provider}) reversed ${resourceType} ${resourceId} but payments-orders/${orderId || 'null'} does not exist — refusing to mint an order from a refund (owner=${uid}, capture=${captureId || 'unknown'}, amount=${refundDetails?.amount || 'unknown'} ${refundDetails?.currency || 'USD'}). Stamped on payments-webhooks/${eventId} as refusal.reason=refund-without-order: the purchase behind this refund never wrote an order and needs manual reconciliation`);
979
+
980
+ return {
981
+ reason: 'refund-without-order',
982
+ captureId: captureId,
983
+ };
984
+ }
985
+
986
+ /**
987
+ * Refuse an event whose resource the provider does not have
988
+ *
989
+ * The lookup is the trust boundary: the provider answered, and its answer was that
990
+ * this resource does not exist. The object the webhook carried says otherwise, but
991
+ * it is only what the caller posted — processing off it is exactly the hole this
992
+ * refusal closes ([#506](https://github.com/Omega-JS-Stack/omega/issues/506)).
993
+ *
994
+ * Acknowledged, not failed: no retry could ever turn a resource the provider does
995
+ * not have into one it does, so the doc completes and the provider stops
996
+ * redelivering an event nothing here will ever act on. Nothing else is written —
997
+ * no subscription, no order, no intent, no conversion — and the stamp on the
998
+ * event's own doc, which already holds the payload as delivered (`raw`), is what a
999
+ * human reconciles from.
1000
+ *
1001
+ * @param {object} options
1002
+ * @param {object} options.ctx - Assistant instance
1003
+ * @param {Error} options.error - The classified failure the lookup threw
1004
+ * @param {string} options.eventId - The webhook doc id (the provider's event id)
1005
+ * @param {string} options.eventType - The provider's event name
1006
+ * @param {string} options.provider - The provider that sent it
1007
+ * @param {string} options.resourceType - The parsed event's resource type
1008
+ * @param {string} options.resourceId - The resource the provider does not have
1009
+ * @param {string|null} options.uid - The owner the event parsed to, if any
1010
+ * @returns {{ reason: string, resourceType: string, resourceId: string }} The refusal stamp for the event doc
1011
+ */
1012
+ function refuseResourceNotFound({ ctx, error, eventId, eventType, provider, resourceType, resourceId, uid }) {
1013
+ ctx.error(`RESOURCE NOT FOUND: ${eventType} (${provider}) named ${resourceType} ${resourceId}, which ${provider} does not have — refusing to process the event off the object its payload carried (event=${eventId}, owner=${uid || 'null'}): ${error.message}. Stamped on payments-webhooks/${eventId} as refusal.reason=resource-not-found`);
1014
+
1015
+ return {
1016
+ reason: 'resource-not-found',
1017
+ resourceType: resourceType,
1018
+ resourceId: resourceId,
1019
+ };
1020
+ }
1021
+
1022
+ /**
1023
+ * Refuse an event whose payload claims a different owner than the provider's record
1024
+ *
1025
+ * The lookup succeeded, so the provider named the uid this resource belongs to.
1026
+ * The payload named another one. Only one of the two can be acted on, and the
1027
+ * payload is whatever the caller posted — so an event naming a REAL subscription
1028
+ * with a forged metadata uid used to move that subscription onto the forged uid
1029
+ * ([#509](https://github.com/Omega-JS-Stack/omega/issues/509)).
1030
+ *
1031
+ * Refused, not corrected: the provider's uid is the trustworthy half, but an event
1032
+ * that lies about its owner has nothing left in it worth writing, and quietly
1033
+ * processing it under the real owner would hide the forgery. Acknowledged rather
1034
+ * than failed, for the same reason as every other terminal decision here — no
1035
+ * retry can make the payload agree with the record.
1036
+ *
1037
+ * @param {object} options
1038
+ * @param {object} options.ctx - Assistant instance
1039
+ * @param {string} options.eventId - The webhook doc id (the provider's event id)
1040
+ * @param {string} options.eventType - The provider's event name
1041
+ * @param {string} options.provider - The provider that sent it
1042
+ * @param {string} options.resourceType - The parsed event's resource type
1043
+ * @param {string} options.resourceId - The resource the event named
1044
+ * @param {string} options.payloadUid - The owner the payload claimed
1045
+ * @param {string} options.providerUid - The owner the provider's record carries
1046
+ * @param {string} options.source - Which of the provider's records answered ('record' | 'hosted-page')
1047
+ * @returns {{ reason: string, payloadUid: string, providerUid: string, source: string }} The refusal stamp for the event doc
1048
+ */
1049
+ function refuseUidMismatch({ ctx, eventId, eventType, provider, resourceType, resourceId, payloadUid, providerUid, source }) {
1050
+ const answered = source === 'hosted-page'
1051
+ ? `whose ${provider} hosted page's pass_thru_content names uid=${providerUid}`
1052
+ : `whose ${provider} record belongs to uid=${providerUid}`;
1053
+
1054
+ ctx.error(`UID MISMATCH: ${eventType} (${provider}) named ${resourceType} ${resourceId}, ${answered}, but the event's payload claims uid=${payloadUid} — refusing to write a resource onto an owner the provider does not agree with (event=${eventId}). Stamped on payments-webhooks/${eventId} as refusal.reason=uid-mismatch: an event that names a real resource under the wrong owner is a forgery until proven otherwise, and needs manual reconciliation`);
1055
+
1056
+ return {
1057
+ reason: 'uid-mismatch',
1058
+ payloadUid: payloadUid,
1059
+ providerUid: providerUid,
1060
+ source: source,
1061
+ };
1062
+ }
1063
+
1064
+ /**
1065
+ * Refuse a refund whose record belongs to a different order
1066
+ *
1067
+ * The refund's numbers come from the provider, looked up by an id the PAYLOAD
1068
+ * carried ([#510](https://github.com/Omega-JS-Stack/omega/issues/510)) — and an
1069
+ * id is only self-consistent when what it names is also what gets written. A
1070
+ * refund id is not: pair a real sale with an unrelated refund id from the same
1071
+ * merchant account and another customer's amount, currency and reason land on this
1072
+ * order, its email and its refund conversion
1073
+ * ([#532](https://github.com/Omega-JS-Stack/omega/issues/532)).
1074
+ *
1075
+ * So the record has to point back at the resource the event named
1076
+ * ([../../../libraries/payment/refund-linkage.js](../../../libraries/payment/refund-linkage.js)),
1077
+ * and one that points elsewhere is refused: acknowledged rather than failed, for
1078
+ * the same reason as every other terminal decision here — no retry can relate two
1079
+ * unrelated records.
1080
+ *
1081
+ * @param {object} options
1082
+ * @param {object} options.ctx - Assistant instance
1083
+ * @param {Error} options.error - The linkage failure the lookup threw
1084
+ * @param {string} options.eventId - The webhook doc id (the provider's event id)
1085
+ * @param {string} options.eventType - The provider's event name
1086
+ * @param {string} options.provider - The provider that sent it
1087
+ * @param {string|null} options.uid - The owner the event parsed to, if any
1088
+ * @returns {{ reason: string, refundType: string, refundId: string, resourceType: string, resourceId: string, linkedTo: string }} The refusal stamp for the event doc
1089
+ */
1090
+ function refuseRefundNotLinked({ ctx, error, eventId, eventType, provider, uid }) {
1091
+ ctx.error(`REFUND NOT LINKED: ${eventType} (${provider}) named ${error.resourceType} ${error.resourceId}, but the ${error.refundType} ${error.refundId} its payload pointed at names ${error.field} ${error.linkedTo} — refusing to record another record's refund on this order (event=${eventId}, owner=${uid || 'null'}). Stamped on payments-webhooks/${eventId} as refusal.reason=refund-not-linked: a refund id the caller chose that resolves to someone else's record is a forgery until proven otherwise, and needs manual reconciliation`);
1092
+
1093
+ return {
1094
+ reason: 'refund-not-linked',
1095
+ refundType: error.refundType,
1096
+ refundId: error.refundId,
1097
+ resourceType: error.resourceType,
1098
+ resourceId: error.resourceId,
1099
+ linkedTo: error.linkedTo,
1100
+ };
1101
+ }
1102
+
1103
+ /**
1104
+ * Refuse to create a user doc for a uid this project has no auth user for
1105
+ *
1106
+ * The event is acknowledged — the route answered 2xx when it stored it, and the
1107
+ * doc is completed rather than failed, so the provider stops redelivering an
1108
+ * event nothing here will ever act on. What a human needs to see it is the
1109
+ * warning plus the stamp on the event's own doc, which already carries the
1110
+ * payload as delivered ([#399](https://github.com/Omega-JS-Stack/omega/issues/399)).
1111
+ *
1112
+ * @param {object} options
1113
+ * @param {object} options.ctx - Assistant instance
1114
+ * @param {string} options.eventId - The webhook doc id (the provider's event id)
1115
+ * @param {string} options.eventType - The provider's event name
1116
+ * @param {string} options.provider - The provider that sent it
1117
+ * @param {string} options.uid - The owner the event resolved to
1118
+ * @param {string|null} options.orderId - The order the event named, if any
1119
+ * @returns {{ reason: string }} The refusal stamp for the event doc
1120
+ */
1121
+ function refuseUserWithoutAuth({ ctx, eventId, eventType, provider, uid, orderId }) {
1122
+ ctx.warn(`USER WITHOUT AUTH: ${eventType} (${provider}) resolved to uid=${uid}, which has no auth user in this project and no user doc — refusing to create one from a payment event (event=${eventId}, order=${orderId || 'null'}). Stamped on payments-webhooks/${eventId} as refusal.reason=user-without-auth: the checkout behind this event belongs to another project, typically a local QA run against the emulator with real test-mode keys`);
1123
+
1124
+ const refusal = { reason: 'user-without-auth' };
1125
+
1126
+ // The stamp is the record; this is the alarm. A log line in Cloud Logging is
1127
+ // only ever read by someone already looking, and the whole point of the guard
1128
+ // is that nobody knows to look — money moved somewhere for a uid this project
1129
+ // does not have (Ian 2026-08-20).
1130
+ //
1131
+ // Reported through the same helper the acknowledged family uses (#550), from
1132
+ // here rather than there: this refusal is decided mid-processing and stamped by
1133
+ // the completion write, so it never passes acknowledgeRefusal().
1134
+ reportRefusal(ctx, {
1135
+ message: `Payment webhook refused: user without auth (uid=${uid})`,
1136
+ refusal: refusal,
1137
+ eventId: eventId,
1138
+ eventType: eventType,
1139
+ provider: provider,
1140
+ uid: uid,
1141
+ extra: { orderId: orderId || null },
1142
+ });
1143
+
1144
+ return refusal;
1145
+ }
1146
+
1147
+ /**
1148
+ * The id of the resource a refund reversed, read off the payload's `up` link
1149
+ *
1150
+ * PayPal points a refund at the capture it reversed with a HATEOAS link whose
1151
+ * `rel` is `up`, and the id is that URL's last segment. Providers that ship no
1152
+ * such links answer null, which the refusal stamp carries honestly rather than
1153
+ * guessing at a capture.
1154
+ *
1155
+ * @param {object} resource - The refund resource fetched from the provider
1156
+ * @returns {string|null}
1157
+ */
1158
+ function extractParentResourceId(resource) {
1159
+ const up = (resource?.links || []).find((link) => link?.rel === 'up');
1160
+
1161
+ if (!up?.href) {
1162
+ return null;
1163
+ }
1164
+
1165
+ return up.href.split('/').filter(Boolean).pop() || null;
1166
+ }
1167
+
1168
+ /**
1169
+ * Extract customer name from a raw payment provider resource
1170
+ *
1171
+ * @param {object} resource - Raw provider resource (Stripe subscription, session, invoice)
1172
+ * @param {string} resourceType - 'subscription' | 'session' | 'invoice'
1173
+ * @returns {{ first: string, last: string }|null}
1174
+ */
1175
+ function extractCustomerName(resource, resourceType) {
1176
+ let fullName = null;
1177
+
1178
+ // Checkout sessions have customer_details.name
1179
+ if (resourceType === 'session') {
1180
+ fullName = resource.customer_details?.name;
1181
+ }
1182
+
1183
+ // Invoices have customer_name
1184
+ if (resourceType === 'invoice') {
1185
+ fullName = resource.customer_name;
1186
+ }
1187
+
1188
+ // PayPal orders have payer.name
1189
+ if (resourceType === 'order') {
1190
+ const givenName = resource.payer?.name?.given_name;
1191
+ const surname = resource.payer?.name?.surname;
1192
+
1193
+ if (givenName) {
1194
+ const { capitalize } = require('../../../libraries/infer-contact.js');
1195
+ return {
1196
+ first: capitalize(givenName) || null,
1197
+ last: capitalize(surname) || null,
1198
+ };
1199
+ }
1200
+ }
1201
+
1202
+ // Chargebee subscriptions carry shipping_address / billing_address with first_name + last_name
1203
+ if (resourceType === 'subscription') {
1204
+ const addr = resource.shipping_address || resource.billing_address;
1205
+ if (addr?.first_name) {
1206
+ const { capitalize } = require('../../../libraries/infer-contact.js');
1207
+ return {
1208
+ first: capitalize(addr.first_name) || null,
1209
+ last: capitalize(addr.last_name) || null,
1210
+ };
1211
+ }
1212
+ }
1213
+
1214
+ if (!fullName) {
1215
+ return null;
1216
+ }
1217
+
1218
+ const { capitalize } = require('../../../libraries/infer-contact.js');
1219
+ const parts = fullName.trim().split(/\s+/);
1220
+ return {
1221
+ first: capitalize(parts[0]) || null,
1222
+ last: capitalize(parts.slice(1).join(' ')) || null,
1223
+ };
1224
+ }