@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,2660 @@
1
+ const uuid = require('uuid');
2
+ const { envPort, CLASSIC_PORTS } = require('../vendor/config/index.js');
3
+ const { isCountedFeature, isPacedFeature, dayShare } = require('../vendor/account/features.js');
4
+
5
+ // Deterministic password for EVERY seeded persona (N6) — makes manual dev signin
6
+ // possible: boot the emulators, open an emulator-connected dev site, and sign in
7
+ // as any persona with email + this password. Personas only ever exist in the
8
+ // emulator (seeding wipes + recreates them), so a fixed password is safe.
9
+ const TEST_ACCOUNT_PASSWORD = 'omega-test-password';
10
+
11
+ /**
12
+ * Resolve the first paid subscription product from config
13
+ * Falls back to 'premium' if no config or no paid products found
14
+ *
15
+ * PAID means the catalog SELLS it — a plan with prices. The free tier is not
16
+ * always named `basic` (a brand may call it `reader`), and a persona seeded onto
17
+ * a free tier that the seeder believed was paid carries no price to quote.
18
+ */
19
+ function getFirstPaidProduct(config) {
20
+ const products = config?.payment?.products || [];
21
+ const paid = products.find(p => p.type === 'subscription' && p.id !== 'basic' && p.prices);
22
+ return paid
23
+ ? { id: paid.id, name: paid.name }
24
+ : { id: 'premium', name: 'Premium' };
25
+ }
26
+
27
+ // How long each billing cadence runs — the SAME periods the test provider's
28
+ // fabricated subscriptions are given (routes/payments/intent/providers/test.js
29
+ // FREQUENCY_TO_PERIOD), so a seeded term matches what a real cycle records.
30
+ const CYCLE_DAYS = { daily: 1, weekly: 7, monthly: 30, annually: 365 };
31
+
32
+ /**
33
+ * Helper to create the expiration date of the billing cycle a live paid
34
+ * subscription is currently in — a real term runs to the end of the cycle it is
35
+ * billed on, never to a date a decade out no provider would ever record.
36
+ * User() checks subscription.expires to determine if subscription is active
37
+ * If expires is in the past (or default 1970), subscription gets downgraded to basic
38
+ *
39
+ * @param {string} [frequency] - Billing cadence ('monthly', 'annually', ...)
40
+ */
41
+ function getCycleExpires(frequency = 'monthly') {
42
+ const days = CYCLE_DAYS[frequency] || CYCLE_DAYS.monthly;
43
+ const cycleEnd = new Date(Date.now() + (days * 86400 * 1000));
44
+ return {
45
+ timestamp: cycleEnd.toISOString(),
46
+ timestampUNIX: Math.floor(cycleEnd.getTime() / 1000),
47
+ };
48
+ }
49
+
50
+ // How long a seeded trial runs when the plan it is on names no length of its own
51
+ // — the trial the framework's own catalogs offer.
52
+ const DEFAULT_TRIAL_DAYS = 14;
53
+
54
+ /**
55
+ * Helper to create the expiration date of a trial that is still RUNNING — a
56
+ * subscription inside its free trial ends when the trial does (the providers
57
+ * bill nothing until then: routes/payments/intent/providers/test.js sets
58
+ * `current_period_end` to `trial_end`), so this dates BOTH `trial.expires` and
59
+ * the subscription's own `expires`.
60
+ *
61
+ * @param {number} [days] - Trial length in days (the plan's `trial.days`)
62
+ */
63
+ function getTrialExpires(days) {
64
+ const trialEnd = new Date(Date.now() + ((days || DEFAULT_TRIAL_DAYS) * 86400 * 1000));
65
+ return {
66
+ timestamp: trialEnd.toISOString(),
67
+ timestampUNIX: Math.floor(trialEnd.getTime() / 1000),
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Resolve what a plan costs and how often it bills, from the brand's own catalog:
73
+ * the cadence it is listed at first and the price carried there. This is the SAME
74
+ * read the unified transforms make (`config.payment.products[].prices[frequency]`
75
+ * — libraries/payment/providers/stripe.js resolvePrice), so a seeded purchase can
76
+ * never quote a number the brand does not sell. No catalog, or a plan with no
77
+ * prices, resolves to nulls — never a hand-typed figure that would drift.
78
+ *
79
+ * @param {object} [config] - @omega.js/backend config
80
+ * @param {string} productId - Product id to price
81
+ * @returns {{ frequency: string|null, price: number|null }}
82
+ */
83
+ function getPlanPricing(config, productId) {
84
+ const product = (config?.payment?.products || []).find(p => p.id === productId);
85
+ const prices = product?.prices || {};
86
+ const frequency = Object.keys(prices)[0] || null;
87
+
88
+ return {
89
+ frequency,
90
+ price: frequency ? prices[frequency] : null,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Resolve a seeded subscription against the brand catalog — the one place the
96
+ * hardcoded persona shapes meet the config, used by BOTH the account definitions
97
+ * and the order fixtures so a persona and its purchase record can never disagree.
98
+ *
99
+ * Three resolutions, all of them catalog reads:
100
+ * 1. The placeholder `premium` product becomes the brand's actual first paid plan.
101
+ * 2. A persona that BOUGHT a subscription (it holds a paid plan, or carries the
102
+ * payment record of one it has since lapsed from) gets the price and cadence
103
+ * of that plan. A free account that never bought anything is left alone.
104
+ * 3. A LIVE paid term ends when its billing cycle does, not a decade out — or,
105
+ * for a term still inside its free trial, when the TRIAL does.
106
+ *
107
+ * @param {object} [subscription] - The persona's seeded subscription block
108
+ * @param {object} [config] - @omega.js/backend config
109
+ * @returns {object} Resolved copy (the input is never mutated)
110
+ */
111
+ function resolveSeededSubscription(subscription, config) {
112
+ const resolved = JSON.parse(JSON.stringify(subscription || {}));
113
+
114
+ if (!resolved.product) {
115
+ return resolved;
116
+ }
117
+
118
+ const paidProduct = getFirstPaidProduct(config);
119
+
120
+ if (resolved.product.id === 'premium') {
121
+ resolved.product.id = paidProduct.id;
122
+ resolved.product.name = paidProduct.name;
123
+ }
124
+
125
+ const payment = resolved.payment || {};
126
+ const boughtSomething = resolved.product.id !== 'basic'
127
+ || Boolean(payment.provider || payment.resourceId || payment.orderId);
128
+
129
+ if (!boughtSomething) {
130
+ return resolved;
131
+ }
132
+
133
+ // The plan that was bought: the one being held, or — for a persona whose term
134
+ // ran out and dropped it back to basic — the paid plan it lapsed from.
135
+ const planId = resolved.product.id !== 'basic' ? resolved.product.id : paidProduct.id;
136
+ const pricing = getPlanPricing(config, planId);
137
+
138
+ // A plan the catalog does not price (a project's own persona, a brand with no
139
+ // catalog) is left exactly as seeded — nulls are not billing data, so none are
140
+ // written, not even onto a persona that already carries a payment record.
141
+ if (!pricing.frequency) {
142
+ return resolved;
143
+ }
144
+
145
+ resolved.payment = {
146
+ ...payment,
147
+ frequency: payment.frequency || pricing.frequency,
148
+ price: payment.price ?? pricing.price,
149
+ };
150
+
151
+ // Only a term that is still running gets re-dated: a lapsed persona's expired
152
+ // term is the state it exists to represent. A cancellation that is still
153
+ // PENDING takes effect when that term ends — the providers date it at the
154
+ // period end (stripe.js resolveCancellation) — so it follows the term here
155
+ // rather than being typed onto a persona and drifting away from it.
156
+ const nowUNIX = Math.floor(Date.now() / 1000);
157
+ if (resolved.trial?.claimed && resolved.expires?.timestampUNIX > nowUNIX) {
158
+ // A term still INSIDE its free trial ends when the TRIAL does, not when a
159
+ // billing cycle nobody has been charged for yet would. The two dates being
160
+ // the SAME is the whole signal: routes/payments/cancel/_is-trialing.js tells
161
+ // a running trial from a converted one by exactly that equality, so both are
162
+ // dated off the trial the catalog offers on this plan.
163
+ const product = (config?.payment?.products || []).find((p) => p.id === planId);
164
+
165
+ resolved.expires = getTrialExpires(product?.trial?.days);
166
+ resolved.trial.expires = { ...resolved.expires };
167
+ } else if (resolved.expires?.timestampUNIX > nowUNIX) {
168
+ resolved.expires = getCycleExpires(resolved.payment.frequency);
169
+ }
170
+
171
+ if (resolved.cancellation?.pending && resolved.expires) {
172
+ resolved.cancellation.date = { ...resolved.expires };
173
+ }
174
+
175
+ // The event that last wrote this subscription. Only a record that names a
176
+ // provider gets one — a record with no provider was never written by one —
177
+ // and the persona's own state says which event it was (getLastWrite). Stamped
178
+ // HERE, after the re-dating above, so the date derivation reads the FINAL
179
+ // term and frequency — stamping earlier read the seeded expires and a payment
180
+ // with no frequency yet, which dated a project persona's write off the wrong
181
+ // cycle entirely.
182
+ if (resolved.payment.provider && !resolved.payment.updatedBy) {
183
+ resolved.payment.updatedBy = getLastWrite(resolved);
184
+ }
185
+
186
+ return resolved;
187
+ }
188
+
189
+ /**
190
+ * Helper to create a past expiration date for expired subscriptions
191
+ */
192
+ function getPastExpires(years = 1) {
193
+ const pastDate = new Date();
194
+ pastDate.setFullYear(pastDate.getFullYear() - years);
195
+ return {
196
+ timestamp: pastDate.toISOString(),
197
+ timestampUNIX: Math.floor(pastDate.getTime() / 1000),
198
+ };
199
+ }
200
+
201
+ /**
202
+ * A moment in the { timestamp, timestampUNIX } shape every date on a user doc wears.
203
+ * @param {Date} [date] - The moment (defaults to now)
204
+ */
205
+ function getStamp(date) {
206
+ const at = date || new Date();
207
+ return {
208
+ timestamp: at.toISOString(),
209
+ timestampUNIX: Math.floor(at.getTime() / 1000),
210
+ };
211
+ }
212
+
213
+ /**
214
+ * A moment N days back — for the recent history a persona carries (when its
215
+ * subscription started, when it was last billed) at a resolution `getPastExpires`
216
+ * (whole years) cannot express.
217
+ * @param {number} days - How many days ago
218
+ */
219
+ function getDaysAgo(days) {
220
+ return getStamp(new Date(Date.now() - (days * 86400 * 1000)));
221
+ }
222
+
223
+ /**
224
+ * A moment N hours back — a device's last check-in is measured in hours, not
225
+ * days: a session dated to the nearest day reads as stale on a list whose whole
226
+ * subject is who is signed in RIGHT NOW.
227
+ * @param {number} hours - How many hours ago
228
+ */
229
+ function getHoursAgo(hours) {
230
+ return getStamp(new Date(Date.now() - (hours * 3600 * 1000)));
231
+ }
232
+
233
+ /**
234
+ * The provider record a REAL subscription leaves on the user doc, for a persona
235
+ * bought through the TEST provider — the shape the unified transform writes
236
+ * (libraries/payment/providers/stripe.js toUnifiedSubscription, stamped `test`).
237
+ *
238
+ * `frequency` and `price` are deliberately absent: they are the CATALOG's answer,
239
+ * and resolveSeededSubscription fills them from the brand's own prices. A figure
240
+ * typed here would drift the moment a brand repriced.
241
+ *
242
+ * `updatedBy` is absent for the same reason — the event that last wrote a
243
+ * subscription follows from the STATE it is in, which resolveSeededSubscription
244
+ * reads off the persona itself (getLastWrite).
245
+ *
246
+ * @param {string} key - The persona key; names the provider resource and its order
247
+ * @param {object} [options]
248
+ * @param {object} [options.startDate] - When the subscription began (default: a year ago)
249
+ * @param {boolean} [options.order] - Whether a purchase record stands behind it (default: true)
250
+ * @returns {object} The subscription's `payment` block
251
+ */
252
+ function getTestPayment(key, options) {
253
+ options = options || {};
254
+
255
+ return {
256
+ provider: 'test',
257
+ ...(options.order === false ? {} : { orderId: `_test-order-${key}` }),
258
+ resourceId: `sub_test_${key.replace(/-/g, '_')}`,
259
+ startDate: options.startDate || getPastExpires(1),
260
+ };
261
+ }
262
+
263
+ /**
264
+ * The webhook event that last wrote a subscription in this state — a real
265
+ * subscription is always stamped with one (`payment.updatedBy`, which the email
266
+ * library reads as the customer's last payment date: libraries/email/constants.js
267
+ * `user_subscription_payment_last_date`). The state IS the answer, so it is
268
+ * derived rather than typed onto every persona.
269
+ *
270
+ * @param {object} subscription - The persona's resolved subscription
271
+ * @returns {object} The `payment.updatedBy` block
272
+ */
273
+ function getLastWrite(subscription) {
274
+ let name = 'invoice.payment_succeeded';
275
+
276
+ if (subscription.status === 'suspended') {
277
+ name = 'invoice.payment_failed';
278
+ } else if (subscription.status === 'cancelled') {
279
+ name = 'customer.subscription.deleted';
280
+ } else if (subscription.cancellation?.pending) {
281
+ name = 'customer.subscription.updated';
282
+ } else if (subscription.trial?.claimed) {
283
+ name = 'customer.subscription.created';
284
+ }
285
+
286
+ // The DATE follows the state like the event name does: a dead term's last
287
+ // write landed when that term ended, a running trial's when the trial began,
288
+ // a live term's when its current cycle opened. Stamping "now" would record a
289
+ // webhook landing today for a term that may have died a year ago — and the
290
+ // email layer prints this stamp as the last payment date
291
+ // (libraries/email/constants.js).
292
+ const nowUNIX = Math.floor(Date.now() / 1000);
293
+ const expires = subscription.expires;
294
+ let date;
295
+ if (!expires?.timestampUNIX) {
296
+ date = getStamp();
297
+ } else if (expires.timestampUNIX <= nowUNIX) {
298
+ date = { timestamp: expires.timestamp, timestampUNIX: expires.timestampUNIX };
299
+ } else if (subscription.trial?.claimed) {
300
+ date = getStamp(new Date((expires.timestampUNIX - (DEFAULT_TRIAL_DAYS * 86400)) * 1000));
301
+ } else {
302
+ const days = CYCLE_DAYS[subscription.payment?.frequency] || CYCLE_DAYS.monthly;
303
+ date = getStamp(new Date((expires.timestampUNIX - (days * 86400)) * 1000));
304
+ }
305
+
306
+ return {
307
+ event: { name, id: `evt_test_${name.replace(/[.]/g, '_')}` },
308
+ date,
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Realistic identities the personas wear (Ian 2026-08-17,
314
+ * [#327](https://github.com/Omega-JS-Stack/omega/issues/327)).
315
+ *
316
+ * Demo-safe by construction: every IP is an IANA documentation block (RFC 5737 —
317
+ * routable nowhere), every telephone number is in the 555-01xx fictional range,
318
+ * and the companies and street addresses are invented. Nothing here can reach, or
319
+ * name, a real person.
320
+ *
321
+ * A locale bundles everything that must AGREE about where a persona lives — the
322
+ * geolocation the request carried, the location on their profile, their phone's
323
+ * country code and the language their browser asked for — so a persona can never
324
+ * be seeded in Munich reading Japanese from a Brazilian IP.
325
+ *
326
+ * `areaCode` carries no trunk zero: `personal.telephone.national` is a NUMBER on
327
+ * the schema, and a leading zero cannot survive one.
328
+ *
329
+ * `postalCode` is written the way its own country writes one — a US ZIP, a
330
+ * Canadian forward-sortation pair, a UK outward/inward code — because the
331
+ * account page collects it as free text and the ad platforms normalize each
332
+ * country's own shape ([#663](https://github.com/Omega-JS-Stack/omega/issues/663)).
333
+ */
334
+ const PROFILE_LOCALES = [
335
+ { continent: 'NA', country: 'US', region: 'California', city: 'San Diego', postalCode: '92101', latitude: 32.7157, longitude: -117.1611, language: 'en-US', callingCode: 1, areaCode: '619', ipBlock: '192.0.2' },
336
+ { continent: 'NA', country: 'US', region: 'New York', city: 'Brooklyn', postalCode: '11201', latitude: 40.6782, longitude: -73.9442, language: 'en-US', callingCode: 1, areaCode: '212', ipBlock: '192.0.2' },
337
+ { continent: 'NA', country: 'CA', region: 'Ontario', city: 'Toronto', postalCode: 'M5H 2N2', latitude: 43.6532, longitude: -79.3832, language: 'en-CA', callingCode: 1, areaCode: '416', ipBlock: '198.51.100' },
338
+ { continent: 'EU', country: 'GB', region: 'England', city: 'Manchester', postalCode: 'M1 1AE', latitude: 53.4808, longitude: -2.2426, language: 'en-GB', callingCode: 44, areaCode: '161', ipBlock: '198.51.100' },
339
+ { continent: 'EU', country: 'DE', region: 'Bavaria', city: 'Munich', postalCode: '80331', latitude: 48.1351, longitude: 11.5820, language: 'de-DE', callingCode: 49, areaCode: '89', ipBlock: '198.51.100' },
340
+ { continent: 'EU', country: 'ES', region: 'Catalonia', city: 'Barcelona', postalCode: '08001', latitude: 41.3874, longitude: 2.1686, language: 'es-ES', callingCode: 34, areaCode: '933', ipBlock: '203.0.113' },
341
+ { continent: 'OC', country: 'AU', region: 'Victoria', city: 'Melbourne', postalCode: '3000', latitude: -37.8136, longitude: 144.9631, language: 'en-AU', callingCode: 61, areaCode: '3', ipBlock: '203.0.113' },
342
+ { continent: 'SA', country: 'BR', region: 'Sao Paulo', city: 'Campinas', postalCode: '13010-001', latitude: -22.9099, longitude: -47.0626, language: 'pt-BR', callingCode: 55, areaCode: '19', ipBlock: '203.0.113' },
343
+ ];
344
+
345
+ /**
346
+ * The devices personas browse on — the exact vocabulary @omega.js/client reports
347
+ * (packages/client/src/modules/utilities.js getContext: lowercase platform and
348
+ * browser, a `desktop|tablet|mobile` device, `web` runtime), paired with the user
349
+ * agent that would actually produce it.
350
+ */
351
+ const PROFILE_CLIENTS = [
352
+ { mobile: false, device: 'desktop', platform: 'mac', browser: 'chrome', vendor: 'Google Inc.', runtime: 'web', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' },
353
+ { mobile: false, device: 'desktop', platform: 'mac', browser: 'safari', vendor: 'Apple Computer, Inc.', runtime: 'web', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15' },
354
+ { mobile: false, device: 'desktop', platform: 'windows', browser: 'edge', vendor: 'Google Inc.', runtime: 'web', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0' },
355
+ { mobile: false, device: 'desktop', platform: 'windows', browser: 'firefox', vendor: '', runtime: 'web', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0' },
356
+ { mobile: true, device: 'mobile', platform: 'ios', browser: 'safari', vendor: 'Apple Computer, Inc.', runtime: 'web', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1' },
357
+ { mobile: true, device: 'mobile', platform: 'android', browser: 'chrome', vendor: 'Google Inc.', runtime: 'web', userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36' },
358
+ { mobile: false, device: 'tablet', platform: 'ios', browser: 'safari', vendor: 'Apple Computer, Inc.', runtime: 'web', userAgent: 'Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/604.1' },
359
+ { mobile: false, device: 'desktop', platform: 'linux', browser: 'chrome', vendor: 'Google Inc.', runtime: 'web', userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' },
360
+ ];
361
+
362
+ const PROFILE_FIRST_NAMES = ['Adrian', 'Beatriz', 'Camille', 'Desmond', 'Elena', 'Felix', 'Greta', 'Hugo', 'Imani', 'Julien', 'Klara', 'Lucas', 'Mira', 'Nadia', 'Omar', 'Priya', 'Rafael', 'Sofia', 'Theo', 'Ursula', 'Viktor', 'Wren', 'Yara', 'Zane'];
363
+ const PROFILE_LAST_NAMES = ['Alvarez', 'Bennett', 'Castellanos', 'Dubois', 'Espinoza', 'Fairbanks', 'Gallagher', 'Haddad', 'Ishikawa', 'Jensen', 'Kowalski', 'Laurent', 'Moreau', 'Nakamura', 'Okafor', 'Petrov', 'Quintero', 'Rossi', 'Sandoval', 'Thorne', 'Ueda', 'Vasquez', 'Whitfield', 'Ziegler'];
364
+ const PROFILE_STREETS = ['12 Harbour Lane', '48 Linden Street', '117 Marlowe Avenue', '205 Kestrel Road', '9 Alderway Close', '341 Beacon Street', '76 Juniper Walk', '158 Copperfield Row'];
365
+ const PROFILE_COMPANIES = ['Northgate Labs', 'Harbourline Studio', 'Meridian Works', 'Copperleaf Media', 'Foxglove Analytics', 'Ridgeway Supply', 'Lanternhouse Co', 'Saltmarsh Digital'];
366
+ const PROFILE_POSITIONS = ['Product Designer', 'Operations Lead', 'Staff Engineer', 'Marketing Manager', 'Founder', 'Data Analyst', 'Content Editor', 'Support Lead'];
367
+ const PROFILE_GENDERS = ['female', 'male', 'non-binary'];
368
+
369
+ /**
370
+ * A stable index derived from a seed string (djb2) — the same persona draws the
371
+ * same profile on every boot, so a screenshot QA took last week still matches the
372
+ * account in front of them, and no dependency or stored fixture is needed.
373
+ * @param {string} seed - The string to hash
374
+ * @param {number} size - Pool size to index into
375
+ */
376
+ function hashIndex(seed, size) {
377
+ let hash = 5381;
378
+
379
+ for (let i = 0; i < seed.length; i++) {
380
+ hash = ((hash * 33) ^ seed.charCodeAt(i)) >>> 0;
381
+ }
382
+
383
+ return hash % size;
384
+ }
385
+
386
+ /**
387
+ * Draw one entry from a pool for a persona, salted so the same key can draw
388
+ * independently from several pools.
389
+ */
390
+ function pick(pool, key, salt) {
391
+ return pool[hashIndex(`${key}#${salt}`, pool.length)];
392
+ }
393
+
394
+ /**
395
+ * The identity a persona would have if it had signed up like a real user: who
396
+ * they are, where they signed up from, and on what device — the sections
397
+ * `routes/user/signup` fills from the request and the user fills from the account
398
+ * page, and which account creation alone leaves as a wall of nulls.
399
+ *
400
+ * @param {string} key - The persona key (its profile is derived from it)
401
+ * @param {string} domain - The brand's domain (the page they signed up on)
402
+ * @returns {object} The `personal` and `activity` sections of a user doc
403
+ */
404
+ function seededProfile(key, domain) {
405
+ const locale = pick(PROFILE_LOCALES, key, 'locale');
406
+ const client = pick(PROFILE_CLIENTS, key, 'client');
407
+ const birthday = new Date(Date.UTC(
408
+ 1975 + hashIndex(`${key}#year`, 25),
409
+ hashIndex(`${key}#month`, 12),
410
+ 1 + hashIndex(`${key}#day`, 28),
411
+ ));
412
+
413
+ return {
414
+ personal: {
415
+ birthday: getStamp(birthday),
416
+ gender: pick(PROFILE_GENDERS, key, 'gender'),
417
+ location: {
418
+ country: locale.country,
419
+ region: locale.region,
420
+ city: locale.city,
421
+ postalCode: locale.postalCode,
422
+ street: pick(PROFILE_STREETS, key, 'street'),
423
+ },
424
+ name: {
425
+ first: pick(PROFILE_FIRST_NAMES, key, 'first'),
426
+ last: pick(PROFILE_LAST_NAMES, key, 'last'),
427
+ },
428
+ company: {
429
+ name: pick(PROFILE_COMPANIES, key, 'company'),
430
+ position: pick(PROFILE_POSITIONS, key, 'position'),
431
+ },
432
+ telephone: {
433
+ countryCode: locale.callingCode,
434
+ // The 555-01xx fictional block, behind the locale's own area code
435
+ national: Number(`${locale.areaCode}5550${100 + hashIndex(`${key}#phone`, 100)}`),
436
+ },
437
+ },
438
+ activity: {
439
+ geolocation: {
440
+ ip: `${locale.ipBlock}.${1 + hashIndex(`${key}#ip`, 250)}`,
441
+ continent: locale.continent,
442
+ country: locale.country,
443
+ region: locale.region,
444
+ city: locale.city,
445
+ latitude: locale.latitude,
446
+ longitude: locale.longitude,
447
+ },
448
+ client: {
449
+ ...client,
450
+ language: locale.language,
451
+ url: `https://${domain}/signup`,
452
+ },
453
+ },
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Fill in every key `source` carries that `target` does not, recursively — so a
459
+ * persona that names only its own `personal.name` keeps that name and gains
460
+ * everything around it. `target` is mutated.
461
+ */
462
+ function fillMissing(target, source) {
463
+ for (const [key, value] of Object.entries(source)) {
464
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
465
+ if (!target[key] || typeof target[key] !== 'object') {
466
+ target[key] = {};
467
+ }
468
+ fillMissing(target[key], value);
469
+ } else if (target[key] === undefined) {
470
+ target[key] = value;
471
+ }
472
+ }
473
+
474
+ return target;
475
+ }
476
+
477
+ /**
478
+ * Who each persona referred, by persona key → the personas that signed up
479
+ * through its affiliate link (Ian 2026-08-18,
480
+ * [#343](https://github.com/Omega-JS-Stack/omega/issues/343)).
481
+ *
482
+ * The referrals name REAL personas rather than invented uids, and that is what
483
+ * makes a mixed list possible at all: the record a signup appends to a referrer
484
+ * carries a uid and nothing else (routes/user/signup/post.js processAffiliate),
485
+ * so whether a referral CONVERTED is a fact about the referred ACCOUNT — one
486
+ * holding a paid plan converted, one still on basic has not. Inventing a status
487
+ * field here would invent a shape no referral has ever had.
488
+ */
489
+ const PERSONA_REFERRALS = {
490
+ // The affiliate owner the signup lane credits — an established referrer, so
491
+ // the list a new referral lands on is never an empty one. It is the ONLY
492
+ // persona carrying referrals (Ian 2026-08-18,
493
+ // [#363](https://github.com/Omega-JS-Stack/omega/issues/363)): every persona
494
+ // demonstrates exactly its own scenario, so the affiliate story belongs to
495
+ // the account built for it and nowhere else.
496
+ referrer: ['premium-active', 'basic', 'premium-expired', 'refunded', 'referred'],
497
+ };
498
+
499
+ /**
500
+ * Who referred each persona, by persona key → the persona whose affiliate link
501
+ * it came in through (Ian 2026-08-19,
502
+ * [#369](https://github.com/Omega-JS-Stack/omega/issues/369)).
503
+ *
504
+ * The INBOUND half of PERSONA_REFERRALS: a referral is two facts written to two
505
+ * docs — the record appended to the referrer's list, and the attribution the
506
+ * signup writes onto the account that arrived — and while only the outbound half
507
+ * was seeded, no persona could show what "you were referred by" renders as. The
508
+ * account carrying it is a persona of its own, never a state added to one that
509
+ * already demonstrates something else.
510
+ */
511
+ const PERSONA_REFERRED_BY = {
512
+ referred: 'referrer',
513
+ };
514
+
515
+ /**
516
+ * How long ago each referral in a persona's list signed up — a spread, so the
517
+ * list reads as a history rather than a batch, and the newest is recent enough
518
+ * to count towards the account page's "this month" stat.
519
+ */
520
+ const REFERRAL_DAYS_AGO = [2, 9, 24, 51, 88];
521
+
522
+ /**
523
+ * The devices a persona is signed in on besides the browser in front of you, by
524
+ * persona key → how many (#343). Two personas carry them: the ones the palette
525
+ * offers as an ordinary customer, which is who QA opens the security panel as.
526
+ */
527
+ const PERSONA_SESSIONS = {
528
+ 'premium-active': 3,
529
+ basic: 2,
530
+ };
531
+
532
+ /** How long ago each of those devices last checked in. */
533
+ const SESSION_HOURS_AGO = [1, 20, 73];
534
+
535
+ /**
536
+ * The Realtime Database path a signed-in app records itself at. `app` is the
537
+ * session id both sessions routes default to (schemas/user/sessions/get.js), so
538
+ * it is the one the account page's security panel reads.
539
+ */
540
+ const SESSION_PATH = 'sessions/app';
541
+
542
+ /**
543
+ * The referral records a persona's affiliate link earned — the exact shape a
544
+ * signup appends to the referrer's doc: `{ uid, timestamp }`, the timestamp an
545
+ * ISO string (processAffiliate writes `ctx.meta.startTime.timestamp`). No
546
+ * `timestampUNIX`: the writer records one field, and a seed carrying two would
547
+ * wear a shape no real referral has.
548
+ *
549
+ * @param {string} key - The persona key
550
+ * @param {object} accounts - Every account definition, keyed (referrals name real personas)
551
+ * @returns {Array|null} The persona's `affiliate.referrals`, or null when it referred nobody
552
+ */
553
+ function buildReferralFixtures(key, accounts) {
554
+ const referred = PERSONA_REFERRALS[key];
555
+
556
+ if (!referred) {
557
+ return null;
558
+ }
559
+
560
+ return referred.map((referredKey, index) => {
561
+ const account = accounts[referredKey];
562
+
563
+ // A referral pointing at nobody is a uid the emulator cannot resolve — the
564
+ // one thing seeding real personas exists to prevent, so it fails loudly.
565
+ if (!account) {
566
+ throw new Error(`Persona '${key}' is seeded as having referred '${referredKey}', which no persona defines`);
567
+ }
568
+
569
+ return {
570
+ uid: account.uid,
571
+ timestamp: getDaysAgo(REFERRAL_DAYS_AGO[index % REFERRAL_DAYS_AGO.length]).timestamp,
572
+ };
573
+ });
574
+ }
575
+
576
+ /**
577
+ * The attribution an account that arrived through an affiliate link carries —
578
+ * `attribution.affiliate`, the block routes/user/signup writes from the request
579
+ * and the account schema defines (packages/account/src/schema.js): the code it
580
+ * came in on, when the link was clicked, and the page it was clicked on.
581
+ *
582
+ * The stamp is READ OFF the referrer's own record for this account rather than
583
+ * dated here: both halves are the SAME signup, and a pair that disagrees about
584
+ * when it happened is a pair no signup could have produced.
585
+ *
586
+ * @param {string} key - The persona key
587
+ * @param {object} accounts - Every account definition, keyed
588
+ * @param {string} domain - The brand's domain (the link they clicked)
589
+ * @returns {object|null} The persona's `attribution.affiliate`, or null for one nobody referred
590
+ */
591
+ function buildInboundReferral(key, accounts, domain) {
592
+ const referrerKey = PERSONA_REFERRED_BY[key];
593
+
594
+ if (!referrerKey) {
595
+ return null;
596
+ }
597
+
598
+ const referrer = accounts[referrerKey];
599
+ const record = (buildReferralFixtures(referrerKey, accounts) || []).find((referral) => referral.uid === accounts[key]?.uid);
600
+
601
+ // Half a pair: an account claiming a referrer whose list never names it. The
602
+ // referral panel would show a referrer nobody referred through.
603
+ if (!referrer || !record) {
604
+ throw new Error(`Persona '${key}' is seeded as referred by '${referrerKey}', which carries no referral for it`);
605
+ }
606
+
607
+ const code = referrer.properties?.affiliate?.code;
608
+
609
+ return {
610
+ code: code,
611
+ timestamp: record.timestamp,
612
+ url: `https://${domain}/?ref=${code}`,
613
+ page: '/',
614
+ };
615
+ }
616
+
617
+ /**
618
+ * The active-session records a persona's other devices leave in the Realtime
619
+ * Database — the shape `routes/user/sessions` queries (`uid`, which the
620
+ * `orderByChild('uid')` filter reads) and the account page's security panel
621
+ * renders (`platform`, `ip`, and the check-in stamp).
622
+ *
623
+ * The devices are drawn from the same client pool the persona's own signup
624
+ * context comes from, starting AFTER the one it signed up on, so the list is
625
+ * three different machines rather than the same one three times. The IPs sit in
626
+ * the persona's own locale block: the same person, at home, on their other
627
+ * devices.
628
+ *
629
+ * @param {string} key - The persona key
630
+ * @param {object} accounts - Every account definition, keyed
631
+ * @returns {object|null} Session id → record, or null for a persona signed in nowhere else
632
+ */
633
+ function buildSessionFixtures(key, accounts) {
634
+ const count = PERSONA_SESSIONS[key];
635
+
636
+ if (!count) {
637
+ return null;
638
+ }
639
+
640
+ const account = accounts[key];
641
+
642
+ if (!account) {
643
+ throw new Error(`No seeded persona named ${key} — there are no session fixtures to seed`);
644
+ }
645
+
646
+ const locale = pick(PROFILE_LOCALES, key, 'locale');
647
+ const signupClient = PROFILE_CLIENTS.indexOf(pick(PROFILE_CLIENTS, key, 'client'));
648
+ const sessions = {};
649
+
650
+ for (let index = 0; index < count; index++) {
651
+ const client = PROFILE_CLIENTS[(signupClient + 1 + index) % PROFILE_CLIENTS.length];
652
+ const at = getHoursAgo(SESSION_HOURS_AGO[index % SESSION_HOURS_AGO.length]);
653
+
654
+ sessions[`_test-session-${key}-${index + 1}`] = {
655
+ uid: account.uid,
656
+ platform: client.platform,
657
+ ip: `${locale.ipBlock}.${1 + hashIndex(`${key}#session${index}`, 250)}`,
658
+ timestamp: at.timestamp,
659
+ timestampUNIX: at.timestampUNIX,
660
+ };
661
+ }
662
+
663
+ return sessions;
664
+ }
665
+
666
+ // The marker the usage persona carries instead of hand-typed counters: only the
667
+ // SEED knows the brand's feature catalog and the persona's resolved plan, so the
668
+ // spread is resolved there ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)).
669
+ const USAGE_SPREAD = '$usageSpread';
670
+
671
+ // The five states the usage persona exists to show, in the order the account
672
+ // page draws them. Each is a function of that feature's own monthly limit, so
673
+ // the seed reads right on any brand's numbers.
674
+ const USAGE_SPREAD_STATES = [
675
+ { name: 'untouched', monthly: () => 0, daily: () => 0 },
676
+ { name: 'half spent', monthly: (limit) => Math.floor(limit / 2), daily: () => 0 },
677
+ // The day's share is gone while the month still has room: the refusal that
678
+ // says "try again tomorrow"
679
+ { name: 'day cap hit', monthly: (limit) => Math.floor(limit / 4), daily: (limit, dayShare) => dayShare },
680
+ // The month is gone: the refusal that says "upgrade"
681
+ { name: 'month cap hit', monthly: (limit) => limit, daily: () => 0 },
682
+ // Admin-granted credits on top of the plan — the month is spent, and the
683
+ // account still has room because of them
684
+ { name: 'override credits', monthly: (limit) => limit, daily: () => 0, override: (limit) => limit * 2 },
685
+ ];
686
+
687
+ /**
688
+ * Expand the usage persona's spread against the brand's own feature catalog and
689
+ * the plan it resolved to: one state per COUNTED feature, in catalog order, so
690
+ * every bar on the account page tells a different story and each tells one.
691
+ *
692
+ * A brand whose catalog names fewer counted features simply seeds fewer states —
693
+ * never a counter for a feature nothing defines.
694
+ *
695
+ * @param {object} subscription - The persona's RESOLVED subscription block
696
+ * @param {object} [config] - @omega.js/backend config
697
+ * @returns {object} A `usage` block: counters per feature, plus `overrides`
698
+ */
699
+ function resolveSeededUsage(subscription, config) {
700
+ const catalog = config?.features || {};
701
+ const productId = subscription?.product?.id;
702
+ const product = (config?.payment?.products || []).find((p) => p.id === productId) || {};
703
+ const values = product.features || {};
704
+
705
+ const counted = Object.keys(catalog).filter((id) => isCountedFeature(catalog[id]));
706
+ const usage = { overrides: {} };
707
+
708
+ counted.forEach((id, index) => {
709
+ const state = USAGE_SPREAD_STATES[index % USAGE_SPREAD_STATES.length];
710
+ const limit = typeof values[id] === 'number' && values[id] > 0 ? values[id] : 0;
711
+
712
+ // Unlimited (or unpriced) has no bar worth previewing — a state built from
713
+ // a limit that does not exist would be fiction
714
+ if (!limit) {
715
+ return;
716
+ }
717
+
718
+ const override = state.override ? state.override(limit) : null;
719
+ const effective = override === null ? limit : override;
720
+ const share = isPacedFeature(catalog[id]) ? dayShare(effective) : 0;
721
+
722
+ usage[id] = {
723
+ monthly: state.monthly(limit),
724
+ daily: state.daily(limit, share),
725
+ total: state.monthly(limit) * 3,
726
+ last: getStamp(),
727
+ };
728
+
729
+ if (override !== null) {
730
+ usage.overrides[id] = override;
731
+ }
732
+ });
733
+
734
+ return usage;
735
+ }
736
+
737
+ /**
738
+ * Static test accounts - always created with fixed properties
739
+ * Used for testing access control levels
740
+ * Both @omega.js/backend and consuming projects rely on these
741
+ *
742
+ * Structure: { id, uid, email, palette, properties }
743
+ * - id: Account identifier
744
+ * - uid: Firebase Auth UID
745
+ * - email: Email with {domain} placeholder (resolved at runtime)
746
+ * - palette: The label the dev palette's account switcher offers this persona
747
+ * under ([#400](https://github.com/Omega-JS-Stack/omega/issues/400)). The
748
+ * seed is the ONE owner of that roster (routes/test/roster hands it over), so
749
+ * a HUMAN-facing persona carries a label, and machinery (the fixtures an
750
+ * automated suite drives) carries none and stays invisible.
751
+ * - properties: Object to merge into user doc after auth:on-create
752
+ *
753
+ * IMPORTANT: Premium accounts MUST have subscription.expires set to a future date
754
+ * and subscription.status set to 'active'
755
+ */
756
+ const STATIC_ACCOUNTS = {
757
+ admin: {
758
+ id: 'admin',
759
+ uid: '_test-admin',
760
+ email: '_test.admin@{domain}',
761
+ palette: 'Admin',
762
+ properties: {
763
+ roles: { admin: true },
764
+ subscription: { product: { id: 'basic' }, status: 'active' },
765
+ personal: { name: { first: 'Admin', last: 'User' } },
766
+ },
767
+ },
768
+ basic: {
769
+ id: 'basic',
770
+ uid: '_test-basic',
771
+ email: '_test.basic@{domain}',
772
+ palette: 'Basic',
773
+ properties: {
774
+ roles: {},
775
+ subscription: { product: { id: 'basic' }, status: 'active' },
776
+ personal: { name: { first: 'Alex', last: 'Basic' } },
777
+ },
778
+ },
779
+ // The steady-state PAYING subscriber — the persona QA signs in as to see what a
780
+ // customer sees ([#327](https://github.com/Omega-JS-Stack/omega/issues/327)). It
781
+ // carries the whole provider record a real purchase leaves, because the
782
+ // payment-gated surfaces read exactly that: the billing panel's winback pitch is
783
+ // offered only where the discount can actually be applied (a provider and the
784
+ // resource it holds the subscription under — core/js/pages/dashboard/account/
785
+ // sections/billing.js), and a hollow seed skipped it while QA expected the offer.
786
+ // Subscribed a year ago, billed on the cycle it is in now.
787
+ 'premium-active': {
788
+ id: 'premium-active',
789
+ uid: '_test-premium-active',
790
+ email: '_test.premium-active@{domain}',
791
+ palette: 'Premium',
792
+ properties: {
793
+ roles: {},
794
+ subscription: { product: { id: 'premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: getTestPayment('premium-active') },
795
+ },
796
+ },
797
+ // Mid-trial ([#301](https://github.com/Omega-JS-Stack/omega/issues/301)): a
798
+ // subscriber INSIDE the free trial the catalog offers on the paid plan —
799
+ // claimed, nothing charged yet, and the term ending exactly when the trial
800
+ // does (the seeder dates both off `trial.days`). Status is `active`, not
801
+ // "trialing": the unified transforms map a provider's trialing status to
802
+ // active (libraries/payment/providers/stripe.js resolveStatus) and the trial
803
+ // block carries the trial fact — which is what
804
+ // routes/payments/cancel/_is-trialing.js reads.
805
+ 'premium-trialing': {
806
+ id: 'premium-trialing',
807
+ uid: '_test-premium-trialing',
808
+ email: '_test.premium-trialing@{domain}',
809
+ palette: 'Trialing',
810
+ properties: {
811
+ roles: {},
812
+ subscription: { product: { id: 'premium' }, status: 'active', expires: getTrialExpires(), cancellation: { pending: false }, trial: { claimed: true, expires: getTrialExpires(), outcome: null }, payment: getTestPayment('premium-trialing', { startDate: getStamp() }) },
813
+ },
814
+ },
815
+ // A former subscriber: bought two years ago, the term ran out a year ago. The
816
+ // purchase record stays behind it — that is what makes it a WINBACK candidate
817
+ // rather than a stranger, and what a real lapsed account looks like.
818
+ 'premium-expired': {
819
+ id: 'premium-expired',
820
+ uid: '_test-premium-expired',
821
+ email: '_test.premium-expired@{domain}',
822
+ palette: 'Expired',
823
+ properties: {
824
+ roles: {},
825
+ subscription: { product: { id: 'premium' }, status: 'cancelled', expires: getPastExpires(), cancellation: { pending: false }, payment: getTestPayment('premium-expired', { startDate: getPastExpires(2) }) },
826
+ },
827
+ },
828
+ // A subscriber whose renewal card failed: the provider suspended the term it
829
+ // had already dated, and the last thing to write the subscription was the
830
+ // failed invoice (getLastWrite).
831
+ 'premium-suspended': {
832
+ id: 'premium-suspended',
833
+ uid: '_test-premium-suspended',
834
+ email: '_test.premium-suspended@{domain}',
835
+ palette: 'Suspended',
836
+ properties: {
837
+ roles: {},
838
+ subscription: { product: { id: 'premium' }, status: 'suspended', expires: getCycleExpires(), cancellation: { pending: false }, payment: getTestPayment('premium-suspended') },
839
+ },
840
+ },
841
+ // Cancellation scheduled: still paid, still served, ending when the term does
842
+ // (resolveSeededSubscription dates `cancellation.date` off that term).
843
+ 'premium-cancelling': {
844
+ id: 'premium-cancelling',
845
+ uid: '_test-premium-cancelling',
846
+ email: '_test.premium-cancelling@{domain}',
847
+ palette: 'Cancelling',
848
+ properties: {
849
+ roles: {},
850
+ subscription: { product: { id: 'premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: true }, payment: getTestPayment('premium-cancelling') },
851
+ },
852
+ },
853
+ // Post-refund end state (N6 persona): the refund webhook cancels the subscription —
854
+ // the refund itself lives on the ORDER doc, not the user doc — so what remains is a
855
+ // cancelled sub on the test provider with no remaining term. "Unauthed" needs no
856
+ // persona: that's http.as('none') / a signed-out browser.
857
+ // It deliberately names NO order: buildOrderFixture writes a purchase record whose
858
+ // `requests.refund` is null, and an order claiming nothing was refunded would
859
+ // contradict the one fact this persona exists to carry.
860
+ refunded: {
861
+ id: 'refunded',
862
+ uid: '_test-refunded',
863
+ email: '_test.refunded@{domain}',
864
+ palette: 'Refunded',
865
+ properties: {
866
+ roles: {},
867
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'cancelled', expires: getDaysAgo(4), cancellation: { pending: false }, payment: getTestPayment('refunded', { order: false, startDate: getDaysAgo(5) }) },
868
+ },
869
+ },
870
+ // The USAGE persona ([#647](https://github.com/Omega-JS-Stack/omega/issues/647))
871
+ // — the ONE exception to "one persona, one purpose" that Ian asked for, and
872
+ // it is a deliberate one: a usage bar is only worth previewing beside its
873
+ // neighbours, so the spread lives on DIFFERENT features of ONE account and
874
+ // each feature still tells exactly one story. Sign in as it from the website
875
+ // like any other persona and the account page shows every state at once:
876
+ // - a feature nobody has touched (the empty bar),
877
+ // - one half spent (the ordinary case),
878
+ // - one whose DAY share is gone while the month still has room
879
+ // (the "try again tomorrow" refusal),
880
+ // - one whose MONTH is gone (the "upgrade" refusal),
881
+ // - one carrying admin-granted override credits on top of the plan.
882
+ // The feature ids are the brand catalog's, so a brand whose catalog names
883
+ // other features simply seeds fewer bars — never a bar with no definition.
884
+ 'usage-spread': {
885
+ id: 'usage-spread',
886
+ uid: '_test-usage-spread',
887
+ email: '_test.usage-spread@{domain}',
888
+ palette: 'Usage',
889
+ properties: {
890
+ roles: {},
891
+ subscription: { product: { id: 'premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: getTestPayment('usage-spread') },
892
+ usage: USAGE_SPREAD,
893
+ },
894
+ },
895
+ delete: {
896
+ id: 'delete',
897
+ uid: '_test-delete',
898
+ email: '_test.delete@{domain}',
899
+ properties: {
900
+ roles: {},
901
+ // Active subscription - deletion should be blocked initially
902
+ subscription: { product: { id: 'premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: getTestPayment('delete') },
903
+ },
904
+ },
905
+ 'delete-by-admin': {
906
+ id: 'delete-by-admin',
907
+ uid: '_test-delete-by-admin',
908
+ email: '_test.delete-by-admin@{domain}',
909
+ properties: {
910
+ roles: {},
911
+ // No subscription - can be deleted immediately by admin
912
+ },
913
+ },
914
+ referrer: {
915
+ id: 'referrer',
916
+ uid: '_test-referrer',
917
+ email: '_test.referrer@{domain}',
918
+ palette: 'Referrer',
919
+ properties: {
920
+ roles: {},
921
+ subscription: { product: { id: 'basic' }, status: 'active' },
922
+ affiliate: { code: 'TESTREF', referrals: [] },
923
+ },
924
+ },
925
+ // The other side of that link (Ian 2026-08-19,
926
+ // [#369](https://github.com/Omega-JS-Stack/omega/issues/369)): the account that
927
+ // CAME IN through the referrer's code, and whose one distinguishing state is
928
+ // exactly that. Its `attribution.affiliate` is resolved in
929
+ // getAccountDefinitions (buildInboundReferral) off the referrer's own record,
930
+ // so the two halves can never disagree.
931
+ //
932
+ // It is deliberately NOT the journey `signup-referred` account: that one exists
933
+ // to EARN its referral live in the signup suite, and seeding one for it would
934
+ // answer the very assertion that suite is there to make. A persona is a bare
935
+ // ROLE, so this one is plain `referred`; the lane-scoped rig is the one
936
+ // carrying its lane's prefix (Ian 2026-08-19).
937
+ referred: {
938
+ id: 'referred',
939
+ uid: '_test-referred',
940
+ email: '_test.referred@{domain}',
941
+ palette: 'Referred',
942
+ properties: {
943
+ roles: {},
944
+ subscription: { product: { id: 'basic' }, status: 'active' },
945
+ },
946
+ },
947
+ };
948
+
949
+ /**
950
+ * Journey test accounts - for testing subscription/payment flows
951
+ * These accounts transition through states via webhook tests
952
+ */
953
+ const JOURNEY_ACCOUNTS = {
954
+ // EVERY account the signup suites sign up lives here — journey, not
955
+ // static: a static seed stamps flags.signupProcessed + consent (cp157),
956
+ // which 400s the very signup these accounts exist to exercise (cp197
957
+ // corpus catch — latently red since cp157; `referrer` stays static, the
958
+ // pre-existing affiliate owner must be established before signups run).
959
+ // The two `consent-*` allow-accounts keep the `_test.allow_*` prefix so
960
+ // the EXTENDED-mode lifecycle test can round-trip SendGrid + Beehiiv
961
+ // (it establishes its own consent state — no seed needed).
962
+ 'consent-granted': {
963
+ id: 'consent-granted',
964
+ uid: '_test-allow-consent-granted',
965
+ email: '_test.allow_consent-granted@{domain}',
966
+ properties: {
967
+ roles: {},
968
+ subscription: { product: { id: 'basic' }, status: 'active' },
969
+ },
970
+ },
971
+ 'consent-declined': {
972
+ id: 'consent-declined',
973
+ uid: '_test-allow-consent-declined',
974
+ email: '_test.allow_consent-declined@{domain}',
975
+ properties: {
976
+ roles: {},
977
+ subscription: { product: { id: 'basic' }, status: 'active' },
978
+ },
979
+ },
980
+ 'consent-missing': {
981
+ id: 'consent-missing',
982
+ uid: '_test-consent-missing',
983
+ email: '_test.consent-missing@{domain}',
984
+ properties: {
985
+ roles: {},
986
+ subscription: { product: { id: 'basic' }, status: 'active' },
987
+ },
988
+ },
989
+ // Used to verify the never-downgrade guard: the test seeds this account's doc with already-
990
+ // granted consent, then re-fires /user/signup with an empty consent payload and asserts the
991
+ // grant is preserved (not flipped to revoked). Dedicated account so the seeded state is isolated.
992
+ 'consent-preserve': {
993
+ id: 'consent-preserve',
994
+ uid: '_test-consent-preserve',
995
+ email: '_test.consent-preserve@{domain}',
996
+ properties: {
997
+ roles: {},
998
+ subscription: { product: { id: 'basic' }, status: 'active' },
999
+ },
1000
+ },
1001
+ // Used to verify buildUserRecord's layered deep-merge: the test seeds this account's doc with
1002
+ // real values (api keys, paid subscription, admin role, a custom non-schema field) + a partial
1003
+ // attribution, fires /user/signup, and asserts the merge PRESERVES those real/custom values
1004
+ // while still filling every schema leaf and applying the signup data on top.
1005
+ 'signup-merge': {
1006
+ id: 'signup-merge',
1007
+ uid: '_test-signup-merge',
1008
+ email: '_test.signup-merge@{domain}',
1009
+ properties: {
1010
+ roles: {},
1011
+ subscription: { product: { id: 'basic' }, status: 'active' },
1012
+ },
1013
+ },
1014
+ 'signup-referred': {
1015
+ id: 'signup-referred',
1016
+ uid: '_test-signup-referred',
1017
+ email: '_test.signup-referred@{domain}',
1018
+ properties: {
1019
+ roles: {},
1020
+ subscription: { product: { id: 'basic' }, status: 'active' },
1021
+ },
1022
+ },
1023
+ 'signup-referred-invalid': {
1024
+ id: 'signup-referred-invalid',
1025
+ uid: '_test-signup-referred-invalid',
1026
+ email: '_test.signup-referred-invalid@{domain}',
1027
+ properties: {
1028
+ roles: {},
1029
+ subscription: { product: { id: 'basic' }, status: 'active' },
1030
+ },
1031
+ },
1032
+ // Signs up so the four onboarding emails can be read back out of the
1033
+ // testing-mode capture and the queue ([#774](https://github.com/Omega-JS-Stack/omega/issues/774)) — its own account, because the
1034
+ // signup is the act under test and no other suite may have spent it.
1035
+ 'signup-emails': {
1036
+ id: 'signup-emails',
1037
+ uid: '_test-signup-emails',
1038
+ email: '_test.signup-emails@{domain}',
1039
+ properties: {
1040
+ roles: {},
1041
+ subscription: { product: { id: 'basic' }, status: 'active' },
1042
+ },
1043
+ },
1044
+ 'signup-referred-disposable': {
1045
+ id: 'signup-referred-disposable',
1046
+ uid: '_test-signup-referred-disposable',
1047
+ email: '_test.signup-referred-disposable@mailinator.com',
1048
+ properties: {
1049
+ roles: {},
1050
+ subscription: { product: { id: 'basic' }, status: 'active' },
1051
+ },
1052
+ },
1053
+ 'journey-payments-upgrade': {
1054
+ id: 'journey-payments-upgrade',
1055
+ uid: '_test-journey-payments-upgrade',
1056
+ email: '_test.journey-payments-upgrade@{domain}',
1057
+ properties: {
1058
+ roles: {},
1059
+ subscription: { product: { id: 'basic' }, status: 'active' },
1060
+ personal: { name: { first: 'Jordan', last: 'Upgrade' } },
1061
+ },
1062
+ },
1063
+ 'journey-payments-cancel': {
1064
+ id: 'journey-payments-cancel',
1065
+ uid: '_test-journey-payments-cancel',
1066
+ email: '_test.journey-payments-cancel@{domain}',
1067
+ properties: {
1068
+ roles: {},
1069
+ subscription: { product: { id: 'basic' }, status: 'active' },
1070
+ personal: { name: { first: 'Casey', last: 'Cancel' } },
1071
+ },
1072
+ },
1073
+ 'journey-payments-suspend': {
1074
+ id: 'journey-payments-suspend',
1075
+ uid: '_test-journey-payments-suspend',
1076
+ email: '_test.journey-payments-suspend@{domain}',
1077
+ properties: {
1078
+ roles: {},
1079
+ subscription: { product: { id: 'basic' }, status: 'active' },
1080
+ personal: { name: { first: 'Sam', last: 'Suspend' } },
1081
+ },
1082
+ },
1083
+ 'journey-payments-trial': {
1084
+ id: 'journey-payments-trial',
1085
+ uid: '_test-journey-payments-trial',
1086
+ email: '_test.journey-payments-trial@{domain}',
1087
+ properties: {
1088
+ roles: {},
1089
+ subscription: { product: { id: 'basic' }, status: 'active' },
1090
+ personal: { name: { first: 'Taylor', last: 'Trial' } },
1091
+ },
1092
+ },
1093
+ 'journey-payments-trial-cancel': {
1094
+ id: 'journey-payments-trial-cancel',
1095
+ uid: '_test-journey-payments-trial-cancel',
1096
+ email: '_test.journey-payments-trial-cancel@{domain}',
1097
+ properties: {
1098
+ roles: {},
1099
+ subscription: { product: { id: 'basic' }, status: 'active' },
1100
+ personal: { name: { first: 'Morgan', last: 'Trial' } },
1101
+ },
1102
+ },
1103
+ 'journey-payments-failure': {
1104
+ id: 'journey-payments-failure',
1105
+ uid: '_test-journey-payments-failure',
1106
+ email: '_test.journey-payments-failure@{domain}',
1107
+ properties: {
1108
+ roles: {},
1109
+ subscription: { product: { id: 'basic' }, status: 'active' },
1110
+ personal: { name: { first: 'Riley', last: 'Failure' } },
1111
+ },
1112
+ },
1113
+ 'journey-payments-decline': {
1114
+ id: 'journey-payments-decline',
1115
+ uid: '_test-journey-payments-decline',
1116
+ email: '_test.journey-payments-decline@{domain}',
1117
+ properties: {
1118
+ roles: {},
1119
+ subscription: { product: { id: 'basic' }, status: 'active' },
1120
+ personal: { name: { first: 'Dana', last: 'Decline' } },
1121
+ },
1122
+ },
1123
+ 'journey-payments-plan-change': {
1124
+ id: 'journey-payments-plan-change',
1125
+ uid: '_test-journey-payments-plan-change',
1126
+ email: '_test.journey-payments-plan-change@{domain}',
1127
+ properties: {
1128
+ roles: {},
1129
+ subscription: { product: { id: 'basic' }, status: 'active' },
1130
+ personal: { name: { first: 'Avery', last: 'Planchg' } },
1131
+ },
1132
+ },
1133
+ 'journey-payments-one-time': {
1134
+ id: 'journey-payments-one-time',
1135
+ uid: '_test-journey-payments-one-time',
1136
+ email: '_test.journey-payments-one-time@{domain}',
1137
+ properties: {
1138
+ roles: {},
1139
+ subscription: { product: { id: 'basic' }, status: 'active' },
1140
+ },
1141
+ },
1142
+ 'journey-payments-intent': {
1143
+ id: 'journey-payments-intent',
1144
+ uid: '_test-journey-payments-intent',
1145
+ email: '_test.journey-payments-intent@{domain}',
1146
+ properties: {
1147
+ roles: {},
1148
+ subscription: { product: { id: 'basic' }, status: 'active' },
1149
+ },
1150
+ },
1151
+ 'journey-payments-cancel-route': {
1152
+ id: 'journey-payments-cancel-route',
1153
+ uid: '_test-journey-payments-cancel-route',
1154
+ email: '_test.journey-payments-cancel-route@{domain}',
1155
+ properties: {
1156
+ roles: {},
1157
+ subscription: { product: { id: 'basic' }, status: 'active' },
1158
+ },
1159
+ },
1160
+ 'route-cancel-success': {
1161
+ id: 'route-cancel-success',
1162
+ uid: '_test-route-cancel-success',
1163
+ email: '_test.route-cancel-success@{domain}',
1164
+ properties: {
1165
+ roles: {},
1166
+ subscription: { product: { id: 'basic' }, status: 'active' },
1167
+ },
1168
+ },
1169
+ 'route-winback-success': {
1170
+ id: 'route-winback-success',
1171
+ uid: '_test-route-winback-success',
1172
+ email: '_test.route-winback-success@{domain}',
1173
+ properties: {
1174
+ roles: {},
1175
+ subscription: { product: { id: 'basic' }, status: 'active' },
1176
+ },
1177
+ },
1178
+ 'journey-payments-portal-route': {
1179
+ id: 'journey-payments-portal-route',
1180
+ uid: '_test-journey-payments-portal-route',
1181
+ email: '_test.journey-payments-portal-route@{domain}',
1182
+ properties: {
1183
+ roles: {},
1184
+ subscription: { product: { id: 'basic' }, status: 'active' },
1185
+ },
1186
+ },
1187
+ 'journey-payments-intent-discount': {
1188
+ id: 'journey-payments-intent-discount',
1189
+ uid: '_test-journey-payments-intent-discount',
1190
+ email: '_test.journey-payments-intent-discount@{domain}',
1191
+ properties: {
1192
+ roles: {},
1193
+ subscription: { product: { id: 'basic' }, status: 'active' },
1194
+ personal: { name: { first: 'Drew', last: 'Discount' } },
1195
+ },
1196
+ },
1197
+ 'intent-discount-validation': {
1198
+ id: 'intent-discount-validation',
1199
+ uid: '_test-intent-discount-validation',
1200
+ email: '_test.intent-discount-validation@{domain}',
1201
+ properties: {
1202
+ roles: {},
1203
+ subscription: { product: { id: 'basic' }, status: 'active' },
1204
+ },
1205
+ },
1206
+ // The discount-amounts suite's purchasers, one per case
1207
+ // ([#406](https://github.com/Omega-JS-Stack/omega/issues/406)): every case
1208
+ // there BUYS a subscription, and the checkout guard refuses a caller who
1209
+ // already holds one, so the cases cannot share an account. They are declared
1210
+ // here for the same reason as the rest — a purchaser is an account, and the
1211
+ // seed is where accounts are born.
1212
+ 'intent-discount-percent-url': {
1213
+ id: 'intent-discount-percent-url',
1214
+ uid: '_test-intent-discount-percent-url',
1215
+ email: '_test.intent-discount-percent-url@{domain}',
1216
+ properties: {
1217
+ roles: {},
1218
+ subscription: { product: { id: 'basic' }, status: 'active' },
1219
+ },
1220
+ },
1221
+ 'intent-discount-percent-payload': {
1222
+ id: 'intent-discount-percent-payload',
1223
+ uid: '_test-intent-discount-percent-payload',
1224
+ email: '_test.intent-discount-percent-payload@{domain}',
1225
+ properties: {
1226
+ roles: {},
1227
+ subscription: { product: { id: 'basic' }, status: 'active' },
1228
+ },
1229
+ },
1230
+ 'intent-discount-percent-invoice': {
1231
+ id: 'intent-discount-percent-invoice',
1232
+ uid: '_test-intent-discount-percent-invoice',
1233
+ email: '_test.intent-discount-percent-invoice@{domain}',
1234
+ properties: {
1235
+ roles: {},
1236
+ subscription: { product: { id: 'basic' }, status: 'active' },
1237
+ },
1238
+ },
1239
+ 'intent-discount-one-time': {
1240
+ id: 'intent-discount-one-time',
1241
+ uid: '_test-intent-discount-one-time',
1242
+ email: '_test.intent-discount-one-time@{domain}',
1243
+ properties: {
1244
+ roles: {},
1245
+ subscription: { product: { id: 'basic' }, status: 'active' },
1246
+ },
1247
+ },
1248
+ 'intent-discount-none': {
1249
+ id: 'intent-discount-none',
1250
+ uid: '_test-intent-discount-none',
1251
+ email: '_test.intent-discount-none@{domain}',
1252
+ properties: {
1253
+ roles: {},
1254
+ subscription: { product: { id: 'basic' }, status: 'active' },
1255
+ },
1256
+ },
1257
+ 'intent-discount-trial': {
1258
+ id: 'intent-discount-trial',
1259
+ uid: '_test-intent-discount-trial',
1260
+ email: '_test.intent-discount-trial@{domain}',
1261
+ properties: {
1262
+ roles: {},
1263
+ subscription: { product: { id: 'basic' }, status: 'active' },
1264
+ },
1265
+ },
1266
+ 'intent-discount-amount-url': {
1267
+ id: 'intent-discount-amount-url',
1268
+ uid: '_test-intent-discount-amount-url',
1269
+ email: '_test.intent-discount-amount-url@{domain}',
1270
+ properties: {
1271
+ roles: {},
1272
+ subscription: { product: { id: 'basic' }, status: 'active' },
1273
+ },
1274
+ },
1275
+ 'intent-discount-amount-payload': {
1276
+ id: 'intent-discount-amount-payload',
1277
+ uid: '_test-intent-discount-amount-payload',
1278
+ email: '_test.intent-discount-amount-payload@{domain}',
1279
+ properties: {
1280
+ roles: {},
1281
+ subscription: { product: { id: 'basic' }, status: 'active' },
1282
+ },
1283
+ },
1284
+ 'intent-discount-amount-trial': {
1285
+ id: 'intent-discount-amount-trial',
1286
+ uid: '_test-intent-discount-amount-trial',
1287
+ email: '_test.intent-discount-amount-trial@{domain}',
1288
+ properties: {
1289
+ roles: {},
1290
+ subscription: { product: { id: 'basic' }, status: 'active' },
1291
+ },
1292
+ },
1293
+ // The zero-total refusal's purchaser ([#786](https://github.com/Omega-JS-Stack/omega/issues/786)).
1294
+ // This case is REFUSED before any provider runs, so it buys nothing — it
1295
+ // still needs an account of its own, because the route reads a real auth user
1296
+ // and user doc before it looks at the price at all.
1297
+ 'intent-discount-zero-total': {
1298
+ id: 'intent-discount-zero-total',
1299
+ uid: '_test-intent-discount-zero-total',
1300
+ email: '_test.intent-discount-zero-total@{domain}',
1301
+ properties: {
1302
+ roles: {},
1303
+ subscription: { product: { id: 'basic' }, status: 'active' },
1304
+ },
1305
+ },
1306
+ 'journey-payments-intent-attribution': {
1307
+ id: 'journey-payments-intent-attribution',
1308
+ uid: '_test-journey-payments-intent-attribution',
1309
+ email: '_test.journey-payments-intent-attribution@{domain}',
1310
+ properties: {
1311
+ roles: {},
1312
+ subscription: { product: { id: 'basic' }, status: 'active' },
1313
+ },
1314
+ },
1315
+ 'journey-payments-intent-trial': {
1316
+ id: 'journey-payments-intent-trial',
1317
+ uid: '_test-journey-payments-intent-trial',
1318
+ email: '_test.journey-payments-intent-trial@{domain}',
1319
+ properties: {
1320
+ roles: {},
1321
+ subscription: { product: { id: 'basic' }, status: 'active' },
1322
+ },
1323
+ },
1324
+ // Dedicated accounts for cancel validation tests — each needs a distinct, non-conflicting subscription state
1325
+ 'cancel-no-provider': {
1326
+ id: 'cancel-no-provider',
1327
+ uid: '_test-cancel-no-provider',
1328
+ email: '_test.cancel-no-provider@{domain}',
1329
+ properties: {
1330
+ roles: {},
1331
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: { provider: null, resourceId: null } },
1332
+ },
1333
+ },
1334
+ 'cancel-already-pending': {
1335
+ id: 'cancel-already-pending',
1336
+ uid: '_test-cancel-already-pending',
1337
+ email: '_test.cancel-already-pending@{domain}',
1338
+ properties: {
1339
+ roles: {},
1340
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: true }, payment: { provider: 'stripe', resourceId: 'sub_test_fake' } },
1341
+ },
1342
+ },
1343
+ 'cancel-unknown-provider': {
1344
+ id: 'cancel-unknown-provider',
1345
+ uid: '_test-cancel-unknown-provider',
1346
+ email: '_test.cancel-unknown-provider@{domain}',
1347
+ properties: {
1348
+ roles: {},
1349
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: { provider: 'unknown-provider', resourceId: 'sub_test_fake' } },
1350
+ },
1351
+ },
1352
+ 'cancel-too-young': {
1353
+ id: 'cancel-too-young',
1354
+ uid: '_test-cancel-too-young',
1355
+ email: '_test.cancel-too-young@{domain}',
1356
+ properties: {
1357
+ roles: {},
1358
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: { provider: 'test', resourceId: 'sub_test_fake', startDate: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } } },
1359
+ },
1360
+ },
1361
+ 'cancel-suspended': {
1362
+ id: 'cancel-suspended',
1363
+ uid: '_test-cancel-suspended',
1364
+ email: '_test.cancel-suspended@{domain}',
1365
+ properties: {
1366
+ roles: {},
1367
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'suspended', expires: getPastExpires(), cancellation: { pending: false }, payment: { provider: 'test', resourceId: 'sub_test_suspended', startDate: getPastExpires() } },
1368
+ },
1369
+ },
1370
+ // A paid subscriber whose order doc never existed (#210): payment.orderId stays null,
1371
+ // so the cancel provider has to resolve the plan's product from the subscription itself.
1372
+ 'cancel-no-order': {
1373
+ id: 'cancel-no-order',
1374
+ uid: '_test-cancel-no-order',
1375
+ email: '_test.cancel-no-order@{domain}',
1376
+ properties: {
1377
+ roles: {},
1378
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: { provider: 'test', resourceId: 'sub_test_no_order', startDate: getPastExpires() } },
1379
+ },
1380
+ },
1381
+ // Dedicated accounts for portal validation tests
1382
+ 'portal-no-provider': {
1383
+ id: 'portal-no-provider',
1384
+ uid: '_test-portal-no-provider',
1385
+ email: '_test.portal-no-provider@{domain}',
1386
+ properties: {
1387
+ roles: {},
1388
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), payment: { provider: null, resourceId: null } },
1389
+ },
1390
+ },
1391
+ 'portal-unknown-provider': {
1392
+ id: 'portal-unknown-provider',
1393
+ uid: '_test-portal-unknown-provider',
1394
+ email: '_test.portal-unknown-provider@{domain}',
1395
+ properties: {
1396
+ roles: {},
1397
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), payment: { provider: 'unknown-provider', resourceId: 'sub_test_fake' } },
1398
+ },
1399
+ },
1400
+ // Dedicated accounts for refund validation tests
1401
+ 'refund-active-no-cancel': {
1402
+ id: 'refund-active-no-cancel',
1403
+ uid: '_test-refund-active-no-cancel',
1404
+ email: '_test.refund-active-no-cancel@{domain}',
1405
+ properties: {
1406
+ roles: {},
1407
+ // A RECENT purchase: this fixture must be refused for having no cancellation,
1408
+ // so its payment stays well inside the refund window the guard behind that
1409
+ // one would apply.
1410
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: { provider: 'test', resourceId: 'sub_test_fake', startDate: getDaysAgo(3) } },
1411
+ },
1412
+ },
1413
+ 'refund-no-provider': {
1414
+ id: 'refund-no-provider',
1415
+ uid: '_test-refund-no-provider',
1416
+ email: '_test.refund-no-provider@{domain}',
1417
+ properties: {
1418
+ roles: {},
1419
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'cancelled', expires: getPastExpires(), cancellation: { pending: false }, payment: { provider: null, resourceId: null } },
1420
+ },
1421
+ },
1422
+ 'refund-unknown-provider': {
1423
+ id: 'refund-unknown-provider',
1424
+ uid: '_test-refund-unknown-provider',
1425
+ email: '_test.refund-unknown-provider@{domain}',
1426
+ properties: {
1427
+ roles: {},
1428
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'cancelled', expires: getPastExpires(), cancellation: { pending: false }, payment: { provider: 'unknown-provider', resourceId: 'sub_test_fake' } },
1429
+ },
1430
+ },
1431
+ 'refund-expired-payment': {
1432
+ id: 'refund-expired-payment',
1433
+ uid: '_test-refund-expired-payment',
1434
+ email: '_test.refund-expired-payment@{domain}',
1435
+ properties: {
1436
+ roles: {},
1437
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'cancelled', expires: getPastExpires(), cancellation: { pending: false }, payment: { provider: 'test', resourceId: 'sub_test_fake', startDate: getPastExpires() } },
1438
+ },
1439
+ },
1440
+ // A paid subscriber, pending cancellation, whose order doc never existed (#216):
1441
+ // payment.orderId stays null, so the refund provider has to resolve the plan's
1442
+ // product from the subscription itself.
1443
+ 'refund-no-order': {
1444
+ id: 'refund-no-order',
1445
+ uid: '_test-refund-no-order',
1446
+ email: '_test.refund-no-order@{domain}',
1447
+ properties: {
1448
+ roles: {},
1449
+ subscription: { product: { id: 'premium', name: 'Premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: true }, payment: { provider: 'test', resourceId: 'sub_test_refund_no_order', startDate: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } } },
1450
+ },
1451
+ },
1452
+ 'route-refund-success': {
1453
+ id: 'route-refund-success',
1454
+ uid: '_test-route-refund-success',
1455
+ email: '_test.route-refund-success@{domain}',
1456
+ properties: {
1457
+ roles: {},
1458
+ subscription: { product: { id: 'basic' }, status: 'active' },
1459
+ },
1460
+ },
1461
+ // Journey: refund webhook transition (charge.refunded fires payment-refunded transition)
1462
+ 'journey-payments-refund-webhook': {
1463
+ id: 'journey-payments-refund-webhook',
1464
+ uid: '_test-journey-payments-refund-webhook',
1465
+ email: '_test.journey-payments-refund-webhook@{domain}',
1466
+ properties: {
1467
+ roles: {},
1468
+ subscription: { product: { id: 'basic' }, status: 'active' },
1469
+ },
1470
+ },
1471
+ // Journey: one-time purchase refunded end-to-end (buy with the test provider, refund it)
1472
+ 'journey-payments-one-time-refund': {
1473
+ id: 'journey-payments-one-time-refund',
1474
+ uid: '_test-journey-payments-one-time-refund',
1475
+ email: '_test.journey-payments-one-time-refund@{domain}',
1476
+ properties: {
1477
+ roles: {},
1478
+ subscription: { product: { id: 'basic' }, status: 'active' },
1479
+ },
1480
+ },
1481
+ // Journey: UID resolution fallback (webhook without uid in metadata, resolved from fetched resource)
1482
+ 'journey-payments-uid-resolution': {
1483
+ id: 'journey-payments-uid-resolution',
1484
+ uid: '_test-journey-payments-uid-resolution',
1485
+ email: '_test.journey-payments-uid-resolution@{domain}',
1486
+ properties: {
1487
+ roles: {},
1488
+ subscription: { product: { id: 'basic' }, status: 'active' },
1489
+ },
1490
+ },
1491
+ // Journey: legacy product ID resolution (webhook with legacy product ID maps to correct product)
1492
+ 'journey-payments-legacy-product': {
1493
+ id: 'journey-payments-legacy-product',
1494
+ uid: '_test-journey-payments-legacy-product',
1495
+ email: '_test.journey-payments-legacy-product@{domain}',
1496
+ properties: {
1497
+ roles: {},
1498
+ subscription: { product: { id: 'basic' }, status: 'active' },
1499
+ },
1500
+ },
1501
+ // ── Suite-exclusive machinery ([#406](https://github.com/Omega-JS-Stack/omega/issues/406)) ──
1502
+ // The accounts these suites used to fabricate mid-test. Any account a test
1503
+ // uses is exclusive to that test, and a HEALTHY one is DECLARED here rather
1504
+ // than minted: the seed is the one home, so everything exists up front and a
1505
+ // suite starts from an account with both halves in sync (auth user + doc)
1506
+ // instead of standing one up itself. Each is named for the suite that drives
1507
+ // it, carries no palette label (machinery is never offered to a human), and
1508
+ // starts free — the state each journey needs is the state IT writes.
1509
+ //
1510
+ // Deliberately BROKEN states stay out: the seed can only make healthy
1511
+ // accounts, so the guard suites that need half a pair (an auth user with no
1512
+ // doc, a doc with no auth user) keep building it themselves.
1513
+ 'journey-payments-winback': {
1514
+ id: 'journey-payments-winback',
1515
+ uid: '_test-journey-payments-winback',
1516
+ email: '_test.journey-payments-winback@{domain}',
1517
+ properties: {
1518
+ roles: {},
1519
+ subscription: { product: { id: 'basic' }, status: 'active' },
1520
+ personal: { name: { first: 'Wren', last: 'Winback' } },
1521
+ },
1522
+ },
1523
+ 'journey-payments-winback-decline': {
1524
+ id: 'journey-payments-winback-decline',
1525
+ uid: '_test-journey-payments-winback-decline',
1526
+ email: '_test.journey-payments-winback-decline@{domain}',
1527
+ properties: {
1528
+ roles: {},
1529
+ subscription: { product: { id: 'basic' }, status: 'active' },
1530
+ personal: { name: { first: 'Wade', last: 'Decline' } },
1531
+ },
1532
+ },
1533
+ 'journey-payments-plan-switch': {
1534
+ id: 'journey-payments-plan-switch',
1535
+ uid: '_test-journey-payments-plan-switch',
1536
+ email: '_test.journey-payments-plan-switch@{domain}',
1537
+ properties: {
1538
+ roles: {},
1539
+ subscription: { product: { id: 'basic' }, status: 'active' },
1540
+ personal: { name: { first: 'Sasha', last: 'Switch' } },
1541
+ },
1542
+ },
1543
+ 'journey-payments-plan-switch-trial': {
1544
+ id: 'journey-payments-plan-switch-trial',
1545
+ uid: '_test-journey-payments-plan-switch-trial',
1546
+ email: '_test.journey-payments-plan-switch-trial@{domain}',
1547
+ properties: {
1548
+ roles: {},
1549
+ subscription: { product: { id: 'basic' }, status: 'active' },
1550
+ personal: { name: { first: 'Sage', last: 'Switch' } },
1551
+ },
1552
+ },
1553
+ 'journey-payments-uncancel': {
1554
+ id: 'journey-payments-uncancel',
1555
+ uid: '_test-journey-payments-uncancel',
1556
+ email: '_test.journey-payments-uncancel@{domain}',
1557
+ properties: {
1558
+ roles: {},
1559
+ subscription: { product: { id: 'basic' }, status: 'active' },
1560
+ personal: { name: { first: 'Uma', last: 'Uncancel' } },
1561
+ },
1562
+ },
1563
+ 'journey-payments-abandoned': {
1564
+ id: 'journey-payments-abandoned',
1565
+ uid: '_test-journey-payments-abandoned',
1566
+ email: '_test.journey-payments-abandoned@{domain}',
1567
+ properties: {
1568
+ roles: {},
1569
+ subscription: { product: { id: 'basic' }, status: 'active' },
1570
+ personal: { name: { first: 'Avery', last: 'Abandon' } },
1571
+ },
1572
+ },
1573
+ 'journey-payments-dispute': {
1574
+ id: 'journey-payments-dispute',
1575
+ uid: '_test-journey-payments-dispute',
1576
+ email: '_test.journey-payments-dispute@{domain}',
1577
+ properties: {
1578
+ roles: {},
1579
+ subscription: { product: { id: 'basic' }, status: 'active' },
1580
+ personal: { name: { first: 'Dana', last: 'Dispute' } },
1581
+ },
1582
+ },
1583
+ // The two webhook suites DELETE this doc before they run: the pipeline writes a
1584
+ // subscriber's doc from scratch only for a uid this project has an auth user for
1585
+ // ([#399](https://github.com/Omega-JS-Stack/omega/issues/399)), and that auth
1586
+ // user is the half the seed owns. The chargebee suite proves the opposite half:
1587
+ // with the provider unreachable, nothing recreates the doc at all
1588
+ // ([#506](https://github.com/Omega-JS-Stack/omega/issues/506)).
1589
+ 'webhook-chargebee-unreachable': {
1590
+ id: 'webhook-chargebee-unreachable',
1591
+ uid: '_test-webhook-chargebee-unreachable',
1592
+ email: '_test.webhook-chargebee-unreachable@{domain}',
1593
+ properties: {
1594
+ roles: {},
1595
+ subscription: { product: { id: 'basic' }, status: 'active' },
1596
+ },
1597
+ },
1598
+ 'webhook-retry-sweep': {
1599
+ id: 'webhook-retry-sweep',
1600
+ uid: '_test-webhook-retry-sweep',
1601
+ email: '_test.webhook-retry-sweep@{domain}',
1602
+ properties: {
1603
+ roles: {},
1604
+ subscription: { product: { id: 'basic' }, status: 'active' },
1605
+ },
1606
+ },
1607
+ // Dedicated accounts for user resolve tests — must not be reused by other tests
1608
+ 'resolve-premium-active': {
1609
+ id: 'resolve-premium-active',
1610
+ uid: '_test-resolve-premium-active',
1611
+ email: '_test.resolve-premium-active@{domain}',
1612
+ properties: {
1613
+ roles: {},
1614
+ subscription: { product: { id: 'premium' }, status: 'active', expires: getCycleExpires(), cancellation: { pending: false }, payment: getTestPayment('resolve-premium-active') },
1615
+ },
1616
+ },
1617
+ 'resolve-premium-expired': {
1618
+ id: 'resolve-premium-expired',
1619
+ uid: '_test-resolve-premium-expired',
1620
+ email: '_test.resolve-premium-expired@{domain}',
1621
+ properties: {
1622
+ roles: {},
1623
+ subscription: { product: { id: 'premium' }, status: 'cancelled', expires: getPastExpires(), cancellation: { pending: false }, payment: getTestPayment('resolve-premium-expired', { startDate: getPastExpires(2) }) },
1624
+ },
1625
+ },
1626
+ // Journey: marketing webhook revocation (test/routes/marketing/webhook.js). The
1627
+ // SendGrid/Beehiiv revoke-event tests repeatedly write consent.marketing.status='revoked'
1628
+ // to the target account — persistent side-effect data, so it must never be the shared
1629
+ // `basic` account (revoked consent would persist for the rest of the run and trip the
1630
+ // email library's consent gate for every later sync of that account).
1631
+ 'journey-webhook-revoke': {
1632
+ id: 'journey-webhook-revoke',
1633
+ uid: '_test-journey-webhook-revoke',
1634
+ email: '_test.journey-webhook-revoke@{domain}',
1635
+ properties: {
1636
+ roles: {},
1637
+ subscription: { product: { id: 'basic' }, status: 'active' },
1638
+ personal: { name: { first: 'Webb', last: 'Revoke' } },
1639
+ },
1640
+ },
1641
+ // Journey: live-provider sync round-trip (test/email/marketing-lifecycle.js, extended
1642
+ // mode only). The `_test.allow_*` email prefix bypasses the `_test.*` marketing block so
1643
+ // sync() reaches real SendGrid/Beehiiv; the suite's cleanup DELETE then removes the
1644
+ // contact AND mirrors revoked consent to this account's doc. Dedicated account so that
1645
+ // side effect stays isolated — the shared `consent-granted` sentinel is used by the
1646
+ // signup and consent-lifecycle suites and must keep its granted state.
1647
+ 'journey-marketing-sync': {
1648
+ id: 'journey-marketing-sync',
1649
+ uid: '_test-journey-marketing-sync',
1650
+ email: '_test.allow_journey-marketing-sync@{domain}',
1651
+ properties: {
1652
+ roles: {},
1653
+ subscription: { product: { id: 'basic' }, status: 'active' },
1654
+ personal: { name: { first: 'Lifecycle', last: 'Sync' } },
1655
+ },
1656
+ },
1657
+ // Flows-lane billing journeys (#209): the root `npm run test:flows` lane
1658
+ // drives a REAL browser through upgrade, cancel, a declined renewal, and a
1659
+ // trial. Ian's rule is one DEDICATED persona per journey — shared with no
1660
+ // other journey and no other lane — so no journey can inherit another's
1661
+ // subscription state. They are their own personas rather than the
1662
+ // same-named `journey-payments-*` ones for exactly that reason (and because
1663
+ // the cancel journey needs a paid start, which the payments cancel journey
1664
+ // must NOT have — it buys its own subscription first).
1665
+ //
1666
+ // Unlike every other journey account these are seeded as ESTABLISHED users
1667
+ // (signup-processed, legal consent granted, like the static personas): the
1668
+ // frontend consent guard signs a processed-but-unconsented doc out on its
1669
+ // NEXT page load, and a browser journey spans several loads.
1670
+ //
1671
+ // Trial-INELIGIBLE by design: this journey proves the DIRECT paid path, so
1672
+ // it must never be handed the 14-day trial the trial journey exists to
1673
+ // cover. Eligibility is NOT read off this doc — /payments/trial-eligibility
1674
+ // and the intent route's own downgrade guard both run the SAME query
1675
+ // (payments-orders where owner == uid and type == 'subscription'), so the
1676
+ // flows lane stands `orderId` up as a lapsed purchase record beside this
1677
+ // seed. The doc carries the matching story: it subscribed two years ago,
1678
+ // the term ran out a year ago, and it is back on basic — the shape the
1679
+ // cancel route itself writes when it resets a dead subscription.
1680
+ 'journey-flows-upgrade': {
1681
+ id: 'journey-flows-upgrade',
1682
+ uid: '_test-journey-flows-upgrade',
1683
+ email: '_test.journey-flows-upgrade@{domain}',
1684
+ palette: 'Journey: Upgrade',
1685
+ properties: {
1686
+ roles: {},
1687
+ subscription: {
1688
+ product: { id: 'basic' },
1689
+ status: 'cancelled',
1690
+ expires: getPastExpires(),
1691
+ cancellation: { pending: false },
1692
+ payment: { provider: 'test', resourceId: 'sub_test_journey_flows_upgrade', orderId: '_test-order-journey-flows-upgrade', startDate: getPastExpires(2) },
1693
+ },
1694
+ personal: { name: { first: 'Jules', last: 'Upgrade' } },
1695
+ flags: { signupProcessed: true },
1696
+ consent: seededConsent(),
1697
+ },
1698
+ },
1699
+ // The one journey that starts PAID: a cancellation needs something to
1700
+ // cancel, and the cancel route requires an active paid subscription with
1701
+ // provider details that is older than 24 hours (its age guard), so the
1702
+ // start state is seeded rather than bought. `orderId` names the purchase
1703
+ // record the flows lane stands up beside this doc — the test cancel
1704
+ // provider reads the plan's provider product id off that order.
1705
+ 'journey-flows-cancel': {
1706
+ id: 'journey-flows-cancel',
1707
+ uid: '_test-journey-flows-cancel',
1708
+ email: '_test.journey-flows-cancel@{domain}',
1709
+ palette: 'Journey: Cancel',
1710
+ properties: {
1711
+ roles: {},
1712
+ subscription: {
1713
+ product: { id: 'premium' },
1714
+ status: 'active',
1715
+ expires: getCycleExpires(),
1716
+ cancellation: { pending: false },
1717
+ payment: { provider: 'test', resourceId: 'sub_test_journey_flows_cancel', orderId: '_test-order-journey-flows-cancel', startDate: getPastExpires() },
1718
+ },
1719
+ personal: { name: { first: 'Kit', last: 'Cancel' } },
1720
+ flags: { signupProcessed: true },
1721
+ consent: seededConsent(),
1722
+ },
1723
+ },
1724
+ // Trial-INELIGIBLE for the same reason, and by the same means: a declined
1725
+ // RENEWAL is a PAYER's event, so this journey's checkout has to land a paid
1726
+ // subscription rather than the 14-day trial a pristine account is offered.
1727
+ // Same lapsed-history shape as the upgrade persona, with its own order
1728
+ // record — the eligibility query is per-owner.
1729
+ 'journey-flows-failure': {
1730
+ id: 'journey-flows-failure',
1731
+ uid: '_test-journey-flows-failure',
1732
+ email: '_test.journey-flows-failure@{domain}',
1733
+ palette: 'Journey: Failure',
1734
+ properties: {
1735
+ roles: {},
1736
+ subscription: {
1737
+ product: { id: 'basic' },
1738
+ status: 'cancelled',
1739
+ expires: getPastExpires(),
1740
+ cancellation: { pending: false },
1741
+ payment: { provider: 'test', resourceId: 'sub_test_journey_flows_failure', orderId: '_test-order-journey-flows-failure', startDate: getPastExpires(2) },
1742
+ },
1743
+ personal: { name: { first: 'Robin', last: 'Failure' } },
1744
+ flags: { signupProcessed: true },
1745
+ consent: seededConsent(),
1746
+ },
1747
+ },
1748
+ 'journey-flows-trial': {
1749
+ id: 'journey-flows-trial',
1750
+ uid: '_test-journey-flows-trial',
1751
+ email: '_test.journey-flows-trial@{domain}',
1752
+ palette: 'Journey: Trial',
1753
+ properties: {
1754
+ roles: {},
1755
+ subscription: { product: { id: 'basic' }, status: 'active' },
1756
+ personal: { name: { first: 'Quinn', last: 'Trial' } },
1757
+ flags: { signupProcessed: true },
1758
+ consent: seededConsent(),
1759
+ },
1760
+ },
1761
+ };
1762
+
1763
+ /**
1764
+ * Google picker personas (Ian 2026-07-16) — the Auth emulator's "Sign in
1765
+ * with Google" picker only lists accounts whose auth record carries a
1766
+ * google.com provider, so a fresh emulator showed "No Google accounts".
1767
+ * These are IMPORT-ONLY (importUsers with providerData — the one server-side
1768
+ * surface that writes it; the emulator refuses imports over an existing uid,
1769
+ * so they never go through createAccount). Deliberately NO user doc at seed
1770
+ * time: their FIRST popup sign-in fires auth:on-create and materializes the
1771
+ * doc exactly like a production Google user. Not part of TEST_ACCOUNTS —
1772
+ * they exist for the picker, not for API-credentialed test flows.
1773
+ */
1774
+ const GOOGLE_ACCOUNTS = {
1775
+ 'google-one': {
1776
+ id: 'google-one',
1777
+ uid: '_test-google-one',
1778
+ email: '_test.google.one@{domain}',
1779
+ name: 'Google One',
1780
+ },
1781
+ 'google-two': {
1782
+ id: 'google-two',
1783
+ uid: '_test-google-two',
1784
+ email: '_test.google.two@{domain}',
1785
+ name: 'Google Two',
1786
+ },
1787
+ };
1788
+
1789
+ /**
1790
+ * All test accounts combined
1791
+ */
1792
+ const TEST_ACCOUNTS = {
1793
+ ...STATIC_ACCOUNTS,
1794
+ ...JOURNEY_ACCOUNTS,
1795
+ };
1796
+
1797
+ /**
1798
+ * Canonical seeded-consent record (mirrors buildConsentRecord's granted
1799
+ * shape, source 'seed'): legal granted — the consent guard's requirement —
1800
+ * and marketing revoked (personas never join real marketing lists; the
1801
+ * _test.* block at the providers is the second fence).
1802
+ * @returns {object} consent user-doc section
1803
+ */
1804
+ function seededConsent() {
1805
+ const now = new Date();
1806
+ const stamp = {
1807
+ timestamp: now.toISOString(),
1808
+ timestampUNIX: Math.floor(now.getTime() / 1000),
1809
+ source: 'seed',
1810
+ ip: null,
1811
+ text: null,
1812
+ };
1813
+ const emptyMeta = { timestamp: null, timestampUNIX: null, source: null, ip: null, text: null };
1814
+
1815
+ return {
1816
+ legal: { status: 'granted', grantedAt: { ...stamp } },
1817
+ marketing: { status: 'revoked', grantedAt: { ...emptyMeta }, revokedAt: { ...stamp } },
1818
+ };
1819
+ }
1820
+
1821
+ /**
1822
+ * The whole seed TABLE — the framework's own accounts, then the project's, in
1823
+ * declaration order. The one merge every seed surface reads, so a project
1824
+ * persona reaches all of them or none of them
1825
+ * ([#712](https://github.com/Omega-JS-Stack/omega/issues/712)).
1826
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js,
1827
+ * keyed by id. A project account may override a built-in one by reusing its key.
1828
+ * @returns {object} The merged table, keyed by account id
1829
+ */
1830
+ function getAccountTable(extraAccounts) {
1831
+ return { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
1832
+ }
1833
+
1834
+ /**
1835
+ * The personas a HUMAN switches between: every seeded account carrying a
1836
+ * `palette` label, in the order the seeder declares them. What
1837
+ * `GET /test/roster` hands the dev palette.
1838
+ *
1839
+ * Composed from the TABLE rather than the assembled definitions: a localpart is
1840
+ * the part of a declared email before the `@`, so it needs no domain and no
1841
+ * catalog to resolve, and the labels only exist on the table.
1842
+ *
1843
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
1844
+ * @returns {{ localpart: string, label: string }[]} The roster, in declaration order
1845
+ */
1846
+ function getPaletteRoster(extraAccounts) {
1847
+ return Object.values(getAccountTable(extraAccounts))
1848
+ .filter((account) => account.palette)
1849
+ .map((account) => ({ localpart: (account.email || '').split('@')[0], label: account.palette }));
1850
+ }
1851
+
1852
+ /**
1853
+ * Get all test account definitions with resolved emails and dynamic product IDs
1854
+ * @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
1855
+ * @param {object} [config] - @omega.js/backend config (used to resolve first paid product)
1856
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js,
1857
+ * keyed by id, each `{ id, uid, email, properties }`. Merged after the built-in
1858
+ * accounts; a project account may override a built-in one by reusing its key.
1859
+ * @returns {object} Account definitions with resolved emails
1860
+ */
1861
+ function getAccountDefinitions(domain, config, extraAccounts) {
1862
+ const accounts = {};
1863
+
1864
+ const all = getAccountTable(extraAccounts);
1865
+
1866
+ for (const [key, account] of Object.entries(all)) {
1867
+ const properties = JSON.parse(JSON.stringify(account.properties || {}));
1868
+
1869
+ // Resolve the seeded subscription against the brand catalog: the real paid
1870
+ // product, and — for a persona that bought one — its price, cadence and the
1871
+ // end of the cycle it is being billed on.
1872
+ if (properties.subscription) {
1873
+ properties.subscription = resolveSeededSubscription(properties.subscription, config);
1874
+ }
1875
+
1876
+ // The usage persona's spread (#647): one state per counted feature the
1877
+ // brand's own catalog defines, priced by the plan it just resolved to.
1878
+ if (properties.usage === USAGE_SPREAD) {
1879
+ properties.usage = resolveSeededUsage(properties.subscription, config);
1880
+ }
1881
+
1882
+ // Every persona is a FULL account, journey ones included (Ian 2026-08-17,
1883
+ // [#327](https://github.com/Omega-JS-Stack/omega/issues/327)) — QA signs in as
1884
+ // one and must see exactly what a real user sees. Account creation fills the
1885
+ // schema's SHAPE and nothing else, so a persona that stopped there rendered as
1886
+ // a nameless account from nowhere on every surface that shows a person. The
1887
+ // profile is DERIVED from the persona key, so it is the same on every boot and
1888
+ // a new persona is born complete; anything a persona names for itself wins.
1889
+ fillMissing(properties, seededProfile(key, domain));
1890
+
1891
+ // The people this persona referred (#343). Resolved HERE rather than typed
1892
+ // into the definition because a referral records the referred account's
1893
+ // UID, and only the assembled table knows what those are.
1894
+ const referrals = buildReferralFixtures(key, all);
1895
+
1896
+ if (referrals) {
1897
+ properties.affiliate = { ...(properties.affiliate || {}), referrals };
1898
+ }
1899
+
1900
+ // And the other half of that referral (#369): the attribution the signup
1901
+ // wrote onto the account that came IN through the link. Resolved here for
1902
+ // the same reason — only the assembled table knows the referrer's code and
1903
+ // the stamp its record for this account carries.
1904
+ const referredBy = buildInboundReferral(key, all, domain);
1905
+
1906
+ if (referredBy) {
1907
+ properties.attribution = { ...(properties.attribution || {}), affiliate: referredBy };
1908
+ }
1909
+
1910
+ // STATIC personas are ESTABLISHED users: born signup-processed with
1911
+ // granted legal consent (source 'seed'), so the frontend consent guard —
1912
+ // which signs out processed-but-unconsented docs as signup orphans —
1913
+ // accepts every auth flow (password, custom token, Google picker).
1914
+ // JOURNEY accounts stay untouched: their tests exercise the signup
1915
+ // pipeline and seed their own flag/consent states explicitly.
1916
+ if (STATIC_ACCOUNTS[key]) {
1917
+ properties.flags = { signupProcessed: true, ...(properties.flags || {}) };
1918
+ properties.consent = properties.consent || seededConsent();
1919
+ }
1920
+
1921
+ accounts[key] = {
1922
+ id: account.id,
1923
+ uid: account.uid,
1924
+ email: (account.email || '').replace('{domain}', domain),
1925
+ properties,
1926
+ };
1927
+ }
1928
+
1929
+ return accounts;
1930
+ }
1931
+
1932
+ /**
1933
+ * Fetch privateKeys for test accounts from Firestore
1934
+ * @param {object} admin - Firebase admin instance
1935
+ * @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
1936
+ * @param {object} [config] - @omega.js/backend config (used to resolve first paid product)
1937
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
1938
+ * @returns {Promise<object>} Account credentials with privateKeys
1939
+ */
1940
+ async function fetchPrivateKeys(admin, domain, config, extraAccounts) {
1941
+ const definitions = getAccountDefinitions(domain, config, extraAccounts);
1942
+ const accounts = {};
1943
+
1944
+ // Fetch all in parallel
1945
+ const entries = Object.entries(definitions);
1946
+ const results = await Promise.all(
1947
+ entries.map(async ([key, account]) => {
1948
+ try {
1949
+ const doc = await admin.firestore().doc(`users/${account.uid}`).get();
1950
+
1951
+ if (doc.exists) {
1952
+ const data = doc.data();
1953
+ return {
1954
+ key,
1955
+ data: {
1956
+ uid: account.uid,
1957
+ email: account.email,
1958
+ privateKey: data.api?.privateKey || null,
1959
+ exists: true,
1960
+ },
1961
+ };
1962
+ }
1963
+
1964
+ return {
1965
+ key,
1966
+ data: {
1967
+ uid: account.uid,
1968
+ email: account.email,
1969
+ privateKey: null,
1970
+ exists: false,
1971
+ },
1972
+ };
1973
+ } catch (error) {
1974
+ console.error(`Error fetching account ${key}:`, error.message);
1975
+ return {
1976
+ key,
1977
+ data: {
1978
+ uid: account.uid,
1979
+ email: account.email,
1980
+ privateKey: null,
1981
+ exists: false,
1982
+ error: error.message,
1983
+ },
1984
+ };
1985
+ }
1986
+ })
1987
+ );
1988
+
1989
+ // Convert array back to object
1990
+ for (const { key, data } of results) {
1991
+ accounts[key] = data;
1992
+ }
1993
+
1994
+ return accounts;
1995
+ }
1996
+
1997
+ /**
1998
+ * Create a single test account
1999
+ * Assumes deleteTestUsers() was called first to ensure clean state
2000
+ * Creates Firebase Auth user, waits for auth:on-create, then merges test properties
2001
+ * @param {object} admin - Firebase admin instance
2002
+ * @param {object} account - Account definition with uid, email, properties
2003
+ * @returns {Promise<object>} Result { uid, email }
2004
+ */
2005
+ async function createAccount(admin, account) {
2006
+ const userRef = admin.firestore().doc(`users/${account.uid}`);
2007
+
2008
+ // The Auth user for this UID was just deleted (deleteTestUsers). Its auth:on-delete
2009
+ // trigger deletes the Firestore doc ASYNCHRONOUSLY and the emulator does NOT guarantee
2010
+ // it fires (or finishes) before our subsequent createUser()'s auth:on-create. A stale
2011
+ // on-delete can therefore land AFTER on-create and silently wipe the freshly-written
2012
+ // doc — leaving the account with no api.clientId/privateKey. That intermittent clobber
2013
+ // is what made the account-structure validation (and every downstream auth/payment test)
2014
+ // flaky. We defend with a verify-and-repair retry: create → wait for the on-create write
2015
+ // to be COMPLETE (api keys present, not just metadata.tag) → merge props → re-verify the
2016
+ // keys survived. If a late on-delete clobbered the doc, recreate from scratch.
2017
+ const maxAttempts = 3;
2018
+
2019
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2020
+ // Create Firebase Auth user - triggers auth:on-create. The deterministic
2021
+ // password is the manual-signin surface (see TEST_ACCOUNT_PASSWORD).
2022
+ await admin.auth().createUser({
2023
+ uid: account.uid,
2024
+ email: account.email,
2025
+ password: TEST_ACCOUNT_PASSWORD,
2026
+ emailVerified: true,
2027
+ }).catch(async (e) => {
2028
+ // A retry may find the Auth user already present (its doc was clobbered, not the
2029
+ // user). Delete it first so the fresh createUser re-fires a clean on-create.
2030
+ if (e.code === 'auth/uid-already-exists') {
2031
+ await admin.auth().deleteUser(account.uid).catch(() => {});
2032
+ await waitForDocGone(userRef);
2033
+ await admin.auth().createUser({
2034
+ uid: account.uid,
2035
+ email: account.email,
2036
+ password: TEST_ACCOUNT_PASSWORD,
2037
+ emailVerified: true,
2038
+ });
2039
+ } else {
2040
+ throw e;
2041
+ }
2042
+ });
2043
+
2044
+ // Wait for auth:on-create to COMPLETE. Poll on the api keys themselves — the fields the
2045
+ // tests actually require — not just metadata.tag, which on its own doesn't prove the
2046
+ // doc wasn't subsequently clobbered.
2047
+ const ready = await waitForAccountReady(userRef);
2048
+
2049
+ // Merge test-specific properties (roles, subscription, etc.)
2050
+ await userRef.set(account.properties, { merge: true });
2051
+
2052
+ // Re-verify after the merge: a late on-delete could have struck between the poll and
2053
+ // here. If the api keys survived, the account is good. Otherwise loop and recreate.
2054
+ const finalDoc = await userRef.get();
2055
+ const data = finalDoc.data() || {};
2056
+ if (ready && data.api?.clientId && data.api?.privateKey) {
2057
+ return { uid: account.uid, email: account.email };
2058
+ }
2059
+
2060
+ // Clobbered (or never completed). Tear down the Auth user so the next attempt starts
2061
+ // from a clean slate, then retry.
2062
+ if (attempt < maxAttempts) {
2063
+ await admin.auth().deleteUser(account.uid).catch(() => {});
2064
+ await waitForDocGone(userRef);
2065
+ }
2066
+ }
2067
+
2068
+ // Exhausted retries — return anyway so the runner reports the downstream failure with a
2069
+ // meaningful test assertion rather than a setup throw.
2070
+ return { uid: account.uid, email: account.email };
2071
+ }
2072
+
2073
+ /**
2074
+ * Import a FRESH auth record carrying a google.com provider so the Auth
2075
+ * emulator's Google sign-in picker lists it (the emulator refuses imports
2076
+ * over an existing uid — these accounts exist only via this import).
2077
+ * @param {object} admin - Firebase admin instance
2078
+ * @param {object} account - Definition with uid, resolved email, name
2079
+ */
2080
+ async function importGoogleAccount(admin, account) {
2081
+ // Clear the uid first — the emulator refuses an import over an existing
2082
+ // localId, and these personas carry no user doc, so the wipe's
2083
+ // Firestore-driven delete never reaches them. A back-to-back suite run
2084
+ // against a warm emulator therefore failed the ENTIRE seed here, which reads
2085
+ // as a red suite while nothing is broken
2086
+ // ([#241](https://github.com/Omega-JS-Stack/omega/issues/241)). Deleting
2087
+ // rather than skipping keeps the import authoritative: the record that lands
2088
+ // is always the definition above, never whatever a previous run left.
2089
+ await admin.auth().deleteUser(account.uid).catch(() => {});
2090
+
2091
+ const result = await admin.auth().importUsers([{
2092
+ uid: account.uid,
2093
+ email: account.email,
2094
+ emailVerified: true,
2095
+ displayName: account.name || account.email,
2096
+ providerData: [{
2097
+ uid: `google-${account.uid}`,
2098
+ providerId: 'google.com',
2099
+ email: account.email,
2100
+ displayName: account.name || account.email,
2101
+ }],
2102
+ }]);
2103
+
2104
+ if (result.failureCount > 0) {
2105
+ throw new Error(`importUsers (google provider): ${result.errors[0]?.error?.message || 'failed'}`);
2106
+ }
2107
+ }
2108
+
2109
+ /**
2110
+ * Poll until a user doc reflects a COMPLETE auth:on-create write (api keys present).
2111
+ * Returns true if it became ready within the window, false on timeout.
2112
+ */
2113
+ async function waitForAccountReady(userRef) {
2114
+ const maxWait = 15000;
2115
+ const pollInterval = 500;
2116
+ let waited = 0;
2117
+
2118
+ while (waited < maxWait) {
2119
+ const doc = await userRef.get();
2120
+ const data = doc.exists ? doc.data() : null;
2121
+ if (
2122
+ data?.metadata?.tag === 'auth:on-create'
2123
+ && data.api?.clientId
2124
+ && data.api?.privateKey
2125
+ ) {
2126
+ return true;
2127
+ }
2128
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
2129
+ waited += pollInterval;
2130
+ }
2131
+
2132
+ return false;
2133
+ }
2134
+
2135
+ /**
2136
+ * Poll until a user doc no longer exists (on-delete settled). Bounded; best-effort.
2137
+ */
2138
+ async function waitForDocGone(userRef) {
2139
+ const maxWait = 10000;
2140
+ const pollInterval = 200;
2141
+ let waited = 0;
2142
+
2143
+ while (waited < maxWait) {
2144
+ const doc = await userRef.get();
2145
+ if (!doc.exists) {
2146
+ return;
2147
+ }
2148
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
2149
+ waited += pollInterval;
2150
+ }
2151
+
2152
+ // Fallback: force-delete the lingering doc so the next create starts clean.
2153
+ await userRef.delete().catch(() => {});
2154
+ }
2155
+
2156
+ /**
2157
+ * Flush the ENTIRE emulator Firestore — every top-level collection, recursively.
2158
+ *
2159
+ * SAFETY: this is destructive, so it ONLY runs when connected to the Firestore
2160
+ * emulator (`FIRESTORE_EMULATOR_HOST` is set — which the test command always
2161
+ * sets). If that env var is absent, this is a no-op, so it can never wipe a real
2162
+ * project's data. The emulator DB is entirely test data, so a full flush is the
2163
+ * simplest correct "clean slate" — no per-collection allowlist to maintain.
2164
+ *
2165
+ * @param {object} admin - Firebase admin instance
2166
+ */
2167
+ async function flushEmulatorFirestore(admin) {
2168
+ if (!process.env.FIRESTORE_EMULATOR_HOST) {
2169
+ // Not pointed at the emulator — refuse to mass-delete. No-op.
2170
+ return;
2171
+ }
2172
+
2173
+ const firestore = admin.firestore();
2174
+ const collections = await firestore.listCollections().catch(() => []);
2175
+
2176
+ await Promise.all(
2177
+ collections.map((collectionRef) => firestore.recursiveDelete(collectionRef).catch(() => {}))
2178
+ );
2179
+ }
2180
+
2181
+ /**
2182
+ * The project whose auth store the wipe clears.
2183
+ *
2184
+ * The bulk-clear URL names a PROJECT, and the Auth emulator answers 200 for a
2185
+ * project it has never heard of — so a wrong id reads as a successful wipe
2186
+ * while every account survives. The id used to be
2187
+ * `process.env.GCLOUD_PROJECT || 'demo-test'` and the test runner's env never
2188
+ * carried GCLOUD_PROJECT, so every run cleared the empty `demo-test` store and
2189
+ * reported the personas deleted
2190
+ * ([#292](https://github.com/Omega-JS-Stack/omega/issues/292)).
2191
+ *
2192
+ * The authoritative identity is the one THIS process reads and writes through:
2193
+ * the admin app's own project id. GCLOUD_PROJECT must AGREE with it, else this
2194
+ * throws rather than reporting a wipe that never happened. In the runner the
2195
+ * two share an ancestry (run-tests.js seeds the admin app from GCLOUD_PROJECT
2196
+ * when it is set), so the disagreement arm is defense-in-depth for callers
2197
+ * that initialize admin themselves. Nothing to resolve throws for the same
2198
+ * reason: the silent default was the bug.
2199
+ *
2200
+ * @param {object} [admin] - Firebase admin instance (its default app names the project).
2201
+ * @param {object} [env] - Environment to read GCLOUD_PROJECT from (defaults to process.env).
2202
+ * @returns {string} The project id to clear.
2203
+ */
2204
+ function resolveWipeProjectId(admin, env) {
2205
+ let fromAdmin = null;
2206
+ try {
2207
+ fromAdmin = admin.app().options.projectId || null;
2208
+ } catch (e) {
2209
+ fromAdmin = null; // No admin, or no default app — the env is the only source left.
2210
+ }
2211
+
2212
+ const fromEnv = (env || process.env).GCLOUD_PROJECT || null;
2213
+
2214
+ if (fromAdmin && fromEnv && fromAdmin !== fromEnv) {
2215
+ throw new Error(`Refusing to wipe: GCLOUD_PROJECT is "${fromEnv}" but this process reads and writes project "${fromAdmin}" — clearing "${fromEnv}" would report success while every account survives`);
2216
+ }
2217
+
2218
+ const projectId = fromAdmin || fromEnv;
2219
+ if (!projectId) {
2220
+ throw new Error('Refusing to wipe: no project id to clear (neither the admin app nor GCLOUD_PROJECT names one) — a default here clears a store this run never touches and reports success');
2221
+ }
2222
+
2223
+ return projectId;
2224
+ }
2225
+
2226
+ /**
2227
+ * Delete all test users (both Auth and Firestore)
2228
+ * Uses TEST_ACCOUNTS (+ any project-defined accounts) as the source of truth for
2229
+ * which UIDs to delete. Deleting Auth users triggers on-delete which handles
2230
+ * Firestore doc + count decrement.
2231
+ * Called before test runs to ensure a clean slate. Flushes the ENTIRE emulator
2232
+ * Firestore (the emulator DB is 100% test data — there's nothing to preserve),
2233
+ * then deletes the Auth test users. `test/_init.js`'s `setup()` reseeds fixtures
2234
+ * afterward. Waits for Firestore docs to be deleted before returning.
2235
+ * @param {object} admin - Firebase admin instance
2236
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
2237
+ * @returns {Promise<object>} Result with deleted count
2238
+ */
2239
+ async function deleteTestUsers(admin, extraAccounts) {
2240
+ const results = { deleted: [], skipped: [], failed: [] };
2241
+
2242
+ // Wipe the entire emulator Firestore up front (guarded to emulator-only).
2243
+ await flushEmulatorFirestore(admin);
2244
+
2245
+ // Clear auth users via the emulator's bulk-clear REST API instead of
2246
+ // individual deleteUser() calls. Individual deletes trigger auth:on-delete
2247
+ // for EACH user, and those triggers fire asynchronously — a late on-delete
2248
+ // can land AFTER the subsequent createTestAccounts() on-create and clobber
2249
+ // the freshly-written doc (80% repro rate in stress tests). The bulk API
2250
+ // clears the auth store without triggering event handlers at all.
2251
+ const authHost = process.env.FIREBASE_AUTH_EMULATOR_HOST;
2252
+
2253
+ if (authHost) {
2254
+ // Resolved OUTSIDE the try: an unresolvable/disagreeing project id must
2255
+ // abort the wipe, not fall through to the individual deletes below. The URL
2256
+ // is a no-op against any other project and the emulator answers 200 to it
2257
+ // anyway (#292).
2258
+ const projectId = resolveWipeProjectId(admin);
2259
+
2260
+ try {
2261
+ const url = `http://${authHost}/emulator/v1/projects/${projectId}/accounts`;
2262
+ const response = await fetch(url, { method: 'DELETE' });
2263
+
2264
+ // A refused clear is not a clear. Falling through to the individual
2265
+ // deletes below is the same recovery a transport error gets.
2266
+ if (!response.ok) {
2267
+ throw new Error(`Auth bulk-clear for project "${projectId}" answered ${response.status}`);
2268
+ }
2269
+
2270
+ // Count all known accounts as deleted (the bulk API doesn't return per-user results)
2271
+ const allAccounts = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
2272
+ results.deleted = Object.values(allAccounts).map(a => a.uid);
2273
+ } catch (e) {
2274
+ // Bulk clear failed — fall back to individual deletes. Say why out loud:
2275
+ // the individual path races auth:on-delete (the documented flake), so a
2276
+ // silent downgrade turns a loud HTTP refusal into an unexplained flaky run.
2277
+ console.warn(`Auth bulk-clear failed (${e.message}); falling back to individual deletes, which can race auth:on-delete`);
2278
+ const allAccounts = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
2279
+ await _deleteAccountsIndividually(admin, allAccounts, results);
2280
+ }
2281
+ } else {
2282
+ // Not running against emulator — fall back to individual deletes
2283
+ const allAccounts = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
2284
+ await _deleteAccountsIndividually(admin, allAccounts, results);
2285
+ }
2286
+
2287
+ // Realtime Database: wipe the `_test` namespace in full. (The Firestore-wide
2288
+ // flush already ran in flushEmulatorFirestore() at the start of this function.)
2289
+ // `admin.database()` throws synchronously when no Database URL is configured,
2290
+ // so guard the whole thing — RTDB is optional for a project.
2291
+ try {
2292
+ await admin.database().ref('_test').remove();
2293
+ } catch (e) {
2294
+ // RTDB not configured / no database URL — ignore.
2295
+ }
2296
+
2297
+ return {
2298
+ success: results.failed.length === 0,
2299
+ deleted: results.deleted.length,
2300
+ skipped: results.skipped.length,
2301
+ failed: results.failed.length,
2302
+ errors: results.failed,
2303
+ };
2304
+ }
2305
+
2306
+ async function _deleteAccountsIndividually(admin, allAccounts, results) {
2307
+ await Promise.all(
2308
+ Object.values(allAccounts).map(async (account) => {
2309
+ try {
2310
+ await admin.auth().deleteUser(account.uid);
2311
+
2312
+ const maxWait = 10000;
2313
+ const interval = 200;
2314
+ let waited = 0;
2315
+
2316
+ while (waited < maxWait) {
2317
+ const doc = await admin.firestore().doc(`users/${account.uid}`).get();
2318
+ if (!doc.exists) {
2319
+ break;
2320
+ }
2321
+ await new Promise(resolve => setTimeout(resolve, interval));
2322
+ waited += interval;
2323
+ }
2324
+
2325
+ if (waited >= maxWait) {
2326
+ await admin.firestore().doc(`users/${account.uid}`).delete().catch(() => {});
2327
+ }
2328
+
2329
+ results.deleted.push(account.uid);
2330
+ } catch (error) {
2331
+ if (error.code === 'auth/user-not-found') {
2332
+ await admin.firestore().doc(`users/${account.uid}`).delete().catch(() => {});
2333
+ results.skipped.push(account.uid);
2334
+ } else {
2335
+ results.failed.push({ uid: account.uid, error: error.message });
2336
+ }
2337
+ }
2338
+ })
2339
+ );
2340
+ }
2341
+
2342
+ /**
2343
+ * Create all test accounts
2344
+ * Assumes deleteTestUsers() was called first to ensure clean state
2345
+ * @param {object} admin - Firebase admin instance
2346
+ * @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
2347
+ * @param {object} [config] - @omega.js/backend config (used to resolve first paid product)
2348
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
2349
+ * @returns {Promise<object>} Result with created/failed counts
2350
+ */
2351
+ async function createTestAccounts(admin, domain, config, extraAccounts) {
2352
+ const definitions = getAccountDefinitions(domain, config, extraAccounts);
2353
+ const results = { created: [], failed: [] };
2354
+
2355
+ // Create all accounts in parallel
2356
+ await Promise.all(
2357
+ Object.entries(definitions).map(async ([key, account]) => {
2358
+ try {
2359
+ await createAccount(admin, account);
2360
+ results.created.push({ id: key, uid: account.uid, email: account.email });
2361
+ } catch (error) {
2362
+ results.failed.push({ id: key, uid: account.uid, email: account.email, error: error.message });
2363
+ }
2364
+ })
2365
+ );
2366
+
2367
+ // Google picker personas — import-only (fresh uids; see GOOGLE_ACCOUNTS)
2368
+ await Promise.all(
2369
+ Object.values(GOOGLE_ACCOUNTS).map(async (account) => {
2370
+ const email = account.email.replace('{domain}', domain);
2371
+ try {
2372
+ await importGoogleAccount(admin, { ...account, email });
2373
+ results.created.push({ id: account.id, uid: account.uid, email });
2374
+ } catch (error) {
2375
+ results.failed.push({ id: account.id, uid: account.uid, email, error: error.message });
2376
+ }
2377
+ })
2378
+ );
2379
+
2380
+ return {
2381
+ success: results.failed.length === 0,
2382
+ created: results.created.length,
2383
+ failed: results.failed.length,
2384
+ accounts: results.created,
2385
+ errors: results.failed,
2386
+ };
2387
+ }
2388
+
2389
+ /**
2390
+ * Build a persona's canonical purchase record — the `payments-orders/{orderId}`
2391
+ * document its seeded subscription names. Pure: the shape only, no writes.
2392
+ *
2393
+ * The record is the persona's own resolved subscription in `unified` (that IS what
2394
+ * a purchase writes to the order — events/firestore/payments-webhooks/on-write.js
2395
+ * writes the same unified object to both the user doc and the order), plus the
2396
+ * `metadata` and `requests` blocks that order carries, and the plan that was BOUGHT:
2397
+ * the plan the persona holds, or — for one whose term lapsed and dropped it back to
2398
+ * basic — the paid plan it lapsed from.
2399
+ *
2400
+ * @param {string} key - Account key in TEST_ACCOUNTS (e.g. 'journey-flows-cancel')
2401
+ * @param {object} [config] - @omega.js/backend config (resolves the paid product)
2402
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js, keyed
2403
+ * by id — a project persona's seeded order is as real as a framework one's
2404
+ * @returns {{ orderId: string, doc: object }|null} null when the persona bought nothing
2405
+ */
2406
+ function buildOrderFixture(key, config, extraAccounts) {
2407
+ const account = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) }[key];
2408
+
2409
+ if (!account) {
2410
+ throw new Error(`No seeded persona named ${key} — there is no order fixture to seed`);
2411
+ }
2412
+
2413
+ const subscription = resolveSeededSubscription(account.properties?.subscription, config);
2414
+ const payment = subscription.payment || {};
2415
+
2416
+ if (!payment.orderId) {
2417
+ return null;
2418
+ }
2419
+
2420
+ // The plan the order records is the persona's own — the fallback is only for a
2421
+ // persona sitting on basic today because the paid term it bought ran out.
2422
+ const product = subscription.product?.id !== 'basic'
2423
+ ? subscription.product
2424
+ : getFirstPaidProduct(config);
2425
+
2426
+ const now = new Date();
2427
+ const stamp = { timestamp: now.toISOString(), timestampUNIX: Math.floor(now.getTime() / 1000) };
2428
+
2429
+ return {
2430
+ orderId: payment.orderId,
2431
+ doc: {
2432
+ id: payment.orderId,
2433
+ type: 'subscription',
2434
+ owner: account.uid,
2435
+ productId: product.id,
2436
+ provider: payment.provider,
2437
+ resourceId: payment.resourceId,
2438
+ unified: {
2439
+ ...subscription,
2440
+ product: { id: product.id, name: product.name },
2441
+ },
2442
+ // A real order carries the cancel/refund requests the routes write onto it
2443
+ // and the stamps of the event that last touched it. Nothing has requested
2444
+ // anything of a seeded order, and no webhook wrote it — the seed did.
2445
+ requests: {
2446
+ cancellation: null,
2447
+ refund: null,
2448
+ },
2449
+ metadata: {
2450
+ created: { ...stamp },
2451
+ updated: { ...stamp },
2452
+ updatedBy: {
2453
+ event: {
2454
+ name: 'seed',
2455
+ id: null,
2456
+ },
2457
+ },
2458
+ },
2459
+ },
2460
+ };
2461
+ }
2462
+
2463
+ /**
2464
+ * Seed a persona's canonical purchase record — the `payments-orders/{orderId}` doc
2465
+ * that account creation itself never writes. Creating a persona writes a user doc and
2466
+ * NOTHING else, so a seeded subscription arrives without the order record every real
2467
+ * purchase leaves behind, and three backend surfaces read exactly that record: the test
2468
+ * cancel provider (it reads the plan's provider product id off the LIVE order), the
2469
+ * test webhook library (it rebuilds a provider subscription from the order's `unified`)
2470
+ * and the per-owner trial-eligibility query (any prior subscription order disqualifies).
2471
+ *
2472
+ * Only the personas whose seed carries `subscription.payment.orderId` have a purchase
2473
+ * record; everything else is a no-op (a pristine account has bought nothing).
2474
+ *
2475
+ * The record's status MIRRORS the seeded subscription's — an order's `unified.status`
2476
+ * IS the state of the subscription it bought, so it is not a second fact to keep in
2477
+ * step: an active paid persona carries a live order, a lapsed one a cancelled one.
2478
+ *
2479
+ * @param {object} admin - Firebase admin instance (pointed at the emulator)
2480
+ * @param {string} key - Account key in TEST_ACCOUNTS (e.g. 'journey-flows-cancel')
2481
+ * @param {object} [config] - @omega.js/backend config (resolves the paid product)
2482
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
2483
+ * @returns {Promise<object|null>} `{ orderId, status }`, or null when the persona carries no order
2484
+ */
2485
+ async function seedOrderFixture(admin, key, config, extraAccounts) {
2486
+ const fixture = buildOrderFixture(key, config, extraAccounts);
2487
+
2488
+ if (!fixture) {
2489
+ return null;
2490
+ }
2491
+
2492
+ await admin.firestore().doc(`payments-orders/${fixture.orderId}`).set(fixture.doc, { merge: true });
2493
+
2494
+ return { orderId: fixture.orderId, status: fixture.doc.unified.status };
2495
+ }
2496
+
2497
+ /**
2498
+ * Seed EVERY persona's purchase record — the boot-seed counterpart of the
2499
+ * per-persona reset (routes/test/reset-account). A seeded subscription naming an
2500
+ * order the emulator never carried is a broken purchase: the cancel provider
2501
+ * resolves no plan, and trial eligibility answers as if nothing was ever bought.
2502
+ *
2503
+ * A project's own personas (`test/_init.js` accounts) are seeded on exactly the
2504
+ * same terms — their subscriptions are resolved by the same seeder, so their
2505
+ * orders must be stood up by it too.
2506
+ *
2507
+ * @param {object} admin - Firebase admin instance (pointed at the emulator)
2508
+ * @param {object} [config] - @omega.js/backend config (resolves the paid product)
2509
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
2510
+ * @returns {Promise<string[]>} The order ids seeded
2511
+ */
2512
+ async function seedOrderFixtures(admin, config, extraAccounts) {
2513
+ const all = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
2514
+ const seeded = await Promise.all(
2515
+ Object.keys(all).map((key) => seedOrderFixture(admin, key, config, extraAccounts)),
2516
+ );
2517
+
2518
+ return seeded.filter(Boolean).map((order) => order.orderId);
2519
+ }
2520
+
2521
+ /**
2522
+ * The emulator's Realtime Database, reached the way the Functions runtime
2523
+ * reaches it. The seeder is an ordinary Node process: its admin app is built
2524
+ * from a project id alone (run-tests.js, cli/commands/emulator.js), so it
2525
+ * carries no database URL at all and `admin.database()` throws. The runtime
2526
+ * inside the emulator is handed one by firebase-tools, and it is that exact URL
2527
+ * — `http://<database emulator>/?ns=<namespace>` — this rebuilds, because a
2528
+ * write to any OTHER namespace is invisible to the route that reads it.
2529
+ *
2530
+ * The namespace is firebase-tools' own: a demo project's default Admin SDK
2531
+ * config names the project itself, every other project its default instance
2532
+ * (firebase-tools emulator/adminSdkConfig.js + functionsEmulator.js
2533
+ * getFirebaseConfig, which takes the first label of that URL's hostname).
2534
+ *
2535
+ * @param {object} admin - Firebase admin instance (pointed at the emulator)
2536
+ * @returns {object|null} The database, or null when nothing names an emulator to write to
2537
+ */
2538
+ function sessionsDatabase(admin) {
2539
+ // Emulator-only, the same fence flushEmulatorFirestore stands behind: seeded
2540
+ // sessions are test data and never touch a real database.
2541
+ if (!process.env.FIRESTORE_EMULATOR_HOST) {
2542
+ return null;
2543
+ }
2544
+
2545
+ let options = {};
2546
+ try {
2547
+ options = admin.app().options || {};
2548
+ } catch (e) {
2549
+ options = {}; // No admin, or no default app.
2550
+ }
2551
+
2552
+ // A process firebase-tools configured itself (the Functions runtime the reset
2553
+ // route runs in) already holds the right URL — take it rather than rebuild it.
2554
+ if (options.databaseURL) {
2555
+ return admin.database();
2556
+ }
2557
+
2558
+ const projectId = options.projectId || process.env.GCLOUD_PROJECT || null;
2559
+
2560
+ if (!projectId) {
2561
+ return null;
2562
+ }
2563
+
2564
+ const port = envPort('database') || CLASSIC_PORTS.database;
2565
+ const namespace = projectId.startsWith('demo-') ? projectId : `${projectId}-default-rtdb`;
2566
+
2567
+ return admin.app().database(`http://127.0.0.1:${port}/?ns=${namespace}`);
2568
+ }
2569
+
2570
+ /**
2571
+ * Seed one persona's active sessions — the other devices it is signed in on,
2572
+ * which live in the Realtime Database rather than on the user doc and which
2573
+ * account creation therefore never writes
2574
+ * ([#343](https://github.com/Omega-JS-Stack/omega/issues/343)). Without them
2575
+ * the account page's security panel shows the browser in front of you and
2576
+ * nothing else, which is not what a real customer's panel looks like.
2577
+ *
2578
+ * The ids are derived from the persona key, so re-seeding REPLACES the same
2579
+ * records rather than piling up a new set on every boot.
2580
+ *
2581
+ * @param {object} admin - Firebase admin instance (pointed at the emulator)
2582
+ * @param {string} key - Account key in TEST_ACCOUNTS (e.g. 'premium-active')
2583
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
2584
+ * @returns {Promise<string[]|null>} The session ids seeded, or null when the persona has none
2585
+ */
2586
+ async function seedSessionFixture(admin, key, extraAccounts) {
2587
+ const all = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
2588
+ const sessions = buildSessionFixtures(key, all);
2589
+
2590
+ if (!sessions) {
2591
+ return null;
2592
+ }
2593
+
2594
+ const database = sessionsDatabase(admin);
2595
+
2596
+ if (!database) {
2597
+ return null;
2598
+ }
2599
+
2600
+ await Promise.all(
2601
+ Object.entries(sessions).map(([id, session]) => database.ref(`${SESSION_PATH}/${id}`).set(session)),
2602
+ );
2603
+
2604
+ return Object.keys(sessions);
2605
+ }
2606
+
2607
+ /**
2608
+ * Seed EVERY persona's active sessions — the boot-seed counterpart of the
2609
+ * per-persona reset (routes/test/reset-account), exactly like the order
2610
+ * fixtures beside it.
2611
+ *
2612
+ * @param {object} admin - Firebase admin instance (pointed at the emulator)
2613
+ * @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
2614
+ * @returns {Promise<string[]>} The session ids seeded
2615
+ */
2616
+ async function seedSessionFixtures(admin, extraAccounts) {
2617
+ const all = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
2618
+ const seeded = await Promise.all(
2619
+ Object.keys(all).map((key) => seedSessionFixture(admin, key, extraAccounts)),
2620
+ );
2621
+
2622
+ return seeded.filter(Boolean).flat();
2623
+ }
2624
+
2625
+ /**
2626
+ * Test data constants - SSOT for test values
2627
+ */
2628
+ const TEST_DATA = {
2629
+ affiliateCode: 'TESTREF',
2630
+ filterUid: 'test-user-uid-12345',
2631
+ defaultProjectId: 'demo-test',
2632
+ };
2633
+
2634
+ module.exports = {
2635
+ STATIC_ACCOUNTS,
2636
+ JOURNEY_ACCOUNTS,
2637
+ GOOGLE_ACCOUNTS,
2638
+ TEST_ACCOUNTS,
2639
+ TEST_DATA,
2640
+ TEST_ACCOUNT_PASSWORD,
2641
+ getFirstPaidProduct,
2642
+ getPlanPricing,
2643
+ getAccountTable,
2644
+ getPaletteRoster,
2645
+ getAccountDefinitions,
2646
+ fetchPrivateKeys,
2647
+ resolveWipeProjectId,
2648
+ deleteTestUsers,
2649
+ createAccount,
2650
+ createTestAccounts,
2651
+ importGoogleAccount,
2652
+ buildOrderFixture,
2653
+ seedOrderFixture,
2654
+ seedOrderFixtures,
2655
+ buildReferralFixtures,
2656
+ buildSessionFixtures,
2657
+ seedSessionFixture,
2658
+ seedSessionFixtures,
2659
+ SESSION_PATH,
2660
+ };