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.
Files changed (222) hide show
  1. package/README.md +21 -1
  2. package/component-catalog.json +220 -8
  3. package/component-catalog.md +21 -9
  4. package/dist/add-module.js +9 -5
  5. package/dist/apply-design.d.ts +9 -0
  6. package/dist/apply-design.js +78 -31
  7. package/dist/derive-architecture.d.ts +8 -1
  8. package/dist/derive-architecture.js +38 -3
  9. package/dist/design-home.d.ts +54 -11
  10. package/dist/design-home.js +160 -22
  11. package/dist/design-package.js +7 -2
  12. package/dist/lib/design-checks.d.ts +6 -1
  13. package/dist/lib/design-checks.js +23 -2
  14. package/dist/lib/env-catalog.js +5 -1
  15. package/dist/module-architecture.js +4 -2
  16. package/dist/programmatic.js +63 -25
  17. package/dist/site-chrome.d.ts +8 -0
  18. package/dist/site-chrome.js +17 -1
  19. package/dist/site-roster.d.ts +4 -2
  20. package/dist/site-roster.js +7 -5
  21. package/dist/spoke-checkout.d.ts +39 -18
  22. package/dist/spoke-checkout.js +177 -236
  23. package/dist/starter-grid.js +16 -0
  24. package/dist/surfaces.js +30 -14
  25. package/dist/sync-routes.d.ts +28 -0
  26. package/dist/sync-routes.js +124 -1
  27. package/dist/template-record.d.ts +4 -3
  28. package/dist/template-record.js +6 -3
  29. package/dist/topology.js +25 -1
  30. package/dist/update.d.ts +6 -1
  31. package/dist/update.js +125 -18
  32. package/package.json +3 -3
  33. package/site-templates/_base/ailk-template.json +2 -2
  34. package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
  35. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  36. package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
  37. package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
  38. package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
  39. package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
  41. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
  43. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
  46. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
  48. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
  49. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
  50. package/templates/README.md +26 -25
  51. package/templates/apps/api/CLAUDE.md +20 -7
  52. package/templates/apps/api/jest.config.cjs +1 -0
  53. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
  54. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
  55. package/templates/apps/api/src/lib/consent-store.ts +28 -0
  56. package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
  57. package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
  58. package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
  59. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
  60. package/templates/apps/api/src/middleware/auth.ts +13 -6
  61. package/templates/apps/api/src/openapi/spec.ts +30 -3
  62. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
  63. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
  64. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
  65. package/templates/apps/api/src/routes/consent/account.ts +266 -0
  66. package/templates/apps/api/src/routes/consent/index.ts +15 -7
  67. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
  68. package/templates/apps/api/src/routes/content/get-page.ts +4 -0
  69. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
  70. package/templates/apps/api/src/routes/leads/index.ts +161 -7
  71. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
  72. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
  73. package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
  74. package/templates/apps/api/src/server.ts +28 -0
  75. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
  76. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
  77. package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
  78. package/templates/apps/api/src/services/consent.ts +95 -0
  79. package/templates/apps/api/src/services/lead-routing.ts +337 -8
  80. package/templates/apps/api/src/services/magnet-completion.ts +242 -0
  81. package/templates/apps/api/src/services/person.ts +40 -8
  82. package/templates/apps/api/src/services/plan-offerings.ts +59 -0
  83. package/templates/apps/api/src/services/project-listings.ts +6 -1
  84. package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
  85. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
  86. package/templates/apps/mcp/jest.config.cjs +1 -0
  87. package/templates/apps/web/.env.example +14 -0
  88. package/templates/apps/web/CLAUDE.md +139 -3
  89. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
  90. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
  91. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
  92. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
  93. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
  94. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
  95. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
  96. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
  97. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
  98. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
  99. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
  100. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
  101. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
  102. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
  103. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
  104. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
  105. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
  106. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
  112. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
  114. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
  115. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
  116. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
  117. package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
  118. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
  119. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
  120. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
  121. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
  122. package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
  123. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
  124. package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
  125. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
  126. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
  127. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
  128. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
  129. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
  130. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
  131. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
  132. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  133. package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
  134. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
  135. package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
  136. package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
  137. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  138. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +24 -1
  139. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
  140. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
  141. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
  142. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +386 -0
  143. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +504 -24
  144. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +12 -3
  145. package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
  146. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
  147. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +184 -41
  148. package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
  149. package/templates/apps/web/app/[locale]/contact/page.tsx +120 -16
  150. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  151. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
  152. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
  153. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
  154. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
  155. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
  156. package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
  157. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
  158. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  159. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  160. package/templates/apps/web/app/[locale]/layout.tsx +23 -2
  161. package/templates/apps/web/app/[locale]/page.tsx +13 -1
  162. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  163. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  164. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
  165. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  166. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  167. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  168. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  169. package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
  170. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
  171. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
  172. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
  173. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  174. package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
  175. package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
  176. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
  177. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
  178. package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
  179. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
  180. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
  181. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
  182. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
  183. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
  184. package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
  185. package/templates/apps/web/auth.ts +28 -2
  186. package/templates/apps/web/jest.config.cjs +1 -0
  187. package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
  188. package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
  189. package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
  190. package/templates/apps/web/lib/__tests__/deck-page.test.ts +249 -0
  191. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -19
  192. package/templates/apps/web/lib/account-invite.ts +37 -0
  193. package/templates/apps/web/lib/checkout-config.ts +27 -0
  194. package/templates/apps/web/lib/checkout.ts +93 -0
  195. package/templates/apps/web/lib/consent-policies.ts +22 -0
  196. package/templates/apps/web/lib/deck-page.ts +192 -0
  197. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
  198. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
  199. package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
  200. package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
  201. package/templates/apps/web/lib/site-chrome.ts +2 -0
  202. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  203. package/templates/apps/web/messages/en.json +40 -12
  204. package/templates/apps/web/package.json +1 -0
  205. package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
  206. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
  207. package/templates/apps/web/src/lib/lead-write.ts +90 -0
  208. package/templates/apps/web/src/lib/rate-limit.ts +79 -0
  209. package/templates/database/CHANGELOG.md +47 -0
  210. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
  211. package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
  212. package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
  213. package/templates/database/__tests__/seed.test.ts +72 -2
  214. package/templates/database/inbox/schema.prisma +17 -2
  215. package/templates/database/jest.config.cjs +1 -0
  216. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
  217. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
  218. package/templates/database/package.json +1 -1
  219. package/templates/database/seed.ts +60 -0
  220. package/templates/tools/jest/pin-timezone.cjs +34 -0
  221. package/versions.json +34 -34
  222. 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(1);
111
- expect(flowDefinitionUpsert).toHaveBeenCalledWith({
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. Written only by
1287
- /// apps/api/src/services/consent.ts.
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")
@@ -1,5 +1,6 @@
1
1
  module.exports = {
2
2
  preset: "ts-jest",
3
+ globalSetup: require.resolve("../tools/jest/pin-timezone.cjs"),
3
4
  testEnvironment: "node",
4
5
  roots: ["<rootDir>"],
5
6
  testMatch: ["**/__tests__/**/*.test.ts"],
@@ -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";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@working-theory/database",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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
  });