create-ailk 0.23.0 → 0.25.1
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/README.md +68 -33
- package/component-catalog.json +3 -3
- package/component-catalog.md +3 -3
- package/dist/add-module.js +10 -3
- package/dist/add-route.d.ts +12 -1
- package/dist/add-route.js +38 -4
- package/dist/cli.js +12 -7
- package/dist/derive-architecture.d.ts +3 -4
- package/dist/derive-architecture.js +75 -22
- package/dist/lib/design-checks.d.ts +9 -4
- package/dist/lib/design-checks.js +13 -7
- package/dist/lib/extract-module-bundle.js +29 -2
- package/dist/lib/fetch-template-snapshot.d.ts +1 -1
- package/dist/lib/fetch-template-snapshot.js +7 -3
- package/dist/lib/import-graph.d.ts +1 -1
- package/dist/lib/import-graph.js +4 -4
- package/dist/lib/module-license-gate.d.ts +12 -1
- package/dist/lib/module-license-gate.js +34 -12
- package/dist/lib/scaffold-render-sweep.d.ts +33 -0
- package/dist/lib/scaffold-render-sweep.js +45 -0
- package/dist/module-architecture.d.ts +20 -3
- package/dist/module-architecture.js +122 -538
- package/dist/paid-sections.d.ts +25 -1
- package/dist/paid-sections.js +88 -3
- package/dist/surfaces.js +10 -2
- package/dist/sync-routes.d.ts +30 -4
- package/dist/sync-routes.js +271 -18
- package/dist/template-record.d.ts +14 -4
- package/dist/template-record.js +23 -5
- package/dist/topology.d.ts +16 -3
- package/dist/topology.js +31 -19
- package/dist/update.d.ts +10 -1
- package/dist/update.js +540 -16
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/template-guard.sh +137 -0
- package/templates/.claude/rules/extending.md +106 -0
- package/templates/.claude/rules/loop-contract.md +2 -2
- package/templates/.claude/settings.json +11 -0
- package/templates/.env.example +9 -5
- package/templates/.github/workflows/ci.yml +7 -4
- package/templates/CLAUDE.md +15 -3
- package/templates/CONVENTIONS.md +8 -10
- package/templates/README.md +14 -13
- package/templates/apps/api/.env.example +9 -2
- package/templates/apps/api/CLAUDE.md +122 -82
- package/templates/apps/api/jest.config.cjs +15 -5
- package/templates/apps/api/package.json +16 -20
- package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
- package/templates/apps/api/src/index.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -4
- package/templates/apps/api/src/modules/commerce.ts +10 -0
- package/templates/apps/api/src/modules/content-write.ts +10 -0
- package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
- package/templates/apps/api/src/modules/scheduling.ts +10 -0
- package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
- package/templates/apps/api/src/modules/workspace.ts +10 -0
- package/templates/apps/api/src/server.ts +117 -1857
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/CLAUDE.md +77 -24
- package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
- package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
- package/templates/apps/mcp/jest.config.cjs +6 -1
- package/templates/apps/mcp/package.json +5 -12
- package/templates/apps/mcp/src/app.ts +8 -7
- package/templates/apps/mcp/src/index.ts +6 -34
- package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
- package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
- package/templates/apps/mcp/src/site.ts +12 -0
- package/templates/apps/web/.env.example +1 -1
- package/templates/apps/web/CLAUDE.md +21 -5
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +28 -7
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
- package/templates/apps/web/app/api/revalidate/route.ts +3 -3
- package/templates/apps/web/auth.ts +2 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
- package/templates/apps/web/lib/auth-email.ts +1 -1
- package/templates/apps/web/lib/magnet-routes.ts +3 -1
- package/templates/apps/web/lib/newsletter.actions.ts +9 -6
- package/templates/apps/web/lib/relay.ts +2 -2
- package/templates/apps/web/lib/release-scope.ts +47 -0
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
- package/templates/customizations.yaml +37 -0
- package/templates/database/prisma.config.ts +10 -14
- package/templates/database/seed-test.ts +2 -2
- package/templates/database/seed.ts +32 -5
- package/templates/package.json +12 -6
- package/templates/pnpm-workspace.yaml +0 -1
- package/templates/project.yaml +16 -8
- package/templates/tools/jest/reporters.cjs +65 -0
- package/templates/tools/lint-architecture.ts +757 -7
- package/templates/tsconfig.json +1 -2
- package/versions.json +48 -35
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
- package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
- package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
- package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
- package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
- package/templates/apps/api/src/bin/spokes.ts +0 -198
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
- package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
- package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
- package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
- package/templates/apps/api/src/config/index.ts +0 -596
- package/templates/apps/api/src/config/modules.ts +0 -83
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
- package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -292
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
- package/templates/apps/api/src/lib/agent-store.ts +0 -613
- package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
- package/templates/apps/api/src/lib/consent-store.ts +0 -363
- package/templates/apps/api/src/lib/content-adapter.ts +0 -112
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
- package/templates/apps/api/src/lib/error-serializer.ts +0 -164
- package/templates/apps/api/src/lib/errors.ts +0 -95
- package/templates/apps/api/src/lib/file-fallback.ts +0 -87
- package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
- package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
- package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
- package/templates/apps/api/src/lib/license-key-store.ts +0 -45
- package/templates/apps/api/src/lib/locales.ts +0 -201
- package/templates/apps/api/src/lib/outbound-http.ts +0 -127
- package/templates/apps/api/src/lib/prisma.ts +0 -21
- package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
- package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
- package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
- package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
- package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
- package/templates/apps/api/src/lib/site.ts +0 -254
- package/templates/apps/api/src/lib/slice-load.ts +0 -90
- package/templates/apps/api/src/lib/spoke-store.ts +0 -180
- package/templates/apps/api/src/lib/stripe.ts +0 -318
- package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
- package/templates/apps/api/src/middleware/auth.ts +0 -864
- package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
- package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -264
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
- package/templates/apps/api/src/openapi/index.ts +0 -18
- package/templates/apps/api/src/openapi/spec.ts +0 -1815
- package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
- package/templates/apps/api/src/routes/aeo/index.ts +0 -107
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
- package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
- package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
- package/templates/apps/api/src/routes/auth/user.ts +0 -305
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -325
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1300
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
- package/templates/apps/api/src/routes/checkout/index.ts +0 -34
- package/templates/apps/api/src/routes/checkout/session.ts +0 -171
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
- package/templates/apps/api/src/routes/consent/account.ts +0 -268
- package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
- package/templates/apps/api/src/routes/consent/index.ts +0 -26
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
- package/templates/apps/api/src/routes/content/get-page.ts +0 -268
- package/templates/apps/api/src/routes/content/index.ts +0 -49
- package/templates/apps/api/src/routes/content/list-pages.ts +0 -95
- package/templates/apps/api/src/routes/content/list-types.ts +0 -19
- package/templates/apps/api/src/routes/content/offerings.ts +0 -184
- package/templates/apps/api/src/routes/content/pricing.ts +0 -176
- package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
- package/templates/apps/api/src/routes/content/search.ts +0 -129
- package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
- package/templates/apps/api/src/routes/leads/index.ts +0 -573
- package/templates/apps/api/src/routes/leads/list.ts +0 -104
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -480
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -465
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
- package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
- package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
- package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
- package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
- package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
- package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
- package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
- package/templates/apps/api/src/routes/requests/index.ts +0 -54
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
- package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
- package/templates/apps/api/src/routes/webhooks/README.md +0 -398
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -448
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -808
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2026
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -528
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
- package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
- package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -867
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
- package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
- package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1176
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -444
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -154
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
- package/templates/apps/api/src/services/abandon-email.ts +0 -313
- package/templates/apps/api/src/services/aeo-score.ts +0 -325
- package/templates/apps/api/src/services/answer-distributions.ts +0 -138
- package/templates/apps/api/src/services/audit-log.ts +0 -961
- package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
- package/templates/apps/api/src/services/consent.ts +0 -834
- package/templates/apps/api/src/services/default-tenant.ts +0 -416
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -755
- package/templates/apps/api/src/services/dunning-policy.ts +0 -294
- package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
- package/templates/apps/api/src/services/entitlements.ts +0 -338
- package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
- package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
- package/templates/apps/api/src/services/lead-promotion.ts +0 -160
- package/templates/apps/api/src/services/lead-routing.ts +0 -1372
- package/templates/apps/api/src/services/leads-inbox.ts +0 -312
- package/templates/apps/api/src/services/listing-config.ts +0 -178
- package/templates/apps/api/src/services/magnet-completion.ts +0 -242
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -295
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -868
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -396
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -681
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
- package/templates/apps/api/src/services/offerings/catalog.ts +0 -308
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -385
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
- package/templates/apps/api/src/services/offerings/connection.ts +0 -1003
- package/templates/apps/api/src/services/offerings/index.ts +0 -614
- package/templates/apps/api/src/services/offerings/source.ts +0 -140
- package/templates/apps/api/src/services/offerings/store.ts +0 -150
- package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
- package/templates/apps/api/src/services/person.ts +0 -1188
- package/templates/apps/api/src/services/plan-offerings.ts +0 -125
- package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
- package/templates/apps/api/src/services/project-listings.ts +0 -2073
- package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
- package/templates/apps/api/src/services/request-confirm.ts +0 -698
- package/templates/apps/api/src/services/revalidation.ts +0 -235
- package/templates/apps/api/src/services/slack-notify.ts +0 -291
- package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
- package/templates/apps/api/src/services/usage-credits.ts +0 -340
- package/templates/apps/api/src/services/verified-email.ts +0 -71
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
- package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
- package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
- package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
- package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
- package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1151
- package/templates/apps/mcp/__tests__/index.test.ts +0 -195
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -65
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -187
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -165
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
- package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -228
- package/templates/apps/mcp/__tests__/locale.test.ts +0 -135
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
- package/templates/apps/mcp/__tests__/read_content.test.ts +0 -474
- package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -225
- package/templates/apps/mcp/__tests__/search_content.test.ts +0 -256
- package/templates/apps/mcp/__tests__/server.test.ts +0 -388
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -307
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -223
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
- package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
- package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
- package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
- package/templates/apps/mcp/src/abuse/index.ts +0 -342
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
- package/templates/apps/mcp/src/boot.ts +0 -167
- package/templates/apps/mcp/src/config/modules.ts +0 -43
- package/templates/apps/mcp/src/http-transport.ts +0 -526
- package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
- package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -142
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -139
- package/templates/apps/mcp/src/lib/errors.ts +0 -61
- package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
- package/templates/apps/mcp/src/lib/locales.ts +0 -171
- package/templates/apps/mcp/src/lib/prisma.ts +0 -39
- package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
- package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
- package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
- package/templates/apps/mcp/src/request-context.ts +0 -52
- package/templates/apps/mcp/src/server.ts +0 -343
- package/templates/apps/mcp/src/serverless-app.ts +0 -88
- package/templates/apps/mcp/src/tools/audit.ts +0 -127
- package/templates/apps/mcp/src/tools/capture_lead.ts +0 -64
- package/templates/apps/mcp/src/tools/create_project.ts +0 -134
- package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
- package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
- package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
- package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
- package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
- package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
- package/templates/apps/mcp/src/tools/index.ts +0 -137
- package/templates/apps/mcp/src/tools/list_pages.ts +0 -74
- package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
- package/templates/apps/mcp/src/tools/proposals.ts +0 -27
- package/templates/apps/mcp/src/tools/purchase.ts +0 -205
- package/templates/apps/mcp/src/tools/read_content.ts +0 -75
- package/templates/apps/mcp/src/tools/request_callback.ts +0 -59
- package/templates/apps/mcp/src/tools/search_content.ts +0 -67
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -57
- package/templates/apps/mcp/src/workspace-server.ts +0 -224
- package/templates/database/CHANGELOG.md +0 -961
- package/templates/database/LICENSE +0 -202
- package/templates/database/NOTICE +0 -2
- package/templates/database/README.md +0 -243
- package/templates/database/__tests__/agents.db.test.ts +0 -320
- package/templates/database/__tests__/appointment.db.test.ts +0 -106
- package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
- package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
- package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
- package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
- package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
- package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
- package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
- package/templates/database/__tests__/flows-presets.test.ts +0 -173
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
- package/templates/database/__tests__/member-area.db.test.ts +0 -257
- package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
- package/templates/database/__tests__/member-contract.db.test.ts +0 -413
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
- package/templates/database/__tests__/offerings-migration.test.ts +0 -197
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
- package/templates/database/__tests__/offerings.db.test.ts +0 -303
- package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
- package/templates/database/__tests__/person-contact.db.test.ts +0 -563
- package/templates/database/__tests__/person-notes.db.test.ts +0 -249
- package/templates/database/__tests__/person.db.test.ts +0 -586
- package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
- package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
- package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
- package/templates/database/__tests__/seed.test.ts +0 -240
- package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
- package/templates/database/__tests__/tenancy.test.ts +0 -141
- package/templates/database/__tests__/user-cascade.test.ts +0 -140
- package/templates/database/_gitignore +0 -8
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/auth/schema.prisma +0 -641
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/config/schema.prisma +0 -441
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/content/schema.prisma +0 -339
- package/templates/database/eslint.config.mjs +0 -69
- package/templates/database/flows/listing-diagnostic.ts +0 -182
- package/templates/database/flows/presets.ts +0 -315
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +0 -3
- package/templates/database/inbox/__tests__/schema.test.ts +0 -44
- package/templates/database/inbox/schema.prisma +0 -1447
- package/templates/database/jest.config.cjs +0 -36
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
- package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
- package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
- package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
- package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
- package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
- package/templates/database/migrations/migration_lock.toml +0 -3
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/ops/schema.prisma +0 -43
- package/templates/database/package.json +0 -89
- package/templates/database/schema.prisma +0 -71
- package/templates/database/scripts/db-generate-locked.sh +0 -80
- package/templates/database/scripts/person-backfill.sql +0 -132
- package/templates/database/tenancy.ts +0 -183
- package/templates/database/tsconfig.build.json +0 -16
- package/templates/database/tsconfig.flows.json +0 -15
- package/templates/database/tsconfig.json +0 -16
- package/templates/database/tsconfig.tenancy.json +0 -14
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file dunning-policy.ts
|
|
3
|
-
* @description The per-tenant dunning grace policy (#6034 §§ D4, D5) — one
|
|
4
|
-
* function that resolves how many days a `past_due`/`unpaid` subscription
|
|
5
|
-
* keeps access before a subsequent `canceled` revokes it, plus the two
|
|
6
|
-
* read-side predicates that decide whether a subscriber has lost access or
|
|
7
|
-
* merely needs to act.
|
|
8
|
-
*
|
|
9
|
-
* NOT marked `oss: false` (§ D1) — `routes/webhooks/stripe.ts`, this file's
|
|
10
|
-
* only caller, deliberately left `commerce.ownedFiles`
|
|
11
|
-
* (`packages/create-ailk/src/module-architecture.ts:1818-1846`) so a keyless
|
|
12
|
-
* scaffold still carries and registers it; gating happens at CALL time on the
|
|
13
|
-
* commerce claim already settled at `stripe.ts:174` (NFR-3), not at file
|
|
14
|
-
* presence.
|
|
15
|
-
*
|
|
16
|
-
* The policy blob lives in `SiteSetting` (`database/config/schema.prisma:15`)
|
|
17
|
-
* — a model with NO reader anywhere in the tree before this file (§ 1.7) and
|
|
18
|
-
* a globally-`@unique` `key` column with no `siteId` column of its own. Per-
|
|
19
|
-
* tenant scoping therefore lives in the key STRING:
|
|
20
|
-
* `commerce.dunning_policy:<siteId>` — never an unsuffixed fallback, which
|
|
21
|
-
* would let one site's policy silently govern another's money.
|
|
22
|
-
*
|
|
23
|
-
* The read never throws (AC-3) — see the resolution table on
|
|
24
|
-
* `dunningPolicyFor` below, which mirrors `listing-config.ts`'s
|
|
25
|
-
* absent/malformed → documented-default contract for the same reason: an
|
|
26
|
-
* anonymous webhook delivery must not 500 because an operator (or nothing at
|
|
27
|
-
* all) wrote a bad policy row.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { logger } from "@working-theory/observability";
|
|
31
|
-
import { z } from "zod";
|
|
32
|
-
|
|
33
|
-
import { isModuleEnabled } from "../config/modules.js";
|
|
34
|
-
import { prisma } from "../lib/prisma.js";
|
|
35
|
-
import { resolveWorkspaceIdForAlias } from "../lib/site.js";
|
|
36
|
-
|
|
37
|
-
/** The lifecycle values this record persists. A narrower set than
|
|
38
|
-
* `SubscriptionLifecycleStatus` (`@working-theory/stripe-client`): `trialing`
|
|
39
|
-
* never persists on its own — an arriving `active` OR `trialing` status is
|
|
40
|
-
* always rewritten to a recorded `status: "active"` (#6034 § D7's clean-
|
|
41
|
-
* recovery contract), so the record's own vocabulary only ever needs three
|
|
42
|
-
* members. */
|
|
43
|
-
export type DunningRecordedStatus = "past_due" | "active" | "canceled";
|
|
44
|
-
|
|
45
|
-
/** One subscription's recorded dunning state (#6034 § D2). */
|
|
46
|
-
export interface DunningEntry {
|
|
47
|
-
status: DunningRecordedStatus;
|
|
48
|
-
accessState: "granted" | "revoked";
|
|
49
|
-
/** ISO-8601, or `null` before the subscription has ever failed. Set once,
|
|
50
|
-
* on first entry into a failing state, and never moved while it stays
|
|
51
|
-
* failing (§ D5) — re-arming on every retry would slide the grace window
|
|
52
|
-
* forward with every Stripe attempt and it would never expire. */
|
|
53
|
-
dunningArmedAt: string | null;
|
|
54
|
-
/** ISO-8601, or `null` while nothing has scheduled a revocation. Present
|
|
55
|
-
* (possibly in the future) the instant a `canceled` event is recorded —
|
|
56
|
-
* the read-side lazily evaluates it rather than any scheduler waking up to
|
|
57
|
-
* flip the row (§ D5 — NFR-7 forbids one). */
|
|
58
|
-
accessRevokesAt: string | null;
|
|
59
|
-
/** Stripe `event.created`, seconds — the ordering watermark § D6's
|
|
60
|
-
* admission rule compares incoming events against. */
|
|
61
|
-
lastEventAt: number;
|
|
62
|
-
/** The durable twin of the in-process `hasSeen` map (§ D6 rule 1) — a
|
|
63
|
-
* replay of the SAME event id is a no-op even after a process restart. */
|
|
64
|
-
lastEventId: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The whole `UserData` blob for the reserved
|
|
68
|
-
* `commerce.subscription_dunning` key — one row per subscriber, keyed by
|
|
69
|
-
* Stripe subscription id because one customer can hold more than one
|
|
70
|
-
* subscription (§ D2 — the map is what stops a second subscription's
|
|
71
|
-
* `active` clobbering the first's `past_due`). */
|
|
72
|
-
export interface DunningRecord {
|
|
73
|
-
v: 1;
|
|
74
|
-
subscriptions: Record<string, DunningEntry>;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** The reserved `UserData` key this record lives at (§ D12 — refused on
|
|
78
|
-
* `GET`/`PUT`/`DELETE /v1/user/data/:key`; the subject of the access
|
|
79
|
-
* decision may not read, write or erase it). */
|
|
80
|
-
export const DUNNING_USER_DATA_KEY = "commerce.subscription_dunning";
|
|
81
|
-
|
|
82
|
-
/** The map is capped so a per-event read-and-rewrite of the whole blob
|
|
83
|
-
* doesn't grow with a customer's entire subscription history (§ D2, § 9
|
|
84
|
-
* SEC-08). Eviction is oldest-first by `lastEventAt`, skipping any entry
|
|
85
|
-
* that is not yet terminal-and-settled (`status: "active"` with
|
|
86
|
-
* `accessState: "granted"`) — an entry still being dunned is never evicted
|
|
87
|
-
* out from under an in-flight decision. */
|
|
88
|
-
export const DUNNING_MAX_SUBSCRIPTIONS = 50;
|
|
89
|
-
|
|
90
|
-
export interface DunningPolicy {
|
|
91
|
-
graceDays: number;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 14 days — chosen because Stripe's Smart Retries schedule runs up to
|
|
95
|
-
* roughly three weeks and the grace clock starts at the FIRST failure, not
|
|
96
|
-
* at cancellation (§ D5). A shorter default would have the platform cut a
|
|
97
|
-
* buyer off while Stripe is still actively recovering the payment. */
|
|
98
|
-
export const DUNNING_POLICY_DEFAULTS: DunningPolicy = { graceDays: 14 };
|
|
99
|
-
|
|
100
|
-
/** A tenant may not configure a grace window longer than this. Clamped, not
|
|
101
|
-
* rejected — an operator's out-of-range number is a mistake to correct, not
|
|
102
|
-
* grounds to fall back to a platform default that may be MORE permissive
|
|
103
|
-
* than what they wrote (`listing-config.ts:55-62`'s disposition). */
|
|
104
|
-
export const DUNNING_GRACE_DAYS_MAX = 90;
|
|
105
|
-
|
|
106
|
-
/** Validated shape of the `SiteSetting.value` blob. Accepts any object with
|
|
107
|
-
* unknown extra keys (`.passthrough()`) so the "is this a JSON object at
|
|
108
|
-
* all" check is separated from the fine-grained `graceDays` validation below
|
|
109
|
-
* — zod v4's own `z.number()` already refuses `NaN`/`Infinity` outright,
|
|
110
|
-
* which would collapse the resolution table's distinct `malformed` and
|
|
111
|
-
* `non_finite` outcomes into one, so `graceDays` is read as `unknown` here
|
|
112
|
-
* and narrowed by hand. Deliberately NOT promoted to
|
|
113
|
-
* `@working-theory/validation` (§ D4) — one consumer, no cross-package
|
|
114
|
-
* reader; promoting it now would widen the published surface for nothing. */
|
|
115
|
-
const DunningPolicyBlobSchema = z.object({}).passthrough();
|
|
116
|
-
|
|
117
|
-
/** Logged once per process, the first time a `multi-tenant` deployment reads
|
|
118
|
-
* the policy (§ D4) — an operator should learn that one grace window governs
|
|
119
|
-
* every seller from a log line, not from a wrongful revocation. Process
|
|
120
|
-
* restart re-arms it, mirroring `enrichment-shim.ts`'s pro-sentinel flag. */
|
|
121
|
-
let deploymentWidePolicyWarned = false;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Resolve `siteId`'s dunning grace policy. Never throws (AC-3) — every
|
|
125
|
-
* branch below resolves to a value, logged at the level the § D4 resolution
|
|
126
|
-
* table specifies:
|
|
127
|
-
*
|
|
128
|
-
* no row → default, info "no_policy_row"
|
|
129
|
-
* not a JSON object → default, error "malformed"
|
|
130
|
-
* `graceDays` absent / wrong type → default, error "malformed"
|
|
131
|
-
* `graceDays` non-finite (e.g. `1e999`) → default, error "non_finite"
|
|
132
|
-
* `graceDays` negative and finite → clamp 0, error "clamped_low"
|
|
133
|
-
* `graceDays` finite and > the ceiling → clamp max, error "clamped_high"
|
|
134
|
-
* `graceDays` non-integer (e.g. `7.5`) → floored, warn "floored"
|
|
135
|
-
* the query itself throws → default, error "read_failed"
|
|
136
|
-
* a valid integer in `[0, DUNNING_GRACE_DAYS_MAX]` → that value, no log
|
|
137
|
-
*
|
|
138
|
-
* `siteId` is a PARAMETER, never read via `currentSiteId()` inside this
|
|
139
|
-
* function (§ D4) — a later per-seller resolution is then a caller change
|
|
140
|
-
* only, no signature churn.
|
|
141
|
-
*/
|
|
142
|
-
export async function dunningPolicyFor(siteId: string): Promise<DunningPolicy> {
|
|
143
|
-
// #6374 D3 — the key is built from the UNRESOLVED `siteId` parameter, and
|
|
144
|
-
// that is deliberate, not an oversight. `site_settings` never had a
|
|
145
|
-
// `site_id` column, so the site alias was embedded in the key STRING
|
|
146
|
-
// (`commerce.dunning_policy:default`); the migration's backfill leaves
|
|
147
|
-
// every `key` verbatim. Rebuilding it from the RESOLVED workspace id would
|
|
148
|
-
// match no row and hand every seller the built-in default policy —
|
|
149
|
-
// silently, which is the failure mode this whole function's log table
|
|
150
|
-
// exists to make visible.
|
|
151
|
-
const key = `commerce.dunning_policy:${siteId}`;
|
|
152
|
-
|
|
153
|
-
if (isModuleEnabled("multi-tenant") && !deploymentWidePolicyWarned) {
|
|
154
|
-
deploymentWidePolicyWarned = true;
|
|
155
|
-
logger.warn(
|
|
156
|
-
{ key, outcome: "deployment_wide_policy" },
|
|
157
|
-
"[dunning-policy] this deployment's grace policy is shared by every seller — the multi-tenant module is enabled but the policy key carries no per-seller resolution yet",
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// #6374 D3 — `site_settings` is now keyed by (workspace_id, key): `key`
|
|
162
|
-
// alone is no longer unique, because two sellers may each hold a
|
|
163
|
-
// `feature_flags` row. The WORKSPACE half is resolved from the alias here;
|
|
164
|
-
// the KEY half keeps its embedded alias (see the note above). An alias
|
|
165
|
-
// that resolves to nothing takes the same branch a missing row already
|
|
166
|
-
// takes — the built-in default policy — rather than throwing on a
|
|
167
|
-
// payment-webhook path.
|
|
168
|
-
const workspaceId = await resolveWorkspaceIdForAlias(siteId);
|
|
169
|
-
if (workspaceId === null) {
|
|
170
|
-
logger.info(
|
|
171
|
-
{ siteId, key, outcome: "no_policy_row" },
|
|
172
|
-
"[dunning-policy] site alias resolves to no workspace — using the platform default",
|
|
173
|
-
);
|
|
174
|
-
return DUNNING_POLICY_DEFAULTS;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
let row: { value: unknown } | null;
|
|
178
|
-
try {
|
|
179
|
-
row = await prisma.siteSetting.findUnique({
|
|
180
|
-
where: { workspaceId_key: { workspaceId, key } },
|
|
181
|
-
select: { value: true },
|
|
182
|
-
});
|
|
183
|
-
} catch (err) {
|
|
184
|
-
logger.error(
|
|
185
|
-
{
|
|
186
|
-
siteId,
|
|
187
|
-
key,
|
|
188
|
-
outcome: "read_failed",
|
|
189
|
-
errName: err instanceof Error ? err.name : "UnknownError",
|
|
190
|
-
},
|
|
191
|
-
"[dunning-policy] policy read failed — using the platform default",
|
|
192
|
-
);
|
|
193
|
-
return DUNNING_POLICY_DEFAULTS;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (!row) {
|
|
197
|
-
logger.info(
|
|
198
|
-
{ siteId, key, outcome: "no_policy_row" },
|
|
199
|
-
"[dunning-policy] no policy row — using the platform default",
|
|
200
|
-
);
|
|
201
|
-
return DUNNING_POLICY_DEFAULTS;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const parsed = DunningPolicyBlobSchema.safeParse(row.value);
|
|
205
|
-
if (!parsed.success) {
|
|
206
|
-
logger.error(
|
|
207
|
-
{ siteId, key, outcome: "malformed" },
|
|
208
|
-
"[dunning-policy] stored policy is not a JSON object — using the platform default",
|
|
209
|
-
);
|
|
210
|
-
return DUNNING_POLICY_DEFAULTS;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const graceDaysRaw: unknown = (parsed.data as Record<string, unknown>)[
|
|
214
|
-
"graceDays"
|
|
215
|
-
];
|
|
216
|
-
if (typeof graceDaysRaw !== "number") {
|
|
217
|
-
logger.error(
|
|
218
|
-
{ siteId, key, outcome: "malformed" },
|
|
219
|
-
"[dunning-policy] stored graceDays is missing or not a number — using the platform default",
|
|
220
|
-
);
|
|
221
|
-
return DUNNING_POLICY_DEFAULTS;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// `Number.isFinite` covers both the reachable `1e999 → Infinity` overflow
|
|
225
|
-
// AND `NaN`, which cannot arrive from `jsonb` in the first place (there is
|
|
226
|
-
// no `NaN` literal in JSON) — one guard, both cases (§ 9 SEC-10).
|
|
227
|
-
if (!Number.isFinite(graceDaysRaw)) {
|
|
228
|
-
logger.error(
|
|
229
|
-
{ siteId, key, outcome: "non_finite" },
|
|
230
|
-
"[dunning-policy] stored graceDays is not finite — using the platform default",
|
|
231
|
-
);
|
|
232
|
-
return DUNNING_POLICY_DEFAULTS;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (graceDaysRaw < 0) {
|
|
236
|
-
logger.error(
|
|
237
|
-
{ siteId, key, outcome: "clamped_low", stored: graceDaysRaw },
|
|
238
|
-
"[dunning-policy] stored graceDays is negative — clamped to 0",
|
|
239
|
-
);
|
|
240
|
-
return { graceDays: 0 };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (graceDaysRaw > DUNNING_GRACE_DAYS_MAX) {
|
|
244
|
-
logger.error(
|
|
245
|
-
{ siteId, key, outcome: "clamped_high", stored: graceDaysRaw },
|
|
246
|
-
"[dunning-policy] stored graceDays exceeds the ceiling — clamped",
|
|
247
|
-
);
|
|
248
|
-
return { graceDays: DUNNING_GRACE_DAYS_MAX };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (!Number.isInteger(graceDaysRaw)) {
|
|
252
|
-
const floored = Math.floor(graceDaysRaw);
|
|
253
|
-
logger.warn(
|
|
254
|
-
{ siteId, key, outcome: "floored", stored: graceDaysRaw },
|
|
255
|
-
"[dunning-policy] stored graceDays is not an integer — floored",
|
|
256
|
-
);
|
|
257
|
-
return { graceDays: floored };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return { graceDays: graceDaysRaw };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Access is gone for this subscription: either already stamped `revoked`, or
|
|
265
|
-
* the lazily-evaluated `accessRevokesAt` instant has passed. No scheduler
|
|
266
|
-
* flips this — every reader evaluates it fresh (§ D5, NFR-7).
|
|
267
|
-
*/
|
|
268
|
-
export function isSubscriberRevoked(
|
|
269
|
-
entry: DunningEntry,
|
|
270
|
-
now: number = Date.now(),
|
|
271
|
-
): boolean {
|
|
272
|
-
return (
|
|
273
|
-
entry.accessState === "revoked" ||
|
|
274
|
-
(entry.accessRevokesAt !== null && now >= Date.parse(entry.accessRevokesAt))
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* The buyer must act — #6038's banner predicate (§ D5), exported so #6038
|
|
280
|
-
* CALLS it rather than re-deriving it. Deliberately WIDER than
|
|
281
|
-
* `status === "past_due"`: a subscription cancelled with its grace window
|
|
282
|
-
* still open carries `status: "canceled"`, `accessState: "granted"` — a
|
|
283
|
-
* buyer about to lose access who has not been told. Both cases are covered
|
|
284
|
-
* here; neither alone is (§ 9 SEC-09).
|
|
285
|
-
*/
|
|
286
|
-
export function isSubscriberActionRequired(
|
|
287
|
-
entry: DunningEntry,
|
|
288
|
-
now: number = Date.now(),
|
|
289
|
-
): boolean {
|
|
290
|
-
return (
|
|
291
|
-
!isSubscriberRevoked(entry, now) &&
|
|
292
|
-
(entry.status === "past_due" || entry.accessRevokesAt !== null)
|
|
293
|
-
);
|
|
294
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file enrichment-shim.ts
|
|
3
|
-
* @description License-gated lead enrichment stub.
|
|
4
|
-
*
|
|
5
|
-
* Today: checkLicense()-gated branch — log a deduped sentinel for pro tier, return
|
|
6
|
-
* lead unchanged. Phase 3: real enrichment replaces this file; the call site
|
|
7
|
-
* (B6 lead-routing pipeline) stays stable.
|
|
8
|
-
*
|
|
9
|
-
* ADR 0008: license key is never logged raw — only the SHA-256 hash prefix
|
|
10
|
-
* (first 8 chars) appears in log output.
|
|
11
|
-
* ADR 0009 §D3: service-layer placement at apps/api/src/services/.
|
|
12
|
-
* ADR 0014: license is a JWT verified locally; tier read via `result.tier`.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { createHash } from "crypto";
|
|
16
|
-
|
|
17
|
-
import { checkLicense } from "@working-theory/license";
|
|
18
|
-
import { logger } from "@working-theory/observability";
|
|
19
|
-
// TODO(#214): swap to canonical Lead from @working-theory/validation when B1 lands.
|
|
20
|
-
import type { CreateLead as Lead } from "@working-theory/validation";
|
|
21
|
-
|
|
22
|
-
// Routes the once-per-process Pro sentinel through @working-theory/observability so it
|
|
23
|
-
// shares the redact + entropy pipeline (S17). The signature stays unchanged.
|
|
24
|
-
|
|
25
|
-
type EnrichedLead = Lead & { _enrichedAt?: string }; // type union shape; today equals Lead
|
|
26
|
-
|
|
27
|
-
// Module-scoped sentinel flag — one log per process lifecycle. Process restart re-arms it.
|
|
28
|
-
let proSentinelLogged = false;
|
|
29
|
-
|
|
30
|
-
function hashPrefix(key: string): string {
|
|
31
|
-
return createHash("sha256").update(key).digest("hex").substring(0, 8);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function enrichLead(lead: Lead): Promise<EnrichedLead | Lead> {
|
|
35
|
-
let result;
|
|
36
|
-
try {
|
|
37
|
-
result = await checkLicense();
|
|
38
|
-
} catch {
|
|
39
|
-
return lead;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (result.tier === "pro" && !proSentinelLogged) {
|
|
43
|
-
const key = process.env["AILK_LICENSE_KEY"];
|
|
44
|
-
const prefix = key ? hashPrefix(key) : "no-key";
|
|
45
|
-
logger.info(
|
|
46
|
-
{ licensePrefix: prefix },
|
|
47
|
-
"[enrichment-shim] Pro server processing not yet operational — falling back to OSS path",
|
|
48
|
-
);
|
|
49
|
-
proSentinelLogged = true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return lead;
|
|
53
|
-
}
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-Organization entitlement resolver (org-scoped billing, #3601).
|
|
3
|
-
*
|
|
4
|
-
* The TENANT-layer entitlement answer: "what is this Organization entitled to?"
|
|
5
|
-
* — distinct from the DEPLOYMENT/operator license layer (`require-paid-tier` →
|
|
6
|
-
* `checkLicense()`, "does this deployment hold the paid module"). The two
|
|
7
|
-
* layers compose; neither is folded into the other (spec D6).
|
|
8
|
-
*
|
|
9
|
-
* ADR-0009 D3: this is cross-namespace service-layer assembly (it reads the
|
|
10
|
-
* auth-namespace Organization billing record), so it lives in
|
|
11
|
-
* `apps/api/src/services/` — NOT in `@working-theory/license`, which stays
|
|
12
|
-
* JWT-pure / DB-free (only the `Entitlements` TYPE lives there; spec D4).
|
|
13
|
-
*
|
|
14
|
-
* Keyed on the Organization; members inherit (no per-user billing read). This
|
|
15
|
-
* foundation fills `features` + `limits.static` (tier + seat count) and returns
|
|
16
|
-
* `limits.metered` as the reserved empty shape FEAT-047 (#3679) fills — this
|
|
17
|
-
* spec builds NO metering engine (spec D5).
|
|
18
|
-
*
|
|
19
|
-
* #9067 D5 — the Billing tab's plan card also reads here: the licence expiry,
|
|
20
|
-
* the Organization's one open plan Checkout, and the customer's billing
|
|
21
|
-
* summary. That summary is the one Stripe read this resolver makes, through
|
|
22
|
-
* `lib/stripe.ts`.
|
|
23
|
-
*/
|
|
24
|
-
import { checkLicense, type Entitlements } from "@working-theory/license";
|
|
25
|
-
import { logger } from "@working-theory/observability";
|
|
26
|
-
import {
|
|
27
|
-
MAGNET_KIND_MIN_RANK,
|
|
28
|
-
orgBillingDetailsSchema,
|
|
29
|
-
planRank,
|
|
30
|
-
type FlowConfig,
|
|
31
|
-
type OrgBillingDetails,
|
|
32
|
-
} from "@working-theory/validation";
|
|
33
|
-
|
|
34
|
-
import { prisma } from "../lib/prisma.js";
|
|
35
|
-
import { readCustomerBillingSummary } from "../lib/stripe.js";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Feature flags unlocked per tier. Free unlocks none; every paid tier unlocks
|
|
39
|
-
* the Pro v1 feature set (mirrors `ProFeature` in `@working-theory/license`).
|
|
40
|
-
* A flat projection, matching AILK's flat billing shape (spec D1) — the richer
|
|
41
|
-
* per-plan allowance model is FEAT-047's, not this foundation's.
|
|
42
|
-
*/
|
|
43
|
-
const PAID_FEATURES: readonly string[] = [
|
|
44
|
-
"lead-destinations",
|
|
45
|
-
"lead-provenance",
|
|
46
|
-
"schema-pack-registry",
|
|
47
|
-
"onboard-site",
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
function featuresForTier(tier: string): readonly string[] {
|
|
51
|
-
return tier === "free" ? [] : PAID_FEATURES;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Resolve the entitlements for one Organization. An unknown / un-billed org
|
|
56
|
-
* (no row, or defaulted columns) resolves to the free-tier baseline
|
|
57
|
-
* (`seats: 1`, `tier: "free"`, no features) — safe-by-default.
|
|
58
|
-
*
|
|
59
|
-
* `limits.metered` is returned as the reserved empty `MeteredLimits` (`{}`);
|
|
60
|
-
* FEAT-047 (#3679) fills it when it ports the metering engine.
|
|
61
|
-
*/
|
|
62
|
-
export async function resolveEntitlements(
|
|
63
|
-
organizationId: string,
|
|
64
|
-
): Promise<Entitlements> {
|
|
65
|
-
const org = await prisma.organization.findUnique({
|
|
66
|
-
where: { id: organizationId },
|
|
67
|
-
select: { tier: true, seatCount: true },
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const tier = org?.tier ?? "free";
|
|
71
|
-
const seats = org?.seatCount ?? 1;
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
features: featuresForTier(tier),
|
|
75
|
-
limits: {
|
|
76
|
-
static: { seats, tier },
|
|
77
|
-
// RESERVED — FEAT-047 (#3679) fills the metered side; empty here.
|
|
78
|
-
metered: {},
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* #6287 — the org's plan RANK (never the deployment-wide `checkLicense()`),
|
|
85
|
-
* for the magnet routes' server-side plan gate. Composes `resolveEntitlements`
|
|
86
|
-
* above + `planRank` (`@working-theory/validation`, the same table the
|
|
87
|
-
* Admin's own UI-only gate reads) and NEVER throws: any failure (a DB
|
|
88
|
-
* error, or a tier string `planRank` doesn't recognize) collapses to
|
|
89
|
-
* `undefined` — the caller's fail-closed signal for a gated kind (spec D
|
|
90
|
-
* "the plan-rank resolver never throws").
|
|
91
|
-
*/
|
|
92
|
-
export async function resolveOrgPlanRank(
|
|
93
|
-
organizationId: string,
|
|
94
|
-
): Promise<number | undefined> {
|
|
95
|
-
try {
|
|
96
|
-
const entitlements = await resolveEntitlements(organizationId);
|
|
97
|
-
return planRank(entitlements.limits.static.tier);
|
|
98
|
-
} catch {
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* #6305 — the minimum plan rank a GATED-SHAPE config requires, independent
|
|
105
|
-
* of the `kind` label the caller chose. Derived from `MAGNET_KIND_MIN_RANK`
|
|
106
|
-
* (never hand-duplicated) as the lowest non-zero rank in that table: today
|
|
107
|
-
* that is the same Pro-tier rank every gated kind already requires, so a
|
|
108
|
-
* gated-shape config on an ungated kind is refused exactly as a gated-kind
|
|
109
|
-
* refusal would be (#6305 AC-1).
|
|
110
|
-
*/
|
|
111
|
-
export const GATED_FLOW_SHAPE_MIN_RANK = Math.min(
|
|
112
|
-
...Object.values(MAGNET_KIND_MIN_RANK).filter((rank) => rank > 0),
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* #6305 — whether a `FlowConfig` carries the shape the flow runtime actually
|
|
117
|
-
* branches on for gated (recommending-mode) behavior, regardless of which
|
|
118
|
-
* `kind` label the row carries. This is the SAME predicate family
|
|
119
|
-
* `deriveFlowKind`-style logic elsewhere in this codebase reads
|
|
120
|
-
* (`mode === "recommending"`, or the presence of `deliverables` / `offers` /
|
|
121
|
-
* `scoring`) — the label and the capability it confers can no longer diverge.
|
|
122
|
-
*/
|
|
123
|
-
export function configHasGatedShape(config: FlowConfig | undefined): boolean {
|
|
124
|
-
if (!config) return false;
|
|
125
|
-
return (
|
|
126
|
-
config.mode === "recommending" ||
|
|
127
|
-
config.deliverables !== undefined ||
|
|
128
|
-
config.offers !== undefined ||
|
|
129
|
-
config.scoring !== undefined
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* The org's Stripe billing-customer identity (#3701 — the apps/api
|
|
135
|
-
* predecessor to the org-billing UI, #3697).
|
|
136
|
-
*
|
|
137
|
-
* `billingCustomerId` is the RAW Stripe customer id — a server-side-only
|
|
138
|
-
* value. Callers (the `/v1/billing` route family) must never return it to the
|
|
139
|
-
* client: the read route projects it to a boolean `hasBillingCustomer`
|
|
140
|
-
* presence flag (spec D2), and the portal route uses it only as an input to
|
|
141
|
-
* `createBillingPortalSession`, never in a response body or log line.
|
|
142
|
-
*/
|
|
143
|
-
export interface OrgBillingIdentity {
|
|
144
|
-
/** Raw Stripe customer id (`cus_xxx`), or `null` when the org has none yet. */
|
|
145
|
-
billingCustomerId: string | null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Reads the org's Stripe billing-customer id by explicit organization id.
|
|
150
|
-
* Touches only the `Organization.billingCustomerId` field — the sanctioned
|
|
151
|
-
* tenant-scope exception for this file (architecture.yaml § Tenant-scope
|
|
152
|
-
* boundary, #3601): no un-scoped tenant/content enumeration.
|
|
153
|
-
*
|
|
154
|
-
* An unknown org resolves to `{ billingCustomerId: null }` — safe-by-default,
|
|
155
|
-
* mirroring `resolveEntitlements`'s un-billed-org handling.
|
|
156
|
-
*/
|
|
157
|
-
export async function getOrgBillingIdentity(
|
|
158
|
-
organizationId: string,
|
|
159
|
-
): Promise<OrgBillingIdentity> {
|
|
160
|
-
const org = await prisma.organization.findUnique({
|
|
161
|
-
where: { id: organizationId },
|
|
162
|
-
select: { billingCustomerId: true },
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
return { billingCustomerId: org?.billingCustomerId ?? null };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Whether ANY Organization row carries the given Stripe customer id (#6038
|
|
170
|
-
* D3.2, closes SA-4). Touches only `Organization.billingCustomerId` by
|
|
171
|
-
* explicit value — the same sanctioned-exception shape as
|
|
172
|
-
* `getOrgBillingIdentity` above (by id) and `resolveEntitlements` (by id),
|
|
173
|
-
* never an un-scoped tenant/content query.
|
|
174
|
-
*
|
|
175
|
-
* Exists because no code in this repo stamps `metadata.organizationId` onto
|
|
176
|
-
* a Stripe customer, so every `customer.created` takes the `User` branch
|
|
177
|
-
* (`webhooks/stripe.ts`'s `upsertBillingIdentity`) and a single `cus_…` can
|
|
178
|
-
* end up projected onto BOTH a `User` row and an `Organization` row. A
|
|
179
|
-
* caller resolving "the caller's own portal" from `User.billingCustomerId`
|
|
180
|
-
* alone would then risk opening the ORGANIZATION's billing record instead —
|
|
181
|
-
* this is the collision check that refuses that case.
|
|
182
|
-
*/
|
|
183
|
-
export async function isBillingCustomerAnOrganization(
|
|
184
|
-
billingCustomerId: string,
|
|
185
|
-
): Promise<boolean> {
|
|
186
|
-
const org = await prisma.organization.findFirst({
|
|
187
|
-
where: { billingCustomerId },
|
|
188
|
-
select: { id: true },
|
|
189
|
-
});
|
|
190
|
-
return org !== null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* #9067 D5 — the deployment licence's expiry, as an ISO string, or `null`
|
|
195
|
-
* for a keyless, expired or invalid licence. The trial state's date
|
|
196
|
-
* (C2). Never throws: a failed read is `null`.
|
|
197
|
-
*/
|
|
198
|
-
export async function resolveLicenseExpiresAt(): Promise<string | null> {
|
|
199
|
-
try {
|
|
200
|
-
const license = await checkLicense();
|
|
201
|
-
return license.expiresAt?.toISOString() ?? null;
|
|
202
|
-
} catch {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* #9067 D13 — the one plan Checkout this Organization has open, or `null`.
|
|
209
|
-
* Server-side only: the session id is never served. An unknown
|
|
210
|
-
* Organization reads `null`.
|
|
211
|
-
*/
|
|
212
|
-
export async function readOrgPlanCheckoutSession(
|
|
213
|
-
organizationId: string,
|
|
214
|
-
): Promise<string | null> {
|
|
215
|
-
const org = await prisma.organization.findUnique({
|
|
216
|
-
where: { id: organizationId },
|
|
217
|
-
select: { planCheckoutSessionId: true },
|
|
218
|
-
});
|
|
219
|
-
return org?.planCheckoutSessionId ?? null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* #9067 D3 step 9b — record the plan Checkout the route just opened, as one
|
|
224
|
-
* compare-and-set: only while the column still holds `expected` and no
|
|
225
|
-
* customer is bound. Answers whether one row moved; throws on a database
|
|
226
|
-
* error.
|
|
227
|
-
*/
|
|
228
|
-
export async function setOrgPlanCheckoutSession(
|
|
229
|
-
organizationId: string,
|
|
230
|
-
expected: string | null,
|
|
231
|
-
next: string,
|
|
232
|
-
): Promise<boolean> {
|
|
233
|
-
const { count } = await prisma.organization.updateMany({
|
|
234
|
-
where: { id: organizationId, planCheckoutSessionId: expected, billingCustomerId: null },
|
|
235
|
-
data: { planCheckoutSessionId: next },
|
|
236
|
-
});
|
|
237
|
-
return count === 1;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* #9067 7b (code review) — bind the Organization to the customer a COMPLETED
|
|
242
|
-
* plan Checkout created, when the webhook's bind never landed (a lost event,
|
|
243
|
-
* a database error the webhook swallowed). The same guarded compare-and-set
|
|
244
|
-
* the webhook runs: only while the column still holds `sessionId` and no
|
|
245
|
-
* customer is bound, and it clears the session. Answers whether one row
|
|
246
|
-
* moved; throws on a database error.
|
|
247
|
-
*/
|
|
248
|
-
export async function bindOrgPlanCheckout(
|
|
249
|
-
organizationId: string,
|
|
250
|
-
sessionId: string,
|
|
251
|
-
customerId: string,
|
|
252
|
-
): Promise<boolean> {
|
|
253
|
-
const { count } = await prisma.organization.updateMany({
|
|
254
|
-
where: { id: organizationId, planCheckoutSessionId: sessionId, billingCustomerId: null },
|
|
255
|
-
data: { billingProvider: "stripe", billingCustomerId: customerId, planCheckoutSessionId: null },
|
|
256
|
-
});
|
|
257
|
-
return count === 1;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** How long one customer's summary is reused, a failure included (#9067 NFR-1). */
|
|
261
|
-
export const ORG_BILLING_SUMMARY_TTL_MS = 60_000;
|
|
262
|
-
/** The most customers held at once; the oldest entry is evicted at the cap. */
|
|
263
|
-
export const ORG_BILLING_SUMMARY_CACHE_MAX = 2_000;
|
|
264
|
-
/** The longest one summary read may take before the keys are left absent (NFR-2). */
|
|
265
|
-
export const ORG_BILLING_SUMMARY_TIMEOUT_MS = 3_000;
|
|
266
|
-
|
|
267
|
-
type OrgBillingSummaryKeys = Partial<Pick<OrgBillingDetails, "renewsAt" | "sites" | "paymentMethod">>;
|
|
268
|
-
|
|
269
|
-
/** Per customer: the settled keys and when they expire. Insertion order is age. */
|
|
270
|
-
const orgBillingSummaryCache = new Map<string, { keys: OrgBillingSummaryKeys; expiresAt: number }>();
|
|
271
|
-
/** Per customer: the one read in flight, joined by every concurrent miss. */
|
|
272
|
-
const orgBillingSummaryInFlight = new Map<string, Promise<OrgBillingSummaryKeys>>();
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* #9067 D5 — the Organization customer's renewal date, site count and card,
|
|
276
|
-
* from the cache when fresh, else one read shared by every concurrent caller
|
|
277
|
-
* of the same customer. The member read's wrapper (`services/subscription.ts`),
|
|
278
|
-
* rebuilt here because importing it would be a cycle. A throw or a timeout
|
|
279
|
-
* answers `{}` (every key absent) and is cached like a success; a value
|
|
280
|
-
* outside `orgBillingDetailsSchema`'s bounds is `null`.
|
|
281
|
-
*/
|
|
282
|
-
export async function readOrgBillingSummary(customerId: string): Promise<OrgBillingSummaryKeys> {
|
|
283
|
-
const cached = orgBillingSummaryCache.get(customerId);
|
|
284
|
-
if (cached && cached.expiresAt > Date.now()) return cached.keys;
|
|
285
|
-
const pending = orgBillingSummaryInFlight.get(customerId);
|
|
286
|
-
if (pending) return pending;
|
|
287
|
-
|
|
288
|
-
const read = settleOrgBillingSummary(customerId).then((keys) => {
|
|
289
|
-
orgBillingSummaryCache.delete(customerId);
|
|
290
|
-
if (orgBillingSummaryCache.size >= ORG_BILLING_SUMMARY_CACHE_MAX) {
|
|
291
|
-
const oldest = orgBillingSummaryCache.keys().next().value;
|
|
292
|
-
if (oldest !== undefined) orgBillingSummaryCache.delete(oldest);
|
|
293
|
-
}
|
|
294
|
-
orgBillingSummaryCache.set(customerId, {
|
|
295
|
-
keys,
|
|
296
|
-
expiresAt: Date.now() + ORG_BILLING_SUMMARY_TTL_MS,
|
|
297
|
-
});
|
|
298
|
-
return keys;
|
|
299
|
-
});
|
|
300
|
-
// Cleared however the read ends, so a rejection is never served forever.
|
|
301
|
-
void read.finally(() => orgBillingSummaryInFlight.delete(customerId)).catch(() => undefined);
|
|
302
|
-
orgBillingSummaryInFlight.set(customerId, read);
|
|
303
|
-
return read;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
async function settleOrgBillingSummary(customerId: string): Promise<OrgBillingSummaryKeys> {
|
|
307
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
308
|
-
try {
|
|
309
|
-
const timedOut = new Promise<"timeout">((resolve) => {
|
|
310
|
-
timer = setTimeout(() => resolve("timeout"), ORG_BILLING_SUMMARY_TIMEOUT_MS);
|
|
311
|
-
});
|
|
312
|
-
const summary = await Promise.race([readCustomerBillingSummary(customerId), timedOut]);
|
|
313
|
-
if (summary === "timeout") throw new Error("timeout");
|
|
314
|
-
const { shape } = orgBillingDetailsSchema;
|
|
315
|
-
const renewsAt = shape.renewsAt.safeParse(summary.renewsAt);
|
|
316
|
-
const sites = shape.sites.safeParse(summary.sites);
|
|
317
|
-
const paymentMethod = shape.paymentMethod.safeParse(summary.paymentMethod);
|
|
318
|
-
return {
|
|
319
|
-
renewsAt: renewsAt.success ? (renewsAt.data ?? null) : null,
|
|
320
|
-
sites: sites.success ? (sites.data ?? null) : null,
|
|
321
|
-
paymentMethod: paymentMethod.success ? (paymentMethod.data ?? null) : null,
|
|
322
|
-
};
|
|
323
|
-
} catch {
|
|
324
|
-
logger.warn(
|
|
325
|
-
{ route: "billing", outcome: "summary_unavailable" },
|
|
326
|
-
"[billing] organization billing summary unavailable",
|
|
327
|
-
);
|
|
328
|
-
return {};
|
|
329
|
-
} finally {
|
|
330
|
-
clearTimeout(timer);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** Test seam: forget every cached and in-flight Organization summary. */
|
|
335
|
-
export function __resetOrgBillingSummaryCache(): void {
|
|
336
|
-
orgBillingSummaryCache.clear();
|
|
337
|
-
orgBillingSummaryInFlight.clear();
|
|
338
|
-
}
|