create-ailk 0.22.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.
Files changed (238) hide show
  1. package/component-catalog.json +149 -5
  2. package/component-catalog.md +7 -6
  3. package/dist/add-module.js +11 -9
  4. package/dist/add-route.d.ts +55 -1
  5. package/dist/add-route.js +294 -12
  6. package/dist/cli.js +27 -10
  7. package/dist/derive-architecture.d.ts +7 -5
  8. package/dist/derive-architecture.js +9 -32
  9. package/dist/lib/design-checks.js +4 -2
  10. package/dist/lib/env-catalog.js +0 -4
  11. package/dist/lib/npm-auth.d.ts +9 -0
  12. package/dist/lib/npm-auth.js +48 -0
  13. package/dist/lib/scaffold-render-sweep.d.ts +3 -2
  14. package/dist/lib/scaffold-render-sweep.js +5 -8
  15. package/dist/module-architecture.js +98 -10
  16. package/dist/paid-sections.d.ts +21 -2
  17. package/dist/paid-sections.js +69 -7
  18. package/dist/seed-flows.d.ts +4 -1
  19. package/dist/seed-flows.js +50 -4
  20. package/dist/spoke-checkout.d.ts +38 -35
  21. package/dist/spoke-checkout.js +66 -229
  22. package/dist/starter-grid.d.ts +0 -3
  23. package/dist/starter-grid.js +24 -14
  24. package/dist/surfaces.js +33 -12
  25. package/dist/sync-routes.d.ts +38 -1
  26. package/dist/sync-routes.js +184 -11
  27. package/dist/template-record.d.ts +1 -1
  28. package/dist/template-record.js +3 -2
  29. package/dist/update.js +10 -0
  30. package/package.json +3 -3
  31. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  32. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  35. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  37. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  38. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  39. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  40. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  41. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  42. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  43. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  44. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  45. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  46. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  47. package/templates/.env.example +9 -7
  48. package/templates/.github/workflows/ci.yml +13 -0
  49. package/templates/README.md +1 -0
  50. package/templates/_gitignore +4 -0
  51. package/templates/apps/api/.env.example +27 -0
  52. package/templates/apps/api/CLAUDE.md +77 -16
  53. package/templates/apps/api/package.json +1 -0
  54. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  55. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  56. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  57. package/templates/apps/api/src/config/index.ts +109 -0
  58. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  59. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  60. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  61. package/templates/apps/api/src/lib/stripe.ts +74 -4
  62. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  63. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  64. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  65. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  66. package/templates/apps/api/src/openapi/spec.ts +10 -7
  67. package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
  68. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  69. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
  70. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  71. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  72. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  73. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  74. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
  75. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  76. package/templates/apps/api/src/routes/consent/account.ts +4 -2
  77. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
  78. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  79. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  80. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  81. package/templates/apps/api/src/routes/content/index.ts +16 -2
  82. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  83. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  84. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  85. package/templates/apps/api/src/routes/leads/index.ts +17 -14
  86. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  87. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  88. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  89. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
  90. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  91. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  92. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  93. package/templates/apps/api/src/routes/webhooks/stripe.ts +91 -17
  94. package/templates/apps/api/src/server.ts +140 -51
  95. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  96. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
  97. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  98. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
  99. package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
  100. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
  101. package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
  102. package/templates/apps/api/src/services/entitlements.ts +157 -1
  103. package/templates/apps/api/src/services/lead-routing.ts +113 -58
  104. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
  105. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  106. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
  107. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  108. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
  109. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
  110. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  111. package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
  112. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
  113. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  114. package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
  115. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  116. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  117. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  118. package/templates/apps/api/src/services/person.ts +124 -9
  119. package/templates/apps/api/src/services/plan-offerings.ts +96 -30
  120. package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
  121. package/templates/apps/mcp/CLAUDE.md +22 -14
  122. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  123. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  124. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  125. package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
  126. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  127. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  128. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  129. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  130. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  131. package/templates/apps/mcp/src/server.ts +5 -0
  132. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  133. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  134. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  135. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  136. package/templates/apps/web/.env.example +10 -31
  137. package/templates/apps/web/CLAUDE.md +315 -42
  138. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
  139. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
  140. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
  141. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  142. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
  143. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
  144. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
  145. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
  146. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  147. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  148. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
  149. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
  150. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
  151. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  152. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
  153. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
  154. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +6 -2
  155. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
  156. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
  157. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  158. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  159. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  160. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  161. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  162. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
  163. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  164. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  165. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  166. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  167. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  168. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  169. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  170. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  171. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  172. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  173. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  174. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  175. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  176. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  177. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  178. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  179. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  180. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  181. package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
  182. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  183. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
  184. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  185. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  186. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
  187. package/templates/apps/web/lib/auth-routes.ts +36 -2
  188. package/templates/apps/web/lib/checkout-config.ts +9 -20
  189. package/templates/apps/web/lib/deck-page.ts +1 -0
  190. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  191. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  192. package/templates/apps/web/lib/magnet-routes.ts +42 -0
  193. package/templates/apps/web/lib/modules.generated.ts +9 -0
  194. package/templates/apps/web/lib/newsletter.actions.ts +202 -0
  195. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  196. package/templates/apps/web/lib/posting-title.ts +17 -0
  197. package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
  198. package/templates/apps/web/messages/en.json +49 -15
  199. package/templates/apps/web/middleware.ts +12 -2
  200. package/templates/apps/web/security-headers.config.ts +17 -0
  201. package/templates/content/en/BlogList/blog.mdx +4 -71
  202. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  203. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  204. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  205. package/templates/content/en/BookCall/start-project.mdx +1 -17
  206. package/templates/content/en/ContactPage/contact.mdx +6 -63
  207. package/templates/content/en/HomePage/home.mdx +3 -21
  208. package/templates/database/CHANGELOG.md +31 -0
  209. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  210. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  211. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  212. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  213. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  214. package/templates/database/__tests__/seed.test.ts +31 -1
  215. package/templates/database/auth/schema.prisma +4 -0
  216. package/templates/database/config/schema.prisma +91 -0
  217. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  218. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  219. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  220. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  221. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  222. package/templates/database/package.json +1 -1
  223. package/templates/database/seed.ts +30 -2
  224. package/versions.json +35 -35
  225. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  226. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  227. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  228. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  229. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  230. package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
  231. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
  232. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
  233. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
  234. package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
  235. package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
  236. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  237. package/templates/apps/web/lib/checkout.ts +0 -93
  238. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -0,0 +1,416 @@
1
+ /**
2
+ * The Offerings fixture and seed (#8876, spec
3
+ * docs/specs/8876-the-offerings-data-model-typed-spec.md D9, AC 3) — what a
4
+ * test needs to build a workspace's shown set with no Stripe call and no
5
+ * Admin UI. Test-only: `apps/api/tsconfig.json` excludes `__tests__`, and
6
+ * jest runs only `*.test.ts`, so nothing here ships.
7
+ *
8
+ * Route suites in Epics 3, 4 and 6 import it from their own `__tests__`, as
9
+ * they import `createInMemoryPrisma`. `seedOfferings` writes through the
10
+ * Prisma delegates, so it works against that double and a real client alike,
11
+ * and refuses to run outside jest.
12
+ *
13
+ * The ids are valid ADR-0031 ids outside the sentinel range, so no fixture
14
+ * Organization reads as the deployment's default. The connection's
15
+ * ciphertext is a placeholder in the envelope shape that decrypts under no
16
+ * key: no real or key-shaped secret appears here.
17
+ */
18
+ import { normalizeCatalog, type CatalogPrice, type CatalogProduct, type OfferingsSourceFor } from "../index.js";
19
+ import type { WorkspaceShownSet } from "@working-theory/validation";
20
+
21
+ export const FIXTURE_ORG = "org_01920000-8876-7000-8000-0000000000a1";
22
+ /** The fixture Organization's workspace that shows Offerings. */
23
+ export const FIXTURE_WS = "wksp_01920000-8876-7000-8000-0000000000b1";
24
+ /** A second workspace of the same Organization, with no Offerings row. */
25
+ export const FIXTURE_WS_2 = "wksp_01920000-8876-7000-8000-0000000000b2";
26
+ /** A second Organization, with no connection. */
27
+ export const FIXTURE_ORG_2 = "org_01920000-8876-7000-8000-0000000000a2";
28
+ /** The second Organization's workspace, with a selection but no connection. */
29
+ export const FIXTURE_ORG_2_WS = "wksp_01920000-8876-7000-8000-0000000000c1";
30
+
31
+ /** Envelope-shaped (`v1.<iv>.<tag>.<ct>`), decrypting under no key. */
32
+ export const FIXTURE_CIPHERTEXT_PLACEHOLDER = "v1.Zml4dHVyZWl2ODg3.Zml4dHVyZS10YWctODg3Ng.Y2lwaGVydGV4dA";
33
+ /** A 43-character base64url routing token. */
34
+ export const FIXTURE_WEBHOOK_TOKEN = "fixtureWebhookToken8876abcdefghijklmnopqrst";
35
+ export const FIXTURE_KEY_LAST4 = "4242";
36
+
37
+ const recurring = (interval: "month" | "year", count = 1) => ({
38
+ interval,
39
+ interval_count: count,
40
+ usage_type: "licensed",
41
+ });
42
+
43
+ /**
44
+ * Stripe-shaped Products and Prices covering the contract's worked examples:
45
+ * `growth` (plan, Monthly and Yearly), `basic` (plan, one-time), `seo-audit`
46
+ * (add-on, Monthly), a quarterly Price (held `needs_lookup_key`), a Product
47
+ * with no `ailk_group` (held `group_invalid`), and one whose name carries a
48
+ * bidi override and markup (normalized, never rendered).
49
+ */
50
+ export const OFFERINGS_FIXTURE_CATALOG: { products: CatalogProduct[]; prices: CatalogPrice[] } = {
51
+ products: [
52
+ {
53
+ id: "prod_fixture_growth",
54
+ active: true,
55
+ name: "Growth",
56
+ description: "For a growing practice.\nEverything in Basic.",
57
+ marketing_features: [{ name: "Unlimited pages" }, { name: "Priority support" }],
58
+ unit_label: null,
59
+ images: ["https://example.com/growth.png"],
60
+ url: "https://example.com/growth",
61
+ metadata: { ailk_group: "plan", ailk_order: "10", ailk_terms_note: "Billed in advance." },
62
+ } as CatalogProduct,
63
+ {
64
+ id: "prod_fixture_basic",
65
+ active: true,
66
+ name: "Basic",
67
+ description: "One site, set up once.",
68
+ marketing_features: [{ name: "Five pages" }],
69
+ unit_label: "site",
70
+ metadata: { ailk_group: "plan", ailk_order: "5" },
71
+ },
72
+ {
73
+ id: "prod_fixture_seo_audit",
74
+ active: true,
75
+ name: "SEO audit",
76
+ description: null,
77
+ marketing_features: [],
78
+ unit_label: null,
79
+ metadata: { ailk_group: "addon", ailk_order: "1", ailk_note: "Delivered within a week." },
80
+ },
81
+ {
82
+ id: "prod_fixture_quarterly",
83
+ active: true,
84
+ name: "Pro quarterly",
85
+ description: "Billed every three months.",
86
+ marketing_features: [],
87
+ unit_label: null,
88
+ metadata: { ailk_group: "plan" },
89
+ },
90
+ {
91
+ id: "prod_fixture_legacy",
92
+ active: true,
93
+ name: "Legacy retainer",
94
+ description: null,
95
+ marketing_features: [],
96
+ unit_label: null,
97
+ metadata: {},
98
+ },
99
+ {
100
+ id: "prod_fixture_bidi",
101
+ active: true,
102
+ name: "‮Audit‬ <b>plus</b>",
103
+ description: null,
104
+ marketing_features: [],
105
+ unit_label: null,
106
+ metadata: { ailk_group: "addon" },
107
+ },
108
+ ],
109
+ prices: [
110
+ {
111
+ id: "price_fixture_growth_monthly",
112
+ active: true,
113
+ product: "prod_fixture_growth",
114
+ lookup_key: "growth_monthly",
115
+ type: "recurring",
116
+ recurring: recurring("month"),
117
+ billing_scheme: "per_unit",
118
+ unit_amount: 4900,
119
+ currency: "usd",
120
+ nickname: "Monthly",
121
+ } as CatalogPrice,
122
+ {
123
+ id: "price_fixture_growth_yearly",
124
+ active: true,
125
+ product: "prod_fixture_growth",
126
+ lookup_key: "growth_yearly",
127
+ type: "recurring",
128
+ recurring: recurring("year"),
129
+ billing_scheme: "per_unit",
130
+ unit_amount: 49000,
131
+ currency: "usd",
132
+ },
133
+ {
134
+ id: "price_fixture_basic_once",
135
+ active: true,
136
+ product: "prod_fixture_basic",
137
+ lookup_key: "basic_once",
138
+ type: "one_time",
139
+ recurring: null,
140
+ billing_scheme: "per_unit",
141
+ unit_amount: 9900,
142
+ currency: "usd",
143
+ },
144
+ {
145
+ id: "price_fixture_seo_audit_monthly",
146
+ active: true,
147
+ product: "prod_fixture_seo_audit",
148
+ lookup_key: "seo-audit_monthly",
149
+ type: "recurring",
150
+ recurring: recurring("month"),
151
+ billing_scheme: "per_unit",
152
+ unit_amount: 1500,
153
+ currency: "usd",
154
+ },
155
+ {
156
+ id: "price_fixture_pro_quarterly",
157
+ active: true,
158
+ product: "prod_fixture_quarterly",
159
+ lookup_key: "pro_quarterly",
160
+ type: "recurring",
161
+ recurring: recurring("month", 3),
162
+ billing_scheme: "per_unit",
163
+ unit_amount: 12000,
164
+ currency: "usd",
165
+ },
166
+ {
167
+ id: "price_fixture_legacy",
168
+ active: true,
169
+ product: "prod_fixture_legacy",
170
+ lookup_key: "wendi_retainer",
171
+ type: "recurring",
172
+ recurring: recurring("month"),
173
+ billing_scheme: "per_unit",
174
+ unit_amount: 200000,
175
+ currency: "usd",
176
+ },
177
+ {
178
+ id: "price_fixture_bidi_monthly",
179
+ active: true,
180
+ product: "prod_fixture_bidi",
181
+ lookup_key: "audit-plus_monthly",
182
+ type: "recurring",
183
+ recurring: recurring("month"),
184
+ billing_scheme: "per_unit",
185
+ unit_amount: 900,
186
+ currency: "usd",
187
+ },
188
+ ],
189
+ };
190
+
191
+ const growthPrices = [
192
+ {
193
+ priceId: "price_fixture_growth_monthly",
194
+ lookupKey: "growth_monthly",
195
+ plan: "growth",
196
+ period: "monthly",
197
+ interval: "month",
198
+ amount: 4900,
199
+ currency: "usd",
200
+ },
201
+ {
202
+ priceId: "price_fixture_growth_yearly",
203
+ lookupKey: "growth_yearly",
204
+ plan: "growth",
205
+ period: "yearly",
206
+ interval: "year",
207
+ amount: 49000,
208
+ currency: "usd",
209
+ },
210
+ ] as const;
211
+
212
+ /** What `normalizeCatalog(OFFERINGS_FIXTURE_CATALOG)` answers, in D5's order. */
213
+ export const EXPECTED_FIXTURE_OFFERINGS = [
214
+ {
215
+ productId: "prod_fixture_basic",
216
+ plan: "basic",
217
+ name: "Basic",
218
+ description: "One site, set up once.",
219
+ marketingFeatures: ["Five pages"],
220
+ unitLabel: "site",
221
+ note: null,
222
+ termsNote: null,
223
+ ailkGroup: "plan",
224
+ ailkOrder: 5,
225
+ prices: [
226
+ {
227
+ priceId: "price_fixture_basic_once",
228
+ lookupKey: "basic_once",
229
+ plan: "basic",
230
+ period: "once",
231
+ interval: null,
232
+ amount: 9900,
233
+ currency: "usd",
234
+ },
235
+ ],
236
+ },
237
+ {
238
+ productId: "prod_fixture_growth",
239
+ plan: "growth",
240
+ name: "Growth",
241
+ description: "For a growing practice.\nEverything in Basic.",
242
+ marketingFeatures: ["Unlimited pages", "Priority support"],
243
+ unitLabel: null,
244
+ note: null,
245
+ termsNote: "Billed in advance.",
246
+ ailkGroup: "plan",
247
+ ailkOrder: 10,
248
+ prices: [...growthPrices],
249
+ },
250
+ {
251
+ productId: "prod_fixture_seo_audit",
252
+ plan: "seo-audit",
253
+ name: "SEO audit",
254
+ description: null,
255
+ marketingFeatures: [],
256
+ unitLabel: null,
257
+ note: "Delivered within a week.",
258
+ termsNote: null,
259
+ ailkGroup: "addon",
260
+ ailkOrder: 1,
261
+ prices: [
262
+ {
263
+ priceId: "price_fixture_seo_audit_monthly",
264
+ lookupKey: "seo-audit_monthly",
265
+ plan: "seo-audit",
266
+ period: "monthly",
267
+ interval: "month",
268
+ amount: 1500,
269
+ currency: "usd",
270
+ },
271
+ ],
272
+ },
273
+ {
274
+ productId: "prod_fixture_bidi",
275
+ plan: "audit-plus",
276
+ name: "Audit <b>plus</b>",
277
+ description: null,
278
+ marketingFeatures: [],
279
+ unitLabel: null,
280
+ note: null,
281
+ termsNote: null,
282
+ ailkGroup: "addon",
283
+ ailkOrder: null,
284
+ prices: [
285
+ {
286
+ priceId: "price_fixture_bidi_monthly",
287
+ lookupKey: "audit-plus_monthly",
288
+ plan: "audit-plus",
289
+ period: "monthly",
290
+ interval: "month",
291
+ amount: 900,
292
+ currency: "usd",
293
+ },
294
+ ],
295
+ },
296
+ ];
297
+
298
+ /** The held list, in catalog order. */
299
+ export const EXPECTED_FIXTURE_HELD = [
300
+ { productId: "prod_fixture_quarterly", name: "Pro quarterly", reason: "needs_lookup_key" },
301
+ { productId: "prod_fixture_legacy", name: "Legacy retainer", reason: "group_invalid" },
302
+ ];
303
+
304
+ /** The Organization's enabled set: every offering but the bidi add-on. */
305
+ export const FIXTURE_ENABLED_PRODUCT_IDS = [
306
+ "prod_fixture_growth",
307
+ "prod_fixture_basic",
308
+ "prod_fixture_seo_audit",
309
+ ];
310
+ /** The first workspace's shown set (growth Recommended). */
311
+ export const FIXTURE_SHOWN_PRODUCT_IDS = [
312
+ "prod_fixture_growth",
313
+ "prod_fixture_basic",
314
+ "prod_fixture_seo_audit",
315
+ ];
316
+
317
+ /** A `sourceFor` whose source answers the fixture catalog. */
318
+ export const fixtureSourceFor: OfferingsSourceFor = () => ({
319
+ listActiveCatalog: async () => structuredClone(OFFERINGS_FIXTURE_CATALOG),
320
+ });
321
+
322
+ /** A `sourceFor` that fails the test on any call. */
323
+ export const refusingSourceFor: OfferingsSourceFor = (organizationId) => {
324
+ throw new Error(`refusingSourceFor: no catalog source may be built (asked for ${organizationId})`);
325
+ };
326
+
327
+ type Delegate = {
328
+ create(args: { data: Record<string, unknown> }): Promise<unknown>;
329
+ };
330
+
331
+ /**
332
+ * Seed the fixture rows through the Prisma delegates: two Organizations and
333
+ * three Workspaces, one `test` connection on the first Organization, its
334
+ * enabled set, and the first workspace's shown set (Recommended `growth`)
335
+ * with its snapshot built by `normalizeCatalog` at `now`. Refuses to run
336
+ * outside jest, so it can never be pointed at a dev or production database.
337
+ */
338
+ export async function seedOfferings(
339
+ prisma: unknown,
340
+ { now = new Date() }: { now?: Date } = {},
341
+ ): Promise<void> {
342
+ if (!process.env.JEST_WORKER_ID) {
343
+ throw new Error("seedOfferings runs only under jest");
344
+ }
345
+ const db = prisma as Record<string, Delegate>;
346
+ for (const [id, slug] of [
347
+ [FIXTURE_ORG, "fixture-8876-a"],
348
+ [FIXTURE_ORG_2, "fixture-8876-b"],
349
+ ] as const) {
350
+ await db.organization!.create({ data: { id, name: slug, slug } });
351
+ }
352
+ for (const [id, organizationId, slug] of [
353
+ [FIXTURE_WS, FIXTURE_ORG, "one"],
354
+ [FIXTURE_WS_2, FIXTURE_ORG, "two"],
355
+ [FIXTURE_ORG_2_WS, FIXTURE_ORG_2, "one"],
356
+ ] as const) {
357
+ await db.workspace!.create({ data: { id, organizationId, name: slug, slug } });
358
+ }
359
+ await db.stripeConnection!.create({
360
+ data: {
361
+ organizationId: FIXTURE_ORG,
362
+ mode: "test",
363
+ keyLast4: FIXTURE_KEY_LAST4,
364
+ credentialCiphertext: FIXTURE_CIPHERTEXT_PLACEHOLDER,
365
+ webhookToken: FIXTURE_WEBHOOK_TOKEN,
366
+ webhookSecretCiphertext: null,
367
+ liveAt: null,
368
+ },
369
+ });
370
+ await db.organizationOfferings!.create({
371
+ data: { organizationId: FIXTURE_ORG, enabledProductIds: [...FIXTURE_ENABLED_PRODUCT_IDS] },
372
+ });
373
+ const { offerings } = normalizeCatalog(OFFERINGS_FIXTURE_CATALOG);
374
+ const shown = new Set(FIXTURE_SHOWN_PRODUCT_IDS);
375
+ await db.workspaceOfferings!.create({
376
+ data: {
377
+ workspaceId: FIXTURE_WS,
378
+ shownProductIds: [...FIXTURE_SHOWN_PRODUCT_IDS],
379
+ recommendedPlanProductId: "prod_fixture_growth",
380
+ recommendedAddonProductId: null,
381
+ snapshot: { v: 1, offerings: offerings.filter((o) => shown.has(o.productId)) },
382
+ snapshotAt: now,
383
+ },
384
+ });
385
+ // The second Organization's workspace has a selection, but its
386
+ // Organization has no connection, so it serves nothing.
387
+ await db.workspaceOfferings!.create({
388
+ data: {
389
+ workspaceId: FIXTURE_ORG_2_WS,
390
+ shownProductIds: ["prod_fixture_growth"],
391
+ recommendedPlanProductId: null,
392
+ recommendedAddonProductId: null,
393
+ snapshot: null,
394
+ snapshotAt: null,
395
+ },
396
+ });
397
+ }
398
+
399
+ /** What `getWorkspaceShownSet(FIXTURE_WS, { now })` answers for the seed. */
400
+ export function expectedShownSet(now: Date): WorkspaceShownSet {
401
+ return {
402
+ workspaceId: FIXTURE_WS,
403
+ organizationId: FIXTURE_ORG,
404
+ productIds: [...FIXTURE_SHOWN_PRODUCT_IDS],
405
+ recommended: { plan: "prod_fixture_growth", addon: null },
406
+ offerings: EXPECTED_FIXTURE_OFFERINGS.filter((o) =>
407
+ FIXTURE_SHOWN_PRODUCT_IDS.includes(o.productId),
408
+ ).map((o) => ({ ...o, recommended: o.productId === "prod_fixture_growth" })) as WorkspaceShownSet["offerings"],
409
+ state: "current",
410
+ snapshotAt: now.toISOString(),
411
+ };
412
+ }
413
+
414
+ /** The seed's shown set at a fixed `now`, for suites that pin the clock. */
415
+ export const FIXTURE_NOW = new Date("2026-10-20T12:00:00.000Z");
416
+ export const EXPECTED_SHOWN_SET: WorkspaceShownSet = expectedShownSet(FIXTURE_NOW);