create-ailk 0.5.0 → 0.6.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/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +142 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -8,16 +8,53 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Generator + datasource blocks live in database/schema.prisma (the root schema
|
|
10
10
|
// file). This file holds only model definitions for the inbox namespace.
|
|
11
|
+
//
|
|
12
|
+
// ── The fence fan-out (#6374) — why `workspaceId` sits where it does ────────
|
|
13
|
+
// Eleven models in this file carry the tenancy fence: a NOT NULL
|
|
14
|
+
// `workspaceId` mapped to `workspace_id`, an `@@index([workspaceId])`, and a
|
|
15
|
+
// `<table>_workspace_site_agree` CHECK added by
|
|
16
|
+
// database/migrations/20260922120000_tenancy_fence. No Prisma relation is
|
|
17
|
+
// declared for it (no Prisma relation directive) and no raw-SQL foreign key
|
|
18
|
+
// is added (ADR-0009 invariant #1) — the CHECK is the fence, and it is
|
|
19
|
+
// strictly tighter than an FK, because `site_id` IS the resolved workspace id.
|
|
11
20
|
|
|
12
21
|
/// Lead — anonymous lead submission (Phase B submit_lead tool / B6 route)
|
|
13
22
|
model Lead {
|
|
14
23
|
id String @id @default(dbgenerated("('lead_'::text || (uuidv7())::text)")) @db.Text
|
|
15
24
|
siteId String @map("site_id")
|
|
25
|
+
// #6203 — the isolation key `tenantDb(tenant).leads` filters on. Plain
|
|
26
|
+
// scalar, no @relation (ADR 0009 D1, the same reasoning `promotedUserId`
|
|
27
|
+
// below already applies). Backfilled by
|
|
28
|
+
// database/migrations/20260919140000_inbox_workspace_key.
|
|
29
|
+
//
|
|
30
|
+
// #6254 AC-3/AC-4/AC-7 — NOT NULL as of
|
|
31
|
+
// database/migrations/20260922120000_tenancy_fence, which also normalizes
|
|
32
|
+
// `siteId` to the resolved workspace id and adds
|
|
33
|
+
// `leads_workspace_site_agree CHECK ("workspace_id" = "site_id")`: since
|
|
34
|
+
// `siteId` IS the workspace id (never a WorkspaceSiteIdentity id — see
|
|
35
|
+
// that model's docblock), Postgres itself now rejects a row whose two
|
|
36
|
+
// columns disagree, in place of the issue's literal (and unbuildable,
|
|
37
|
+
// ADR-0009-crossing) composite FK — docs/database/namespaces.md. Every
|
|
38
|
+
// write site derives this value server-side from the resolved site
|
|
39
|
+
// alias; never from a request body (AC-6).
|
|
40
|
+
workspaceId String @map("workspace_id")
|
|
16
41
|
source String
|
|
17
42
|
email String
|
|
18
43
|
payload Json
|
|
19
44
|
createdAt DateTime @default(now()) @map("created_at")
|
|
20
45
|
|
|
46
|
+
// #6204 — the triage stamp. NOT `promotedAt`'s sibling: `promotedAt` below
|
|
47
|
+
// is the lead→user PROMOTION stamp, written by the email-verification
|
|
48
|
+
// reconciler and true of a lead regardless of whether anyone has answered
|
|
49
|
+
// it. `handledAt` is written by an operator (or an agent acting for one)
|
|
50
|
+
// clearing a MESSAGE out of the Needs-you queue (spec D8). Nullable, no
|
|
51
|
+
// default — every existing row reads NULL, i.e. unhandled (spec D5).
|
|
52
|
+
// `handledBy` is the actor, mirroring `ProjectListing.decidedBy` below
|
|
53
|
+
// (spec D4/S-7) — plain scalar, no @relation (ADR 0009 D2), the same
|
|
54
|
+
// posture `promotedUserId` already takes.
|
|
55
|
+
handledAt DateTime? @map("handled_at")
|
|
56
|
+
handledBy String? @map("handled_by")
|
|
57
|
+
|
|
21
58
|
// Lead → user promotion (#3586). Plain scalars — no cross-namespace
|
|
22
59
|
// @relation into auth.User (ADR 0009 invariant #1). Set by
|
|
23
60
|
// apps/api/src/services/lead-promotion.ts once the reconciling user's
|
|
@@ -29,6 +66,18 @@ model Lead {
|
|
|
29
66
|
@@index([siteId])
|
|
30
67
|
@@index([createdAt])
|
|
31
68
|
@@index([email])
|
|
69
|
+
@@index([workspaceId])
|
|
70
|
+
// #6204 — the message-inbox read: workspaceId = $1 AND handledAt IS NULL
|
|
71
|
+
// AND source = ANY($2), ordered by createdAt desc. Two columns, not three
|
|
72
|
+
// (spec D10): `source` is an IN over four values Postgres applies cheaply
|
|
73
|
+
// after this narrowing, so it is not worth widening the index on every
|
|
74
|
+
// write.
|
|
75
|
+
@@index([workspaceId, handledAt])
|
|
76
|
+
// #6246 — the per-magnet read: workspaceId = $1 AND source = $2, ordered
|
|
77
|
+
// by createdAt desc (spec § 1.5/D1). `source` here is an EXACT-equality
|
|
78
|
+
// `'flow:' + <magnet slug>` value, not the `handledAt` index's allowlist
|
|
79
|
+
// `IN` — a distinct query shape that composite does not serve.
|
|
80
|
+
@@index([workspaceId, source])
|
|
32
81
|
@@map("leads")
|
|
33
82
|
}
|
|
34
83
|
|
|
@@ -36,27 +85,91 @@ model Lead {
|
|
|
36
85
|
model Subscriber {
|
|
37
86
|
id String @id @default(dbgenerated("('subscr_'::text || (uuidv7())::text)")) @db.Text
|
|
38
87
|
siteId String @map("site_id")
|
|
88
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
89
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
90
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
91
|
+
// subscribers_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
92
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
93
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
94
|
+
// value, never one from a request body (AC-6).
|
|
95
|
+
workspaceId String @map("workspace_id")
|
|
39
96
|
email String
|
|
40
97
|
source String
|
|
41
98
|
createdAt DateTime @default(now()) @map("created_at")
|
|
42
99
|
|
|
43
100
|
@@unique([siteId, email])
|
|
44
101
|
@@index([siteId])
|
|
102
|
+
@@index([workspaceId])
|
|
45
103
|
@@map("subscribers")
|
|
46
104
|
}
|
|
47
105
|
|
|
48
|
-
/// Callback — callback request (Phase B request_callback tool / B6 route)
|
|
106
|
+
/// Callback — callback request (Phase B request_callback tool / B6 route),
|
|
107
|
+
/// plus the request-and-confirm lifecycle (#5970, D-67 / SUB-100).
|
|
108
|
+
///
|
|
109
|
+
/// The eleven trailing members are the confirm/pay lifecycle. They are
|
|
110
|
+
/// deliberately ON THIS MODEL rather than on a second `ServiceRequest` row
|
|
111
|
+
/// (spec #5970 D1), and the cost of that choice is stated rather than hidden:
|
|
112
|
+
/// every ORDINARY callback row — the overwhelming majority — now carries
|
|
113
|
+
/// eleven NULL columns it will never use. The alternative duplicates
|
|
114
|
+
/// siteId/name/phone/preferredTime/payload or carries a cross-row join for a
|
|
115
|
+
/// one-to-one relationship, and it puts a SECOND write inside the anonymous
|
|
116
|
+
/// lead-capture path, which is a never-gate surface. Eleven nullable columns
|
|
117
|
+
/// is the cheaper side.
|
|
49
118
|
model Callback {
|
|
50
119
|
id String @id @default(dbgenerated("('callbk_'::text || (uuidv7())::text)")) @db.Text
|
|
51
120
|
siteId String @map("site_id")
|
|
121
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
122
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
123
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
124
|
+
// callbacks_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
125
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
126
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
127
|
+
// value, never one from a request body (AC-6).
|
|
128
|
+
workspaceId String @map("workspace_id")
|
|
52
129
|
name String
|
|
53
130
|
phone String
|
|
54
131
|
preferredTime String? @map("preferred_time")
|
|
55
132
|
payload Json
|
|
56
133
|
createdAt DateTime @default(now()) @map("created_at")
|
|
57
134
|
|
|
135
|
+
// ── request-and-confirm lifecycle (#5970) ──────────────────────────────
|
|
136
|
+
/// sha-256 hex digest of the tenant's confirm token. The token itself is
|
|
137
|
+
/// emailed once and never stored; NULLED when the credential is spent,
|
|
138
|
+
/// which is the single-use mechanism. `@unique` mirrors `resumeTokenHash` —
|
|
139
|
+
/// a digest collision is a bug, not a state.
|
|
140
|
+
confirmTokenHash String? @unique @map("confirm_token_hash")
|
|
141
|
+
/// When the credential stops resolving (mint + 7 days).
|
|
142
|
+
confirmTokenExpiresAt DateTime? @map("confirm_token_expires_at")
|
|
143
|
+
/// Stamped by the tenant's confirm POST. Also the "already confirmed" predicate.
|
|
144
|
+
confirmedAt DateTime? @map("confirmed_at")
|
|
145
|
+
/// What the tenant agreed to, as they typed it — a string like
|
|
146
|
+
/// `preferredTime` above, not a parsed instant.
|
|
147
|
+
confirmedTime String? @map("confirmed_time")
|
|
148
|
+
/// Where the hosted payment URL is sent. Typed on the confirm form,
|
|
149
|
+
/// prefilled from `payload.email` when the capture form carried one.
|
|
150
|
+
buyerEmail String? @map("buyer_email")
|
|
151
|
+
/// MINOR units, never major — Stripe's own `unit_amount` convention.
|
|
152
|
+
amountMinor Int? @map("amount_minor")
|
|
153
|
+
/// Lowercase ISO-4217.
|
|
154
|
+
currency String?
|
|
155
|
+
/// The Payment Link the confirm step minted — THE artifact id the webhook
|
|
156
|
+
/// stamp matches on (`session.payment_link`).
|
|
157
|
+
stripePaymentLinkId String? @map("stripe_payment_link_id")
|
|
158
|
+
/// The session written from the same event, for support lookups. NOT a
|
|
159
|
+
/// match key, which is why it carries no index.
|
|
160
|
+
stripeCheckoutSessionId String? @map("stripe_checkout_session_id")
|
|
161
|
+
/// NULL-forever at #5970's boundary — present so the hosted-invoice rung
|
|
162
|
+
/// lands additively, with no second migration.
|
|
163
|
+
stripeInvoiceId String? @map("stripe_invoice_id")
|
|
164
|
+
/// Stamped exactly once by the Stripe webhook; the `paidAt: null` guard on
|
|
165
|
+
/// that update IS the durable idempotency key.
|
|
166
|
+
paidAt DateTime? @map("paid_at")
|
|
167
|
+
|
|
58
168
|
@@index([siteId])
|
|
59
169
|
@@index([createdAt])
|
|
170
|
+
@@index([stripePaymentLinkId])
|
|
171
|
+
@@index([stripeInvoiceId])
|
|
172
|
+
@@index([workspaceId])
|
|
60
173
|
@@map("callbacks")
|
|
61
174
|
}
|
|
62
175
|
|
|
@@ -67,8 +180,51 @@ model Callback {
|
|
|
67
180
|
model FlowDefinition {
|
|
68
181
|
id String @id @default(dbgenerated("('flow_'::text || (uuidv7())::text)")) @db.Text
|
|
69
182
|
siteId String @map("site_id")
|
|
183
|
+
/// #6155 AD-2 — the workspace isolation key. Backfilled from `siteId`: the
|
|
184
|
+
/// literal "default" resolves to the seeded sentinel workspace
|
|
185
|
+
/// (wksp_00000000-0000-7000-8000-000000000001); any other value resolves
|
|
186
|
+
/// only by exact `workspaces.id` match. Plain string, no `@relation`: ADR
|
|
187
|
+
/// 0009 forbids a Prisma relation across the inbox -> auth (and, per
|
|
188
|
+
/// #6254, inbox -> config) namespace boundary, the same reason
|
|
189
|
+
/// `Asset.workspaceId` (database/content/schema.prisma) carries none.
|
|
190
|
+
///
|
|
191
|
+
/// #6254 AC-3/AC-4/AC-7 — NOT NULL as of
|
|
192
|
+
/// database/migrations/20260922120000_tenancy_fence, which also
|
|
193
|
+
/// normalizes `siteId` to the resolved workspace id and adds
|
|
194
|
+
/// `flow_definitions_workspace_site_agree CHECK ("workspace_id" =
|
|
195
|
+
/// "site_id")` — see `Lead.workspaceId` above for the full rationale
|
|
196
|
+
/// (identical here). Every write site derives this value server-side;
|
|
197
|
+
/// never from a request body (AC-6).
|
|
198
|
+
workspaceId String @map("workspace_id")
|
|
70
199
|
slug String
|
|
71
200
|
config Json
|
|
201
|
+
/// #6156 D1/D3 — the magnet's classification, derived structurally (never
|
|
202
|
+
/// from slug) by `deriveFlowKind` on CREATE only: special-report |
|
|
203
|
+
/// diagnostic | listicle | waitlist | contact | newsletter. Keeps its
|
|
204
|
+
/// column default ('contact') — unlike `status`, a mis-classified magnet
|
|
205
|
+
/// is a mislabel, not a publication or security control.
|
|
206
|
+
kind String @default("contact")
|
|
207
|
+
/// #6156 D1 — the operator-facing name; the slug is not a name. NOT NULL
|
|
208
|
+
/// with NO column default (D1): a nameless magnet must never enter the
|
|
209
|
+
/// table silently, so every future writer is required by `tsc` to state
|
|
210
|
+
/// one, exactly as `status` below is required to state a status.
|
|
211
|
+
title String
|
|
212
|
+
/// #6156 D1/D4 — an operator-declared publish address, site-relative only
|
|
213
|
+
/// (`flowLandingPageSchema`, packages/validation/src/schemas/flow.ts).
|
|
214
|
+
/// NULL means "no operator-declared landing page; the canonical
|
|
215
|
+
/// `/{locale}/flows/{slug}` route serves it" — never a derived guess, since
|
|
216
|
+
/// a magnet has THREE public addresses (the indexed landing route, the
|
|
217
|
+
/// walk route one segment deeper, and the results route), and only the
|
|
218
|
+
/// operator can say which one they are publishing.
|
|
219
|
+
landingPage String? @db.Text @map("landing_page")
|
|
220
|
+
/// #6156 D1/D2/D10 — draft | live. NOT NULL with NO column default (D1,
|
|
221
|
+
/// SEC-08): silence must never publish, so every future writer is required
|
|
222
|
+
/// by `tsc` to name a status. Existing rows are backfilled 'live' by the
|
|
223
|
+
/// migration (every row that exists today is already serving). The
|
|
224
|
+
/// `flow_definitions_status_domain` CHECK (D2) closes the deny-list gate in
|
|
225
|
+
/// `resolveSession` (D10): an unknown value is unstorable, so "not
|
|
226
|
+
/// 'draft'" and "'live'" are the same set at the database level.
|
|
227
|
+
status String
|
|
72
228
|
// #5036 D6d — the listing this definition was projected from, when it was.
|
|
73
229
|
// Null for every flow that is not a listing diagnostic. Before this column
|
|
74
230
|
// the only link was that the generated flow's slug HAPPENED to equal the
|
|
@@ -81,6 +237,7 @@ model FlowDefinition {
|
|
|
81
237
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
82
238
|
|
|
83
239
|
@@unique([siteId, slug])
|
|
240
|
+
@@index([workspaceId])
|
|
84
241
|
@@map("flow_definitions")
|
|
85
242
|
}
|
|
86
243
|
|
|
@@ -94,6 +251,18 @@ model FlowDefinition {
|
|
|
94
251
|
model FlowSession {
|
|
95
252
|
id String @id @default(dbgenerated("('flsess_'::text || (uuidv7())::text)")) @db.Text
|
|
96
253
|
siteId String @map("site_id")
|
|
254
|
+
/// #6155 AD-2 — the workspace isolation key, same backfill contract as
|
|
255
|
+
/// `FlowDefinition.workspaceId` above; #6254 makes it NOT NULL plus the
|
|
256
|
+
/// same `flow_sessions_workspace_site_agree` equality CHECK — see
|
|
257
|
+
/// `Lead.workspaceId` (database/inbox/schema.prisma, top of file) for the
|
|
258
|
+
/// full rationale. Declared here, immediately after `siteId` and before
|
|
259
|
+
/// `state`'s empty-object-literal default below: the drift guard's model
|
|
260
|
+
/// regex (`tests/architecture/tenant-scope-pattern.test.ts`) truncates a
|
|
261
|
+
/// model's captured body at its first closing brace — including one
|
|
262
|
+
/// inside a comment or a string literal, not only in code — so a field
|
|
263
|
+
/// placed after that point (or a stray brace pair written ABOVE it, as
|
|
264
|
+
/// this very note must avoid) would be invisible to it.
|
|
265
|
+
workspaceId String @map("workspace_id")
|
|
97
266
|
// Informational only — never joined on (ADR 0009 D1: logical, not FK,
|
|
98
267
|
// isolation). Session resumption + completion key on flowSlug, not this
|
|
99
268
|
// column; deliberate, per code-review finding on #3956's acceptance PR.
|
|
@@ -138,6 +307,7 @@ model FlowSession {
|
|
|
138
307
|
|
|
139
308
|
@@index([siteId])
|
|
140
309
|
@@index([siteId, completedAt])
|
|
310
|
+
@@index([workspaceId])
|
|
141
311
|
@@index([email])
|
|
142
312
|
// Backs the abandon-email sweep's per-definition selection query
|
|
143
313
|
// (apps/api/src/services/abandon-email.ts — flowId equality + completedAt
|
|
@@ -151,16 +321,32 @@ model FlowSession {
|
|
|
151
321
|
@@map("flow_sessions")
|
|
152
322
|
}
|
|
153
323
|
|
|
154
|
-
/// DeliverableAsset —
|
|
155
|
-
/// config block references by `key` (#3961 D1).
|
|
156
|
-
/// (Postgres bytea) behind one internal
|
|
157
|
-
/// reversibility is structural (a later
|
|
158
|
-
/// resolver + the upload CLI, never the
|
|
159
|
-
/// email link shape). Uploaded via the
|
|
160
|
-
/// (apps/api/src/bin/deliverable-upload.ts) only —
|
|
324
|
+
/// DeliverableAsset — LEGACY (#6310 D3/D11). An operator-uploaded asset the
|
|
325
|
+
/// funnel's `deliverables` config block references by `key` (#3961 D1).
|
|
326
|
+
/// Bytes live IN THE DATABASE (Postgres bytea) behind one internal
|
|
327
|
+
/// resolver — zero new infrastructure; reversibility is structural (a later
|
|
328
|
+
/// blob-storage swap changes only the resolver + the upload CLI, never the
|
|
329
|
+
/// config contract, grant model, or email link shape). Uploaded via the
|
|
330
|
+
/// `deliverable:upload` CLI (apps/api/src/bin/deliverable-upload.ts) only —
|
|
331
|
+
/// no HTTP upload route.
|
|
332
|
+
///
|
|
333
|
+
/// Since #6310: a magnet deliverable resolves against the workspace-scoped
|
|
334
|
+
/// `Asset` model (database/content/schema.prisma) FIRST; this store is the
|
|
335
|
+
/// FALLBACK, tried only on an `Asset` miss (D2). No existing row is
|
|
336
|
+
/// migrated — D3 grounds why in the two server-internal write/read paths —
|
|
337
|
+
/// so every row here keeps delivering, unmigrated, until an operator
|
|
338
|
+
/// confirms none remain (§2.3's exit condition).
|
|
161
339
|
model DeliverableAsset {
|
|
162
340
|
id String @id @default(dbgenerated("('dasset_'::text || (uuidv7())::text)")) @db.Text
|
|
163
341
|
siteId String @map("site_id")
|
|
342
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
343
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
344
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
345
|
+
// deliverable_assets_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
346
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
347
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
348
|
+
// value, never one from a request body (AC-6).
|
|
349
|
+
workspaceId String @map("workspace_id")
|
|
164
350
|
// The assetKey a flow config's deliverables.assets entry references.
|
|
165
351
|
key String
|
|
166
352
|
// Download filename (Content-Disposition). Stored RAW at upload time
|
|
@@ -178,6 +364,7 @@ model DeliverableAsset {
|
|
|
178
364
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
179
365
|
|
|
180
366
|
@@unique([siteId, key])
|
|
367
|
+
@@index([workspaceId])
|
|
181
368
|
@@map("deliverable_assets")
|
|
182
369
|
}
|
|
183
370
|
|
|
@@ -188,12 +375,63 @@ model DeliverableAsset {
|
|
|
188
375
|
/// value — only the uuidv7-random grant id, delivered once via the lead's
|
|
189
376
|
/// own email. `assetId`/`leadId` are stable-id refs (ADR 0009 invariant 1
|
|
190
377
|
/// posture) — no cross-model `@relation`.
|
|
378
|
+
///
|
|
379
|
+
/// `assetId`'s three id classes (#6310): `asset_` (a workspace `Asset`,
|
|
380
|
+
/// scoped serve via `workspaceId` below), `dasset_` (legacy
|
|
381
|
+
/// `DeliverableAsset`, scoped by `siteId` above), `playbook:` (the FEAT-074
|
|
382
|
+
/// U4b virtual asset — no store row at all).
|
|
191
383
|
model DeliverableGrant {
|
|
192
384
|
id String @id @default(dbgenerated("('dgrant_'::text || (uuidv7())::text)")) @db.Text
|
|
193
385
|
siteId String @map("site_id")
|
|
194
386
|
assetId String @map("asset_id")
|
|
195
387
|
leadId String @map("lead_id")
|
|
196
388
|
sessionId String @map("session_id")
|
|
389
|
+
/// The workspace that authorized this grant's asset resolution (#6310
|
|
390
|
+
/// D15). Plain string, no
|
|
391
|
+
/// @relation (ADR 0009 invariant 1) — the same shape
|
|
392
|
+
/// FlowDefinition.workspaceId carries. The serve path reads it ONLY for
|
|
393
|
+
/// the `asset_` class, and a NULL there is a 404, never an unscoped read.
|
|
394
|
+
///
|
|
395
|
+
/// #6374 CHANGED WHO WRITES IT, AND THE SAFETY ARGUMENT MOVED WITH IT.
|
|
396
|
+
/// Before #6374 this was written ONLY when `assetId` was an `asset_` id,
|
|
397
|
+
/// and stayed NULL for a legacy `dasset_` grant and a `playbook:` virtual
|
|
398
|
+
/// one. As of #6374 BOTH mint sites stamp it unconditionally
|
|
399
|
+
/// (services/deliverable-fulfillment.ts, services/listing-csv-sweep.ts),
|
|
400
|
+
/// so NO newly-minted grant is NULL and the fail-closed sentinel below
|
|
401
|
+
/// now protects PRE-EXISTING rows only — the migration still touches none
|
|
402
|
+
/// of them.
|
|
403
|
+
///
|
|
404
|
+
/// That is safe because of a DIFFERENT invariant than the one that used to
|
|
405
|
+
/// carry it, and the new one is more fragile, so it is written down here
|
|
406
|
+
/// rather than left implicit. What makes stamping safe is NOT "the column
|
|
407
|
+
/// is NULL on legacy grants" — it no longer is. It is that
|
|
408
|
+
/// routes/deliverables/index.ts branches on the `assetId` PREFIX, and only
|
|
409
|
+
/// the `asset_` arm ever reads this column: the `playbook:` arm scopes by
|
|
410
|
+
/// siteId + leadId, and the legacy `dasset_` arm scopes by
|
|
411
|
+
/// `asset.siteId === grant.siteId`. Neither reads `workspaceId`, so a
|
|
412
|
+
/// stamped legacy grant gains no reach it did not already have.
|
|
413
|
+
/// DO NOT make the legacy or playbook arm consult this column without
|
|
414
|
+
/// re-deriving that argument — doing so WOULD be an authorization
|
|
415
|
+
/// widening, which is exactly what the pre-#6374 NULL used to prevent.
|
|
416
|
+
///
|
|
417
|
+
/// #6254 DELIBERATELY DOES NOT TOUCH THIS FIELD. It stays nullable, with
|
|
418
|
+
/// no equality CHECK — unlike Lead/ProjectListing/FlowDefinition/
|
|
419
|
+
/// FlowSession above, this column has no source column to derive from
|
|
420
|
+
/// (its own migration says so: "no source column to derive from ... a
|
|
421
|
+
/// wholly new fact, captured only at a FUTURE mint"), and `NULL` is the
|
|
422
|
+
/// serve path's fail-closed sentinel (`deliverable-asset-store.ts`
|
|
423
|
+
/// `readDeliverableAsset` opens `if (workspaceId === null) return null`).
|
|
424
|
+
/// Stamping `workspace_id := site_id` would hand a legacy grant an
|
|
425
|
+
/// authorization scope no mint ever granted, on a bearer-token download
|
|
426
|
+
/// path — a security regression, not a tightening. That reasoning still
|
|
427
|
+
/// holds for EXISTING rows, which is why the column stays nullable and why
|
|
428
|
+
/// #6374's migration adds no column and rewrites no row. Carved out of the
|
|
429
|
+
/// #6254 retrofit by the operator (spec §10.1 Q2 -> carve out); #6374
|
|
430
|
+
/// fences it at mint grain, where the workspace is actually known, and
|
|
431
|
+
/// adds the NULL-TOLERANT
|
|
432
|
+
/// deliverable_grants_workspace_site_agree CHECK — so a STAMPED grant can
|
|
433
|
+
/// never disagree with its site while a legacy grant stays storable.
|
|
434
|
+
workspaceId String? @map("workspace_id")
|
|
197
435
|
createdAt DateTime @default(now()) @map("created_at")
|
|
198
436
|
// Post-mint send-outcome stamp (#3998 D1) — set at most once, by the
|
|
199
437
|
// send-and-stamp tail shared between fulfillDeliverable and the
|
|
@@ -223,6 +461,14 @@ model DeliverableGrant {
|
|
|
223
461
|
model WaitlistExperiment {
|
|
224
462
|
id String @id @default(dbgenerated("('wlexp_'::text || (uuidv7())::text)")) @db.Text
|
|
225
463
|
siteId String @map("site_id")
|
|
464
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
465
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
466
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
467
|
+
// waitlist_experiments_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
468
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
469
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
470
|
+
// value, never one from a request body (AC-6).
|
|
471
|
+
workspaceId String @map("workspace_id")
|
|
226
472
|
key String
|
|
227
473
|
mode String // lead-capture | qualifying | paid
|
|
228
474
|
status String // draft | live | closed
|
|
@@ -231,6 +477,7 @@ model WaitlistExperiment {
|
|
|
231
477
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
232
478
|
|
|
233
479
|
@@unique([siteId, key])
|
|
480
|
+
@@index([workspaceId])
|
|
234
481
|
@@map("waitlist_experiments")
|
|
235
482
|
}
|
|
236
483
|
|
|
@@ -240,6 +487,14 @@ model WaitlistExperiment {
|
|
|
240
487
|
model WaitlistScore {
|
|
241
488
|
id String @id @default(dbgenerated("('wlscore_'::text || (uuidv7())::text)")) @db.Text
|
|
242
489
|
siteId String @map("site_id")
|
|
490
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
491
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
492
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
493
|
+
// waitlist_scores_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
494
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
495
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
496
|
+
// value, never one from a request body (AC-6).
|
|
497
|
+
workspaceId String @map("workspace_id")
|
|
243
498
|
leadId String @map("lead_id")
|
|
244
499
|
experimentKey String @map("experiment_key")
|
|
245
500
|
waitlistKey String @map("waitlist_key")
|
|
@@ -261,6 +516,7 @@ model WaitlistScore {
|
|
|
261
516
|
// that `scoreCompletedSignup` relies on, per project rather than globally.
|
|
262
517
|
@@unique([leadId, experimentKey])
|
|
263
518
|
@@index([siteId, experimentKey, waitlistKey])
|
|
519
|
+
@@index([workspaceId])
|
|
264
520
|
@@map("waitlist_scores")
|
|
265
521
|
}
|
|
266
522
|
|
|
@@ -277,6 +533,14 @@ model WaitlistScore {
|
|
|
277
533
|
model PlaybookInstance {
|
|
278
534
|
id String @id @default(dbgenerated("('pbk_'::text || (uuidv7())::text)")) @db.Text
|
|
279
535
|
siteId String @map("site_id")
|
|
536
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
537
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
538
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
539
|
+
// playbook_instances_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
540
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
541
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
542
|
+
// value, never one from a request body (AC-6).
|
|
543
|
+
workspaceId String @map("workspace_id")
|
|
280
544
|
leadId String @map("lead_id")
|
|
281
545
|
experimentKey String @map("experiment_key")
|
|
282
546
|
scoreId String @map("score_id")
|
|
@@ -288,6 +552,7 @@ model PlaybookInstance {
|
|
|
288
552
|
createdAt DateTime @default(now()) @map("created_at")
|
|
289
553
|
|
|
290
554
|
@@index([siteId, leadId, createdAt])
|
|
555
|
+
@@index([workspaceId])
|
|
291
556
|
@@map("playbook_instances")
|
|
292
557
|
}
|
|
293
558
|
|
|
@@ -306,6 +571,14 @@ model PlaybookInstance {
|
|
|
306
571
|
model WaitlistFollowUpSend {
|
|
307
572
|
id String @id @default(dbgenerated("('wlfu_'::text || (uuidv7())::text)")) @db.Text
|
|
308
573
|
siteId String @map("site_id")
|
|
574
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
575
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
576
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
577
|
+
// waitlist_followup_sends_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
578
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
579
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
580
|
+
// value, never one from a request body (AC-6).
|
|
581
|
+
workspaceId String @map("workspace_id")
|
|
309
582
|
leadId String @map("lead_id")
|
|
310
583
|
followUpKey String @map("follow_up_key")
|
|
311
584
|
sentAt DateTime @map("sent_at")
|
|
@@ -313,6 +586,7 @@ model WaitlistFollowUpSend {
|
|
|
313
586
|
|
|
314
587
|
@@unique([leadId, followUpKey])
|
|
315
588
|
@@index([siteId])
|
|
589
|
+
@@index([workspaceId])
|
|
316
590
|
@@map("waitlist_followup_sends")
|
|
317
591
|
}
|
|
318
592
|
|
|
@@ -327,12 +601,21 @@ model WaitlistFollowUpSend {
|
|
|
327
601
|
/// visible in the data, not only in the sweep's control flow.
|
|
328
602
|
model ListingCsvSend {
|
|
329
603
|
id String @id @default(dbgenerated("('lcsv_'::text || (uuidv7())::text)")) @db.Text
|
|
604
|
+
// #6374 D2 — the tenancy fence, in the shape this model's ABSENT site_id
|
|
605
|
+
// forces (operator disposition, 2026-09-20). It gets NOT NULL plus the
|
|
606
|
+
// listing_csv_sends_workspace_id_format CHECK and NOTHING else: with no
|
|
607
|
+
// site_id column there is nothing for an equality CHECK to compare, and a
|
|
608
|
+
// composite FK to ProjectListing — legal, both models are inbox — was
|
|
609
|
+
// offered and declined in favour of ONE fence shape. Backfilled and
|
|
610
|
+
// stamped from the parent listing row, never from a request body (AC-6).
|
|
611
|
+
workspaceId String @map("workspace_id")
|
|
330
612
|
listingId String @map("listing_id")
|
|
331
613
|
sentAt DateTime @map("sent_at")
|
|
332
614
|
rowCount Int @map("row_count")
|
|
333
615
|
grantId String @map("grant_id")
|
|
334
616
|
|
|
335
617
|
@@index([listingId, sentAt])
|
|
618
|
+
@@index([workspaceId])
|
|
336
619
|
@@map("listing_csv_sends")
|
|
337
620
|
}
|
|
338
621
|
|
|
@@ -350,6 +633,14 @@ model ListingCsvSend {
|
|
|
350
633
|
model FlowCheckout {
|
|
351
634
|
id String @id @default(dbgenerated("('flchk_'::text || (uuidv7())::text)")) @db.Text
|
|
352
635
|
siteId String @map("site_id")
|
|
636
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
637
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
638
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
639
|
+
// flow_checkouts_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
640
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
641
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
642
|
+
// value, never one from a request body (AC-6).
|
|
643
|
+
workspaceId String @map("workspace_id")
|
|
353
644
|
flowSlug String @map("flow_slug")
|
|
354
645
|
experimentKey String @map("experiment_key")
|
|
355
646
|
sessionId String @map("session_id")
|
|
@@ -388,6 +679,7 @@ model FlowCheckout {
|
|
|
388
679
|
@@index([siteId, experimentKey, status])
|
|
389
680
|
// Onboarding's read: everything a lead bought.
|
|
390
681
|
@@index([leadId])
|
|
682
|
+
@@index([workspaceId])
|
|
391
683
|
@@map("flow_checkouts")
|
|
392
684
|
}
|
|
393
685
|
|
|
@@ -430,6 +722,18 @@ model FlowCheckout {
|
|
|
430
722
|
model ProjectListing {
|
|
431
723
|
id String @id @default(dbgenerated("('prjl_'::text || (uuidv7())::text)")) @db.Text
|
|
432
724
|
siteId String @map("site_id")
|
|
725
|
+
// #6203 — the isolation key `tenantDb(tenant).projectListings` filters on.
|
|
726
|
+
// Plain scalar, no @relation (ADR 0009 D1, same reasoning as ownerUserId
|
|
727
|
+
// below).
|
|
728
|
+
//
|
|
729
|
+
// #6254 AC-3/AC-4/AC-7 — NOT NULL as of
|
|
730
|
+
// database/migrations/20260922120000_tenancy_fence, which also normalizes
|
|
731
|
+
// `siteId` to the resolved workspace id and adds
|
|
732
|
+
// `project_listings_workspace_site_agree CHECK ("workspace_id" =
|
|
733
|
+
// "site_id")` — see `Lead.workspaceId` (top of database/inbox/schema.prisma)
|
|
734
|
+
// for the full rationale. Every write site derives this value
|
|
735
|
+
// server-side; never from a request body (AC-6).
|
|
736
|
+
workspaceId String @map("workspace_id")
|
|
433
737
|
// #5036 — written at SUBMIT, not at start. The founder's contact details are
|
|
434
738
|
// already captured on THIS row at step one, so a `Lead` at step one adds no
|
|
435
739
|
// capture; what it does add is an operator notification email per abandoned
|
|
@@ -445,6 +749,10 @@ model ProjectListing {
|
|
|
445
749
|
name String?
|
|
446
750
|
oneLiner String? @map("one_liner")
|
|
447
751
|
problem String? @db.Text
|
|
752
|
+
// #5096 — `problem`'s narrative other half ("how are you solving it?").
|
|
753
|
+
// OPT-IN, not required: it is absent from the platform's default
|
|
754
|
+
// `collectedFields` mask, so a site collects it only by naming it.
|
|
755
|
+
solution String? @db.Text
|
|
448
756
|
url String?
|
|
449
757
|
// #5036 D2 — the six-rung product ladder that replaced the four-value
|
|
450
758
|
// `stage`. The migration remaps every old value by an explicit map; there
|
|
@@ -574,7 +882,7 @@ model ProjectListing {
|
|
|
574
882
|
status String
|
|
575
883
|
// #5036 D1/D6 — when the founder ticked step one's consent box. Never null:
|
|
576
884
|
// a row cannot exist without it, because `start` requires it.
|
|
577
|
-
consentAt DateTime @default(now()) @map("consent_at")
|
|
885
|
+
consentAt DateTime @default(now()) @map("consent_at") @db.Timestamptz(3)
|
|
578
886
|
// #5036 D1 — sha-256 of the opaque resume token, never the token itself.
|
|
579
887
|
// The token is the ONLY credential on an anonymous surface, so the column
|
|
580
888
|
// holds a digest an attacker who reads the table still cannot present.
|
|
@@ -636,6 +944,64 @@ model ProjectListing {
|
|
|
636
944
|
// a link nobody proved, which is the bug this column exists to remove.
|
|
637
945
|
ownerUserId String? @map("owner_user_id")
|
|
638
946
|
ownerLinkedAt DateTime? @map("owner_linked_at")
|
|
947
|
+
// #6314 — the listicle-kind `FlowDefinition` that CAPTURED this
|
|
948
|
+
// application: the magnet the visitor arrived through, so a magnet's own
|
|
949
|
+
// Applications list can be scoped to its own submissions rather than to the
|
|
950
|
+
// whole workspace (nothing constrains a workspace to one listicle magnet —
|
|
951
|
+
// `FlowDefinition` carries `@@unique([siteId, slug])` and no
|
|
952
|
+
// `@@unique([siteId, kind])`, so workspace-only scoping would conflate two).
|
|
953
|
+
//
|
|
954
|
+
// NOT `FlowDefinition.listingId` reversed: that column is written at
|
|
955
|
+
// APPROVAL time by `services/project-listing-flow.ts`, when an approved
|
|
956
|
+
// listing is projected into its own per-listing diagnostic sub-flow. A
|
|
957
|
+
// different link, at a different time, in the other direction.
|
|
958
|
+
//
|
|
959
|
+
// AN ID, NOT A SLUG (operator ruling 2026-09-19): a slug is a natural key
|
|
960
|
+
// `@@unique([siteId, slug])` implies is mutable, and `Lead.source`'s
|
|
961
|
+
// `'flow:' + slug` pattern is the weaker one, used there only because that
|
|
962
|
+
// write path has no resolve step. This one does — the wire carries a slug
|
|
963
|
+
// (no public surface hands a visitor a `flow_<uuidv7>`), and
|
|
964
|
+
// `startProjectListingDraft` resolves it server-side against `siteId` +
|
|
965
|
+
// `workspaceId` + `kind: "listicle"` before storing the id.
|
|
966
|
+
//
|
|
967
|
+
// PLAIN SCALAR REF — no `@relation`, no foreign key, no `onDelete`
|
|
968
|
+
// (operator decision 2026-09-20, withdrawing the ruling's "on-delete
|
|
969
|
+
// set-null" clause). Both models live in `inbox/`, so ADR 0009 invariant #1
|
|
970
|
+
// (which bans CROSS-namespace relations) does not reach this column and
|
|
971
|
+
// permission was never the question — the convention is: every
|
|
972
|
+
// intra-`inbox` reference on this model and its siblings (`leadId`,
|
|
973
|
+
// `listingId`, `grantId`, `flowSessionId`) is a plain scalar, and of the 24
|
|
974
|
+
// FOREIGN KEY constraints in `database/migrations/**` every one targets
|
|
975
|
+
// `auth`. The composite shape specifically was offered and declined on
|
|
976
|
+
// `ListingCsvSend` below.
|
|
977
|
+
//
|
|
978
|
+
// WHAT THAT GIVES UP, stated rather than argued away: the database does not
|
|
979
|
+
// constrain this to name a real magnet, nor one in the same workspace. Three
|
|
980
|
+
// things bound it — the resolve carries `siteId` and `kind`, so storing a
|
|
981
|
+
// foreign id needs the service layer to actively misbehave; nothing deletes
|
|
982
|
+
// a `FlowDefinition` (`routes/magnets/index.ts` D5: "No DELETE"), so
|
|
983
|
+
// `SET NULL` would have described no live code path; and the read carries
|
|
984
|
+
// the injected `workspaceId` alongside this id, so a dangling or foreign
|
|
985
|
+
// value yields NO rows, never another workspace's.
|
|
986
|
+
//
|
|
987
|
+
// WRITE-ONCE, at START. The magnet a visitor arrived through is knowable
|
|
988
|
+
// only on arrival — by submit, ten steps and possibly a resume-email
|
|
989
|
+
// round-trip later, the referring context is gone. `draftUpdateData` never
|
|
990
|
+
// learns this key, so no patch body can re-attribute a draft after the fact,
|
|
991
|
+
// the same shape `emailVerifiedAt`'s write-once stamp has.
|
|
992
|
+
//
|
|
993
|
+
// PERMANENTLY NULLABLE, and NOT a backfill target. Every row written before
|
|
994
|
+
// this column has no evidence naming the magnet that captured it — no
|
|
995
|
+
// referrer, no session link, no slug — so a backfill would have to guess
|
|
996
|
+
// "the workspace's only listicle magnet", which is exactly the conflation
|
|
997
|
+
// this column exists to remove, made permanent and unauditable. It is the
|
|
998
|
+
// reason `founder_name` parts (#5407), the address parts (#5279) and
|
|
999
|
+
// `owner_user_id` (#5295) each backfill nothing: a value that can only be
|
|
1000
|
+
// guessed must not be asserted. NULL means "not attributed" — never
|
|
1001
|
+
// "attribution failed" and never "captured directly": an application
|
|
1002
|
+
// started through a direct link AFTER this ships is NULL for the same
|
|
1003
|
+
// reason and is indistinguishable from a legacy row.
|
|
1004
|
+
capturedByFlowId String? @map("captured_by_flow_id")
|
|
639
1005
|
createdAt DateTime @default(now()) @map("created_at")
|
|
640
1006
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
641
1007
|
|
|
@@ -650,6 +1016,15 @@ model ProjectListing {
|
|
|
650
1016
|
// design: the promotion match (`founderEmail` + `siteId` + null owner) is
|
|
651
1017
|
// already served by the (site_id, founder_email) index above.
|
|
652
1018
|
@@index([ownerUserId])
|
|
1019
|
+
// #6203 — backs a per-workspace list (tenantDb(tenant).projectListings.list).
|
|
1020
|
+
@@index([workspaceId])
|
|
1021
|
+
// #6314 — backs the per-magnet Applications read
|
|
1022
|
+
// (tenantDb(tenant).projectListings.listByCapturingFlow), whose WHERE is
|
|
1023
|
+
// exactly these two columns. Leading on `workspaceId`, matching the
|
|
1024
|
+
// `@@index([workspaceId])` convention the fence set on this table. ONE
|
|
1025
|
+
// index: no read reaches `capturedByFlowId` without a tenant, and with no
|
|
1026
|
+
// foreign key there is no cascade wanting a referencing-side index either.
|
|
1027
|
+
@@index([workspaceId, capturedByFlowId])
|
|
653
1028
|
@@map("project_listings")
|
|
654
1029
|
}
|
|
655
1030
|
|
|
@@ -673,6 +1048,14 @@ model ProjectListing {
|
|
|
673
1048
|
model ConsentGrant {
|
|
674
1049
|
id String @id @default(dbgenerated("('cnsnt_'::text || (uuidv7())::text)")) @db.Text
|
|
675
1050
|
siteId String @map("site_id")
|
|
1051
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
1052
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
1053
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
1054
|
+
// consent_grants_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
1055
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
1056
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
1057
|
+
// value, never one from a request body (AC-6).
|
|
1058
|
+
workspaceId String @map("workspace_id")
|
|
676
1059
|
subjectType String @map("subject_type")
|
|
677
1060
|
subjectId String @map("subject_id")
|
|
678
1061
|
purposeId String @map("purpose_id")
|
|
@@ -686,5 +1069,65 @@ model ConsentGrant {
|
|
|
686
1069
|
@@index([siteId, subjectType, subjectId])
|
|
687
1070
|
// "Who agreed to this purpose" — the cross-surface count the id exists for (D1/D2).
|
|
688
1071
|
@@index([siteId, purposeId])
|
|
1072
|
+
@@index([workspaceId])
|
|
689
1073
|
@@map("consent_grants")
|
|
690
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
/// Appointment — a Cal.com booking, normalized from the webhook receiver (#6085, brief H7).
|
|
1077
|
+
///
|
|
1078
|
+
/// siteId-scoped with no FK, like every other inbox model (ADR 0009 D2). The
|
|
1079
|
+
/// (siteId, provider, providerBookingUid) unique constraint IS the idempotency
|
|
1080
|
+
/// key: every write is a guarded updateMany, and the create that follows a zero
|
|
1081
|
+
/// match races on THIS index — which is what makes "exactly one row per booking
|
|
1082
|
+
/// uid" a DATABASE property rather than a property of a process that may have
|
|
1083
|
+
/// restarted. `provider` is in the key because a deployment may one day run two
|
|
1084
|
+
/// providers against one receiver and their uid spaces do not coordinate (#4288).
|
|
1085
|
+
model Appointment {
|
|
1086
|
+
id String @id @default(dbgenerated("('appt_'::text || (uuidv7())::text)")) @db.Text
|
|
1087
|
+
siteId String @map("site_id") @db.VarChar(200)
|
|
1088
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
1089
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
1090
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
1091
|
+
// appointments_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
1092
|
+
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
1093
|
+
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
1094
|
+
// value, never one from a request body (AC-6).
|
|
1095
|
+
workspaceId String @map("workspace_id")
|
|
1096
|
+
/// Cal.com's own booking `uid` — stable across reschedules of the same booking.
|
|
1097
|
+
/// Bounded: a value above Postgres's btree entry limit cannot be indexed at all.
|
|
1098
|
+
providerBookingUid String @map("provider_booking_uid") @db.VarChar(200)
|
|
1099
|
+
/// Provider identifier, part of the unique key (#4288).
|
|
1100
|
+
provider String @default("cal") @db.VarChar(32)
|
|
1101
|
+
startTime DateTime @map("start_time")
|
|
1102
|
+
endTime DateTime @map("end_time")
|
|
1103
|
+
/// The event's own title as the provider stated it. PII-adjacent: never logged (NFR-1).
|
|
1104
|
+
title String? @db.VarChar(300)
|
|
1105
|
+
/// Free text typed by an anonymous prospect — bounded at the same 120/254 the
|
|
1106
|
+
/// outbound seam already bounds them at (packages/validation SchedulingAttendeeSchema).
|
|
1107
|
+
attendeeName String @map("attendee_name") @db.VarChar(120)
|
|
1108
|
+
attendeeEmail String @map("attendee_email") @db.VarChar(254)
|
|
1109
|
+
attendeeTimeZone String? @map("attendee_time_zone") @db.VarChar(64)
|
|
1110
|
+
/// Normalized lifecycle state — see § D5. One of: pending | confirmed | cancelled.
|
|
1111
|
+
status String @db.VarChar(32)
|
|
1112
|
+
/// The payment flag. BOOKING_PAID sets it true; nothing else in this issue reads it.
|
|
1113
|
+
paid Boolean @default(false)
|
|
1114
|
+
/// Set by BOOKING_PAYMENT_INITIATED; a checkout was opened on Cal's side.
|
|
1115
|
+
paymentInitiatedAt DateTime? @map("payment_initiated_at")
|
|
1116
|
+
/// Set by BOOKING_NO_SHOW_UPDATED.
|
|
1117
|
+
noShow Boolean @default(false) @map("no_show")
|
|
1118
|
+
cancelledAt DateTime? @map("cancelled_at")
|
|
1119
|
+
/// Three INDEPENDENT watermarks, one per event family (§ D7). One shared column
|
|
1120
|
+
/// forced a choice between dropping a late payment fact and letting a replayed
|
|
1121
|
+
/// one revert state; three columns make each family monotonic on its own terms.
|
|
1122
|
+
lifecycleEventAt DateTime @map("lifecycle_event_at")
|
|
1123
|
+
paymentEventAt DateTime? @map("payment_event_at")
|
|
1124
|
+
noShowEventAt DateTime? @map("no_show_event_at")
|
|
1125
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
1126
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
1127
|
+
|
|
1128
|
+
@@unique([siteId, provider, providerBookingUid])
|
|
1129
|
+
@@index([siteId, startTime])
|
|
1130
|
+
@@index([createdAt])
|
|
1131
|
+
@@index([workspaceId])
|
|
1132
|
+
@@map("appointments")
|
|
1133
|
+
}
|