create-ailk 0.21.0 → 0.23.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/component-catalog.json +186 -8
- package/component-catalog.md +9 -7
- package/dist/add-module.js +20 -14
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +294 -12
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +12 -3
- package/dist/derive-architecture.js +20 -8
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +5 -5
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +3 -2
- package/dist/lib/scaffold-render-sweep.js +5 -8
- package/dist/module-architecture.js +99 -9
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/programmatic.js +19 -5
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/spoke-checkout.d.ts +65 -41
- package/dist/spoke-checkout.js +208 -430
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +52 -26
- package/dist/sync-routes.d.ts +38 -1
- package/dist/sync-routes.js +187 -13
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +8 -4
- package/dist/topology.js +25 -1
- package/dist/update.js +15 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +9 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +27 -0
- package/templates/apps/api/CLAUDE.md +87 -17
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/lib/stripe.ts +74 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/spec.ts +33 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +268 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +4 -0
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +164 -7
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +245 -36
- package/templates/apps/api/src/server.ts +163 -46
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1383 -133
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +431 -47
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +164 -17
- package/templates/apps/api/src/services/plan-offerings.ts +125 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
- package/templates/apps/mcp/CLAUDE.md +22 -14
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +24 -31
- package/templates/apps/web/CLAUDE.md +393 -9
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +141 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +101 -25
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +16 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +7 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +42 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +202 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +71 -20
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +69 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +64 -1
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +62 -3
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -48,12 +48,17 @@
|
|
|
48
48
|
*
|
|
49
49
|
* Billing upsert (customer.created / checkout.session.completed):
|
|
50
50
|
* Billing-entity routing (org-scoped billing, #3601, spec D2): a checkout
|
|
51
|
-
* carrying metadata.organizationId
|
|
51
|
+
* carrying metadata.organizationId takes the Organization path (multi-tenant
|
|
52
52
|
* module); absent → the User path (single-tenant).
|
|
53
53
|
*
|
|
54
|
-
* Organization path (metadata.organizationId present)
|
|
54
|
+
* Organization path (metadata.organizationId present) — proof-based since
|
|
55
|
+
* #9067 D13: the metadata names the target, the recorded session proves it.
|
|
55
56
|
* 1. Typed composite match already pointing at this org → idempotent no-op
|
|
56
|
-
* 2.
|
|
57
|
+
* 2. A checkout.session.completed binds only when the Organization's
|
|
58
|
+
* `planCheckoutSessionId` is this session's id and no customer is
|
|
59
|
+
* bound yet (one guarded updateMany, which also clears the session)
|
|
60
|
+
* 3. Anything else — an unrecorded session, a bound customer, any
|
|
61
|
+
* customer.created — logs org_bind_unproven and writes nothing
|
|
57
62
|
*
|
|
58
63
|
* User path (no organizationId):
|
|
59
64
|
* 1. Typed composite match: (billingProvider="stripe", billingCustomerId=customer)
|
|
@@ -96,6 +101,16 @@
|
|
|
96
101
|
* `stripeSessionId`), with `session.customer` as the customer id only on a
|
|
97
102
|
* live-mode event. Logs an outcome kind only; never fails the 200.
|
|
98
103
|
*
|
|
104
|
+
* Checkout terms grant (checkout.session.completed, #8765 D14):
|
|
105
|
+
* Chained after the Person step, `recordCheckoutTermsGrant` records the
|
|
106
|
+
* buyer's acceptance of the session's terms box as one `checkout` row of
|
|
107
|
+
* the consent ledger — only for a session whose `consent.terms_of_service`
|
|
108
|
+
* is `accepted` and whose server-written `metadata.ailk_terms` names
|
|
109
|
+
* `account_terms` at its current version (a Payment Link, invoice or older
|
|
110
|
+
* session carries none), for the Person `checkoutBuyerPerson` resolves.
|
|
111
|
+
* Logs `{ event: "checkout-consent", outcome, eventIdPrefix }` only; never
|
|
112
|
+
* fails the 200.
|
|
113
|
+
*
|
|
99
114
|
* Dunning (invoice.payment_failed / customer.subscription.updated, #6034):
|
|
100
115
|
* Records a per-subscriber lifecycle state in a `UserData` row behind a
|
|
101
116
|
* reserved-key guard (routes/auth/user.ts). `invoice.payment_failed` arms
|
|
@@ -108,6 +123,11 @@
|
|
|
108
123
|
import { track } from "@working-theory/analytics/server";
|
|
109
124
|
import type { Prisma } from "@working-theory/database";
|
|
110
125
|
import { isId } from "@working-theory/ids";
|
|
126
|
+
import {
|
|
127
|
+
ACCOUNT_TERMS_PURPOSE_ID,
|
|
128
|
+
consentPurposeById,
|
|
129
|
+
SITE_CONSENT_CONFIG_DEFAULTS,
|
|
130
|
+
} from "@working-theory/validation";
|
|
111
131
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
112
132
|
|
|
113
133
|
import { config } from "../../config/index.js";
|
|
@@ -133,6 +153,8 @@ import {
|
|
|
133
153
|
} from "../../lib/stripe.js";
|
|
134
154
|
import { hasSeen, markSeen } from "../../lib/webhook-idempotency.js";
|
|
135
155
|
import { recordWorkspaceAuditAction } from "../../services/audit-log.js";
|
|
156
|
+
import { checkoutBuyerPerson } from "../../services/checkout-buyer.js";
|
|
157
|
+
import { recordCheckoutConsent } from "../../services/consent.js";
|
|
136
158
|
import { isUniqueViolation } from "../../services/default-tenant.js";
|
|
137
159
|
import {
|
|
138
160
|
DUNNING_MAX_SUBSCRIPTIONS,
|
|
@@ -142,8 +164,8 @@ import {
|
|
|
142
164
|
type DunningRecord,
|
|
143
165
|
} from "../../services/dunning-policy.js";
|
|
144
166
|
import {
|
|
145
|
-
|
|
146
|
-
type
|
|
167
|
+
resolveCheckoutPerson,
|
|
168
|
+
type CheckoutPersonResult,
|
|
147
169
|
} from "../../services/person.js";
|
|
148
170
|
|
|
149
171
|
const TELEMETRY_TIMEOUT_MS = 2000;
|
|
@@ -314,6 +336,23 @@ export const stripeWebhookHandler = async (
|
|
|
314
336
|
return;
|
|
315
337
|
}
|
|
316
338
|
|
|
339
|
+
// #8932 D9 (contract W8) — an Offerings-stamped event belongs to the
|
|
340
|
+
// per-Organization endpoint, which must still be able to act on it, so it
|
|
341
|
+
// is acked here BEFORE `markSeen` and nothing is written. Presence decides,
|
|
342
|
+
// whatever the value; any other event takes the path below unchanged.
|
|
343
|
+
if (
|
|
344
|
+
offeringsStampSourcesOf(event).some((source) =>
|
|
345
|
+
Object.hasOwn(source, OFFERINGS_STAMP_KEY),
|
|
346
|
+
)
|
|
347
|
+
) {
|
|
348
|
+
request.log.info(
|
|
349
|
+
{ eventType: event.type, eventIdPrefix, outcome: "offerings_path" },
|
|
350
|
+
"stripe webhook event left to the per-Organization endpoint",
|
|
351
|
+
);
|
|
352
|
+
await reply.code(200).send({ received: true });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
317
356
|
request.log.info(
|
|
318
357
|
{ eventType: event.type, eventIdPrefix },
|
|
319
358
|
"stripe webhook event received",
|
|
@@ -353,15 +392,16 @@ export const stripeWebhookHandler = async (
|
|
|
353
392
|
email?: string | null;
|
|
354
393
|
metadata?: Record<string, string> | null;
|
|
355
394
|
};
|
|
356
|
-
// Billing-entity routing (spec D2):
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
// User path (single-tenant, degrade-clean).
|
|
395
|
+
// Billing-entity routing (spec D2): metadata.organizationId present →
|
|
396
|
+
// the Organization path, which a customer's own metadata can never
|
|
397
|
+
// prove (#9067 D13: only a recorded plan session binds), so it writes
|
|
398
|
+
// nothing; absent → the User path (single-tenant, degrade-clean).
|
|
360
399
|
const organizationId = customer.metadata?.["organizationId"] ?? null;
|
|
361
400
|
if (organizationId) {
|
|
362
401
|
await upsertOrgBillingIdentity(request, {
|
|
363
402
|
organizationId,
|
|
364
403
|
customerId: customer.id,
|
|
404
|
+
sessionId: null,
|
|
365
405
|
eventIdPrefix,
|
|
366
406
|
});
|
|
367
407
|
} else {
|
|
@@ -404,6 +444,7 @@ export const stripeWebhookHandler = async (
|
|
|
404
444
|
? upsertOrgBillingIdentity(request, {
|
|
405
445
|
organizationId,
|
|
406
446
|
customerId: session.customer,
|
|
447
|
+
sessionId: session.id ?? null,
|
|
407
448
|
eventIdPrefix,
|
|
408
449
|
})
|
|
409
450
|
: upsertBillingIdentity(request, {
|
|
@@ -429,13 +470,24 @@ export const stripeWebhookHandler = async (
|
|
|
429
470
|
session,
|
|
430
471
|
eventIdPrefix,
|
|
431
472
|
outcome: "completed",
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
473
|
+
})
|
|
474
|
+
.then(() =>
|
|
475
|
+
recordFlowCheckoutPerson(request, {
|
|
476
|
+
session,
|
|
477
|
+
livemode: event.livemode === true,
|
|
478
|
+
eventIdPrefix,
|
|
479
|
+
}),
|
|
480
|
+
)
|
|
481
|
+
// #8765 D14 — after the Person step, whose Person is a flow
|
|
482
|
+
// checkout's grant subject.
|
|
483
|
+
.then((flowPerson) =>
|
|
484
|
+
recordCheckoutTermsGrant(request, {
|
|
485
|
+
session,
|
|
486
|
+
livemode: event.livemode === true,
|
|
487
|
+
eventIdPrefix,
|
|
488
|
+
flowPerson,
|
|
489
|
+
}),
|
|
490
|
+
),
|
|
439
491
|
stampRequestPaid(request, { session, eventIdPrefix }),
|
|
440
492
|
stampProposalPaid(request, { session, eventId: event.id, eventIdPrefix }),
|
|
441
493
|
]);
|
|
@@ -531,6 +583,8 @@ type CheckoutSessionObject = {
|
|
|
531
583
|
currency?: string | null;
|
|
532
584
|
customer?: string | null;
|
|
533
585
|
customer_details?: { email?: string | null } | null;
|
|
586
|
+
/** The terms box's answer (#8765 D14): `accepted` when the buyer ticked it. */
|
|
587
|
+
consent?: { terms_of_service?: string | null } | null;
|
|
534
588
|
metadata?: Record<string, string> | null;
|
|
535
589
|
};
|
|
536
590
|
|
|
@@ -616,6 +670,28 @@ function siteStampSourcesOf(event: {
|
|
|
616
670
|
.filter((source) => source !== null) as Record<string, string>[];
|
|
617
671
|
}
|
|
618
672
|
|
|
673
|
+
/** The metadata key the hub's Offerings checkout stamps on its sessions (contract A6, #8932 D9). */
|
|
674
|
+
const OFFERINGS_STAMP_KEY = "ailk_offering";
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Where an Offerings stamp may sit (#8932 D9): every site-stamp source
|
|
678
|
+
* (`siteStampSourcesOf` — the Subscription, the Customer and an invoice's
|
|
679
|
+
* three) plus, for a `checkout.session.*` event whose object is a session,
|
|
680
|
+
* the session's own metadata.
|
|
681
|
+
*/
|
|
682
|
+
function offeringsStampSourcesOf(event: {
|
|
683
|
+
type: string;
|
|
684
|
+
data?: unknown;
|
|
685
|
+
}): Record<string, string>[] {
|
|
686
|
+
const sources = siteStampSourcesOf(event);
|
|
687
|
+
const object = objectOrNull(objectOrNull(event.data)?.["object"]);
|
|
688
|
+
if (event.type.startsWith("checkout.session.") && object?.["object"] === "checkout.session") {
|
|
689
|
+
const metadata = objectOrNull(object["metadata"]);
|
|
690
|
+
if (metadata !== null) sources.push(metadata as Record<string, string>);
|
|
691
|
+
}
|
|
692
|
+
return sources;
|
|
693
|
+
}
|
|
694
|
+
|
|
619
695
|
/**
|
|
620
696
|
* Minimal projected shape of a Stripe Invoice object for the dunning arms
|
|
621
697
|
* (#6034 FR-1). `subscription` and `customer` are EXPANDABLE Stripe fields —
|
|
@@ -750,7 +826,7 @@ async function stampFlowCheckout(
|
|
|
750
826
|
/**
|
|
751
827
|
* The hub checkout's Person (#852 D6): after `stampFlowCheckout`, find or
|
|
752
828
|
* create the Person of the completed checkout's lead
|
|
753
|
-
* (`services/person.ts`'s `
|
|
829
|
+
* (`services/person.ts`'s `resolveCheckoutPerson`). Runs only for a paid (or
|
|
754
830
|
* nothing-due) session of THIS site — the foreign-site filter has already
|
|
755
831
|
* returned — that carries `metadata.flowCheckoutId`. The customer id's ONE
|
|
756
832
|
* source is `session.customer`, written onto the Person only for a
|
|
@@ -758,7 +834,8 @@ async function stampFlowCheckout(
|
|
|
758
834
|
* delivery whose Person step failed is repaired by the next one.
|
|
759
835
|
*
|
|
760
836
|
* Logs `{ eventIdPrefix, flowCheckoutIdPrefix, outcome }` only — never the
|
|
761
|
-
* email or the customer id.
|
|
837
|
+
* email or the customer id. Answers the Person, or `null` when the step did
|
|
838
|
+
* not run or found none (#8765 D14). Never throws: the response stays 200.
|
|
762
839
|
*/
|
|
763
840
|
async function recordFlowCheckoutPerson(
|
|
764
841
|
request: FastifyRequest,
|
|
@@ -771,33 +848,137 @@ async function recordFlowCheckoutPerson(
|
|
|
771
848
|
livemode: boolean;
|
|
772
849
|
eventIdPrefix: string;
|
|
773
850
|
},
|
|
774
|
-
): Promise<
|
|
851
|
+
): Promise<CheckoutPersonResult["person"]> {
|
|
775
852
|
const flowCheckoutId = session.metadata?.["flowCheckoutId"];
|
|
776
|
-
if (!flowCheckoutId) return;
|
|
853
|
+
if (!flowCheckoutId) return null;
|
|
777
854
|
if (
|
|
778
855
|
session.payment_status !== "paid" &&
|
|
779
856
|
session.payment_status !== "no_payment_required"
|
|
780
857
|
) {
|
|
781
|
-
return;
|
|
858
|
+
return null;
|
|
782
859
|
}
|
|
783
860
|
const flowCheckoutIdPrefix = flowCheckoutId.substring(0, 8);
|
|
784
|
-
let
|
|
861
|
+
let result: CheckoutPersonResult;
|
|
785
862
|
try {
|
|
786
|
-
|
|
863
|
+
result = await resolveCheckoutPerson({
|
|
787
864
|
flowCheckoutId,
|
|
788
865
|
stripeSessionId: session.id,
|
|
789
866
|
customer: session.customer,
|
|
790
867
|
livemode,
|
|
791
868
|
});
|
|
792
869
|
} catch {
|
|
793
|
-
|
|
870
|
+
result = { outcome: "person_failed", person: null };
|
|
794
871
|
}
|
|
795
|
-
const line = { eventIdPrefix, flowCheckoutIdPrefix, outcome };
|
|
796
|
-
if (outcome === "person_failed") {
|
|
872
|
+
const line = { eventIdPrefix, flowCheckoutIdPrefix, outcome: result.outcome };
|
|
873
|
+
if (result.outcome === "person_failed") {
|
|
797
874
|
request.log.error(line, "flow checkout person failed — continuing");
|
|
798
875
|
} else {
|
|
799
876
|
request.log.info(line, "flow checkout person");
|
|
800
877
|
}
|
|
878
|
+
return result.person;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/** The metadata key the Checkout creators stamp with the terms the box showed (#8765 D12). */
|
|
882
|
+
const TERMS_STAMP_KEY = "ailk_terms";
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* `<purposeId>@<policyVersion>` of the account terms as the registry reads
|
|
886
|
+
* them now — the only stamp a grant is recorded for (SEC-8765-E) — and that
|
|
887
|
+
* version, which `recordCheckoutConsent` checks against the site's registry.
|
|
888
|
+
* Derived, never written down, so a version bump retires every older stamp.
|
|
889
|
+
*/
|
|
890
|
+
function currentTermsStamp(): { stamp: string; policyVersion: number } | null {
|
|
891
|
+
const purpose = consentPurposeById(
|
|
892
|
+
SITE_CONSENT_CONFIG_DEFAULTS,
|
|
893
|
+
ACCOUNT_TERMS_PURPOSE_ID,
|
|
894
|
+
);
|
|
895
|
+
return purpose
|
|
896
|
+
? {
|
|
897
|
+
stamp: `${purpose.id}@${purpose.policyVersion}`,
|
|
898
|
+
policyVersion: purpose.policyVersion,
|
|
899
|
+
}
|
|
900
|
+
: null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* The buyer's terms grant (#8765 D14). Writes nothing — and logs why — for a
|
|
905
|
+
* session whose box was not accepted (`consent_absent`: Stripe enforced the
|
|
906
|
+
* box, so the order completes regardless), one that does not carry the
|
|
907
|
+
* current `ailk_terms` stamp (`terms_unstamped`: a Payment Link, an invoice,
|
|
908
|
+
* an older session or other terms), or one whose buyer has no Person
|
|
909
|
+
* (`person_unresolved`). Otherwise `recordCheckoutConsent` writes at most one
|
|
910
|
+
* row per Person and session (`recorded` / `unchanged`), or none when the
|
|
911
|
+
* site's registry has moved past the stamped version (`version_changed`).
|
|
912
|
+
*
|
|
913
|
+
* Logs `{ event, outcome, eventIdPrefix }` only (plus `errName` on `failed`)
|
|
914
|
+
* — never an email or a Person id. Never throws: the response stays 200.
|
|
915
|
+
*/
|
|
916
|
+
async function recordCheckoutTermsGrant(
|
|
917
|
+
request: FastifyRequest,
|
|
918
|
+
{
|
|
919
|
+
session,
|
|
920
|
+
livemode,
|
|
921
|
+
eventIdPrefix,
|
|
922
|
+
flowPerson,
|
|
923
|
+
}: {
|
|
924
|
+
session: CheckoutSessionObject;
|
|
925
|
+
livemode: boolean;
|
|
926
|
+
eventIdPrefix: string;
|
|
927
|
+
/** A flow checkout's Person, from `recordFlowCheckoutPerson`. */
|
|
928
|
+
flowPerson: CheckoutPersonResult["person"];
|
|
929
|
+
},
|
|
930
|
+
): Promise<void> {
|
|
931
|
+
const line = (outcome: string) => ({
|
|
932
|
+
event: "checkout-consent",
|
|
933
|
+
outcome,
|
|
934
|
+
eventIdPrefix,
|
|
935
|
+
});
|
|
936
|
+
try {
|
|
937
|
+
if (session.consent?.terms_of_service !== "accepted") {
|
|
938
|
+
request.log.info(
|
|
939
|
+
line("consent_absent"),
|
|
940
|
+
"checkout terms grant: box not accepted",
|
|
941
|
+
);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const stamp: unknown = session.metadata?.[TERMS_STAMP_KEY];
|
|
945
|
+
const expected = currentTermsStamp();
|
|
946
|
+
if (expected === null || stamp !== expected.stamp) {
|
|
947
|
+
request.log.info(
|
|
948
|
+
line("terms_unstamped"),
|
|
949
|
+
"checkout terms grant: session carries no current terms stamp",
|
|
950
|
+
);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const sessionId = session.id;
|
|
954
|
+
const buyer =
|
|
955
|
+
typeof sessionId === "string" && sessionId !== ""
|
|
956
|
+
? await checkoutBuyerPerson(session, livemode, flowPerson)
|
|
957
|
+
: null;
|
|
958
|
+
if (buyer === null || typeof sessionId !== "string") {
|
|
959
|
+
request.log.info(
|
|
960
|
+
line("person_unresolved"),
|
|
961
|
+
"checkout terms grant: no Person for this buyer",
|
|
962
|
+
);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const outcome = await recordCheckoutConsent({
|
|
966
|
+
workspaceId: buyer.workspaceId,
|
|
967
|
+
personId: buyer.personId,
|
|
968
|
+
sessionId,
|
|
969
|
+
policyVersion: expected.policyVersion,
|
|
970
|
+
});
|
|
971
|
+
request.log.info(line(outcome), "checkout terms grant");
|
|
972
|
+
} catch (err) {
|
|
973
|
+
// The error's class only (a Prisma error's message can carry values).
|
|
974
|
+
request.log.error(
|
|
975
|
+
{
|
|
976
|
+
...line("failed"),
|
|
977
|
+
errName: err instanceof Error ? err.name : "UnknownError",
|
|
978
|
+
},
|
|
979
|
+
"checkout terms grant failed — continuing",
|
|
980
|
+
);
|
|
981
|
+
}
|
|
801
982
|
}
|
|
802
983
|
|
|
803
984
|
/**
|
|
@@ -1196,14 +1377,19 @@ async function upsertBillingIdentity(
|
|
|
1196
1377
|
|
|
1197
1378
|
/**
|
|
1198
1379
|
* Link Organization.billingProvider + billingCustomerId for a Stripe customer
|
|
1199
|
-
* (org-scoped billing, #3601, spec D2/D3)
|
|
1200
|
-
* metadata.organizationId
|
|
1201
|
-
*
|
|
1380
|
+
* (org-scoped billing, #3601, spec D2/D3), only on proof (#9067 D13,
|
|
1381
|
+
* SEC-9067-1 to -3). `metadata.organizationId` names the target; the proof
|
|
1382
|
+
* is the session id POST /v1/billing/plan-checkout recorded on that
|
|
1383
|
+
* Organization. The bind is one guarded `updateMany` on `{ id,
|
|
1384
|
+
* planCheckoutSessionId: sessionId, billingCustomerId: null }`, which also
|
|
1385
|
+
* clears the recorded session — so a session no route recorded, a session
|
|
1386
|
+
* recorded by another deployment's database, a re-point of a bound customer
|
|
1387
|
+
* and a `customer.created` (`sessionId` null: a customer's metadata proves
|
|
1388
|
+
* nothing) all bind nothing and log `org_bind_unproven`.
|
|
1202
1389
|
*
|
|
1203
1390
|
* Idempotent: a typed-composite match (billingProvider="stripe",
|
|
1204
1391
|
* billingCustomerId=customerId) already pointing at THIS org is a no-op on
|
|
1205
|
-
* replay.
|
|
1206
|
-
* linked.
|
|
1392
|
+
* replay.
|
|
1207
1393
|
*
|
|
1208
1394
|
* DB failures are caught and logged (never re-thrown) — a broken link must not
|
|
1209
1395
|
* make Stripe retry (which would re-fire analytics + billing on eventual
|
|
@@ -1214,9 +1400,24 @@ async function upsertOrgBillingIdentity(
|
|
|
1214
1400
|
{
|
|
1215
1401
|
organizationId,
|
|
1216
1402
|
customerId,
|
|
1403
|
+
sessionId,
|
|
1217
1404
|
eventIdPrefix,
|
|
1218
|
-
}: {
|
|
1405
|
+
}: {
|
|
1406
|
+
organizationId: string;
|
|
1407
|
+
customerId: string;
|
|
1408
|
+
sessionId: string | null;
|
|
1409
|
+
eventIdPrefix: string;
|
|
1410
|
+
},
|
|
1219
1411
|
): Promise<void> {
|
|
1412
|
+
const unproven = () =>
|
|
1413
|
+
request.log.warn(
|
|
1414
|
+
{ eventIdPrefix, outcome: "org_bind_unproven" },
|
|
1415
|
+
"org billing identity not linked — no recorded plan checkout proves it",
|
|
1416
|
+
);
|
|
1417
|
+
if (sessionId === null) {
|
|
1418
|
+
unproven();
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1220
1421
|
try {
|
|
1221
1422
|
const existingByBilling = await prisma.organization.findFirst({
|
|
1222
1423
|
where: {
|
|
@@ -1233,22 +1434,30 @@ async function upsertOrgBillingIdentity(
|
|
|
1233
1434
|
return;
|
|
1234
1435
|
}
|
|
1235
1436
|
|
|
1236
|
-
await prisma.organization.
|
|
1237
|
-
where: { id: organizationId },
|
|
1437
|
+
const { count } = await prisma.organization.updateMany({
|
|
1438
|
+
where: { id: organizationId, planCheckoutSessionId: sessionId, billingCustomerId: null },
|
|
1238
1439
|
data: {
|
|
1239
1440
|
billingProvider: "stripe",
|
|
1240
1441
|
billingCustomerId: customerId,
|
|
1442
|
+
planCheckoutSessionId: null,
|
|
1241
1443
|
},
|
|
1242
1444
|
});
|
|
1445
|
+
if (count !== 1) {
|
|
1446
|
+
unproven();
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1243
1449
|
request.log.info(
|
|
1244
1450
|
{ eventIdPrefix },
|
|
1245
|
-
"org billing identity linked from checkout
|
|
1451
|
+
"org billing identity linked from the recorded plan checkout",
|
|
1246
1452
|
);
|
|
1247
1453
|
} catch (err) {
|
|
1454
|
+
// #9067 FR-6 — the name and Prisma code only: some Prisma messages
|
|
1455
|
+
// print the call's arguments, which here carry the session and customer ids.
|
|
1248
1456
|
request.log.error(
|
|
1249
1457
|
{
|
|
1250
1458
|
eventIdPrefix,
|
|
1251
|
-
|
|
1459
|
+
errName: err instanceof Error ? err.name : "unknown",
|
|
1460
|
+
code: (err as { code?: unknown } | null)?.code,
|
|
1252
1461
|
},
|
|
1253
1462
|
"org billing identity upsert failed — continuing",
|
|
1254
1463
|
);
|