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
|
@@ -20,22 +20,6 @@ sections:
|
|
|
20
20
|
"primaryCta": { "label": "{Get in touch}", "href": "/contact" },
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
-
{
|
|
24
|
-
"id": "add-scheduling",
|
|
25
|
-
"type": "cta",
|
|
26
|
-
"data":
|
|
27
|
-
{
|
|
28
|
-
"variant": "box",
|
|
29
|
-
"headline": "{What booking an onsite service call would do for your visitors}",
|
|
30
|
-
"description": "{One or two sentences on what live scheduling adds once it's on}",
|
|
31
|
-
"primaryCta":
|
|
32
|
-
{
|
|
33
|
-
"label": "Add scheduling with Pro",
|
|
34
|
-
"href": "https://ailaunchkit.ai/pricing",
|
|
35
|
-
"external": true,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
23
|
{
|
|
40
24
|
"id": "back-to-meet",
|
|
41
25
|
"type": "content-block",
|
|
@@ -53,4 +37,4 @@ sections:
|
|
|
53
37
|
]
|
|
54
38
|
---
|
|
55
39
|
|
|
56
|
-
{/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A
|
|
40
|
+
{/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A site that carries the scheduling module renders the hub's meeting page instead, reading this seed only for its URL, its JSON-LD and its fallback title (#8843 C5). Placeholder copy — replace every {brace} with your own words before you launch. */}
|
|
@@ -20,22 +20,6 @@ sections:
|
|
|
20
20
|
"primaryCta": { "label": "{Get in touch}", "href": "/contact" },
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
-
{
|
|
24
|
-
"id": "add-scheduling",
|
|
25
|
-
"type": "cta",
|
|
26
|
-
"data":
|
|
27
|
-
{
|
|
28
|
-
"variant": "box",
|
|
29
|
-
"headline": "{What booking a project kickoff would do for your visitors}",
|
|
30
|
-
"description": "{One or two sentences on what live scheduling adds once it's on}",
|
|
31
|
-
"primaryCta":
|
|
32
|
-
{
|
|
33
|
-
"label": "Add scheduling with Pro",
|
|
34
|
-
"href": "https://ailaunchkit.ai/pricing",
|
|
35
|
-
"external": true,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
23
|
{
|
|
40
24
|
"id": "back-to-meet",
|
|
41
25
|
"type": "content-block",
|
|
@@ -53,4 +37,4 @@ sections:
|
|
|
53
37
|
]
|
|
54
38
|
---
|
|
55
39
|
|
|
56
|
-
{/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A
|
|
40
|
+
{/* Rendered from frontmatter `sections[]` by the /meet/[slug] placeholder route. A site that carries the scheduling module renders the hub's meeting page instead, reading this seed only for its URL, its JSON-LD and its fallback title (#8843 C5). Placeholder copy — replace every {brace} with your own words before you launch. */}
|
|
@@ -1,73 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: "ContactPage"
|
|
3
3
|
slug: "contact"
|
|
4
|
-
title: "
|
|
5
|
-
description: "
|
|
4
|
+
title: "Get in touch"
|
|
5
|
+
description: "{One sentence on who should get in touch and how soon you reply}"
|
|
6
6
|
sections:
|
|
7
7
|
[
|
|
8
8
|
{
|
|
9
|
-
"id": "
|
|
10
|
-
"type": "
|
|
11
|
-
"data":
|
|
12
|
-
{
|
|
13
|
-
"variant": "split",
|
|
14
|
-
"eyebrow": "Contact",
|
|
15
|
-
"headline": "Get every message in your email, free",
|
|
16
|
-
"subheadline": "Or upgrade to Pro to save every message as a lead in your own marketing database.",
|
|
17
|
-
"checklist":
|
|
18
|
-
[
|
|
19
|
-
"Every message arrives in your email",
|
|
20
|
-
"No database or setup to start",
|
|
21
|
-
"Upgrade any time to keep a lead inbox",
|
|
22
|
-
],
|
|
23
|
-
"secondaryCta":
|
|
24
|
-
{
|
|
25
|
-
"label": "Save my leads",
|
|
26
|
-
"href": "https://ailaunchkit.ai/pricing",
|
|
27
|
-
"variant": "outline",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"id": "meeting-types",
|
|
33
|
-
"type": "items",
|
|
34
|
-
"data":
|
|
35
|
-
{
|
|
36
|
-
"headline": "{Add appointment scheduling to your site}",
|
|
37
|
-
"variant": "tiles",
|
|
38
|
-
"columns": 2,
|
|
39
|
-
"items":
|
|
40
|
-
[
|
|
41
|
-
{
|
|
42
|
-
"title": "Book a Call",
|
|
43
|
-
"description": "{A quick intro call.}",
|
|
44
|
-
"link": { "label": "Book a Call", "href": "/meet/book-a-call" },
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"title": "Onsite Estimate",
|
|
48
|
-
"description": "{We come to you and quote the work.}",
|
|
49
|
-
"link":
|
|
50
|
-
{
|
|
51
|
-
"label": "Onsite Estimate",
|
|
52
|
-
"href": "/meet/onsite-estimate",
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"title": "Start Project",
|
|
57
|
-
"description": "{Kick off a project you're ready to start.}",
|
|
58
|
-
"link":
|
|
59
|
-
{ "label": "Start Project", "href": "/meet/start-project" },
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"title": "Onsite Service",
|
|
63
|
-
"description": "{Book a service visit at your location.}",
|
|
64
|
-
"link":
|
|
65
|
-
{ "label": "Onsite Service", "href": "/meet/onsite-service" },
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
},
|
|
9
|
+
"id": "contact-form",
|
|
10
|
+
"type": "contact-form",
|
|
11
|
+
"data": { "variant": "column", "fields": [] },
|
|
69
12
|
},
|
|
70
13
|
]
|
|
71
14
|
---
|
|
72
15
|
|
|
73
|
-
{/* Rendered from frontmatter `sections[]`. The page's one contact surface
|
|
16
|
+
{/* Rendered from frontmatter `sections[]`. The page's one contact surface is the workspace's Contact form magnet: its heading, sentence, fields, consent, button and ending all come from the magnet, mounted by the /contact route in this section's slot. To change the form, change the magnet in Admin → Magnets. The `description` is the page's meta description — replace it before you launch. */}
|
|
@@ -39,27 +39,9 @@ sections:
|
|
|
39
39
|
"headlineStyle": "label",
|
|
40
40
|
"logos":
|
|
41
41
|
[
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"darkSrc": "/logos/salessmyth-light.svg",
|
|
46
|
-
"width": 1118,
|
|
47
|
-
"height": 147,
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"name": "AI Launch Kit",
|
|
51
|
-
"src": "/logos/ai-launch-kit.svg",
|
|
52
|
-
"darkSrc": "/logos/ai-launch-kit-light.svg",
|
|
53
|
-
"width": 1248,
|
|
54
|
-
"height": 144,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"name": "Atomic Agent",
|
|
58
|
-
"src": "/logos/atomic-agent.svg",
|
|
59
|
-
"darkSrc": "/logos/atomic-agent-light.svg",
|
|
60
|
-
"width": 1320,
|
|
61
|
-
"height": 148,
|
|
62
|
-
},
|
|
42
|
+
{ "name": "Placeholder logo", "src": "/logomark.svg" },
|
|
43
|
+
{ "name": "Placeholder logo", "src": "/logomark.svg" },
|
|
44
|
+
{ "name": "Placeholder logo", "src": "/logomark.svg" },
|
|
63
45
|
],
|
|
64
46
|
"layout": "inline",
|
|
65
47
|
"treatment": "monochrome",
|
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# @working-theory/database
|
|
2
2
|
|
|
3
|
+
## 0.44.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#8955](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8955) [`1a84162`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/1a841626a9e5664d926a2f186425f35e8757a6b4) Thanks [@samuelhenry](https://github.com/samuelhenry)! - The Offerings data model and its typed read services ([#8876](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8876)).
|
|
8
|
+
|
|
9
|
+
- **`@working-theory/database`** — three config tables, in migration `20261020120000_offerings_data_model`: `StripeConnection` (an Organization's own Stripe account by a restricted key — mode, last four, the key and webhook secret as ciphertext the database refuses in clear, a webhook token), `OrganizationOfferings` (the Products an Organization has switched on) and `WorkspaceOfferings` (what a workspace shows, its Recommended plan and add-on, and the last-good Stripe snapshot). Deploy the migration before the API. No existing table is touched.
|
|
10
|
+
- **`@working-theory/ids`** — registers the `stripecon`, `orgoffer` and `wsoffer` prefixes for the new models.
|
|
11
|
+
- **`@working-theory/validation`** — the Offerings data contract: `parseLookupKey` (the one lookup-key parser, `<plan>_<monthly|yearly|once>`), `periodOfStripePrice`, the text bounds, and the typed shapes `offeringsProductSchema`, `offeringsSnapshotSchema`, `orgEnabledProductsSchema`, `workspaceShownSetSchema` and `stripeConnectionSummarySchema`.
|
|
12
|
+
- **`create-ailk`** — a scaffold's `apps/api` carries the Offerings read services (`getOrgEnabledProducts`, `getWorkspaceShownSet`, `getSellableProductIds`, the bounded snapshot refresh and the connection summary). No route reads them yet, so a site's behaviour is unchanged.
|
|
13
|
+
|
|
14
|
+
- [#8968](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8968) [`8da1163`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/8da1163fd04f083fd8d408a4a7d762b756b23274) Thanks [@samuelhenry](https://github.com/samuelhenry)! - The Offerings Checkout session record ([#8930](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8930)).
|
|
15
|
+
|
|
16
|
+
- **`@working-theory/database`** — one config table, in migration `20261021120000_offerings_checkout_session`: `OfferingsCheckoutSession`, one row per Checkout Session the hub created (its id, workspace, Organization, Price, lookup key and mode), with a CHECK on every column but `created_at`. Deploy the migration before the API. No existing table is touched.
|
|
17
|
+
- **`@working-theory/ids`** — registers the `ocsess` prefix for the new model.
|
|
18
|
+
- **`create-ailk`** — a scaffold's `apps/api` carries the record's one store (`recordCheckoutSession`, `readCheckoutSessionRecord`). No route calls it yet, so a site's behaviour is unchanged.
|
|
19
|
+
|
|
20
|
+
- [#9091](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/9091) [`23cf71c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/23cf71c316c9e44c6109c76439cb64281e6368d5) Thanks [@samuelhenry](https://github.com/samuelhenry)! - A workspace's Offerings shown set is chosen per Stripe Price, and the Admin's Offerings page reads like Magnets ([#9070](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/9070)).
|
|
21
|
+
|
|
22
|
+
- **`@working-theory/database`** — migration `20261023120000_offerings_shown_price_ids` adds `workspace_offerings.shown_price_ids TEXT[] NOT NULL DEFAULT '{}'` and two CHECKs (at most 300 non-null ids; Price ids only beside a shown Product). No row is rewritten: an empty array means every Price of each shown Product, so every workspace saved before this release shows exactly what it showed before. **Deploy the migration before the API, and the API before the Admin.** While the API and the Admin are on different versions the Offerings page cannot save (nothing is written and nothing new is sold).
|
|
23
|
+
- **`@working-theory/validation`** — new exports `OFFERINGS_PRICE_IDS_MAX`, `offeringsPriceIdsSchema`, `workspaceOfferingsShownEntrySchema` and `workspaceOfferingsShownSchema`. Changed shapes: `workspaceOfferingsSelectionInputSchema` is now one Price's switch, `{ productId, period, shown }`, so no body names a Recommended or a whole set; `workspaceOfferingsSelectionSchema` gains `shownPriceIds`, empty exactly when `shownProductIds` is; `workspaceOfferingsViewSchema`'s available branch answers `shown` (`{ productId, periods }` per Product) in place of `shownProductIds`.
|
|
24
|
+
- **`create-ailk`** — a scaffold's `PUT /v1/workspaces/:workspaceId/offerings` takes one Price per save and keeps or clears a stored Recommended but never sets one (`recommended_not_shown` is retired; a period the Product has no Price for is `400 price_not_offered`). Its audit row's `shown`, `added` and `removed` count Prices, not Products, from this release. `/pricing`, the agent pricing tools, the plan picker and checkout show and sell only the shown Prices, so a seller can show a yearly Price without its monthly sibling; a Price replaced in Stripe stays off until it is switched on. The Admin's Offerings page is a Title / Tags / Live table, one row per Price, with a New recommender button and no Recommended control.
|
|
25
|
+
|
|
26
|
+
**Rollback.** Roll the API back, then run `UPDATE workspace_offerings SET shown_price_ids = '{}'` so every row reads at the Product grain again and the old route's saves succeed. Tell each owner who saved per Price that every Price of their shown Products — on `/pricing` and at checkout — is on sale again. Never drop the column while a per-Price row exists without that notice.
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [[`6ad87d6`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/6ad87d65d91013ac9c0945a62efbf28c627b1fce), [`d9c2cd3`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d9c2cd3c601d9a267d4d29cb9bcef77ec71cd518), [`407810f`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/407810fbd63a02d14773aef36a10c1f9f04dd5e3), [`5875c18`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/5875c189f72f66ea2af7b7c79a47baba53a329d1), [`13c0b3d`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/13c0b3dd64a105292962821f5b0050c5cabc2d45), [`82bc2f1`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/82bc2f1ecab1a12f23a45e326355841ba87d9f04), [`ea18e21`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/ea18e2181241ba9357b4dcfa3a1d2f4c3ffe4518), [`8754f86`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/8754f866b49003965c4be28cbe19b1f040aec4af), [`1a84162`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/1a841626a9e5664d926a2f186425f35e8757a6b4), [`74cb7b3`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/74cb7b37adea5d27adc1f3ec3ea58b9941167c4d), [`25b4b61`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/25b4b6199d8632167bd5501a4e12d4774eed432a), [`8da1163`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/8da1163fd04f083fd8d408a4a7d762b756b23274), [`bd1c9b5`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/bd1c9b576ac337596e585145f09b08696e9f412e), [`d1dacc5`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d1dacc5d76a9cc91f6ad3b1883067e327b5e3d3c), [`fff5487`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/fff5487a6d14a3010bf22cb99e7554e6542449a0), [`23cf71c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/23cf71c316c9e44c6109c76439cb64281e6368d5), [`75ecc58`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/75ecc584f43793bb5234f69e0f80b15327a99236), [`85ae357`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/85ae357410ebd6c11b08b04355a9ede1d2088d2d), [`059c892`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/059c89249f7db3dc106f133ef37180e9eb1e318a), [`c563360`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/c563360db386758aa2d25309ab693a18a41e4139)]:
|
|
31
|
+
- @working-theory/validation@0.44.0
|
|
32
|
+
- @working-theory/ids@0.15.0
|
|
33
|
+
|
|
34
|
+
## 0.43.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- [#8775](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8775) [`ee8e713`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/ee8e713b8194e8d65052f4b1dca928ca0c970318) Thanks [@claudio-planck](https://github.com/claudio-planck)! - A per-magnet completion setting — lead only, invite to account, or continue to checkout ([#8757](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8757), Phase 1).
|
|
39
|
+
|
|
40
|
+
- **`@working-theory/database`** — `flow_definitions` gains `completion` (`lead` | `invite` | `checkout`, default `lead`) and `checkout_offering`, closed by three CHECKs. The migration backfills `onboarding`, `request-quote` and `job-application` to `invite` by slug; `contact` and every other magnet stay `lead`. Deploy the migration before the API.
|
|
41
|
+
- **`@working-theory/validation`** — `flow-completion.ts`: `FLOW_COMPLETIONS`, `flowCompletionSettingSchema`, `flowCheckoutOfferingSchema`, `STANDARD_MAGNET_COMPLETIONS`, `leadSubmitNextSchema`, `accountEmailHintSchema`, `accountInvitePath`, the invite caps and the email defaults.
|
|
42
|
+
- **`@working-theory/api-client`** — `Magnet` carries `completion` and `checkoutOffering`, `MagnetUpdateInput` can set them, and `LeadWriteResponse` gains an optional `next`.
|
|
43
|
+
- **`@working-theory/ui`** — `AuthCard` takes an optional `defaultEmail`; `LeadCaptureFormState`'s success state takes an optional `next` link.
|
|
44
|
+
- **`@working-theory/analytics`** — `email` and `login_hint` are scrubbed from URLs like `session_id`.
|
|
45
|
+
- **`create-ailk`** — the API: the magnet PATCH writes the setting alone (`checkout` needs a person, the `commerce` claim and a published offering) and every full row serves it; a `flow:<slug>` submit to an `invite` magnet answers `next` and sends one capped confirmation email whose link starts sign-in with the address prefilled. The web: /contact's thank-you shows "Track this in your account", and sign-in opens with a hinted email on a site and through the hub. The Admin: one "When someone submits" control on the magnet page.
|
|
46
|
+
|
|
47
|
+
- [#8807](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8807) [`956bfe6`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/956bfe60fff4bd9a43135e6a2fb79e6f012e14f8) Thanks [@claudio-planck](https://github.com/claudio-planck)! - Every hosted Stripe Checkout session collects terms acceptance, and a completed checkout records a dated grant for the buyer ([#8765](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8765), PR B).
|
|
48
|
+
|
|
49
|
+
- **`@working-theory/stripe-client`** — both session creators set `consent_collection.terms_of_service: "required"` and a `custom_text.terms_of_service_acceptance` message: the site's account-terms sentence with its Terms, Privacy Policy and Working Agreement links resolved on the session's own return origin. Every session is stamped `metadata.ailk_terms` (`account_terms@2` by default). An optional `termsOfService` input overrides the sentence and its links. A Stripe refusal for a missing Terms URL is rethrown as `TermsOfServiceUrlMissingError`. **Precondition:** the seller's Stripe account needs a Terms of Service URL in its Dashboard's Public details, or Stripe refuses every session with the box.
|
|
50
|
+
- **`@working-theory/validation`** — `CONSENT_SOURCES` gains `checkout`.
|
|
51
|
+
- **`@working-theory/database`** — a migration widens the `consent_grants_source_known` CHECK to the eight sources. It writes no row.
|
|
52
|
+
- **`create-ailk`** — the API: the hub's `checkout.session.completed` webhook records one `checkout` grant per session for the buyer's Person when the session carries the terms stamp and Stripe recorded the consent as accepted. A checkout row is an unverified (any-source) row. The Admin labels the source "Checkout".
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- [#8767](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8767) [`5ff341d`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/5ff341d683d35a024fc1073c1b010aa56956a1d2) Thanks [@claudio-planck](https://github.com/claudio-planck)! - A JobPosting names where to apply, and a standard Job application magnet is seeded like the Contact form and drawn on every job page ([#8746](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8746)).
|
|
57
|
+
|
|
58
|
+
- **`@working-theory/schema`** — `ContentPageInput` gains an optional `origin` (the site's origin, passed by the caller; the package never reads the environment for it). When it is present, the JobPosting builder puts a `potentialAction` `ApplyAction` on the JobPosting whose `target` is the job page's own URL plus `#apply`: `<origin>/<locale>/careers/<slug>#apply`, the same form the page's BreadcrumbList uses. `buildJobPostingNode` takes an optional `applyUrl`. Every other builder ignores `origin`, so CareersList keeps its ItemList unchanged.
|
|
59
|
+
- **`@working-theory/page-renderer`** — `toContentPageInput` passes the render's origin (the one the BreadcrumbList is built from).
|
|
60
|
+
- **`@working-theory/validation`** — the magnet's one source: `JOB_APPLICATION_FLOW_SLUG` (`job-application`), `JOB_APPLICATION_FLOW_TITLE`, `JOB_APPLICATION_LANDING_PAGE` (`/careers`) and `jobApplicationFlowConfig` — one field step of First name, Last name and Email (required), Phone (`tel`) and LinkedIn URL (`url`). `RESERVED_FLOW_SLUGS` gains `job-application`, so no flow-engine write, spoke seed or new listing takes it.
|
|
61
|
+
- **`@working-theory/database`** — the database seed writes the default workspace's Job application magnet: `diagnostic`, `live`, landing page `/careers`, insert-only.
|
|
62
|
+
- **`create-ailk`** — the API seeds the magnet into every workspace insert-only (after `POST /v1/workspaces` and from the seed-presets CLI for existing workspaces), a magnet can be neither renamed from nor to `job-application`, and `GET /v1/content/:type/:slug` passes `_site.mdx`'s `baseUrl` as the origin, so the API's JobPosting JSON-LD carries the ApplyAction too. Each job page `/careers/<slug>` renders the magnet's field step under an `#apply` anchor, falling back to the same built-in form when the magnet is absent or off; a submission is a `flow:job-application` lead whose `name` is first + last and whose hidden `job` is the posting's slug, and the form thanks the applicant with the `careers` catalog's own `applicationSuccess` line. The `/careers` index carries no form.
|
|
63
|
+
|
|
64
|
+
- [#8788](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8788) [`3a89742`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/3a89742f7b816deca6cd604863f2fd041a79abfa) Thanks [@claudio-planck](https://github.com/claudio-planck)! - The standard magnets are seeded with their completion setting ([#8759](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8759)), so a workspace created after the [#8757](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8757) migration no longer depends on its backfill.
|
|
65
|
+
|
|
66
|
+
- **`@working-theory/database`** — the database seed writes `completion` on the default workspace's magnets: `invite` on `request-quote` and `job-application`, `lead` on `contact`. On a fresh install the migration's backfill runs before the seed and finds no rows.
|
|
67
|
+
- **`create-ailk`** — the API's insert-only seed writes `lead` on the Contact form and `invite` on the onboarding and Job application magnets, and `POST /v1/workspaces` creates the `request-quote` magnet at `invite`.
|
|
68
|
+
|
|
69
|
+
- Updated dependencies [[`5032b3e`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/5032b3eefb05bbe67ebddf1b4b07904717100472), [`5ff341d`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/5ff341d683d35a024fc1073c1b010aa56956a1d2), [`ee8e713`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/ee8e713b8194e8d65052f4b1dca928ca0c970318), [`c74bd72`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/c74bd7270fa2cb82bebedfb1b53f89ca6e09917f), [`956bfe6`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/956bfe60fff4bd9a43135e6a2fb79e6f012e14f8), [`97237f2`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/97237f278006a43e17d58709ed9ac3b4f62ae76f), [`1eb9688`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/1eb9688708dbc65cd4e2fdb6efeb40880e1606f0), [`b001413`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/b001413238973f4ecb3d4a5a18968cd7732210d8)]:
|
|
70
|
+
- @working-theory/validation@0.43.0
|
|
71
|
+
|
|
3
72
|
## 0.42.0
|
|
4
73
|
|
|
5
74
|
### Patch Changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file consent-source-checkout-migration.test.ts
|
|
3
|
+
*
|
|
4
|
+
* DB-free proof of the consent_source_checkout migration's shape (#8765, spec
|
|
5
|
+
* docs/specs/8765-sign-up-refuses-without-the-spec.md D13, AC-8). Reads the
|
|
6
|
+
* migration's own text: it drops `consent_grants_source_known` and re-adds it
|
|
7
|
+
* `NOT VALID` with exactly the values of `CONSENT_SOURCES`, then `VALIDATE`s
|
|
8
|
+
* it — with no `UPDATE`, `INSERT` or `DELETE`, so the append-only ledger is
|
|
9
|
+
* never written.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { CONSENT_SOURCES } from "@working-theory/validation";
|
|
15
|
+
|
|
16
|
+
const MIGRATION = join(
|
|
17
|
+
__dirname,
|
|
18
|
+
"..",
|
|
19
|
+
"migrations",
|
|
20
|
+
"20261019120000_consent_source_checkout",
|
|
21
|
+
"migration.sql",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/** The migration's statements, comments stripped, whitespace collapsed. */
|
|
25
|
+
function statements(): string[] {
|
|
26
|
+
const text = readFileSync(MIGRATION, "utf8")
|
|
27
|
+
.split("\n")
|
|
28
|
+
.filter((line) => !line.trimStart().startsWith("--"))
|
|
29
|
+
.join("\n");
|
|
30
|
+
return text
|
|
31
|
+
.split(";")
|
|
32
|
+
.map((s) => s.replace(/\s+/g, " ").trim())
|
|
33
|
+
.filter((s) => s.length > 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("the consent_source_checkout migration (#8765 D13, AC-8)", () => {
|
|
37
|
+
it("drops the source CHECK, re-adds it NOT VALID, then VALIDATEs it — nothing else", () => {
|
|
38
|
+
const shapes = statements().map((s) => {
|
|
39
|
+
if (
|
|
40
|
+
s ===
|
|
41
|
+
'ALTER TABLE "consent_grants" DROP CONSTRAINT "consent_grants_source_known"'
|
|
42
|
+
) {
|
|
43
|
+
return "drop";
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
/^ALTER TABLE "consent_grants" ADD CONSTRAINT "consent_grants_source_known" CHECK \(.*\) NOT VALID$/.test(
|
|
47
|
+
s,
|
|
48
|
+
)
|
|
49
|
+
) {
|
|
50
|
+
return "add";
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
s ===
|
|
54
|
+
'ALTER TABLE "consent_grants" VALIDATE CONSTRAINT "consent_grants_source_known"'
|
|
55
|
+
) {
|
|
56
|
+
return "validate";
|
|
57
|
+
}
|
|
58
|
+
return `unexpected: ${s}`;
|
|
59
|
+
});
|
|
60
|
+
expect(shapes).toEqual(["drop", "add", "validate"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("names exactly the eight values of CONSENT_SOURCES, checkout among them", () => {
|
|
64
|
+
const add = statements().find((s) => s.includes("ADD CONSTRAINT"));
|
|
65
|
+
const list = add?.match(/CHECK \("source" IN \(([^)]*)\)\)/)?.[1];
|
|
66
|
+
expect(list).toBeDefined();
|
|
67
|
+
const values = (list ?? "")
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((v) => v.trim().replace(/^'|'$/g, ""));
|
|
70
|
+
expect(values).toHaveLength(8);
|
|
71
|
+
expect([...values].sort()).toEqual([...CONSENT_SOURCES].sort());
|
|
72
|
+
expect(values).toContain("checkout");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("writes no row: no UPDATE, INSERT or DELETE", () => {
|
|
76
|
+
for (const s of statements())
|
|
77
|
+
expect(s).not.toMatch(/\b(UPDATE|INSERT|DELETE)\b/i);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file flow-completion-migration.test.ts
|
|
3
|
+
*
|
|
4
|
+
* DB-free proof of the flow_completion migration's shape (#8757, spec
|
|
5
|
+
* docs/specs/8757-a-per-magnet-completion-setting-spec.md D2, AC-2). Reads
|
|
6
|
+
* the migration's own text: it adds exactly the two columns and three named
|
|
7
|
+
* CHECKs, each added `NOT VALID` then `VALIDATE`d, and its one `UPDATE` is the
|
|
8
|
+
* slug backfill between its markers — naming exactly the standard magnets
|
|
9
|
+
* whose default is `invite` (`STANDARD_MAGNET_COMPLETIONS`). It inserts and
|
|
10
|
+
* deletes nothing. The live-database half is `flow-completion.db.test.ts`.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { STANDARD_MAGNET_COMPLETIONS } from "@working-theory/validation";
|
|
16
|
+
|
|
17
|
+
const MIGRATION = join(
|
|
18
|
+
__dirname,
|
|
19
|
+
"..",
|
|
20
|
+
"migrations",
|
|
21
|
+
"20261018120000_flow_completion",
|
|
22
|
+
"migration.sql",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const CHECKS = [
|
|
26
|
+
"flow_definitions_completion_domain",
|
|
27
|
+
"flow_definitions_checkout_offering_pair",
|
|
28
|
+
"flow_definitions_checkout_offering_format",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** The migration's statements, comments stripped, whitespace collapsed. */
|
|
32
|
+
function statements(): string[] {
|
|
33
|
+
const text = readFileSync(MIGRATION, "utf8")
|
|
34
|
+
.split("\n")
|
|
35
|
+
.filter((line) => !line.trimStart().startsWith("--"))
|
|
36
|
+
.join("\n");
|
|
37
|
+
return text
|
|
38
|
+
.split(";")
|
|
39
|
+
.map((s) => s.replace(/\s+/g, " ").trim())
|
|
40
|
+
.filter((s) => s.length > 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The text between the `-- BEGIN completion backfill` / `-- END …` markers. */
|
|
44
|
+
function backfillBlock(): string {
|
|
45
|
+
const match = /-- BEGIN completion backfill\n([\s\S]*?)-- END completion backfill/.exec(
|
|
46
|
+
readFileSync(MIGRATION, "utf8"),
|
|
47
|
+
);
|
|
48
|
+
if (!match) throw new Error("completion backfill markers not found");
|
|
49
|
+
return match[1]!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("the flow_completion migration (#8757 D2, AC-2)", () => {
|
|
53
|
+
it("adds exactly the two columns, the completion default lead", () => {
|
|
54
|
+
const adds = statements().filter((s) => /^ALTER TABLE "flow_definitions" ADD COLUMN /.test(s));
|
|
55
|
+
expect(adds).toHaveLength(1);
|
|
56
|
+
const columns = adds[0]!.split(/ADD COLUMN /).slice(1).map((c) => c.trim().replace(/,$/, ""));
|
|
57
|
+
expect(columns).toEqual([
|
|
58
|
+
`"completion" VARCHAR(16) NOT NULL DEFAULT 'lead'`,
|
|
59
|
+
`"checkout_offering" VARCHAR(40)`,
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("adds the three named CHECKs NOT VALID, then VALIDATEs each", () => {
|
|
64
|
+
const all = statements();
|
|
65
|
+
const added = all
|
|
66
|
+
.map((s) => /^ALTER TABLE "flow_definitions" ADD CONSTRAINT "([a-z_]+)" CHECK \(.*\) NOT VALID$/.exec(s)?.[1])
|
|
67
|
+
.filter((name): name is string => name !== undefined);
|
|
68
|
+
const validated = all
|
|
69
|
+
.map((s) => /^ALTER TABLE "flow_definitions" VALIDATE CONSTRAINT "([a-z_]+)"$/.exec(s)?.[1])
|
|
70
|
+
.filter((name): name is string => name !== undefined);
|
|
71
|
+
expect(added).toEqual(CHECKS);
|
|
72
|
+
expect(validated).toEqual(CHECKS);
|
|
73
|
+
for (const name of CHECKS) {
|
|
74
|
+
expect(all.findIndex((s) => s.includes(`VALIDATE CONSTRAINT "${name}"`))).toBeGreaterThan(
|
|
75
|
+
all.findIndex((s) => s.includes(`ADD CONSTRAINT "${name}"`)),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("holds exactly one UPDATE, inside the backfill markers, naming the invite defaults", () => {
|
|
81
|
+
const updates = statements().filter((s) => /\bUPDATE\b/i.test(s));
|
|
82
|
+
expect(updates).toHaveLength(1);
|
|
83
|
+
const block = backfillBlock().replace(/\s+/g, " ").trim();
|
|
84
|
+
expect(block).toBe(updates[0] + ";");
|
|
85
|
+
const list = /WHERE "slug" IN \(([^)]*)\)/.exec(block)?.[1];
|
|
86
|
+
const slugs = (list ?? "").split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
87
|
+
const invite = Object.entries(STANDARD_MAGNET_COMPLETIONS)
|
|
88
|
+
.filter(([, completion]) => completion === "invite")
|
|
89
|
+
.map(([slug]) => slug);
|
|
90
|
+
expect([...slugs].sort()).toEqual([...invite].sort());
|
|
91
|
+
expect(block).toContain(`SET "completion" = 'invite'`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("inserts and deletes nothing", () => {
|
|
95
|
+
for (const s of statements()) expect(s).not.toMatch(/\b(INSERT|DELETE)\b/i);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("every statement is one of the expected shapes", () => {
|
|
99
|
+
const unexpected = statements().filter(
|
|
100
|
+
(s) =>
|
|
101
|
+
!/^ALTER TABLE "flow_definitions" (ADD COLUMN|ADD CONSTRAINT|VALIDATE CONSTRAINT) /.test(s) &&
|
|
102
|
+
!/^UPDATE "flow_definitions" SET "completion" = 'invite' WHERE "slug" IN \(/.test(s),
|
|
103
|
+
);
|
|
104
|
+
expect(unexpected).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("schema.prisma declares both fields on FlowDefinition", () => {
|
|
108
|
+
const prisma = readFileSync(join(__dirname, "..", "inbox", "schema.prisma"), "utf8");
|
|
109
|
+
const model = /model FlowDefinition \{([\s\S]*?)\n\}/.exec(prisma)?.[1] ?? "";
|
|
110
|
+
expect(model).toMatch(/\n\s*completion String @default\("lead"\) @db\.VarChar\(16\)\n/);
|
|
111
|
+
expect(model).toMatch(
|
|
112
|
+
/\n\s*checkoutOffering String\? @map\("checkout_offering"\) @db\.VarChar\(40\)\n/,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file flow-completion.db.test.ts
|
|
3
|
+
*
|
|
4
|
+
* LIVE-DATABASE proof of a magnet's completion columns (#8757, spec
|
|
5
|
+
* docs/specs/8757-a-per-magnet-completion-setting-spec.md D2, AC-2). Requires
|
|
6
|
+
* DATABASE_URL pointing at a Postgres with the migrations applied — SKIPS
|
|
7
|
+
* when DATABASE_URL is unset (the repo convention; the `apps/api Route Tests
|
|
8
|
+
* (Postgres)` workflow exercises it).
|
|
9
|
+
*
|
|
10
|
+
* Uses the `pg` driver directly, in ONE transaction that is rolled back. What
|
|
11
|
+
* it proves that a mock cannot:
|
|
12
|
+
* 1. The migration's backfill block, run over magnets at the five slugs,
|
|
13
|
+
* leaves `contact` and `form` at `lead` and sets the three process
|
|
14
|
+
* magnets to `invite`; a second run changes nothing.
|
|
15
|
+
* 2. Each CHECK refuses what it names.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
import { Client } from "pg";
|
|
21
|
+
|
|
22
|
+
const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
|
|
23
|
+
|
|
24
|
+
const MIGRATION_SQL = readFileSync(
|
|
25
|
+
join(__dirname, "..", "migrations", "20261018120000_flow_completion", "migration.sql"),
|
|
26
|
+
"utf8",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/** The text between the `-- BEGIN completion backfill` / `-- END …` markers. */
|
|
30
|
+
function backfillBlock(sql: string): string {
|
|
31
|
+
const match = /-- BEGIN completion backfill\n([\s\S]*?)-- END completion backfill/.exec(sql);
|
|
32
|
+
if (!match?.[1]) throw new Error("completion backfill markers not found");
|
|
33
|
+
return match[1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const WS = "wksp_01920000-8757-7000-8000-0000000000c1";
|
|
37
|
+
const SLUGS = ["contact", "onboarding", "request-quote", "job-application", "form"] as const;
|
|
38
|
+
|
|
39
|
+
describeDb("the magnet completion columns (#8757 D2, AC-2, live DB)", () => {
|
|
40
|
+
let db: Client;
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
db = new Client({ connectionString: process.env.DATABASE_URL });
|
|
44
|
+
await db.connect();
|
|
45
|
+
await db.query("BEGIN");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
await db.query("ROLLBACK");
|
|
50
|
+
await db.end();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** Insert one magnet at `slug`, with any of the completion columns. */
|
|
54
|
+
const insertMagnet = (slug: string, cols: Record<string, unknown> = {}) => {
|
|
55
|
+
const all: Record<string, unknown> = {
|
|
56
|
+
site_id: WS,
|
|
57
|
+
workspace_id: WS,
|
|
58
|
+
slug,
|
|
59
|
+
config: JSON.stringify({ steps: [] }),
|
|
60
|
+
title: `Magnet ${slug}`,
|
|
61
|
+
status: "draft",
|
|
62
|
+
updated_at: new Date("2026-10-01T09:00:00.000Z"),
|
|
63
|
+
...cols,
|
|
64
|
+
};
|
|
65
|
+
const names = Object.keys(all);
|
|
66
|
+
return db.query<{ id: string }>(
|
|
67
|
+
`INSERT INTO "flow_definitions" (${names.map((n) => `"${n}"`).join(", ")})
|
|
68
|
+
VALUES (${names.map((_, i) => `$${i + 1}`).join(", ")}) RETURNING "id"`,
|
|
69
|
+
names.map((n) => all[n]),
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Run `run` inside a savepoint; resolve to the violated constraint's name, or null. */
|
|
74
|
+
const violation = async (run: () => Promise<unknown>): Promise<string | null> => {
|
|
75
|
+
await db.query("SAVEPOINT attempt");
|
|
76
|
+
try {
|
|
77
|
+
await run();
|
|
78
|
+
await db.query("RELEASE SAVEPOINT attempt");
|
|
79
|
+
return null;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
await db.query("ROLLBACK TO SAVEPOINT attempt");
|
|
82
|
+
return (err as { constraint?: string }).constraint ?? String(err);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const completions = async () =>
|
|
87
|
+
Object.fromEntries(
|
|
88
|
+
(
|
|
89
|
+
await db.query<{ slug: string; completion: string; checkout_offering: string | null }>(
|
|
90
|
+
`SELECT "slug", "completion", "checkout_offering" FROM "flow_definitions" WHERE "workspace_id" = $1`,
|
|
91
|
+
[WS],
|
|
92
|
+
)
|
|
93
|
+
).rows.map((r) => [r.slug, [r.completion, r.checkout_offering]]),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
it("the backfill sets the three process magnets to invite and leaves contact and form at lead, idempotently", async () => {
|
|
97
|
+
for (const slug of SLUGS) await insertMagnet(slug);
|
|
98
|
+
const before = await completions();
|
|
99
|
+
for (const slug of SLUGS) expect(before[slug]).toEqual(["lead", null]);
|
|
100
|
+
|
|
101
|
+
await db.query(backfillBlock(MIGRATION_SQL));
|
|
102
|
+
const after = await completions();
|
|
103
|
+
expect(after).toEqual({
|
|
104
|
+
contact: ["lead", null],
|
|
105
|
+
form: ["lead", null],
|
|
106
|
+
onboarding: ["invite", null],
|
|
107
|
+
"request-quote": ["invite", null],
|
|
108
|
+
"job-application": ["invite", null],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const rerun = await db.query(backfillBlock(MIGRATION_SQL));
|
|
112
|
+
expect(await completions()).toEqual(after);
|
|
113
|
+
// The re-run matches the same rows but stores the same values.
|
|
114
|
+
expect(rerun.rowCount).toBeGreaterThanOrEqual(3);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it.each([
|
|
118
|
+
["checkout with no offering", { completion: "checkout", checkout_offering: null }, "flow_definitions_checkout_offering_pair"],
|
|
119
|
+
["an offering beside lead", { completion: "lead", checkout_offering: "retainer" }, "flow_definitions_checkout_offering_pair"],
|
|
120
|
+
["a capitalised key", { completion: "checkout", checkout_offering: "Bad" }, "flow_definitions_checkout_offering_format"],
|
|
121
|
+
["an underscored key", { completion: "checkout", checkout_offering: "a_b" }, "flow_definitions_checkout_offering_format"],
|
|
122
|
+
["a fourth value", { completion: "other", checkout_offering: null }, "flow_definitions_completion_domain"],
|
|
123
|
+
])("refuses %s", async (_label, cols, constraint) => {
|
|
124
|
+
expect(await violation(() => insertMagnet(`refused-${constraint.length}-${String(cols.completion)}`, cols))).toBe(
|
|
125
|
+
constraint,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("accepts checkout with a listed-shape offering", async () => {
|
|
130
|
+
expect(
|
|
131
|
+
await violation(() =>
|
|
132
|
+
insertMagnet("paid-waitlist", { completion: "checkout", checkout_offering: "retainer" }),
|
|
133
|
+
),
|
|
134
|
+
).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
});
|