@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.
- package/LICENSE +98 -0
- package/README.md +1053 -0
- package/bin/omega +2 -0
- package/bin/omega-backend +2 -0
- package/bin/omg +2 -0
- package/cli.js +3 -0
- package/dist/cli/command-table.js +268 -0
- package/dist/cli/commands/auth.js +259 -0
- package/dist/cli/commands/base-command.js +451 -0
- package/dist/cli/commands/build.js +27 -0
- package/dist/cli/commands/clean.js +13 -0
- package/dist/cli/commands/clear.js +11 -0
- package/dist/cli/commands/cwd.js +9 -0
- package/dist/cli/commands/deploy.js +205 -0
- package/dist/cli/commands/emulator-orphans.js +440 -0
- package/dist/cli/commands/emulator.js +1829 -0
- package/dist/cli/commands/firebase-init.js +101 -0
- package/dist/cli/commands/firestore.js +261 -0
- package/dist/cli/commands/indexes.js +51 -0
- package/dist/cli/commands/install.js +151 -0
- package/dist/cli/commands/logs.js +358 -0
- package/dist/cli/commands/mcp.js +41 -0
- package/dist/cli/commands/migrate-markers.js +176 -0
- package/dist/cli/commands/migrate-rules.js +90 -0
- package/dist/cli/commands/migrate.js +59 -0
- package/dist/cli/commands/serve.js +208 -0
- package/dist/cli/commands/setup-tests/base-test.js +174 -0
- package/dist/cli/commands/setup-tests/emulator-config.js +109 -0
- package/dist/cli/commands/setup-tests/env-runtime-config-deprecated.js +47 -0
- package/dist/cli/commands/setup-tests/firebase-admin.js +49 -0
- package/dist/cli/commands/setup-tests/firebase-auth.js +26 -0
- package/dist/cli/commands/setup-tests/firebase-cli.js +26 -0
- package/dist/cli/commands/setup-tests/firebase-functions.js +49 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-file.js +72 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-in-json.js +20 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-required.js +108 -0
- package/dist/cli/commands/setup-tests/firestore-indexes-synced.js +236 -0
- package/dist/cli/commands/setup-tests/firestore-rules-file.js +115 -0
- package/dist/cli/commands/setup-tests/firestore-rules-in-json.js +49 -0
- package/dist/cli/commands/setup-tests/functions-package.js +44 -0
- package/dist/cli/commands/setup-tests/gcloud-cli.js +26 -0
- package/dist/cli/commands/setup-tests/gitignore.js +122 -0
- package/dist/cli/commands/setup-tests/helpers/merge-line-files.js +28 -0
- package/dist/cli/commands/setup-tests/helpers/required-indexes.js +109 -0
- package/dist/cli/commands/setup-tests/helpers/seed-campaigns.js +246 -0
- package/dist/cli/commands/setup-tests/helpers.js +72 -0
- package/dist/cli/commands/setup-tests/hosting-folder.js +23 -0
- package/dist/cli/commands/setup-tests/hosting-rewrites.js +52 -0
- package/dist/cli/commands/setup-tests/index.js +111 -0
- package/dist/cli/commands/setup-tests/is-firebase-project.js +21 -0
- package/dist/cli/commands/setup-tests/java-installed.js +27 -0
- package/dist/cli/commands/setup-tests/legacy-tests-cleanup.js +43 -0
- package/dist/cli/commands/setup-tests/marketing-campaigns-seeded.js +230 -0
- package/dist/cli/commands/setup-tests/node-version.js +60 -0
- package/dist/cli/commands/setup-tests/npm-project-scripts.js +42 -0
- package/dist/cli/commands/setup-tests/nvmrc-version.js +36 -0
- package/dist/cli/commands/setup-tests/omega-backend.js +42 -0
- package/dist/cli/commands/setup-tests/omega-config.js +84 -0
- package/dist/cli/commands/setup-tests/project-directories.js +34 -0
- package/dist/cli/commands/setup-tests/project-id-consistency.js +201 -0
- package/dist/cli/commands/setup-tests/public-html-files.js +22 -0
- package/dist/cli/commands/setup-tests/realtime-rules-file.js +69 -0
- package/dist/cli/commands/setup-tests/realtime-rules-in-json.js +20 -0
- package/dist/cli/commands/setup-tests/remoteconfig-template-file.js +32 -0
- package/dist/cli/commands/setup-tests/remoteconfig-template-in-json.js +31 -0
- package/dist/cli/commands/setup-tests/service-account.js +86 -0
- package/dist/cli/commands/setup-tests/storage-lifecycle-policy.js +81 -0
- package/dist/cli/commands/setup-tests/storage-rules-file.js +32 -0
- package/dist/cli/commands/setup-tests/storage-rules-in-json.js +20 -0
- package/dist/cli/commands/stripe.js +14 -0
- package/dist/cli/commands/test-lanes/stripe-live.js +440 -0
- package/dist/cli/commands/test.js +976 -0
- package/dist/cli/commands/update.js +27 -0
- package/dist/cli/commands/version.js +10 -0
- package/dist/cli/commands/watch.js +196 -0
- package/dist/cli/flags.js +13 -0
- package/dist/cli/index.js +173 -0
- package/dist/cli/run.js +33 -0
- package/dist/cli/utils/attach-log-file.js +4 -0
- package/dist/cli/utils/compile-rules.js +1155 -0
- package/dist/cli/utils/ensure-target.js +314 -0
- package/dist/cli/utils/project-type.js +140 -0
- package/dist/cli/utils/public-files.js +49 -0
- package/dist/cli/utils/safe-install.js +4 -0
- package/dist/cli/utils/spawn-shell.js +94 -0
- package/dist/cli/utils/stage-functions.js +338 -0
- package/dist/cli/utils/stage-local-packages.js +210 -0
- package/dist/cli/utils/target-checks.js +161 -0
- package/dist/cli/utils/target.js +24 -0
- package/dist/cli/utils/ui.js +292 -0
- package/dist/defaults/AGENTS.md +125 -0
- package/dist/defaults/CHANGELOG.md +15 -0
- package/dist/defaults/CLAUDE.md +1 -0
- package/dist/defaults/_.gitignore +65 -0
- package/dist/defaults/docs/README.md +17 -0
- package/dist/defaults/test/README.md +65 -0
- package/dist/defaults/test/_init.js +14 -0
- package/dist/defaults/test/helpers/connect-trap.js +72 -0
- package/dist/defaults/test/unit/registration.test.js +143 -0
- package/dist/defaults/test/unit/rules-posture.test.js +101 -0
- package/dist/defaults/test/unit/socket-free.test.js +43 -0
- package/dist/manager/events/auth/before-create.js +89 -0
- package/dist/manager/events/auth/before-signin.js +76 -0
- package/dist/manager/events/auth/on-create.js +112 -0
- package/dist/manager/events/auth/on-delete.js +141 -0
- package/dist/manager/events/auth/utils.js +93 -0
- package/dist/manager/events/cron/daily/blog-auto-publisher.js +287 -0
- package/dist/manager/events/cron/daily/data-requests.js +57 -0
- package/dist/manager/events/cron/daily/expire-paypal-cancellations.js +191 -0
- package/dist/manager/events/cron/daily/marketing-prune.js +486 -0
- package/dist/manager/events/cron/daily/reset-usage.js +188 -0
- package/dist/manager/events/cron/daily/trial-lapse-sweep.js +355 -0
- package/dist/manager/events/cron/daily.js +3 -0
- package/dist/manager/events/cron/frequent/abandoned-carts.js +204 -0
- package/dist/manager/events/cron/frequent/email-queue.js +71 -0
- package/dist/manager/events/cron/frequent/marketing-campaigns.js +463 -0
- package/dist/manager/events/cron/frequent/retry-failed-webhooks.js +70 -0
- package/dist/manager/events/cron/frequent.js +3 -0
- package/dist/manager/events/cron/runner.js +64 -0
- package/dist/manager/events/firestore/notifications/on-write.js +123 -0
- package/dist/manager/events/firestore/payments-disputes/on-write.js +256 -0
- package/dist/manager/events/firestore/payments-disputes/providers/stripe.js +218 -0
- package/dist/manager/events/firestore/payments-disputes/providers/test.js +231 -0
- package/dist/manager/events/firestore/payments-webhooks/analytics.js +687 -0
- package/dist/manager/events/firestore/payments-webhooks/on-write.js +1224 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/index.js +395 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +58 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +28 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-refunded.js +30 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/send-email.js +74 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-removed.js +14 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +28 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/checkout-declined.js +18 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +67 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +25 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +26 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js +34 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +42 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +33 -0
- package/dist/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-winback.js +17 -0
- package/dist/manager/functions/_legacy/actions/create-post-handler.js +188 -0
- package/dist/manager/functions/_legacy/actions/generate-uuid.js +63 -0
- package/dist/manager/functions/_legacy/actions/sign-up-handler.js +205 -0
- package/dist/manager/functions/_legacy/admin/create-post.js +207 -0
- package/dist/manager/functions/_legacy/admin/firestore-write.js +72 -0
- package/dist/manager/functions/_legacy/admin/get-stats.js +218 -0
- package/dist/manager/functions/_legacy/admin/query.js +198 -0
- package/dist/manager/functions/_legacy/admin/send-notification.js +206 -0
- package/dist/manager/functions/_legacy/template.js +33 -0
- package/dist/manager/functions/_legacy/test/authenticate.js +40 -0
- package/dist/manager/functions/_legacy/test/webhook.js +37 -0
- package/dist/manager/functions/wrappers/mailchimp/addToList.js +25 -0
- package/dist/manager/helpers/analytics.js +498 -0
- package/dist/manager/helpers/api-manager.js +313 -0
- package/dist/manager/helpers/backend-router.js +37 -0
- package/dist/manager/helpers/context/authenticate.js +215 -0
- package/dist/manager/helpers/context/client-info.js +91 -0
- package/dist/manager/helpers/context/index.js +228 -0
- package/dist/manager/helpers/context/logging.js +150 -0
- package/dist/manager/helpers/context/parse.js +160 -0
- package/dist/manager/helpers/context/respond.js +247 -0
- package/dist/manager/helpers/event-middleware.js +109 -0
- package/dist/manager/helpers/metadata.js +32 -0
- package/dist/manager/helpers/middleware.js +541 -0
- package/dist/manager/helpers/redact-secret.js +25 -0
- package/dist/manager/helpers/resolved-config.js +34 -0
- package/dist/manager/helpers/roles.js +69 -0
- package/dist/manager/helpers/safe-compare.js +31 -0
- package/dist/manager/helpers/schema-engine.js +314 -0
- package/dist/manager/helpers/schema-zod.js +203 -0
- package/dist/manager/helpers/settings.js +215 -0
- package/dist/manager/helpers/usage.js +541 -0
- package/dist/manager/helpers/user.js +57 -0
- package/dist/manager/helpers/utilities.js +565 -0
- package/dist/manager/index.js +1404 -0
- package/dist/manager/libraries/abandoned-cart-config.js +12 -0
- package/dist/manager/libraries/ai/index.js +321 -0
- package/dist/manager/libraries/ai/prompt.js +85 -0
- package/dist/manager/libraries/ai/providers/anthropic-format.js +297 -0
- package/dist/manager/libraries/ai/providers/anthropic.js +169 -0
- package/dist/manager/libraries/ai/providers/claude-code.js +183 -0
- package/dist/manager/libraries/ai/providers/openai.js +1272 -0
- package/dist/manager/libraries/ai/providers/test.js +257 -0
- package/dist/manager/libraries/ai/tokens.js +54 -0
- package/dist/manager/libraries/analytics/conversions.js +478 -0
- package/dist/manager/libraries/analytics/match-data.js +573 -0
- package/dist/manager/libraries/analytics/signup.js +120 -0
- package/dist/manager/libraries/auth-user.js +46 -0
- package/dist/manager/libraries/content/feed-parser.js +198 -0
- package/dist/manager/libraries/content/ghostii.js +191 -0
- package/dist/manager/libraries/content/source-resolver.js +629 -0
- package/dist/manager/libraries/email/constants.js +532 -0
- package/dist/manager/libraries/email/data/blocked-local-parts.json +55 -0
- package/dist/manager/libraries/email/data/blocked-local-patterns.js +14 -0
- package/dist/manager/libraries/email/data/corporate-domains.json +23 -0
- package/dist/manager/libraries/email/data/custom-disposable-domains.json +58 -0
- package/dist/manager/libraries/email/data/disposable-domains.json +8184 -0
- package/dist/manager/libraries/email/data/typo-domains.js +83 -0
- package/dist/manager/libraries/email/disposable-domains.js +92 -0
- package/dist/manager/libraries/email/generators/lib/filter.js +179 -0
- package/dist/manager/libraries/email/generators/lib/image-host.js +344 -0
- package/dist/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
- package/dist/manager/libraries/email/generators/lib/markdown-renderer.js +294 -0
- package/dist/manager/libraries/email/generators/lib/mjml-template.js +120 -0
- package/dist/manager/libraries/email/generators/lib/structure.js +299 -0
- package/dist/manager/libraries/email/generators/lib/svg-illustrator.js +197 -0
- package/dist/manager/libraries/email/generators/lib/templates/base.js +229 -0
- package/dist/manager/libraries/email/generators/lib/templates/card.js +33 -0
- package/dist/manager/libraries/email/generators/lib/templates/classic-schema.js +59 -0
- package/dist/manager/libraries/email/generators/lib/templates/clean.js +82 -0
- package/dist/manager/libraries/email/generators/lib/templates/editorial/helpers.js +100 -0
- package/dist/manager/libraries/email/generators/lib/templates/editorial/index.js +318 -0
- package/dist/manager/libraries/email/generators/lib/templates/feedback.js +101 -0
- package/dist/manager/libraries/email/generators/lib/templates/field-report/helpers.js +138 -0
- package/dist/manager/libraries/email/generators/lib/templates/field-report/index.js +494 -0
- package/dist/manager/libraries/email/generators/lib/templates/index.js +47 -0
- package/dist/manager/libraries/email/generators/lib/templates/newsletter-shared.js +610 -0
- package/dist/manager/libraries/email/generators/lib/templates/order.js +461 -0
- package/dist/manager/libraries/email/generators/lib/templates/plain.js +83 -0
- package/dist/manager/libraries/email/generators/lib/templates/shared-campaign.js +64 -0
- package/dist/manager/libraries/email/generators/newsletter.js +899 -0
- package/dist/manager/libraries/email/index.js +146 -0
- package/dist/manager/libraries/email/marketing/index.js +754 -0
- package/dist/manager/libraries/email/prepare.js +365 -0
- package/dist/manager/libraries/email/providers/beehiiv.js +589 -0
- package/dist/manager/libraries/email/providers/sendgrid.js +845 -0
- package/dist/manager/libraries/email/transactional/index.js +510 -0
- package/dist/manager/libraries/email/utm.js +86 -0
- package/dist/manager/libraries/email/validation-provider-neverbounce.js +70 -0
- package/dist/manager/libraries/email/validation-provider-zerobounce.js +42 -0
- package/dist/manager/libraries/email/validation.js +255 -0
- package/dist/manager/libraries/env.js +198 -0
- package/dist/manager/libraries/infer-contact.js +101 -0
- package/dist/manager/libraries/load-provider.js +65 -0
- package/dist/manager/libraries/notification.js +256 -0
- package/dist/manager/libraries/openai.js +13 -0
- package/dist/manager/libraries/payment/discount-codes.js +204 -0
- package/dist/manager/libraries/payment/fetch-failure.js +56 -0
- package/dist/manager/libraries/payment/license.js +44 -0
- package/dist/manager/libraries/payment/order-id.js +57 -0
- package/dist/manager/libraries/payment/provider-errors.js +71 -0
- package/dist/manager/libraries/payment/providers/chargebee.js +817 -0
- package/dist/manager/libraries/payment/providers/coinbase.js +330 -0
- package/dist/manager/libraries/payment/providers/paypal.js +987 -0
- package/dist/manager/libraries/payment/providers/stripe.js +755 -0
- package/dist/manager/libraries/payment/providers/test.js +250 -0
- package/dist/manager/libraries/payment/refund-linkage.js +78 -0
- package/dist/manager/libraries/payment/refund-policy.js +113 -0
- package/dist/manager/libraries/payment/winback.js +55 -0
- package/dist/manager/libraries/prompts/infer-contact.md +78 -0
- package/dist/manager/libraries/rate-limits.js +17 -0
- package/dist/manager/libraries/recaptcha.js +58 -0
- package/dist/manager/libraries/user-doc.js +234 -0
- package/dist/manager/routes/admin/backup/post.js +106 -0
- package/dist/manager/routes/admin/cron/post.js +37 -0
- package/dist/manager/routes/admin/database/get.js +35 -0
- package/dist/manager/routes/admin/database/post.js +34 -0
- package/dist/manager/routes/admin/email/post.js +74 -0
- package/dist/manager/routes/admin/firestore/get.js +35 -0
- package/dist/manager/routes/admin/firestore/post.js +56 -0
- package/dist/manager/routes/admin/firestore/query/post.js +128 -0
- package/dist/manager/routes/admin/hook/post.js +104 -0
- package/dist/manager/routes/admin/infer-contact/post.js +35 -0
- package/dist/manager/routes/admin/notification/post.js +37 -0
- package/dist/manager/routes/admin/payment/post.js +56 -0
- package/dist/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
- package/dist/manager/routes/admin/post/dispatch-deploy.js +36 -0
- package/dist/manager/routes/admin/post/post.js +487 -0
- package/dist/manager/routes/admin/post/put.js +146 -0
- package/dist/manager/routes/admin/post/templates/post.html +14 -0
- package/dist/manager/routes/admin/repo/content/post.js +106 -0
- package/dist/manager/routes/admin/stats/get.js +222 -0
- package/dist/manager/routes/admin/users/disable/post.js +50 -0
- package/dist/manager/routes/admin/users/list/get.js +110 -0
- package/dist/manager/routes/admin/users/sync/post.js +119 -0
- package/dist/manager/routes/brand/get.js +26 -0
- package/dist/manager/routes/content/post/get.js +110 -0
- package/dist/manager/routes/general/email/post.js +89 -0
- package/dist/manager/routes/general/email/templates/general/download-app-link.js +42 -0
- package/dist/manager/routes/general/uuid/post.js +31 -0
- package/dist/manager/routes/handler/post/post.js +142 -0
- package/dist/manager/routes/health/get.js +43 -0
- package/dist/manager/routes/index.js +11 -0
- package/dist/manager/routes/marketing/campaign/delete.js +45 -0
- package/dist/manager/routes/marketing/campaign/get.js +69 -0
- package/dist/manager/routes/marketing/campaign/post.js +124 -0
- package/dist/manager/routes/marketing/campaign/put.js +86 -0
- package/dist/manager/routes/marketing/campaign/utils.js +59 -0
- package/dist/manager/routes/marketing/contact/delete.js +100 -0
- package/dist/manager/routes/marketing/contact/post.js +149 -0
- package/dist/manager/routes/marketing/contact/put.js +37 -0
- package/dist/manager/routes/marketing/email-preferences/post.js +275 -0
- package/dist/manager/routes/marketing/webhook/forward/post.js +170 -0
- package/dist/manager/routes/marketing/webhook/post.js +123 -0
- package/dist/manager/routes/marketing/webhook/providers/beehiiv.js +197 -0
- package/dist/manager/routes/marketing/webhook/providers/sendgrid.js +194 -0
- package/dist/manager/routes/payments/cancel/_is-trialing.js +40 -0
- package/dist/manager/routes/payments/cancel/post.js +149 -0
- package/dist/manager/routes/payments/cancel/providers/chargebee.js +41 -0
- package/dist/manager/routes/payments/cancel/providers/paypal.js +56 -0
- package/dist/manager/routes/payments/cancel/providers/stripe.js +33 -0
- package/dist/manager/routes/payments/cancel/providers/test.js +118 -0
- package/dist/manager/routes/payments/discount/get.js +25 -0
- package/dist/manager/routes/payments/dispute-alert/post.js +117 -0
- package/dist/manager/routes/payments/dispute-alert/providers/chargeblast.js +55 -0
- package/dist/manager/routes/payments/intent/post.js +341 -0
- package/dist/manager/routes/payments/intent/providers/chargebee.js +197 -0
- package/dist/manager/routes/payments/intent/providers/coinbase.js +94 -0
- package/dist/manager/routes/payments/intent/providers/paypal.js +244 -0
- package/dist/manager/routes/payments/intent/providers/stripe.js +141 -0
- package/dist/manager/routes/payments/intent/providers/test.js +341 -0
- package/dist/manager/routes/payments/plan/post.js +147 -0
- package/dist/manager/routes/payments/plan/providers/chargebee.js +62 -0
- package/dist/manager/routes/payments/plan/providers/paypal.js +62 -0
- package/dist/manager/routes/payments/plan/providers/stripe.js +59 -0
- package/dist/manager/routes/payments/plan/providers/test.js +113 -0
- package/dist/manager/routes/payments/portal/post.js +113 -0
- package/dist/manager/routes/payments/portal/providers/chargebee.js +62 -0
- package/dist/manager/routes/payments/portal/providers/paypal.js +25 -0
- package/dist/manager/routes/payments/portal/providers/stripe.js +62 -0
- package/dist/manager/routes/payments/portal/providers/test.js +18 -0
- package/dist/manager/routes/payments/refund/post.js +207 -0
- package/dist/manager/routes/payments/refund/providers/chargebee.js +147 -0
- package/dist/manager/routes/payments/refund/providers/coinbase.js +40 -0
- package/dist/manager/routes/payments/refund/providers/paypal.js +239 -0
- package/dist/manager/routes/payments/refund/providers/stripe.js +160 -0
- package/dist/manager/routes/payments/refund/providers/test.js +204 -0
- package/dist/manager/routes/payments/trial-eligibility/get.js +29 -0
- package/dist/manager/routes/payments/uncancel/post.js +106 -0
- package/dist/manager/routes/payments/uncancel/providers/chargebee.js +30 -0
- package/dist/manager/routes/payments/uncancel/providers/paypal.js +25 -0
- package/dist/manager/routes/payments/uncancel/providers/stripe.js +27 -0
- package/dist/manager/routes/payments/uncancel/providers/test.js +114 -0
- package/dist/manager/routes/payments/webhook/post.js +157 -0
- package/dist/manager/routes/payments/webhook/providers/chargebee.js +218 -0
- package/dist/manager/routes/payments/webhook/providers/coinbase.js +91 -0
- package/dist/manager/routes/payments/webhook/providers/paypal.js +214 -0
- package/dist/manager/routes/payments/webhook/providers/stripe.js +182 -0
- package/dist/manager/routes/payments/webhook/providers/test.js +18 -0
- package/dist/manager/routes/payments/winback/post.js +276 -0
- package/dist/manager/routes/payments/winback/providers/chargebee.js +24 -0
- package/dist/manager/routes/payments/winback/providers/paypal.js +22 -0
- package/dist/manager/routes/payments/winback/providers/stripe.js +41 -0
- package/dist/manager/routes/payments/winback/providers/test.js +42 -0
- package/dist/manager/routes/restart/index.js +37 -0
- package/dist/manager/routes/special/electron-client/post.js +59 -0
- package/dist/manager/routes/test/authenticate/get.js +11 -0
- package/dist/manager/routes/test/health/get.js +8 -0
- package/dist/manager/routes/test/index.js +16 -0
- package/dist/manager/routes/test/lab/post.js +17 -0
- package/dist/manager/routes/test/redirect/get.js +27 -0
- package/dist/manager/routes/test/reset-account/post.js +79 -0
- package/dist/manager/routes/test/roster/get.js +40 -0
- package/dist/manager/routes/test/schema/post.js +20 -0
- package/dist/manager/routes/test/usage/post.js +44 -0
- package/dist/manager/routes/test/webhook/post.js +21 -0
- package/dist/manager/routes/user/api-keys/post.js +51 -0
- package/dist/manager/routes/user/connections/_context.js +194 -0
- package/dist/manager/routes/user/connections/_grant.js +247 -0
- package/dist/manager/routes/user/connections/_lease.js +184 -0
- package/dist/manager/routes/user/connections/_providers.js +191 -0
- package/dist/manager/routes/user/connections/_state.js +117 -0
- package/dist/manager/routes/user/connections/delete.js +55 -0
- package/dist/manager/routes/user/connections/get.js +167 -0
- package/dist/manager/routes/user/connections/post.js +370 -0
- package/dist/manager/routes/user/connections/providers/discord.js +41 -0
- package/dist/manager/routes/user/connections/providers/google.js +55 -0
- package/dist/manager/routes/user/connections/providers/kick.js +68 -0
- package/dist/manager/routes/user/connections/providers/spotify.js +40 -0
- package/dist/manager/routes/user/connections/providers/twitch.js +52 -0
- package/dist/manager/routes/user/data-request/delete.js +84 -0
- package/dist/manager/routes/user/data-request/get.js +229 -0
- package/dist/manager/routes/user/data-request/post.js +136 -0
- package/dist/manager/routes/user/delete.js +149 -0
- package/dist/manager/routes/user/feedback/post.js +81 -0
- package/dist/manager/routes/user/get.js +14 -0
- package/dist/manager/routes/user/orders/get.js +95 -0
- package/dist/manager/routes/user/sessions/delete.js +95 -0
- package/dist/manager/routes/user/sessions/get.js +40 -0
- package/dist/manager/routes/user/settings/validate/post.js +86 -0
- package/dist/manager/routes/user/signup/post.js +662 -0
- package/dist/manager/routes/user/subscription/get.js +77 -0
- package/dist/manager/routes/user/token/post.js +30 -0
- package/dist/manager/routes/verts/delete.js +43 -0
- package/dist/manager/routes/verts/get.js +49 -0
- package/dist/manager/routes/verts/post.js +61 -0
- package/dist/manager/routes/verts/put.js +75 -0
- package/dist/manager/routes/verts/redirect/get.js +36 -0
- package/dist/manager/routes/verts/serve/get.js +67 -0
- package/dist/manager/routes/verts/utils.js +436 -0
- package/dist/manager/schemas/admin/backup/post.js +8 -0
- package/dist/manager/schemas/admin/cron/post.js +8 -0
- package/dist/manager/schemas/admin/database/get.js +5 -0
- package/dist/manager/schemas/admin/database/post.js +6 -0
- package/dist/manager/schemas/admin/email/post.js +31 -0
- package/dist/manager/schemas/admin/firestore/get.js +5 -0
- package/dist/manager/schemas/admin/firestore/post.js +8 -0
- package/dist/manager/schemas/admin/firestore/query/post.js +5 -0
- package/dist/manager/schemas/admin/hook/post.js +8 -0
- package/dist/manager/schemas/admin/infer-contact/post.js +6 -0
- package/dist/manager/schemas/admin/notification/post.js +23 -0
- package/dist/manager/schemas/admin/payment/post.js +8 -0
- package/dist/manager/schemas/admin/post/post.js +23 -0
- package/dist/manager/schemas/admin/post/put.js +13 -0
- package/dist/manager/schemas/admin/repo/content/post.js +10 -0
- package/dist/manager/schemas/admin/stats/get.js +5 -0
- package/dist/manager/schemas/admin/users/disable/post.js +9 -0
- package/dist/manager/schemas/admin/users/list/get.js +10 -0
- package/dist/manager/schemas/admin/users/sync/post.js +7 -0
- package/dist/manager/schemas/brand/get.js +3 -0
- package/dist/manager/schemas/content/post/get.js +8 -0
- package/dist/manager/schemas/general/email/post.js +10 -0
- package/dist/manager/schemas/general/uuid/post.js +9 -0
- package/dist/manager/schemas/handler/post/post.js +13 -0
- package/dist/manager/schemas/health/get.js +3 -0
- package/dist/manager/schemas/marketing/campaign/delete.js +8 -0
- package/dist/manager/schemas/marketing/campaign/get.js +13 -0
- package/dist/manager/schemas/marketing/campaign/post.js +43 -0
- package/dist/manager/schemas/marketing/campaign/put.js +37 -0
- package/dist/manager/schemas/marketing/contact/delete.js +8 -0
- package/dist/manager/schemas/marketing/contact/post.js +14 -0
- package/dist/manager/schemas/marketing/contact/put.js +8 -0
- package/dist/manager/schemas/marketing/email-preferences/post.js +15 -0
- package/dist/manager/schemas/marketing/webhook/forward/post.js +8 -0
- package/dist/manager/schemas/marketing/webhook/post.js +7 -0
- package/dist/manager/schemas/payments/cancel/post.js +15 -0
- package/dist/manager/schemas/payments/discount/get.js +8 -0
- package/dist/manager/schemas/payments/dispute-alert/post.js +6 -0
- package/dist/manager/schemas/payments/intent/post.js +28 -0
- package/dist/manager/schemas/payments/plan/post.js +11 -0
- package/dist/manager/schemas/payments/portal/post.js +12 -0
- package/dist/manager/schemas/payments/refund/post.js +14 -0
- package/dist/manager/schemas/payments/trial-eligibility/get.js +7 -0
- package/dist/manager/schemas/payments/uncancel/post.js +9 -0
- package/dist/manager/schemas/payments/webhook/post.js +6 -0
- package/dist/manager/schemas/payments/winback/post.js +12 -0
- package/dist/manager/schemas/restart/index.js +5 -0
- package/dist/manager/schemas/special/electron-client/post.js +11 -0
- package/dist/manager/schemas/test/authenticate/get.js +3 -0
- package/dist/manager/schemas/test/health/get.js +3 -0
- package/dist/manager/schemas/test/index.js +5 -0
- package/dist/manager/schemas/test/lab/post.js +3 -0
- package/dist/manager/schemas/test/redirect/get.js +7 -0
- package/dist/manager/schemas/test/reset-account/post.js +3 -0
- package/dist/manager/schemas/test/roster/get.js +3 -0
- package/dist/manager/schemas/test/schema/post.js +70 -0
- package/dist/manager/schemas/test/usage/post.js +8 -0
- package/dist/manager/schemas/test/webhook/post.js +7 -0
- package/dist/manager/schemas/user/api-keys/post.js +6 -0
- package/dist/manager/schemas/user/connections/delete.js +8 -0
- package/dist/manager/schemas/user/connections/get.js +21 -0
- package/dist/manager/schemas/user/connections/post.js +12 -0
- package/dist/manager/schemas/user/data-request/delete.js +3 -0
- package/dist/manager/schemas/user/data-request/get.js +5 -0
- package/dist/manager/schemas/user/data-request/post.js +6 -0
- package/dist/manager/schemas/user/delete.js +6 -0
- package/dist/manager/schemas/user/feedback/post.js +8 -0
- package/dist/manager/schemas/user/get.js +3 -0
- package/dist/manager/schemas/user/orders/get.js +6 -0
- package/dist/manager/schemas/user/sessions/delete.js +6 -0
- package/dist/manager/schemas/user/sessions/get.js +6 -0
- package/dist/manager/schemas/user/settings/validate/post.js +8 -0
- package/dist/manager/schemas/user/signup/post.js +27 -0
- package/dist/manager/schemas/user/subscription/get.js +5 -0
- package/dist/manager/schemas/user/token/post.js +5 -0
- package/dist/manager/schemas/verts/delete.js +8 -0
- package/dist/manager/schemas/verts/get.js +9 -0
- package/dist/manager/schemas/verts/post.js +23 -0
- package/dist/manager/schemas/verts/put.js +20 -0
- package/dist/manager/schemas/verts/redirect/get.js +10 -0
- package/dist/manager/schemas/verts/serve/get.js +14 -0
- package/dist/manager/server-manager.js +69 -0
- package/dist/mcp/client.js +98 -0
- package/dist/mcp/handler.js +521 -0
- package/dist/mcp/index.js +157 -0
- package/dist/mcp/tools.js +555 -0
- package/dist/mcp/utils.js +108 -0
- package/dist/omega-bin.js +6 -0
- package/dist/require.js +3 -0
- package/dist/test/fixtures/firebase-project/.firebaserc +5 -0
- package/dist/test/fixtures/firebase-project/config/omega.json5 +131 -0
- package/dist/test/fixtures/firebase-project/database.rules.json +6 -0
- package/dist/test/fixtures/firebase-project/firebase.json +49 -0
- package/dist/test/fixtures/firebase-project/firestore.indexes.json +4 -0
- package/dist/test/fixtures/firebase-project/package.json +14 -0
- package/dist/test/fixtures/firebase-project/src/index.js +11 -0
- package/dist/test/fixtures/firebase-project/storage.rules +8 -0
- package/dist/test/parse-audit.js +25 -0
- package/dist/test/run-tests.js +90 -0
- package/dist/test/runner.js +982 -0
- package/dist/test/seed.js +242 -0
- package/dist/test/test-accounts.js +2660 -0
- package/dist/test/utils/assertions.js +189 -0
- package/dist/test/utils/email-capture.js +252 -0
- package/dist/test/utils/extended-mode-warning.js +11 -0
- package/dist/test/utils/firestore-rules-client.js +277 -0
- package/dist/test/utils/http-client.js +228 -0
- package/dist/test/utils/test-mode-file.js +192 -0
- package/dist/utils/merge-line-files.js +6 -0
- package/dist/utils/scaffold-defaults.js +71 -0
- package/dist/utils/test-lanes.js +62 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/bare-requires.js +153 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/defaults-engine.js +372 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/deploy.js +286 -0
- package/dist/vendor/devkit/license.js +155 -0
- package/dist/vendor/devkit/local-https.js +360 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/merge-line-files.js +296 -0
- package/dist/vendor/devkit/npm-registry.js +52 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/parse-audit.js +74 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/scaffold-guard.js +96 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/test/assert.js +120 -0
- package/dist/vendor/devkit/test/define-cases.js +104 -0
- package/dist/vendor/devkit/test/runner-core.js +554 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/env.js +49 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/dist/vendor/monitoring/node.js +72 -0
- package/docs/admin-post-route.md +57 -0
- package/docs/ai-library.md +171 -0
- package/docs/architecture.md +62 -0
- package/docs/audit.md +70 -0
- package/docs/auth-hooks.md +74 -0
- package/docs/build-system.md +65 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/cli-firestore-auth.md +85 -0
- package/docs/cli-logs.md +67 -0
- package/docs/cli-output.md +146 -0
- package/docs/code-patterns.md +77 -0
- package/docs/common-mistakes.md +12 -0
- package/docs/common-operations.md +60 -0
- package/docs/connections.md +212 -0
- package/docs/consent.md +362 -0
- package/docs/directory-structure.md +150 -0
- package/docs/email-system.md +459 -0
- package/docs/environment-detection.md +93 -0
- package/docs/file-naming.md +10 -0
- package/docs/firestore.md +134 -0
- package/docs/ghostii.md +240 -0
- package/docs/index.md +299 -0
- package/docs/key-files.md +37 -0
- package/docs/logging.md +60 -0
- package/docs/marketing-campaigns.md +407 -0
- package/docs/marketing-fields.md +25 -0
- package/docs/mcp.md +222 -0
- package/docs/migration.md +129 -0
- package/docs/payment-system.md +856 -0
- package/docs/paypal-sandbox-qa.md +105 -0
- package/docs/response-headers.md +9 -0
- package/docs/routes.md +211 -0
- package/docs/sanitization.md +71 -0
- package/docs/schemas.md +163 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/stripe-webhook-forwarding.md +20 -0
- package/docs/test-boot-layer.md +67 -0
- package/docs/test-framework.md +583 -0
- package/docs/usage-rate-limiting.md +121 -0
- package/docs/verts.md +29 -0
- package/package.json +143 -0
- package/templates/config/omega.json5 +322 -0
- package/templates/database.rules.json +82 -0
- package/templates/firebase.json +67 -0
- package/templates/firestore.framework.rules +191 -0
- package/templates/firestore.indexes.json +4 -0
- package/templates/firestore.rules +59 -0
- package/templates/index.js +12 -0
- package/templates/public/404.html +26 -0
- package/templates/public/index.html +24 -0
- package/templates/remoteconfig.template.json +1 -0
- package/templates/storage-lifecycle-config-1-day.json +9 -0
- package/templates/storage-lifecycle-config-30-days.json +9 -0
- package/templates/storage.rules +11 -0
- package/test/_init/accounts-validation.js +58 -0
- package/test/ai/tools-live.test.js +171 -0
- package/test/analytics/conversion-delivery.test.js +1013 -0
- package/test/analytics/match-normalization.test.js +238 -0
- package/test/analytics/signup-conversion.test.js +135 -0
- package/test/boot/cli-dispatch.test.js +142 -0
- package/test/boot/defaults-scaffold.test.js +248 -0
- package/test/boot/deploy-staging.test.js +446 -0
- package/test/boot/disposable-domains.test.js +154 -0
- package/test/boot/emulator-boots.test.js +39 -0
- package/test/boot/emulator-port-preflight.test.js +126 -0
- package/test/boot/emulator-ready-timeout.test.js +46 -0
- package/test/boot/project-id-refresh.test.js +73 -0
- package/test/boot/suite-portability.test.js +278 -0
- package/test/boot/update-command.test.js +63 -0
- package/test/cli/cross-platform.test.js +168 -0
- package/test/cli/custom-project-type.test.js +426 -0
- package/test/cli/deploy-license-env.test.js +100 -0
- package/test/cli/emulator-adoption.test.js +254 -0
- package/test/cli/emulator-orphans.test.js +658 -0
- package/test/cli/emulator-ownership.test.js +177 -0
- package/test/cli/emulator-port-retry.test.js +395 -0
- package/test/cli/emulator-shutdown.test.js +788 -0
- package/test/cli/emulator-stale-reap.test.js +574 -0
- package/test/cli/ensure-target.test.js +325 -0
- package/test/cli/flags.test.js +64 -0
- package/test/cli/https-trust.test.js +38 -0
- package/test/cli/install-update-dispatch.test.js +47 -0
- package/test/cli/lane-environments.test.js +159 -0
- package/test/cli/marketing-campaigns-seeded.test.js +194 -0
- package/test/cli/migrate-bare-requires.test.js +128 -0
- package/test/cli/migrate-markers.test.js +530 -0
- package/test/cli/required-indexes.test.js +64 -0
- package/test/cli/rules-compile.test.js +622 -0
- package/test/cli/rules-migration-deferral.test.js +259 -0
- package/test/cli/rules-version.test.js +104 -0
- package/test/cli/setup-load-files.test.js +66 -0
- package/test/cli/setup-offline-mode.test.js +276 -0
- package/test/cli/setup-retired.test.js +64 -0
- package/test/cli/setup-shared-project-indexes.test.js +262 -0
- package/test/cli/setup-tests-requires.test.js +45 -0
- package/test/cli/stage-env-compose.test.js +316 -0
- package/test/cli/stripe-live-lane.test.js +303 -0
- package/test/cli/target.test.js +182 -0
- package/test/cli/templates.test.js +66 -0
- package/test/cli/test-runner-env.test.js +115 -0
- package/test/cli/test-stack-shutdown.test.js +275 -0
- package/test/cli/test-target-no-match.test.js +128 -0
- package/test/cli/verb-logs.test.js +118 -0
- package/test/cli/version-dispatch.test.js +39 -0
- package/test/content/blog-generate.test.js +164 -0
- package/test/email/campaign-config-fault.test.js +376 -0
- package/test/email/campaign-cron-pipeline.test.js +536 -0
- package/test/email/campaign-send.test.js +47 -0
- package/test/email/consent-lifecycle.test.js +258 -0
- package/test/email/content-html-policy.test.js +270 -0
- package/test/email/feedback-and-plain-send.test.js +54 -0
- package/test/email/fixtures/clean.json +30 -0
- package/test/email/fixtures/editorial.json +30 -0
- package/test/email/fixtures/field-report.json +53 -0
- package/test/email/identity.test.js +321 -0
- package/test/email/marketing/consent-gate.test.js +265 -0
- package/test/email/marketing/custom-fields-catalog.test.js +259 -0
- package/test/email/marketing/prune-per-provider.test.js +762 -0
- package/test/email/marketing/remove-log-privacy.test.js +225 -0
- package/test/email/marketing-lifecycle.test.js +139 -0
- package/test/email/newsletter-generate.test.js +853 -0
- package/test/email/newsletter-svg-tokens.test.js +51 -0
- package/test/email/newsletter-templates.test.js +492 -0
- package/test/email/order-one-time-cta.test.js +97 -0
- package/test/email/render-content.test.js +248 -0
- package/test/email/safe-url.test.js +0 -0
- package/test/email/sanitize-images.test.js +66 -0
- package/test/email/send-log-privacy.test.js +107 -0
- package/test/email/templates.test.js +288 -0
- package/test/email/testing-capture.test.js +260 -0
- package/test/email/transactional-send.test.js +36 -0
- package/test/email/transactional.test.js +562 -0
- package/test/email/unsubscribe-groups.test.js +155 -0
- package/test/email/unsubscribe-key.test.js +91 -0
- package/test/email/validation-cases.test.js +152 -0
- package/test/email/validation.test.js +746 -0
- package/test/events/auth-delete-conversion.test.js +138 -0
- package/test/events/auth-delete-race.test.js +211 -0
- package/test/events/auth-on-create-log.test.js +164 -0
- package/test/events/auth-signup-conversion.test.js +141 -0
- package/test/events/auth-signup-limit.test.js +142 -0
- package/test/events/auth-trigger-log-privacy.test.js +256 -0
- package/test/events/cron-job-doc-shape.test.js +101 -0
- package/test/events/cron-reset-usage.test.js +116 -0
- package/test/events/notification-conversion.test.js +182 -0
- package/test/events/payments/_webhook-harness.js +307 -0
- package/test/events/payments/abandoned-cart-activity.test.js +135 -0
- package/test/events/payments/analytics-payment-events.test.js +945 -0
- package/test/events/payments/dispute-email-status.test.js +55 -0
- package/test/events/payments/journey-payments-abandoned.test.js +126 -0
- package/test/events/payments/journey-payments-cancel-endpoint.test.js +102 -0
- package/test/events/payments/journey-payments-cancel-no-order.test.js +76 -0
- package/test/events/payments/journey-payments-cancel.test.js +184 -0
- package/test/events/payments/journey-payments-decline.test.js +225 -0
- package/test/events/payments/journey-payments-discount.test.js +91 -0
- package/test/events/payments/journey-payments-dispute.test.js +190 -0
- package/test/events/payments/journey-payments-failure.test.js +148 -0
- package/test/events/payments/journey-payments-legacy-product.test.js +151 -0
- package/test/events/payments/journey-payments-one-time-decline.test.js +127 -0
- package/test/events/payments/journey-payments-one-time-failure.test.js +113 -0
- package/test/events/payments/journey-payments-one-time-refund.test.js +186 -0
- package/test/events/payments/journey-payments-one-time.test.js +175 -0
- package/test/events/payments/journey-payments-plan-change.test.js +146 -0
- package/test/events/payments/journey-payments-plan-switch-trial.test.js +161 -0
- package/test/events/payments/journey-payments-plan-switch.test.js +182 -0
- package/test/events/payments/journey-payments-refund-no-order.test.js +76 -0
- package/test/events/payments/journey-payments-refund-webhook.test.js +225 -0
- package/test/events/payments/journey-payments-suspend.test.js +183 -0
- package/test/events/payments/journey-payments-trial-cancel.test.js +132 -0
- package/test/events/payments/journey-payments-trial.test.js +173 -0
- package/test/events/payments/journey-payments-uid-resolution.test.js +134 -0
- package/test/events/payments/journey-payments-uncancel.test.js +157 -0
- package/test/events/payments/journey-payments-upgrade.test.js +137 -0
- package/test/events/payments/journey-payments-winback-decline.test.js +171 -0
- package/test/events/payments/journey-payments-winback.test.js +195 -0
- package/test/events/payments/paypal-expiry-cron.test.js +166 -0
- package/test/events/payments/purchase-failed-handler.test.js +80 -0
- package/test/events/payments/purchase-refunded-handler.test.js +108 -0
- package/test/events/payments/test-processor-doc-shape.test.js +162 -0
- package/test/events/payments/transition-order-emails.test.js +185 -0
- package/test/events/payments/transition-promo-lines.test.js +192 -0
- package/test/events/payments/transitions-detect.test.js +767 -0
- package/test/events/payments/trial-lapse-sweep-staleness.test.js +173 -0
- package/test/events/payments/trial-lapse-sweep.test.js +359 -0
- package/test/events/payments/webhook-atomic-writes.test.js +69 -0
- package/test/events/payments/webhook-chargebee-unreachable.test.js +106 -0
- package/test/events/payments/webhook-discount-clear.test.js +189 -0
- package/test/events/payments/webhook-failure-intent.test.js +93 -0
- package/test/events/payments/webhook-hosted-page-uid-trust.test.js +212 -0
- package/test/events/payments/webhook-ordering.test.js +326 -0
- package/test/events/payments/webhook-provider-lookup-trust.test.js +206 -0
- package/test/events/payments/webhook-refund-amount-trust.test.js +223 -0
- package/test/events/payments/webhook-refund-envelope-shape.test.js +148 -0
- package/test/events/payments/webhook-refund-linkage-trust.test.js +404 -0
- package/test/events/payments/webhook-refund-transaction-fallback.test.js +184 -0
- package/test/events/payments/webhook-refund-without-order.test.js +170 -0
- package/test/events/payments/webhook-refusal-reporting.test.js +194 -0
- package/test/events/payments/webhook-refused-intent.test.js +142 -0
- package/test/events/payments/webhook-retry-sweep.test.js +252 -0
- package/test/events/payments/webhook-transition-claim.test.js +389 -0
- package/test/events/payments/webhook-uid-trust.test.js +203 -0
- package/test/events/payments/webhook-user-without-auth.test.js +161 -0
- package/test/fixtures/chargebee/invoice-one-time.json +27 -0
- package/test/fixtures/chargebee/subscription-active.json +44 -0
- package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
- package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
- package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
- package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
- package/test/fixtures/chargebee/subscription-paused.json +42 -0
- package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
- package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
- package/test/fixtures/coinbase/charge-confirmed.json +50 -0
- package/test/fixtures/coinbase/charge-failed.json +34 -0
- package/test/fixtures/coinbase/charge-pending.json +34 -0
- package/test/fixtures/migrate/ported-route.js.txt +11 -0
- package/test/fixtures/paypal/capture-completed.json +51 -0
- package/test/fixtures/paypal/capture-refunded.json +43 -0
- package/test/fixtures/paypal/order-approved.json +62 -0
- package/test/fixtures/paypal/order-completed.json +110 -0
- package/test/fixtures/paypal/sale-refunded.json +38 -0
- package/test/fixtures/paypal/subscription-active.json +76 -0
- package/test/fixtures/paypal/subscription-cancelled.json +50 -0
- package/test/fixtures/paypal/subscription-suspended.json +65 -0
- package/test/fixtures/stripe/checkout-session-completed.json +130 -0
- package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
- package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
- package/test/fixtures/stripe/invoice-subscription-payment-succeeded.json +28 -0
- package/test/fixtures/stripe/subscription-active.json +161 -0
- package/test/fixtures/stripe/subscription-canceled.json +161 -0
- package/test/fixtures/stripe/subscription-trialing.json +161 -0
- package/test/helpers/_shared-config.js +15 -0
- package/test/helpers/ai-request-payload.test.js +619 -0
- package/test/helpers/ai-schema-resolve.test.js +125 -0
- package/test/helpers/ai-test-provider.test.js +227 -0
- package/test/helpers/ai-token-accounting.test.js +150 -0
- package/test/helpers/ai-tools-format.test.js +384 -0
- package/test/helpers/analytics-no-id-notice.test.js +146 -0
- package/test/helpers/analytics-user-data.test.js +194 -0
- package/test/helpers/api-manager.test.js +311 -0
- package/test/helpers/backend-router.test.js +136 -0
- package/test/helpers/content/blog-auto-publisher.test.js +482 -0
- package/test/helpers/content/feed-parser.test.js +529 -0
- package/test/helpers/content/ghostii-blocks.test.js +135 -0
- package/test/helpers/content/ghostii-feed-integration.test.js +405 -0
- package/test/helpers/content/ghostii-write-article.test.js +244 -0
- package/test/helpers/dev-only-routes.test.js +225 -0
- package/test/helpers/env-reader.test.js +362 -0
- package/test/helpers/environment.test.js +263 -0
- package/test/helpers/event-middleware.test.js +407 -0
- package/test/helpers/infer-contact.test.js +157 -0
- package/test/helpers/lane-url.test.js +90 -0
- package/test/helpers/merge-line-files.test.js +272 -0
- package/test/helpers/metadata.test.js +122 -0
- package/test/helpers/middleware-request-log.test.js +134 -0
- package/test/helpers/middleware-user-log.test.js +124 -0
- package/test/helpers/payment/chargebee/parse-webhook.test.js +490 -0
- package/test/helpers/payment/chargebee/refund-details.test.js +198 -0
- package/test/helpers/payment/chargebee/to-unified-one-time.test.js +148 -0
- package/test/helpers/payment/chargebee/to-unified-subscription.test.js +649 -0
- package/test/helpers/payment/coinbase/create-intent.test.js +247 -0
- package/test/helpers/payment/coinbase/parse-webhook.test.js +195 -0
- package/test/helpers/payment/coinbase/refund-unsupported.test.js +141 -0
- package/test/helpers/payment/coinbase/to-unified-one-time.test.js +201 -0
- package/test/helpers/payment/discount-codes.test.js +141 -0
- package/test/helpers/payment/extract-resource.test.js +113 -0
- package/test/helpers/payment/fetch-failure.test.js +215 -0
- package/test/helpers/payment/license-gate.test.js +108 -0
- package/test/helpers/payment/order-id.test.js +99 -0
- package/test/helpers/payment/paypal/create-intent.test.js +382 -0
- package/test/helpers/payment/paypal/fetch-capture.test.js +123 -0
- package/test/helpers/payment/paypal/fetch-sale.test.js +183 -0
- package/test/helpers/payment/paypal/parse-webhook.test.js +678 -0
- package/test/helpers/payment/paypal/refund-details.test.js +202 -0
- package/test/helpers/payment/paypal/resolve-plan-id.test.js +108 -0
- package/test/helpers/payment/paypal/switch-plan.test.js +92 -0
- package/test/helpers/payment/paypal/to-unified-one-time.test.js +383 -0
- package/test/helpers/payment/paypal/to-unified-subscription.test.js +884 -0
- package/test/helpers/payment/stripe/fetch-charge.test.js +147 -0
- package/test/helpers/payment/stripe/parse-webhook.test.js +448 -0
- package/test/helpers/payment/stripe/refund-details.test.js +147 -0
- package/test/helpers/payment/stripe/to-unified-one-time.test.js +307 -0
- package/test/helpers/payment/stripe/to-unified-subscription.test.js +709 -0
- package/test/helpers/persona-domain.test.js +87 -0
- package/test/helpers/recaptcha.test.js +166 -0
- package/test/helpers/resolved-config.test.js +106 -0
- package/test/helpers/response-log-redaction.test.js +262 -0
- package/test/helpers/roles.test.js +155 -0
- package/test/helpers/route-context-debug-gate.test.js +118 -0
- package/test/helpers/route-context-logging.test.js +214 -0
- package/test/helpers/route-context.test.js +129 -0
- package/test/helpers/safe-compare.test.js +114 -0
- package/test/helpers/sanitize.test.js +223 -0
- package/test/helpers/schema-engine.test.js +537 -0
- package/test/helpers/schema-zod.test.js +554 -0
- package/test/helpers/seed-accounts-load.test.js +163 -0
- package/test/helpers/seed-google-personas.test.js +114 -0
- package/test/helpers/seeded-personas.test.js +823 -0
- package/test/helpers/settings.test.js +292 -0
- package/test/helpers/setup-engines-pin.test.js +68 -0
- package/test/helpers/setup-manifest-sync.test.js +116 -0
- package/test/helpers/slugify.test.js +395 -0
- package/test/helpers/storage.test.js +195 -0
- package/test/helpers/test-banner-latch.test.js +139 -0
- package/test/helpers/usage-consume.test.js +460 -0
- package/test/helpers/usage-log-privacy.test.js +121 -0
- package/test/helpers/user-doc-heal.test.js +540 -0
- package/test/helpers/user.test.js +771 -0
- package/test/helpers/webhook-forward.test.js +419 -0
- package/test/helpers/wipe-auth-project.test.js +143 -0
- package/test/mcp/discovery.test.js +52 -0
- package/test/mcp/oauth.test.js +160 -0
- package/test/mcp/protocol.test.js +278 -0
- package/test/mcp/roles.test.js +203 -0
- package/test/mcp/utils.test.js +245 -0
- package/test/notification/identity.test.js +155 -0
- package/test/routes/admin/create-post.test.js +362 -0
- package/test/routes/admin/database.test.js +133 -0
- package/test/routes/admin/deduplicate-image-alts.test.js +191 -0
- package/test/routes/admin/email-content-html.test.js +58 -0
- package/test/routes/admin/email-request-log.test.js +86 -0
- package/test/routes/admin/email.test.js +116 -0
- package/test/routes/admin/firestore-query.test.js +206 -0
- package/test/routes/admin/firestore.test.js +129 -0
- package/test/routes/admin/infer-contact.test.js +220 -0
- package/test/routes/admin/notification.test.js +199 -0
- package/test/routes/admin/post-convert-image.test.js +159 -0
- package/test/routes/admin/post-deploy-flag.test.js +81 -0
- package/test/routes/admin/post-download-error.test.js +90 -0
- package/test/routes/admin/post-resize-image.test.js +185 -0
- package/test/routes/admin/post.test.js +369 -0
- package/test/routes/admin/repo-content.test.js +223 -0
- package/test/routes/admin/stats.test.js +114 -0
- package/test/routes/admin/users-disable.test.js +63 -0
- package/test/routes/admin/users-list.test.js +71 -0
- package/test/routes/content/post.test.js +60 -0
- package/test/routes/general/uuid.test.js +133 -0
- package/test/routes/health.test.js +110 -0
- package/test/routes/marketing/campaign.test.js +184 -0
- package/test/routes/marketing/contact.test.js +416 -0
- package/test/routes/marketing/email-preferences.test.js +293 -0
- package/test/routes/marketing/push-send.test.js +34 -0
- package/test/routes/marketing/webhook-forward.test.js +63 -0
- package/test/routes/marketing/webhook.test.js +641 -0
- package/test/routes/payments/_route-harness.js +145 -0
- package/test/routes/payments/cancel-provider-errors.test.js +227 -0
- package/test/routes/payments/cancel-skip-guards.test.js +126 -0
- package/test/routes/payments/cancel-trialing.test.js +428 -0
- package/test/routes/payments/cancel.test.js +165 -0
- package/test/routes/payments/dedup-race.test.js +186 -0
- package/test/routes/payments/discount.test.js +82 -0
- package/test/routes/payments/dispute-alert.test.js +324 -0
- package/test/routes/payments/intent-discount-amounts.test.js +550 -0
- package/test/routes/payments/intent-discount-coupons.test.js +293 -0
- package/test/routes/payments/intent-one-time-metadata.test.js +122 -0
- package/test/routes/payments/intent-purchaser-guard.test.js +139 -0
- package/test/routes/payments/intent-zero-total.test.js +260 -0
- package/test/routes/payments/intent.test.js +413 -0
- package/test/routes/payments/plan.test.js +367 -0
- package/test/routes/payments/portal-return-url.test.js +107 -0
- package/test/routes/payments/portal.test.js +94 -0
- package/test/routes/payments/refund-one-time.test.js +254 -0
- package/test/routes/payments/refund-paypal-proration.test.js +190 -0
- package/test/routes/payments/refund.test.js +181 -0
- package/test/routes/payments/trial-eligibility.test.js +73 -0
- package/test/routes/payments/uncancel.test.js +270 -0
- package/test/routes/payments/webhook-stripe-invoice.test.js +129 -0
- package/test/routes/payments/webhook-stripe-refund-one-time.test.js +130 -0
- package/test/routes/payments/webhook-test-provider.test.js +156 -0
- package/test/routes/payments/webhook.test.js +115 -0
- package/test/routes/payments/winback-stripe-coupon.test.js +126 -0
- package/test/routes/payments/winback.test.js +597 -0
- package/test/routes/test/authenticate.test.js +79 -0
- package/test/routes/test/redirect.test.js +66 -0
- package/test/routes/test/reset-account.test.js +126 -0
- package/test/routes/test/roster.test.js +101 -0
- package/test/routes/test/schema.test.js +556 -0
- package/test/routes/test/usage.test.js +374 -0
- package/test/routes/user/api-keys.test.js +158 -0
- package/test/routes/user/connections-grant.test.js +759 -0
- package/test/routes/user/connections-identity.test.js +260 -0
- package/test/routes/user/connections-log-privacy.test.js +206 -0
- package/test/routes/user/connections-refresh-lease.test.js +696 -0
- package/test/routes/user/connections-return.test.js +261 -0
- package/test/routes/user/connections-uid.test.js +516 -0
- package/test/routes/user/delete.test.js +138 -0
- package/test/routes/user/feedback.test.js +106 -0
- package/test/routes/user/orders.test.js +233 -0
- package/test/routes/user/sessions.test.js +231 -0
- package/test/routes/user/settings-validate.test.js +84 -0
- package/test/routes/user/signup-emails.test.js +113 -0
- package/test/routes/user/signup-location.test.js +84 -0
- package/test/routes/user/signup-log-privacy.test.js +112 -0
- package/test/routes/user/signup.test.js +579 -0
- package/test/routes/user/subscription.test.js +101 -0
- package/test/routes/user/token.test.js +111 -0
- package/test/routes/user/user.test.js +158 -0
- package/test/routes/verts/cache.test.js +78 -0
- package/test/routes/verts/click-destination.test.js +76 -0
- package/test/routes/verts/crud.test.js +194 -0
- package/test/routes/verts/redirect.test.js +147 -0
- package/test/routes/verts/selection.test.js +237 -0
- package/test/routes/verts/serve.test.js +185 -0
- package/test/routes/verts/unit-document.test.js +117 -0
- package/test/rules/_environment.js +66 -0
- package/test/rules/brand-merge.test.js +195 -0
- package/test/rules/field-helpers.test.js +309 -0
- package/test/rules/notifications.test.js +577 -0
- package/test/rules/payments-carts.test.js +406 -0
- package/test/rules/sessions.test.js +170 -0
- package/test/rules/user.test.js +517 -0
- package/test/rules/verts.test.js +101 -0
- package/test/security/fetch-log-secrets.test.js +124 -0
- package/test/security/primitives.test.js +105 -0
- package/test/security/repo-pinning.test.js +77 -0
- 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
|
+
};
|