create-ailk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/component-catalog.json +220 -8
- package/component-catalog.md +21 -9
- package/dist/add-module.js +9 -5
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/derive-architecture.d.ts +8 -1
- package/dist/derive-architecture.js +38 -3
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/env-catalog.js +5 -1
- package/dist/module-architecture.js +4 -2
- package/dist/programmatic.js +63 -25
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +39 -18
- package/dist/spoke-checkout.js +177 -236
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +30 -14
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +124 -1
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +6 -3
- package/dist/topology.js +25 -1
- package/dist/update.d.ts +6 -1
- package/dist/update.js +125 -18
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/README.md +26 -25
- package/templates/apps/api/CLAUDE.md +20 -7
- package/templates/apps/api/jest.config.cjs +1 -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/consent-store.ts +28 -0
- 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/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/openapi/spec.ts +30 -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/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/account.ts +266 -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/get-page.ts +4 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +161 -7
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -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/lead-routing.ts +337 -8
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/person.ts +40 -8
- package/templates/apps/api/src/services/plan-offerings.ts +59 -0
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/web/.env.example +14 -0
- package/templates/apps/web/CLAUDE.md +139 -3
- 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]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
- 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 +24 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +386 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +504 -24
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +12 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +184 -41
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +120 -16
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
- 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 +106 -1
- 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]/account-invite.actions.ts +35 -0
- 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 +23 -2
- 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]/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]/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]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- 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/checkout/[offering]/route.ts +48 -0
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
- 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/webhooks/stripe/__tests__/route.test.ts +92 -0
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +249 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -19
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/checkout-config.ts +27 -0
- package/templates/apps/web/lib/checkout.ts +93 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +192 -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/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +447 -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 +40 -12
- package/templates/apps/web/package.json +1 -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/database/CHANGELOG.md +47 -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__/seed.test.ts +72 -2
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/jest.config.cjs +1 -0
- 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/package.json +1 -1
- package/templates/database/seed.ts +60 -0
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +34 -34
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
|
@@ -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
|
+
});
|
|
@@ -11,9 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
+
CONTACT_FORM_FLOW_SLUG,
|
|
15
|
+
contactFormFlowConfig,
|
|
16
|
+
JOB_APPLICATION_FLOW_SLUG,
|
|
17
|
+
jobApplicationFlowConfig,
|
|
14
18
|
REQUEST_QUOTE_FLOW_SLUG,
|
|
15
19
|
REQUEST_QUOTE_FLOW_TITLE,
|
|
16
20
|
requestQuoteFlowConfig,
|
|
21
|
+
STANDARD_MAGNET_COMPLETIONS,
|
|
17
22
|
} from "@working-theory/validation";
|
|
18
23
|
|
|
19
24
|
import {
|
|
@@ -107,8 +112,8 @@ describe("provisionDefaultWorkspace — the request-quote magnet (#8168 D8, C4,
|
|
|
107
112
|
|
|
108
113
|
await provisionDefaultWorkspace(prisma);
|
|
109
114
|
|
|
110
|
-
expect(flowDefinitionUpsert).toHaveBeenCalledTimes(
|
|
111
|
-
expect(flowDefinitionUpsert).
|
|
115
|
+
expect(flowDefinitionUpsert).toHaveBeenCalledTimes(3);
|
|
116
|
+
expect(flowDefinitionUpsert).toHaveBeenNthCalledWith(1, {
|
|
112
117
|
where: {
|
|
113
118
|
siteId_slug: { siteId: DEFAULT_WORKSPACE_ID, slug: REQUEST_QUOTE_FLOW_SLUG },
|
|
114
119
|
},
|
|
@@ -121,6 +126,7 @@ describe("provisionDefaultWorkspace — the request-quote magnet (#8168 D8, C4,
|
|
|
121
126
|
title: REQUEST_QUOTE_FLOW_TITLE,
|
|
122
127
|
status: "draft",
|
|
123
128
|
customersEnabled: true,
|
|
129
|
+
completion: STANDARD_MAGNET_COMPLETIONS[REQUEST_QUOTE_FLOW_SLUG],
|
|
124
130
|
},
|
|
125
131
|
// Empty: an existing (site_id, slug) row is never overwritten (D8, C6).
|
|
126
132
|
update: {},
|
|
@@ -138,3 +144,67 @@ describe("provisionDefaultWorkspace — the request-quote magnet (#8168 D8, C4,
|
|
|
138
144
|
expect(flowDefinitionUpsert).not.toHaveBeenCalled();
|
|
139
145
|
});
|
|
140
146
|
});
|
|
147
|
+
|
|
148
|
+
describe("provisionDefaultWorkspace — the Contact form magnet (#8661 D3, AC-1)", () => {
|
|
149
|
+
it("upserts the default workspace's live Contact form, insert-if-absent", async () => {
|
|
150
|
+
const { prisma, flowDefinitionUpsert } = makeStubPrisma(0);
|
|
151
|
+
|
|
152
|
+
await provisionDefaultWorkspace(prisma);
|
|
153
|
+
|
|
154
|
+
expect(flowDefinitionUpsert).toHaveBeenNthCalledWith(2, {
|
|
155
|
+
where: {
|
|
156
|
+
siteId_slug: { siteId: DEFAULT_WORKSPACE_ID, slug: CONTACT_FORM_FLOW_SLUG },
|
|
157
|
+
},
|
|
158
|
+
create: {
|
|
159
|
+
siteId: DEFAULT_WORKSPACE_ID,
|
|
160
|
+
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
161
|
+
slug: "contact",
|
|
162
|
+
config: contactFormFlowConfig,
|
|
163
|
+
kind: "contact",
|
|
164
|
+
title: "Contact form",
|
|
165
|
+
status: "live",
|
|
166
|
+
customersEnabled: false,
|
|
167
|
+
completion: STANDARD_MAGNET_COMPLETIONS[CONTACT_FORM_FLOW_SLUG],
|
|
168
|
+
landingPage: "/contact",
|
|
169
|
+
},
|
|
170
|
+
// Empty: a re-run never overwrites the owner's edits.
|
|
171
|
+
update: {},
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("makes no flowDefinition call when a workspace already exists", async () => {
|
|
176
|
+
const { prisma, flowDefinitionUpsert } = makeStubPrisma(1);
|
|
177
|
+
|
|
178
|
+
await provisionDefaultWorkspace(prisma);
|
|
179
|
+
|
|
180
|
+
expect(flowDefinitionUpsert).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("provisionDefaultWorkspace — the Job application magnet (#8746 D3, AC-2)", () => {
|
|
185
|
+
it("upserts the default workspace's live Job application, insert-if-absent", async () => {
|
|
186
|
+
const { prisma, flowDefinitionUpsert } = makeStubPrisma(0);
|
|
187
|
+
|
|
188
|
+
await provisionDefaultWorkspace(prisma);
|
|
189
|
+
|
|
190
|
+
expect(flowDefinitionUpsert).toHaveBeenNthCalledWith(3, {
|
|
191
|
+
where: {
|
|
192
|
+
siteId_slug: { siteId: DEFAULT_WORKSPACE_ID, slug: JOB_APPLICATION_FLOW_SLUG },
|
|
193
|
+
},
|
|
194
|
+
create: {
|
|
195
|
+
siteId: DEFAULT_WORKSPACE_ID,
|
|
196
|
+
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
197
|
+
slug: "job-application",
|
|
198
|
+
config: jobApplicationFlowConfig,
|
|
199
|
+
kind: "diagnostic",
|
|
200
|
+
title: "Job application",
|
|
201
|
+
status: "live",
|
|
202
|
+
customersEnabled: false,
|
|
203
|
+
completion: STANDARD_MAGNET_COMPLETIONS[JOB_APPLICATION_FLOW_SLUG],
|
|
204
|
+
landingPage: "/careers",
|
|
205
|
+
},
|
|
206
|
+
// Empty: a re-run never overwrites the owner's edits.
|
|
207
|
+
update: {},
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -473,6 +473,18 @@ model FlowDefinition {
|
|
|
473
473
|
/// `request-quote` magnet is `draft` (never served anonymously) with
|
|
474
474
|
/// `customersEnabled = true`.
|
|
475
475
|
customersEnabled Boolean @default(false) @map("customers_enabled")
|
|
476
|
+
/// #8757 D1/D2 — what happens after someone submits the magnet: `lead`
|
|
477
|
+
/// (the thank-you only), `invite` (an account link with the email
|
|
478
|
+
/// prefilled) or `checkout` (continue to `checkoutOffering`). A column, on
|
|
479
|
+
/// the `customersEnabled` footing: no seed or config names it. Closed by
|
|
480
|
+
/// the `flow_definitions_completion_domain` CHECK; the standard defaults are
|
|
481
|
+
/// the migration's backfill by slug (`STANDARD_MAGNET_COMPLETIONS`).
|
|
482
|
+
completion String @default("lead") @db.VarChar(16)
|
|
483
|
+
/// #8757 D1/D2 — the offering key a `checkout` magnet continues to; NULL
|
|
484
|
+
/// exactly when `completion` is not `checkout`
|
|
485
|
+
/// (`flow_definitions_checkout_offering_pair`), in the spoke checkout slug
|
|
486
|
+
/// format (`flow_definitions_checkout_offering_format`).
|
|
487
|
+
checkoutOffering String? @map("checkout_offering") @db.VarChar(40)
|
|
476
488
|
createdAt DateTime @default(now()) @map("created_at")
|
|
477
489
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
478
490
|
|
|
@@ -1283,8 +1295,11 @@ model ProjectListing {
|
|
|
1283
1295
|
/// would prove nothing once that row is edited, and `statementHash` groups
|
|
1284
1296
|
/// identical wordings. `locale` is the locale the statement was actually
|
|
1285
1297
|
/// served in, not the one requested. `purposeId` is the site-config registry
|
|
1286
|
-
/// id — the join key across surfaces.
|
|
1287
|
-
/// apps/
|
|
1298
|
+
/// id — the join key across surfaces. Two writers: apps/api/src/services/consent.ts
|
|
1299
|
+
/// (every source), and the web sign-up hook apps/web/lib/sign-up-agreements.ts
|
|
1300
|
+
/// (#8765 D4), which writes only `sign_up` rows (subject `user`) of a user it
|
|
1301
|
+
/// creates in the same Better Auth transaction, from evidence the API built
|
|
1302
|
+
/// with consent.ts's own functions.
|
|
1288
1303
|
model ConsentGrant {
|
|
1289
1304
|
id String @id @default(dbgenerated("('cnsnt_'::text || (uuidv7())::text)")) @db.Text
|
|
1290
1305
|
siteId String @map("site_id")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- #8757 — a per-magnet completion setting (spec
|
|
2
|
+
-- docs/specs/8757-a-per-magnet-completion-setting-spec.md D1, D2).
|
|
3
|
+
--
|
|
4
|
+
-- flow_definitions (2 columns) — what happens after someone submits the
|
|
5
|
+
-- magnet (`completion`: `lead`, `invite` or `checkout`),
|
|
6
|
+
-- and, for `checkout` only, the offering key it continues
|
|
7
|
+
-- to. The vocabulary is `FLOW_COMPLETIONS` and
|
|
8
|
+
-- `flowCheckoutOfferingSchema` in
|
|
9
|
+
-- packages/validation/src/schemas/flow-completion.ts.
|
|
10
|
+
--
|
|
11
|
+
-- Every existing row reads `completion = 'lead'` from the constant default —
|
|
12
|
+
-- Postgres stores the default in the catalog and rewrites no row for it, so
|
|
13
|
+
-- `contact` and every waitlist get `lead` with no statement. Each CHECK below
|
|
14
|
+
-- is added NOT VALID and then VALIDATEd; every existing row passes each one
|
|
15
|
+
-- (`lead` with a NULL offering).
|
|
16
|
+
--
|
|
17
|
+
-- The one UPDATE is the standard-defaults backfill BY SLUG (the issue's D2,
|
|
18
|
+
-- D7): `onboarding`, `request-quote` and `job-application` → `invite`
|
|
19
|
+
-- (`STANDARD_MAGNET_COMPLETIONS`). A test re-reads the block between the
|
|
20
|
+
-- markers. `updated_at` is Prisma-managed, so the backfill leaves it alone.
|
|
21
|
+
-- A magnet created after this migration takes the column default until the
|
|
22
|
+
-- seed constants carry `completion` (#8759).
|
|
23
|
+
--
|
|
24
|
+
-- Rollout: deploy this migration BEFORE the API code that reads and writes the
|
|
25
|
+
-- columns; code without them fails every magnet read.
|
|
26
|
+
--
|
|
27
|
+
-- Rollback: ALTER TABLE "flow_definitions" DROP COLUMN "completion", DROP
|
|
28
|
+
-- COLUMN "checkout_offering"; (the CHECKs go with them).
|
|
29
|
+
|
|
30
|
+
-- AlterTable
|
|
31
|
+
ALTER TABLE "flow_definitions" ADD COLUMN "completion" VARCHAR(16) NOT NULL DEFAULT 'lead',
|
|
32
|
+
ADD COLUMN "checkout_offering" VARCHAR(40);
|
|
33
|
+
|
|
34
|
+
-- A magnet completes as a lead, an invite or a checkout, nothing else.
|
|
35
|
+
ALTER TABLE "flow_definitions" ADD CONSTRAINT "flow_definitions_completion_domain"
|
|
36
|
+
CHECK ("completion" IN ('lead', 'invite', 'checkout')) NOT VALID;
|
|
37
|
+
ALTER TABLE "flow_definitions" VALIDATE CONSTRAINT "flow_definitions_completion_domain";
|
|
38
|
+
|
|
39
|
+
-- An offering is named exactly when the magnet continues to checkout.
|
|
40
|
+
ALTER TABLE "flow_definitions" ADD CONSTRAINT "flow_definitions_checkout_offering_pair"
|
|
41
|
+
CHECK (("completion" = 'checkout') = ("checkout_offering" IS NOT NULL)) NOT VALID;
|
|
42
|
+
ALTER TABLE "flow_definitions" VALIDATE CONSTRAINT "flow_definitions_checkout_offering_pair";
|
|
43
|
+
|
|
44
|
+
-- An offering key the spoke checkout route can serve.
|
|
45
|
+
ALTER TABLE "flow_definitions" ADD CONSTRAINT "flow_definitions_checkout_offering_format"
|
|
46
|
+
CHECK ("checkout_offering" IS NULL OR "checkout_offering" ~ '^[a-z0-9][a-z0-9-]{0,39}$') NOT VALID;
|
|
47
|
+
ALTER TABLE "flow_definitions" VALIDATE CONSTRAINT "flow_definitions_checkout_offering_format";
|
|
48
|
+
|
|
49
|
+
-- BEGIN completion backfill
|
|
50
|
+
UPDATE "flow_definitions" SET "completion" = 'invite'
|
|
51
|
+
WHERE "slug" IN ('onboarding', 'request-quote', 'job-application');
|
|
52
|
+
-- END completion backfill
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- #8765 — a `checkout` consent source (spec
|
|
2
|
+
-- docs/specs/8765-sign-up-refuses-without-the-spec.md D13).
|
|
3
|
+
--
|
|
4
|
+
-- consent_grants — the source vocabulary gains `checkout`: the buyer's
|
|
5
|
+
-- terms box on a Stripe Checkout session, recorded by the
|
|
6
|
+
-- hub webhook against the buyer's Person (D14).
|
|
7
|
+
--
|
|
8
|
+
-- DDL only. The CHECK is replaced, never relaxed: the old one is dropped and
|
|
9
|
+
-- the new one, the seven existing values plus `checkout`, is added NOT VALID
|
|
10
|
+
-- and then VALIDATEd, so every existing row is checked against it. No row is
|
|
11
|
+
-- written, and the append-only triggers do not fire on DDL.
|
|
12
|
+
--
|
|
13
|
+
-- The source vocabulary is packages/validation CONSENT_SOURCES.
|
|
14
|
+
--
|
|
15
|
+
-- Rollout: deploy this migration BEFORE the API code that writes a
|
|
16
|
+
-- `checkout` row; the webhook's insert otherwise fails the CHECK (it never
|
|
17
|
+
-- throws, so the order still completes, but no grant is recorded).
|
|
18
|
+
--
|
|
19
|
+
-- Rollback: ALTER TABLE "consent_grants" DROP CONSTRAINT
|
|
20
|
+
-- "consent_grants_source_known"; then re-add it with the seven values of
|
|
21
|
+
-- 20261014120000_consent_ledger — which fails while any `checkout` row
|
|
22
|
+
-- exists, and the ledger is append-only.
|
|
23
|
+
|
|
24
|
+
ALTER TABLE "consent_grants" DROP CONSTRAINT "consent_grants_source_known";
|
|
25
|
+
ALTER TABLE "consent_grants" ADD CONSTRAINT "consent_grants_source_known"
|
|
26
|
+
CHECK ("source" IN ('cookie_banner', 'sign_up', 'magnet', 'account_settings', 'email_unsubscribe', 'api', 'legacy', 'checkout')) NOT VALID;
|
|
27
|
+
ALTER TABLE "consent_grants" VALIDATE CONSTRAINT "consent_grants_source_known";
|
|
@@ -26,6 +26,14 @@ import "dotenv/config";
|
|
|
26
26
|
import { basename } from "node:path";
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
|
+
CONTACT_FORM_FLOW_SLUG,
|
|
30
|
+
CONTACT_FORM_FLOW_TITLE,
|
|
31
|
+
CONTACT_FORM_LANDING_PAGE,
|
|
32
|
+
contactFormFlowConfig,
|
|
33
|
+
JOB_APPLICATION_FLOW_SLUG,
|
|
34
|
+
JOB_APPLICATION_FLOW_TITLE,
|
|
35
|
+
JOB_APPLICATION_LANDING_PAGE,
|
|
36
|
+
jobApplicationFlowConfig,
|
|
29
37
|
REQUEST_QUOTE_FLOW_SLUG,
|
|
30
38
|
REQUEST_QUOTE_FLOW_TITLE,
|
|
31
39
|
requestQuoteFlowConfig,
|
|
@@ -74,6 +82,13 @@ export type DefaultWorkspacePrismaClient = Pick<
|
|
|
74
82
|
* backfill finds no workspace and seeds nothing; this is where the default
|
|
75
83
|
* workspace's magnet comes from. Insert-if-absent: an `upsert` with an empty
|
|
76
84
|
* `update`, so an existing `(site_id, slug)` row is never overwritten.
|
|
85
|
+
*
|
|
86
|
+
* #8661 D3 — and its live Contact form magnet, the same way, and (#8746 D3)
|
|
87
|
+
* its live Job application magnet. An existing deployment (a workspace
|
|
88
|
+
* already present) gets its rows from the seed-presets CLI instead.
|
|
89
|
+
*
|
|
90
|
+
* #8759 — each names its #8757 D2 completion: the migration's backfill ran
|
|
91
|
+
* before this seed and found no rows, so the seed writes the value itself.
|
|
77
92
|
*/
|
|
78
93
|
export async function provisionDefaultWorkspace(
|
|
79
94
|
prisma: DefaultWorkspacePrismaClient,
|
|
@@ -125,6 +140,51 @@ export async function provisionDefaultWorkspace(
|
|
|
125
140
|
title: REQUEST_QUOTE_FLOW_TITLE,
|
|
126
141
|
status: "draft",
|
|
127
142
|
customersEnabled: true,
|
|
143
|
+
completion: "invite",
|
|
144
|
+
},
|
|
145
|
+
update: {},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await prisma.flowDefinition.upsert({
|
|
149
|
+
where: {
|
|
150
|
+
siteId_slug: {
|
|
151
|
+
siteId: DEFAULT_WORKSPACE_ID,
|
|
152
|
+
slug: CONTACT_FORM_FLOW_SLUG,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
create: {
|
|
156
|
+
siteId: DEFAULT_WORKSPACE_ID,
|
|
157
|
+
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
158
|
+
slug: CONTACT_FORM_FLOW_SLUG,
|
|
159
|
+
config: contactFormFlowConfig,
|
|
160
|
+
kind: "contact",
|
|
161
|
+
title: CONTACT_FORM_FLOW_TITLE,
|
|
162
|
+
status: "live",
|
|
163
|
+
customersEnabled: false,
|
|
164
|
+
completion: "lead",
|
|
165
|
+
landingPage: CONTACT_FORM_LANDING_PAGE,
|
|
166
|
+
},
|
|
167
|
+
update: {},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await prisma.flowDefinition.upsert({
|
|
171
|
+
where: {
|
|
172
|
+
siteId_slug: {
|
|
173
|
+
siteId: DEFAULT_WORKSPACE_ID,
|
|
174
|
+
slug: JOB_APPLICATION_FLOW_SLUG,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
create: {
|
|
178
|
+
siteId: DEFAULT_WORKSPACE_ID,
|
|
179
|
+
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
180
|
+
slug: JOB_APPLICATION_FLOW_SLUG,
|
|
181
|
+
config: jobApplicationFlowConfig,
|
|
182
|
+
kind: "diagnostic",
|
|
183
|
+
title: JOB_APPLICATION_FLOW_TITLE,
|
|
184
|
+
status: "live",
|
|
185
|
+
customersEnabled: false,
|
|
186
|
+
completion: "invite",
|
|
187
|
+
landingPage: JOB_APPLICATION_LANDING_PAGE,
|
|
128
188
|
},
|
|
129
189
|
update: {},
|
|
130
190
|
});
|