@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,987 @@
|
|
|
1
|
+
const powertools = require('node-powertools');
|
|
2
|
+
const fetchFailure = require('../fetch-failure.js');
|
|
3
|
+
const assertRefundLinkage = require('../refund-linkage.js');
|
|
4
|
+
const env = require('../../env.js');
|
|
5
|
+
const assertLicensedPayments = require('../license.js');
|
|
6
|
+
|
|
7
|
+
// Epoch zero timestamps (used as default/empty dates)
|
|
8
|
+
const EPOCH_ZERO = powertools.timestamp(new Date(0), { output: 'string' });
|
|
9
|
+
const EPOCH_ZERO_UNIX = powertools.timestamp(EPOCH_ZERO, { output: 'unix' });
|
|
10
|
+
|
|
11
|
+
// PayPal interval → unified frequency map
|
|
12
|
+
const INTERVAL_TO_FREQUENCY = { YEAR: 'annually', MONTH: 'monthly', WEEK: 'weekly', DAY: 'daily' };
|
|
13
|
+
const FREQUENCY_TO_INTERVAL = { annually: 'YEAR', monthly: 'MONTH', weekly: 'WEEK', daily: 'DAY' };
|
|
14
|
+
|
|
15
|
+
// PayPal API base URLs
|
|
16
|
+
const LIVE_URL = 'https://api-m.paypal.com';
|
|
17
|
+
const SANDBOX_URL = 'https://api-m.sandbox.paypal.com';
|
|
18
|
+
|
|
19
|
+
// Cached access token, expiry, and resolved base URL
|
|
20
|
+
let cachedToken = null;
|
|
21
|
+
let tokenExpiresAt = 0;
|
|
22
|
+
let resolvedBaseUrl = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Try to authenticate against a specific PayPal endpoint
|
|
26
|
+
* @param {string} auth - Base64-encoded client_id:secret
|
|
27
|
+
* @param {string} baseUrl - PayPal API base URL
|
|
28
|
+
* @returns {Promise<object|null>} Token data or null if auth failed
|
|
29
|
+
*/
|
|
30
|
+
async function tryAuth(auth, baseUrl) {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`${baseUrl}/v1/oauth2/token`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Authorization': `Basic ${auth}`,
|
|
36
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
37
|
+
},
|
|
38
|
+
body: 'grant_type=client_credentials',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return await response.json();
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* PayPal shared library
|
|
53
|
+
* Provides API helpers, resource fetching, and unified transformations
|
|
54
|
+
*/
|
|
55
|
+
const PayPal = {
|
|
56
|
+
/**
|
|
57
|
+
* Initialize or return a PayPal access token
|
|
58
|
+
* Tries both live and sandbox endpoints in parallel on first auth
|
|
59
|
+
* @returns {Promise<string>} Access token
|
|
60
|
+
*/
|
|
61
|
+
async init() {
|
|
62
|
+
// A keyless deploy runs no live payments (#320) — before the token, and
|
|
63
|
+
// before the cache below can hand one back
|
|
64
|
+
assertLicensedPayments('PayPal');
|
|
65
|
+
|
|
66
|
+
// Return cached token if still valid (with 60s buffer)
|
|
67
|
+
if (cachedToken && Date.now() < tokenExpiresAt - 60000) {
|
|
68
|
+
return cachedToken;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const clientId = env.get('PAYPAL_CLIENT_ID');
|
|
72
|
+
const clientSecret = env.get('PAYPAL_CLIENT_SECRET');
|
|
73
|
+
|
|
74
|
+
if (!clientId || !clientSecret) {
|
|
75
|
+
throw new Error('PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET environment variables are required');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
79
|
+
|
|
80
|
+
// First auth — try both endpoints in parallel to detect environment
|
|
81
|
+
if (!resolvedBaseUrl) {
|
|
82
|
+
const [liveResult, sandboxResult] = await Promise.all([
|
|
83
|
+
tryAuth(auth, LIVE_URL),
|
|
84
|
+
tryAuth(auth, SANDBOX_URL),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
if (liveResult) {
|
|
88
|
+
resolvedBaseUrl = LIVE_URL;
|
|
89
|
+
cachedToken = liveResult.access_token;
|
|
90
|
+
tokenExpiresAt = Date.now() + (liveResult.expires_in * 1000);
|
|
91
|
+
return cachedToken;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (sandboxResult) {
|
|
95
|
+
resolvedBaseUrl = SANDBOX_URL;
|
|
96
|
+
cachedToken = sandboxResult.access_token;
|
|
97
|
+
tokenExpiresAt = Date.now() + (sandboxResult.expires_in * 1000);
|
|
98
|
+
return cachedToken;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new Error('PayPal auth failed on both live and sandbox — check your client ID and secret');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Subsequent auths — use the resolved endpoint
|
|
105
|
+
const result = await tryAuth(auth, resolvedBaseUrl);
|
|
106
|
+
|
|
107
|
+
if (!result) {
|
|
108
|
+
throw new Error(`PayPal auth failed (${resolvedBaseUrl})`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
cachedToken = result.access_token;
|
|
112
|
+
tokenExpiresAt = Date.now() + (result.expires_in * 1000);
|
|
113
|
+
|
|
114
|
+
return cachedToken;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Make an authenticated PayPal API request
|
|
119
|
+
* @param {string} endpoint - API path (e.g., '/v1/billing/subscriptions/I-xxx')
|
|
120
|
+
* @param {object} options - fetch options (method, body, etc.)
|
|
121
|
+
* @returns {Promise<object>} Parsed JSON response
|
|
122
|
+
*/
|
|
123
|
+
async request(endpoint, options = {}) {
|
|
124
|
+
const token = await this.init();
|
|
125
|
+
|
|
126
|
+
const response = await fetch(`${resolvedBaseUrl}${endpoint}`, {
|
|
127
|
+
...options,
|
|
128
|
+
headers: {
|
|
129
|
+
'Authorization': `Bearer ${token}`,
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
...options.headers,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 204 No Content
|
|
136
|
+
if (response.status === 204) {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data = await response.json();
|
|
141
|
+
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const msg = data.message || data.error_description || JSON.stringify(data);
|
|
144
|
+
throw new Error(`PayPal API ${response.status}: ${msg}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return data;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Fetch the latest resource from PayPal's API
|
|
152
|
+
* PayPal's answer is the only trusted source — a lookup that fails throws the
|
|
153
|
+
* classified failure ([../fetch-failure.js](../fetch-failure.js)) instead of
|
|
154
|
+
* degrading to the webhook payload
|
|
155
|
+
*
|
|
156
|
+
* For orders: captures the payment first (moves funds), then returns the captured order
|
|
157
|
+
*
|
|
158
|
+
* @param {string} resourceType - 'subscription', 'order', or 'sale'
|
|
159
|
+
* @param {string} resourceId - PayPal resource ID (e.g., 'I-xxx', an order ID, or a sale ID)
|
|
160
|
+
* @param {object} context - Additional context (e.g., { config })
|
|
161
|
+
* @returns {object} Full PayPal resource object
|
|
162
|
+
*/
|
|
163
|
+
async fetchResource(resourceType, resourceId, context) {
|
|
164
|
+
try {
|
|
165
|
+
if (resourceType === 'subscription') {
|
|
166
|
+
const sub = await this.request(`/v1/billing/subscriptions/${resourceId}`);
|
|
167
|
+
|
|
168
|
+
// Fetch the plan to get product_id (subscription doesn't include it)
|
|
169
|
+
if (sub.plan_id) {
|
|
170
|
+
try {
|
|
171
|
+
const plan = await this.request(`/v1/billing/plans/${sub.plan_id}`);
|
|
172
|
+
sub._plan = plan;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// Plan fetch failed — continue without it
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return sub;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (resourceType === 'order') {
|
|
182
|
+
// Capture the order to move funds, then return the captured state
|
|
183
|
+
const captured = await this.request(`/v2/checkout/orders/${resourceId}/capture`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return captured;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (resourceType === 'capture') {
|
|
191
|
+
// The v2 twin of the sale branch below. A capture created by this
|
|
192
|
+
// framework's own v2 Orders flow carries our custom_id directly, so
|
|
193
|
+
// there is nothing to fold — one read is the whole answer
|
|
194
|
+
// ([#240](https://github.com/Omega-JS-Stack/omega/issues/240)).
|
|
195
|
+
return await this.request(`/v2/payments/captures/${resourceId}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (resourceType === 'sale') {
|
|
199
|
+
// The refund of a one-time purchase names the SALE it reversed. A v1 sale
|
|
200
|
+
// carries the money and the payment behind it, never our custom_id — the
|
|
201
|
+
// parent payment's transaction is the only place uid/orderId/productId
|
|
202
|
+
// live, so fold it onto the sale the way the subscription case folds its
|
|
203
|
+
// plan ([#224](https://github.com/Omega-JS-Stack/omega/issues/224)).
|
|
204
|
+
const sale = await this.request(`/v1/payments/sale/${resourceId}`);
|
|
205
|
+
|
|
206
|
+
// v1 sometimes spells the field `custom` — normalize before deciding a
|
|
207
|
+
// second read is needed, so identifiers already in hand are never dropped
|
|
208
|
+
if (!sale.custom_id && sale.custom) {
|
|
209
|
+
sale.custom_id = sale.custom;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!sale.custom_id && sale.parent_payment) {
|
|
213
|
+
try {
|
|
214
|
+
const payment = await this.request(`/v1/payments/payment/${sale.parent_payment}`);
|
|
215
|
+
const transaction = payment.transactions?.[0];
|
|
216
|
+
const custom = transaction?.custom_id || transaction?.custom || null;
|
|
217
|
+
|
|
218
|
+
if (custom) {
|
|
219
|
+
sale.custom_id = custom;
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// Parent payment fetch failed — the sale itself is still the live
|
|
223
|
+
// answer, but without a folded custom_id the pipeline cannot name the
|
|
224
|
+
// order this refund hits, so the miss must be visible in the logs.
|
|
225
|
+
const message = `paypal fetchResource(sale/${resourceId}) could not read parent payment ${sale.parent_payment} for identifiers: ${e?.message || e}`;
|
|
226
|
+
|
|
227
|
+
if (context?.ctx?.warn) {
|
|
228
|
+
context.ctx.warn(message);
|
|
229
|
+
} else {
|
|
230
|
+
console.warn(`[@omega.js/backend:payment:paypal] ${message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return sale;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
throw new Error(`Unknown resource type: ${resourceType}`);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
// An order fetch IS the capture, so its failure also means the money never moved
|
|
241
|
+
throw fetchFailure(e, {
|
|
242
|
+
provider: 'paypal',
|
|
243
|
+
fn: 'fetchResource',
|
|
244
|
+
resourceType,
|
|
245
|
+
resourceId,
|
|
246
|
+
consequence: resourceType === 'order'
|
|
247
|
+
? 'the order was NOT captured, so the funds have NOT moved'
|
|
248
|
+
: null,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Transform a raw PayPal subscription object into the unified subscription shape
|
|
255
|
+
*
|
|
256
|
+
* @param {object} rawSubscription - Raw PayPal subscription object (with _plan attached)
|
|
257
|
+
* @param {object} options
|
|
258
|
+
* @param {object} options.config - @omega.js/backend config (must contain products array)
|
|
259
|
+
* @param {string} options.eventName - Name of the webhook event
|
|
260
|
+
* @param {string} options.eventId - ID of the webhook event
|
|
261
|
+
* @returns {object} Unified subscription object
|
|
262
|
+
*/
|
|
263
|
+
toUnifiedSubscription(rawSubscription, options) {
|
|
264
|
+
options = options || {};
|
|
265
|
+
const config = options.config || {};
|
|
266
|
+
|
|
267
|
+
const status = resolveStatus(rawSubscription);
|
|
268
|
+
const cancellation = resolveCancellation(rawSubscription);
|
|
269
|
+
const trial = resolveTrial(rawSubscription);
|
|
270
|
+
const frequency = resolveFrequency(rawSubscription);
|
|
271
|
+
const product = resolveProduct(rawSubscription, config);
|
|
272
|
+
const expires = resolveExpires(rawSubscription);
|
|
273
|
+
const startDate = resolveStartDate(rawSubscription);
|
|
274
|
+
const price = resolvePrice(product.id, frequency, config);
|
|
275
|
+
|
|
276
|
+
// Parse custom_id for uid and orderId
|
|
277
|
+
const customData = parseCustomId(rawSubscription.custom_id);
|
|
278
|
+
|
|
279
|
+
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
280
|
+
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
product: product,
|
|
284
|
+
status: status,
|
|
285
|
+
expires: expires,
|
|
286
|
+
trial: trial,
|
|
287
|
+
cancellation: cancellation,
|
|
288
|
+
payment: {
|
|
289
|
+
provider: 'paypal',
|
|
290
|
+
orderId: customData.orderId || null,
|
|
291
|
+
resourceId: rawSubscription.id || null,
|
|
292
|
+
frequency: frequency,
|
|
293
|
+
price: price,
|
|
294
|
+
startDate: startDate,
|
|
295
|
+
updatedBy: {
|
|
296
|
+
event: {
|
|
297
|
+
name: options.eventName || null,
|
|
298
|
+
id: options.eventId || null,
|
|
299
|
+
},
|
|
300
|
+
date: {
|
|
301
|
+
timestamp: now,
|
|
302
|
+
timestampUNIX: nowUNIX,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Transform a raw PayPal one-time payment resource into a unified shape
|
|
311
|
+
*
|
|
312
|
+
* @param {object} rawResource - Raw PayPal resource (capture, order, etc.)
|
|
313
|
+
* @param {object} options
|
|
314
|
+
* @returns {object} Unified one-time payment object
|
|
315
|
+
*/
|
|
316
|
+
toUnifiedOneTime(rawResource, options) {
|
|
317
|
+
options = options || {};
|
|
318
|
+
const config = options.config || {};
|
|
319
|
+
|
|
320
|
+
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
321
|
+
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
322
|
+
|
|
323
|
+
// Resolve product from purchase_units custom_id (orders) or top-level custom_id (subscriptions)
|
|
324
|
+
const purchaseCustomId = rawResource.purchase_units?.[0]?.custom_id;
|
|
325
|
+
const customData = parseCustomId(purchaseCustomId || rawResource.custom_id);
|
|
326
|
+
const productId = customData.productId;
|
|
327
|
+
const product = resolveProductOneTime(productId, config);
|
|
328
|
+
const price = resolvePrice(productId, 'once', config);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
product: product,
|
|
332
|
+
status: rawResource.status === 'COMPLETED' ? 'completed' : rawResource.status?.toLowerCase() || 'unknown',
|
|
333
|
+
payment: {
|
|
334
|
+
provider: 'paypal',
|
|
335
|
+
orderId: customData.orderId || null,
|
|
336
|
+
resourceId: rawResource.id || null,
|
|
337
|
+
price: price,
|
|
338
|
+
updatedBy: {
|
|
339
|
+
event: {
|
|
340
|
+
name: options.eventName || null,
|
|
341
|
+
id: options.eventId || null,
|
|
342
|
+
},
|
|
343
|
+
date: {
|
|
344
|
+
timestamp: now,
|
|
345
|
+
timestampUNIX: nowUNIX,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Resolve a PayPal plan ID from product config at runtime
|
|
354
|
+
* Fetches plans for the PayPal product ID and matches by interval + amount +
|
|
355
|
+
* TRIAL-cycle presence — the manager's own matching rule
|
|
356
|
+
*
|
|
357
|
+
* PayPal puts a trial on the PLAN, never on the subscribe call, so a product
|
|
358
|
+
* with trial days carries a TWIN pair per interval: one with the TRIAL cycle
|
|
359
|
+
* and one without. `trial` picks between them, which is the only way a buyer
|
|
360
|
+
* who is not owed a trial gets a plan with no free cycle on it
|
|
361
|
+
* ([#761](https://github.com/Omega-JS-Stack/omega/issues/761)). A trial
|
|
362
|
+
* product whose skip-trial twin has not been minted yet (the manager walk has
|
|
363
|
+
* not run since) throws rather than falling back to the trial twin — the fix
|
|
364
|
+
* is a manage run, never a free period nobody granted.
|
|
365
|
+
*
|
|
366
|
+
* @param {object} product - Product from config
|
|
367
|
+
* @param {string} frequency - 'monthly', 'annually', etc.
|
|
368
|
+
* @param {boolean} [trial=false] - Whether this checkout is taking the trial
|
|
369
|
+
* @returns {Promise<string>} PayPal plan ID
|
|
370
|
+
*/
|
|
371
|
+
async resolvePlanId(product, frequency, trial = false) {
|
|
372
|
+
if (product.archived) {
|
|
373
|
+
throw new Error(`Product ${product.id} is archived`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const paypalProductId = product.paypal?.productId;
|
|
377
|
+
|
|
378
|
+
if (!paypalProductId) {
|
|
379
|
+
throw new Error(`No PayPal product ID for ${product.id}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const expectedAmount = product.prices?.[frequency];
|
|
383
|
+
|
|
384
|
+
if (!expectedAmount) {
|
|
385
|
+
throw new Error(`No price configured for ${product.id}/${frequency}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Fetch plans with full details (Prefer header includes billing_cycles in list response)
|
|
389
|
+
// TODO: Paginate — page_size=20 only returns first page. Fine for now (each product has ~2-4 plans in 1:1 model) but will break if a product ever accumulates >20 plans.
|
|
390
|
+
const response = await this.request(`/v1/billing/plans?product_id=${paypalProductId}&page_size=20&total_required=true`, {
|
|
391
|
+
headers: { 'Prefer': 'return=representation' },
|
|
392
|
+
});
|
|
393
|
+
const plans = response.plans || [];
|
|
394
|
+
|
|
395
|
+
// Map frequency to PayPal interval unit
|
|
396
|
+
const intervalUnit = FREQUENCY_TO_INTERVAL[frequency] || 'MONTH';
|
|
397
|
+
|
|
398
|
+
// Find matching active plan by interval + amount + trial-cycle presence
|
|
399
|
+
for (const plan of plans) {
|
|
400
|
+
if (plan.status !== 'ACTIVE') {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const cycle = plan.billing_cycles?.find(c => c.tenure_type === 'REGULAR');
|
|
405
|
+
|
|
406
|
+
if (!cycle) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const planInterval = cycle.frequency?.interval_unit;
|
|
411
|
+
const planAmount = parseFloat(cycle.pricing_scheme?.fixed_price?.value || '0');
|
|
412
|
+
const planHasTrial = !!plan.billing_cycles?.find(c => c.tenure_type === 'TRIAL');
|
|
413
|
+
|
|
414
|
+
if (planInterval === intervalUnit && planAmount === expectedAmount && planHasTrial === !!trial) {
|
|
415
|
+
return plan.id;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
throw new Error(`No active PayPal plan for ${product.id}/${frequency} at $${expectedAmount} ${trial ? 'with' : 'without'} a trial cycle (product: ${paypalProductId}) — run the manager payment walk to mint it`);
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Extract the resource a PayPal webhook envelope carries
|
|
424
|
+
* Identifiers only — the payload never drives state ([../fetch-failure.js](../fetch-failure.js))
|
|
425
|
+
*
|
|
426
|
+
* @param {object} raw - Raw PayPal webhook payload
|
|
427
|
+
* @returns {object|null}
|
|
428
|
+
*/
|
|
429
|
+
extractResource(raw) {
|
|
430
|
+
return raw?.resource || null;
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Extract the internal orderId from a PayPal resource
|
|
435
|
+
* Stripe stores orderId in resource.metadata.orderId, but PayPal stores it in custom_id
|
|
436
|
+
*
|
|
437
|
+
* @param {object} resource - Raw PayPal resource (subscription or order)
|
|
438
|
+
* @returns {string|null}
|
|
439
|
+
*/
|
|
440
|
+
getOrderId(resource) {
|
|
441
|
+
const purchaseCustomId = resource.purchase_units?.[0]?.custom_id;
|
|
442
|
+
const customData = parseCustomId(purchaseCustomId || resource.custom_id);
|
|
443
|
+
return customData.orderId || null;
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Extract the UID from a PayPal resource's custom_id
|
|
448
|
+
* Used to resolve UID after fetchResource() for events like PAYMENT.SALE
|
|
449
|
+
* where the initial webhook payload doesn't carry custom_id
|
|
450
|
+
*
|
|
451
|
+
* @param {object} resource - Raw PayPal resource (subscription or order)
|
|
452
|
+
* @returns {string|null}
|
|
453
|
+
*/
|
|
454
|
+
getUid(resource) {
|
|
455
|
+
const purchaseCustomId = resource.purchase_units?.[0]?.custom_id;
|
|
456
|
+
const customData = parseCustomId(purchaseCustomId || resource.custom_id);
|
|
457
|
+
return customData?.uid || null;
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* What a PayPal refund actually moved, read back from PayPal's own refund record
|
|
462
|
+
*
|
|
463
|
+
* The resource already in hand is no use here: it is the sale or the capture the
|
|
464
|
+
* refund reversed — the ORIGINAL payment, whose amount is the purchase price and
|
|
465
|
+
* not the refund's, which is simply wrong for a partial refund. And the amounts
|
|
466
|
+
* used to come from the webhook envelope, so a payload naming an inflated total
|
|
467
|
+
* wrote that number onto the order and into the customer's refund email
|
|
468
|
+
* ([#510](https://github.com/Omega-JS-Stack/omega/issues/510)).
|
|
469
|
+
*
|
|
470
|
+
* So the refund is fetched by its OWN id — the one the parser keeps beside the
|
|
471
|
+
* resourceId it reassigned to the sale/capture — and only the id comes from the
|
|
472
|
+
* event, the same trust level as the resourceId every lookup here starts from.
|
|
473
|
+
* v2 capture refunds read at `/v2/payments/refunds/{id}`; v1 sale refunds (both
|
|
474
|
+
* the one-time and the subscription path) at `/v1/payments/refund/{id}`.
|
|
475
|
+
*
|
|
476
|
+
* The record that answers has to point BACK at the sale or capture the event
|
|
477
|
+
* named, or it is another order's refund and the event is refused
|
|
478
|
+
* ([../refund-linkage.js](../refund-linkage.js)).
|
|
479
|
+
*
|
|
480
|
+
* @param {object} resource - The resource the refund reversed, as PayPal answered for it (the record it must link back to)
|
|
481
|
+
* @param {object} [options] - { refundId, eventType, resourceType, raw, ctx }
|
|
482
|
+
* @returns {Promise<{ amount: string|null, currency: string, reason: string|null }>}
|
|
483
|
+
*/
|
|
484
|
+
async getRefundDetails(resource, options = {}) {
|
|
485
|
+
// A doc stored before the parser threaded the id still names the refund in the
|
|
486
|
+
// envelope it carries — an identifier, read the same way resourceId is
|
|
487
|
+
const refundId = options.refundId || options.raw?.resource?.id || null;
|
|
488
|
+
|
|
489
|
+
if (!refundId) {
|
|
490
|
+
const message = `paypal getRefundDetails(): ${options.eventType || 'refund event'} names no refund id, so PayPal cannot be asked what came back — the refund is recorded with no amount rather than with the payload's`;
|
|
491
|
+
|
|
492
|
+
if (options.ctx?.warn) {
|
|
493
|
+
options.ctx.warn(message);
|
|
494
|
+
} else {
|
|
495
|
+
console.warn(`[@omega.js/backend:payment:paypal] ${message}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return { amount: null, currency: 'USD', reason: null };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const endpoint = options.eventType === 'PAYMENT.CAPTURE.REFUNDED'
|
|
502
|
+
? `/v2/payments/refunds/${refundId}`
|
|
503
|
+
: `/v1/payments/refund/${refundId}`;
|
|
504
|
+
|
|
505
|
+
let refund;
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
refund = await this.request(endpoint);
|
|
509
|
+
} catch (e) {
|
|
510
|
+
throw fetchFailure(e, { provider: 'paypal', fn: 'getRefundDetails', resourceType: 'refund', resourceId: refundId });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// The refund is a SECOND record, keyed by an id the payload chose: without a
|
|
514
|
+
// link back to the sale or capture this event is about, an unrelated refund
|
|
515
|
+
// from the same account would have booked its amount onto this order
|
|
516
|
+
// ([#532](https://github.com/Omega-JS-Stack/omega/issues/532)). PayPal writes
|
|
517
|
+
// that link both ways round — a v1 refund names its `sale_id`, a v2 refund
|
|
518
|
+
// links `up` to its capture. A refund of a SUBSCRIPTION's sale names the sale,
|
|
519
|
+
// never the billing agreement the event resolved to, so there is nothing to
|
|
520
|
+
// compare and the linkage helper says so out loud.
|
|
521
|
+
assertRefundLinkage({
|
|
522
|
+
provider: 'paypal',
|
|
523
|
+
refundType: 'refund',
|
|
524
|
+
refundId: refundId,
|
|
525
|
+
resourceType: options.resourceType || 'resource',
|
|
526
|
+
resourceId: resource?.id || null,
|
|
527
|
+
expected: resource?.id || null,
|
|
528
|
+
ctx: options.ctx,
|
|
529
|
+
...refundLink(refund, options.resourceType),
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
// v1 spells the amount `total`/`currency`, v2 spells it
|
|
534
|
+
// `value`/`currency_code` — a v2 capture refund read only the v1
|
|
535
|
+
// spelling and landed a null amount on the order record and the
|
|
536
|
+
// customer's refund email
|
|
537
|
+
// ([#240](https://github.com/Omega-JS-Stack/omega/issues/240)).
|
|
538
|
+
amount: refund?.amount?.total || refund?.amount?.value || refund?.total_refunded_amount?.value || null,
|
|
539
|
+
currency: refund?.amount?.currency || refund?.amount?.currency_code || 'USD',
|
|
540
|
+
reason: refund?.reason_code || null,
|
|
541
|
+
};
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Build the custom_id string for PayPal subscriptions and orders
|
|
546
|
+
* Format: uid:{uid},orderId:{orderId} or uid:{uid},orderId:{orderId},productId:{productId}
|
|
547
|
+
*
|
|
548
|
+
* @param {string} uid - User's Firebase UID
|
|
549
|
+
* @param {string} orderId - Our internal order ID
|
|
550
|
+
* @param {string} [productId] - Product ID (used for one-time payments)
|
|
551
|
+
* @returns {string}
|
|
552
|
+
*/
|
|
553
|
+
buildCustomId(uid, orderId, productId) {
|
|
554
|
+
let customId = `uid:${uid},orderId:${orderId}`;
|
|
555
|
+
|
|
556
|
+
if (productId) {
|
|
557
|
+
customId += `,productId:${productId}`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return customId;
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The back-pointer a PayPal refund record carries to the payment it reversed
|
|
566
|
+
*
|
|
567
|
+
* A v1 refund names its `sale_id`; a v2 refund links `up` to its capture. The
|
|
568
|
+
* refund of a SUBSCRIPTION's sale names the sale, never the billing agreement the
|
|
569
|
+
* event resolved to, so that path has nothing to compare and says so
|
|
570
|
+
* ([refund-linkage.js](../refund-linkage.js)).
|
|
571
|
+
*
|
|
572
|
+
* @param {object} refund - The fetched refund record
|
|
573
|
+
* @param {string} resourceType - The event's resource type ('sale' | 'capture')
|
|
574
|
+
* @returns {{ field: string|null, found: string|null }}
|
|
575
|
+
*/
|
|
576
|
+
function refundLink(refund, resourceType) {
|
|
577
|
+
if (resourceType === 'sale') {
|
|
578
|
+
return { field: 'sale_id', found: refund?.sale_id || null };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (resourceType === 'capture') {
|
|
582
|
+
return { field: 'links[rel=up]', found: parseUpLinkId(refund?.links) };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return { field: null, found: null };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* The id a v2 record points `up` at, read off its HATEOAS links
|
|
590
|
+
*
|
|
591
|
+
* PayPal names a v2 refund's parent capture with a link whose `rel` is `up`, and
|
|
592
|
+
* the id is that URL's last segment — the only back-pointer a v2 refund carries
|
|
593
|
+
* ([#532](https://github.com/Omega-JS-Stack/omega/issues/532)).
|
|
594
|
+
*
|
|
595
|
+
* @param {Array|undefined} links - The record's HATEOAS links
|
|
596
|
+
* @returns {string|null}
|
|
597
|
+
*/
|
|
598
|
+
function parseUpLinkId(links) {
|
|
599
|
+
const up = (links || []).find((link) => link?.rel === 'up');
|
|
600
|
+
|
|
601
|
+
if (!up?.href) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return up.href.split('/').filter(Boolean).pop() || null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Parse the custom_id string from a PayPal subscription
|
|
610
|
+
* Format: uid:{uid},orderId:{orderId}
|
|
611
|
+
*
|
|
612
|
+
* @param {string} customId - The custom_id string
|
|
613
|
+
* @returns {{ uid: string|null, orderId: string|null, productId: string|null }}
|
|
614
|
+
*/
|
|
615
|
+
function parseCustomId(customId) {
|
|
616
|
+
if (!customId) {
|
|
617
|
+
return { uid: null, orderId: null, productId: null };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const result = { uid: null, orderId: null, productId: null };
|
|
621
|
+
|
|
622
|
+
for (const part of customId.split(',')) {
|
|
623
|
+
const [key, ...valueParts] = part.split(':');
|
|
624
|
+
const value = valueParts.join(':'); // Handle values that contain colons
|
|
625
|
+
|
|
626
|
+
if (key === 'uid') {
|
|
627
|
+
result.uid = value || null;
|
|
628
|
+
} else if (key === 'orderId') {
|
|
629
|
+
result.orderId = value || null;
|
|
630
|
+
} else if (key === 'productId') {
|
|
631
|
+
result.productId = value || null;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return result;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Is this subscription still INSIDE its free trial?
|
|
640
|
+
*
|
|
641
|
+
* The trial WINDOW is the answer, not the payment record: conversion happens at
|
|
642
|
+
* the trial's end, so a subscription whose trial end is still ahead of it cannot
|
|
643
|
+
* have converted, whatever money has changed hands. A plan carrying a setup fee
|
|
644
|
+
* charges one on day zero, and reading that fee as a paid billing period is what
|
|
645
|
+
* made a cancelled trial look like a paid term with time left on it
|
|
646
|
+
* ([#267](https://github.com/Omega-JS-Stack/omega/issues/267)).
|
|
647
|
+
*
|
|
648
|
+
* A trial with no computable end (no `start_time`) reads FALSE — this decides
|
|
649
|
+
* whether access is revoked, and missing data must never be the thing that
|
|
650
|
+
* revokes it.
|
|
651
|
+
*
|
|
652
|
+
* @param {object} raw - Raw PayPal subscription (with _plan attached)
|
|
653
|
+
* @returns {boolean}
|
|
654
|
+
*/
|
|
655
|
+
function isInTrial(raw) {
|
|
656
|
+
const trial = resolveTrial(raw);
|
|
657
|
+
|
|
658
|
+
return !!(trial.claimed && trial.expires.timestampUNIX > Math.floor(Date.now() / 1000));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Calculate when the current billing period ends based on last payment + interval
|
|
663
|
+
* Used for cancelled subs to determine remaining access time
|
|
664
|
+
*
|
|
665
|
+
* @param {object} raw - Raw PayPal subscription (with _plan attached)
|
|
666
|
+
* @returns {Date|null} Period end date, or null if cannot be calculated
|
|
667
|
+
*/
|
|
668
|
+
function calculatePeriodEnd(raw) {
|
|
669
|
+
// A TRIAL has no paid period to serve out, so cancelling one ends access NOW
|
|
670
|
+
// (Ian's ruling, 2026-08-15 — we do not keep serving a trial we know will not
|
|
671
|
+
// convert). This is the ONE place the three cancelled-subscription readers
|
|
672
|
+
// below share, so status, cancellation and expiry can never disagree about it.
|
|
673
|
+
if (isInTrial(raw)) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const lastPayment = raw.billing_info?.last_payment?.time;
|
|
678
|
+
|
|
679
|
+
if (!lastPayment) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const plan = raw._plan;
|
|
684
|
+
const regularCycle = plan?.billing_cycles?.find(c => c.tenure_type === 'REGULAR');
|
|
685
|
+
|
|
686
|
+
if (!regularCycle) {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const unit = regularCycle.frequency.interval_unit;
|
|
691
|
+
const count = regularCycle.frequency.interval_count || 1;
|
|
692
|
+
const lastDate = new Date(lastPayment);
|
|
693
|
+
|
|
694
|
+
if (unit === 'YEAR') {
|
|
695
|
+
lastDate.setFullYear(lastDate.getFullYear() + count);
|
|
696
|
+
} else if (unit === 'MONTH') {
|
|
697
|
+
lastDate.setMonth(lastDate.getMonth() + count);
|
|
698
|
+
} else if (unit === 'WEEK') {
|
|
699
|
+
lastDate.setDate(lastDate.getDate() + (count * 7));
|
|
700
|
+
} else if (unit === 'DAY') {
|
|
701
|
+
lastDate.setDate(lastDate.getDate() + count);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return lastDate;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Map PayPal subscription status to unified status
|
|
709
|
+
*
|
|
710
|
+
* | PayPal Status | Unified Status |
|
|
711
|
+
* |------------------|----------------|
|
|
712
|
+
* | ACTIVE | active |
|
|
713
|
+
* | SUSPENDED | suspended |
|
|
714
|
+
* | CANCELLED (period remaining) | active (with cancellation.pending) |
|
|
715
|
+
* | CANCELLED (period ended) | cancelled |
|
|
716
|
+
* | EXPIRED | cancelled |
|
|
717
|
+
* | APPROVAL_PENDING | cancelled |
|
|
718
|
+
* | APPROVED | active |
|
|
719
|
+
*/
|
|
720
|
+
function resolveStatus(raw) {
|
|
721
|
+
const status = raw.status;
|
|
722
|
+
|
|
723
|
+
if (status === 'ACTIVE' || status === 'APPROVED') {
|
|
724
|
+
return 'active';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (status === 'SUSPENDED') {
|
|
728
|
+
return 'suspended';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// CANCELLED — check if user still has paid time remaining
|
|
732
|
+
if (status === 'CANCELLED') {
|
|
733
|
+
const periodEnd = calculatePeriodEnd(raw);
|
|
734
|
+
|
|
735
|
+
if (periodEnd && periodEnd > new Date()) {
|
|
736
|
+
// User still has access until period end — treat as active with pending cancellation
|
|
737
|
+
return 'active';
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return 'cancelled';
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// EXPIRED, APPROVAL_PENDING, or anything else
|
|
744
|
+
return 'cancelled';
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Resolve cancellation state from PayPal subscription
|
|
749
|
+
*
|
|
750
|
+
* PayPal has no cancel_at_period_end like Stripe. When cancelled:
|
|
751
|
+
* - If billing period hasn't ended: pending=true, date=period end (user keeps access)
|
|
752
|
+
* - If billing period has ended: pending=false, date=cancellation time (fully cancelled)
|
|
753
|
+
*/
|
|
754
|
+
function resolveCancellation(raw) {
|
|
755
|
+
if (raw.status === 'CANCELLED') {
|
|
756
|
+
const periodEnd = calculatePeriodEnd(raw);
|
|
757
|
+
|
|
758
|
+
// Period still active — pending cancellation (user keeps access until period end)
|
|
759
|
+
if (periodEnd && periodEnd > new Date()) {
|
|
760
|
+
const periodEndStr = powertools.timestamp(periodEnd, { output: 'string' });
|
|
761
|
+
|
|
762
|
+
return {
|
|
763
|
+
pending: true,
|
|
764
|
+
date: {
|
|
765
|
+
timestamp: periodEndStr,
|
|
766
|
+
timestampUNIX: powertools.timestamp(periodEndStr, { output: 'unix' }),
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Period has ended — fully cancelled
|
|
772
|
+
const cancelDate = raw.status_update_time
|
|
773
|
+
? powertools.timestamp(new Date(raw.status_update_time), { output: 'string' })
|
|
774
|
+
: EPOCH_ZERO;
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
pending: false,
|
|
778
|
+
date: {
|
|
779
|
+
timestamp: cancelDate,
|
|
780
|
+
timestampUNIX: cancelDate !== EPOCH_ZERO
|
|
781
|
+
? powertools.timestamp(cancelDate, { output: 'unix' })
|
|
782
|
+
: EPOCH_ZERO_UNIX,
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return {
|
|
788
|
+
pending: false,
|
|
789
|
+
date: {
|
|
790
|
+
timestamp: EPOCH_ZERO,
|
|
791
|
+
timestampUNIX: EPOCH_ZERO_UNIX,
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Resolve trial state from PayPal subscription
|
|
798
|
+
* PayPal trials are represented as billing_cycles with tenure_type === 'TRIAL'
|
|
799
|
+
*/
|
|
800
|
+
function resolveTrial(raw) {
|
|
801
|
+
// Check if the plan has a trial cycle
|
|
802
|
+
const plan = raw._plan || {};
|
|
803
|
+
const trialCycle = plan.billing_cycles?.find(c => c.tenure_type === 'TRIAL');
|
|
804
|
+
|
|
805
|
+
if (!trialCycle) {
|
|
806
|
+
return {
|
|
807
|
+
claimed: false,
|
|
808
|
+
expires: { timestamp: EPOCH_ZERO, timestampUNIX: EPOCH_ZERO_UNIX },
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// PayPal doesn't expose exact trial start/end dates on the subscription
|
|
813
|
+
// We can calculate from start_time + trial duration
|
|
814
|
+
const startTime = raw.start_time ? new Date(raw.start_time) : null;
|
|
815
|
+
|
|
816
|
+
if (!startTime) {
|
|
817
|
+
return {
|
|
818
|
+
claimed: true,
|
|
819
|
+
expires: { timestamp: EPOCH_ZERO, timestampUNIX: EPOCH_ZERO_UNIX },
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Calculate trial end based on trial cycle frequency
|
|
824
|
+
const trialFreq = trialCycle.frequency;
|
|
825
|
+
const trialCount = trialCycle.total_cycles || 1;
|
|
826
|
+
const trialEnd = new Date(startTime);
|
|
827
|
+
|
|
828
|
+
if (trialFreq?.interval_unit === 'DAY') {
|
|
829
|
+
trialEnd.setDate(trialEnd.getDate() + (trialFreq.interval_count || 1) * trialCount);
|
|
830
|
+
} else if (trialFreq?.interval_unit === 'MONTH') {
|
|
831
|
+
trialEnd.setMonth(trialEnd.getMonth() + (trialFreq.interval_count || 1) * trialCount);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const trialEndStr = powertools.timestamp(trialEnd, { output: 'string' });
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
claimed: true,
|
|
838
|
+
expires: {
|
|
839
|
+
timestamp: trialEndStr,
|
|
840
|
+
timestampUNIX: powertools.timestamp(trialEndStr, { output: 'unix' }),
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Resolve billing frequency from PayPal subscription
|
|
847
|
+
*/
|
|
848
|
+
function resolveFrequency(raw) {
|
|
849
|
+
// Try _plan first (fetched separately)
|
|
850
|
+
const plan = raw._plan || {};
|
|
851
|
+
const regularCycle = plan.billing_cycles?.find(c => c.tenure_type === 'REGULAR');
|
|
852
|
+
|
|
853
|
+
if (regularCycle?.frequency?.interval_unit) {
|
|
854
|
+
return INTERVAL_TO_FREQUENCY[regularCycle.frequency.interval_unit] || null;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Fallback: try inline plan info from ?fields=plan
|
|
858
|
+
const inlinePlan = raw.plan;
|
|
859
|
+
if (inlinePlan?.billing_cycles) {
|
|
860
|
+
const cycle = inlinePlan.billing_cycles.find(c => c.tenure_type === 'REGULAR');
|
|
861
|
+
if (cycle?.frequency?.interval_unit) {
|
|
862
|
+
return INTERVAL_TO_FREQUENCY[cycle.frequency.interval_unit] || null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Resolve product by matching the PayPal product ID against config products
|
|
871
|
+
* Uses: sub._plan.product_id → match config product.paypal.productId
|
|
872
|
+
* Also checks paypal.legacyProductIds[] for migrated products
|
|
873
|
+
*/
|
|
874
|
+
function resolveProduct(raw, config) {
|
|
875
|
+
// Get PayPal product ID from the plan (attached during fetchResource)
|
|
876
|
+
const paypalProductId = raw._plan?.product_id || null;
|
|
877
|
+
|
|
878
|
+
if (!paypalProductId || !config.payment?.products) {
|
|
879
|
+
return { id: 'basic', name: 'Basic' };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
for (const product of config.payment.products) {
|
|
883
|
+
// Check current product ID
|
|
884
|
+
if (product.paypal?.productId === paypalProductId) {
|
|
885
|
+
return { id: product.id, name: product.name || product.id };
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Check legacy product IDs (for migrated products)
|
|
889
|
+
if (product.paypal?.legacyProductIds?.includes(paypalProductId)) {
|
|
890
|
+
return { id: product.id, name: product.name || product.id };
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return { id: 'basic', name: 'Basic' };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Resolve product for one-time payments
|
|
899
|
+
*/
|
|
900
|
+
function resolveProductOneTime(productId, config) {
|
|
901
|
+
if (!productId || !config.payment?.products) {
|
|
902
|
+
return { id: productId || 'unknown', name: 'Unknown' };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const product = config.payment.products.find(p => p.id === productId);
|
|
906
|
+
|
|
907
|
+
if (!product) {
|
|
908
|
+
return { id: productId, name: productId };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return { id: product.id, name: product.name || product.id };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Resolve subscription expiration from PayPal data
|
|
916
|
+
*
|
|
917
|
+
* For cancelled subs with remaining time, uses calculated period end.
|
|
918
|
+
* For active subs, uses next_billing_time.
|
|
919
|
+
*/
|
|
920
|
+
function resolveExpires(raw) {
|
|
921
|
+
// Cancelled subs with remaining time — use calculated period end
|
|
922
|
+
if (raw.status === 'CANCELLED') {
|
|
923
|
+
const periodEnd = calculatePeriodEnd(raw);
|
|
924
|
+
|
|
925
|
+
if (periodEnd && periodEnd > new Date()) {
|
|
926
|
+
const expiresStr = powertools.timestamp(periodEnd, { output: 'string' });
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
timestamp: expiresStr,
|
|
930
|
+
timestampUNIX: powertools.timestamp(expiresStr, { output: 'unix' }),
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Active subs: PayPal's billing_info.next_billing_time is the closest to "period end"
|
|
936
|
+
const nextBilling = raw.billing_info?.next_billing_time;
|
|
937
|
+
|
|
938
|
+
if (!nextBilling) {
|
|
939
|
+
return {
|
|
940
|
+
timestamp: EPOCH_ZERO,
|
|
941
|
+
timestampUNIX: EPOCH_ZERO_UNIX,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const expiresDate = powertools.timestamp(new Date(nextBilling), { output: 'string' });
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
timestamp: expiresDate,
|
|
949
|
+
timestampUNIX: powertools.timestamp(expiresDate, { output: 'unix' }),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Resolve subscription start date from PayPal data
|
|
955
|
+
*/
|
|
956
|
+
function resolveStartDate(raw) {
|
|
957
|
+
const startTime = raw.start_time || raw.create_time;
|
|
958
|
+
|
|
959
|
+
if (!startTime) {
|
|
960
|
+
return {
|
|
961
|
+
timestamp: EPOCH_ZERO,
|
|
962
|
+
timestampUNIX: EPOCH_ZERO_UNIX,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const startDate = powertools.timestamp(new Date(startTime), { output: 'string' });
|
|
967
|
+
|
|
968
|
+
return {
|
|
969
|
+
timestamp: startDate,
|
|
970
|
+
timestampUNIX: powertools.timestamp(startDate, { output: 'unix' }),
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Resolve the display price for a product/frequency from config
|
|
976
|
+
*/
|
|
977
|
+
function resolvePrice(productId, frequency, config) {
|
|
978
|
+
const product = config.payment?.products?.find(p => p.id === productId);
|
|
979
|
+
|
|
980
|
+
if (!product || !product.prices) {
|
|
981
|
+
return 0;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return product.prices[frequency] || 0;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
module.exports = PayPal;
|