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