create-ailk 0.22.0 → 0.24.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 (287) hide show
  1. package/README.md +27 -14
  2. package/component-catalog.json +149 -5
  3. package/component-catalog.md +7 -6
  4. package/dist/add-module.js +17 -10
  5. package/dist/add-route.d.ts +55 -1
  6. package/dist/add-route.js +301 -13
  7. package/dist/cli.js +27 -10
  8. package/dist/derive-architecture.d.ts +7 -5
  9. package/dist/derive-architecture.js +26 -32
  10. package/dist/lib/design-checks.js +4 -2
  11. package/dist/lib/env-catalog.js +0 -4
  12. package/dist/lib/npm-auth.d.ts +9 -0
  13. package/dist/lib/npm-auth.js +48 -0
  14. package/dist/lib/scaffold-render-sweep.d.ts +36 -2
  15. package/dist/lib/scaffold-render-sweep.js +50 -8
  16. package/dist/module-architecture.js +115 -10
  17. package/dist/paid-sections.d.ts +21 -2
  18. package/dist/paid-sections.js +69 -7
  19. package/dist/seed-flows.d.ts +4 -1
  20. package/dist/seed-flows.js +50 -4
  21. package/dist/spoke-checkout.d.ts +38 -35
  22. package/dist/spoke-checkout.js +66 -229
  23. package/dist/starter-grid.d.ts +0 -3
  24. package/dist/starter-grid.js +24 -14
  25. package/dist/surfaces.js +33 -12
  26. package/dist/sync-routes.d.ts +68 -5
  27. package/dist/sync-routes.js +455 -29
  28. package/dist/template-record.d.ts +1 -1
  29. package/dist/template-record.js +3 -2
  30. package/dist/update.js +10 -0
  31. package/package.json +3 -3
  32. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  33. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  35. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  38. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  39. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  40. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  41. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  42. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  43. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  44. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  45. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  46. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  47. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  48. package/templates/.env.example +13 -7
  49. package/templates/.github/workflows/ci.yml +13 -0
  50. package/templates/CLAUDE.md +4 -2
  51. package/templates/README.md +1 -0
  52. package/templates/_gitignore +4 -0
  53. package/templates/apps/api/.env.example +34 -0
  54. package/templates/apps/api/CLAUDE.md +94 -19
  55. package/templates/apps/api/package.json +1 -0
  56. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  57. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  58. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  59. package/templates/apps/api/src/config/index.ts +109 -0
  60. package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
  61. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  62. package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
  63. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  64. package/templates/apps/api/src/lib/site.ts +4 -1
  65. package/templates/apps/api/src/lib/stripe.ts +77 -4
  66. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  67. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  68. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  69. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  70. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
  71. package/templates/apps/api/src/openapi/spec.ts +343 -7
  72. package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
  73. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  74. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
  75. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  76. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  77. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  78. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  79. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
  80. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  81. package/templates/apps/api/src/routes/consent/account.ts +4 -2
  82. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
  83. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  84. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  85. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  86. package/templates/apps/api/src/routes/content/get-page.ts +34 -119
  87. package/templates/apps/api/src/routes/content/index.ts +16 -2
  88. package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
  89. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  90. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  91. package/templates/apps/api/src/routes/content/search.ts +16 -45
  92. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  93. package/templates/apps/api/src/routes/leads/index.ts +377 -308
  94. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
  95. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
  96. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
  97. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  98. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
  99. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  100. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  101. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
  102. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  103. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  104. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  105. package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
  106. package/templates/apps/api/src/server.ts +165 -51
  107. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
  108. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  109. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
  110. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  111. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
  112. package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
  113. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
  114. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
  115. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
  116. package/templates/apps/api/src/services/abandon-email.ts +28 -5
  117. package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
  118. package/templates/apps/api/src/services/entitlements.ts +157 -1
  119. package/templates/apps/api/src/services/lead-routing.ts +113 -58
  120. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
  121. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  122. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
  123. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  124. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
  125. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
  126. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  127. package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
  128. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
  129. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  130. package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
  131. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  132. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  133. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  134. package/templates/apps/api/src/services/person.ts +147 -9
  135. package/templates/apps/api/src/services/plan-offerings.ts +96 -30
  136. package/templates/apps/api/src/services/project-listings.ts +43 -31
  137. package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
  138. package/templates/apps/mcp/CLAUDE.md +32 -18
  139. package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
  140. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  141. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  142. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  143. package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
  144. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
  145. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
  146. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
  147. package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
  148. package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
  149. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  150. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
  151. package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
  152. package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
  153. package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
  154. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  155. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  156. package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
  157. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
  158. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  159. package/templates/apps/mcp/package.json +1 -0
  160. package/templates/apps/mcp/src/abuse/index.ts +18 -11
  161. package/templates/apps/mcp/src/http-transport.ts +47 -9
  162. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
  163. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
  164. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  165. package/templates/apps/mcp/src/lib/site-config.ts +53 -0
  166. package/templates/apps/mcp/src/server.ts +5 -0
  167. package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
  168. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  169. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  170. package/templates/apps/mcp/src/tools/index.ts +22 -21
  171. package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
  172. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  173. package/templates/apps/mcp/src/tools/read_content.ts +46 -40
  174. package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
  175. package/templates/apps/mcp/src/tools/search_content.ts +20 -24
  176. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
  177. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  178. package/templates/apps/web/.env.example +10 -31
  179. package/templates/apps/web/CLAUDE.md +324 -42
  180. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
  181. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
  182. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
  183. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  184. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
  185. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
  186. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
  187. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
  188. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  189. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  190. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
  191. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
  192. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
  193. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  194. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  195. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
  196. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
  197. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
  198. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  199. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
  200. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
  201. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  202. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  203. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  204. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  205. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  206. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
  207. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  208. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  209. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  210. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  211. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  212. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  213. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  214. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  215. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  216. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  217. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  218. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  219. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  220. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  221. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  222. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  223. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  224. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  225. package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
  226. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  227. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
  228. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  229. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  230. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
  231. package/templates/apps/web/lib/auth-routes.ts +36 -2
  232. package/templates/apps/web/lib/checkout-config.ts +9 -20
  233. package/templates/apps/web/lib/deck-page.ts +1 -0
  234. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  235. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  236. package/templates/apps/web/lib/magnet-routes.ts +44 -0
  237. package/templates/apps/web/lib/modules.generated.ts +9 -0
  238. package/templates/apps/web/lib/newsletter.actions.ts +205 -0
  239. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  240. package/templates/apps/web/lib/posting-title.ts +17 -0
  241. package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
  242. package/templates/apps/web/messages/en.json +49 -15
  243. package/templates/apps/web/middleware.ts +12 -2
  244. package/templates/apps/web/security-headers.config.ts +17 -0
  245. package/templates/content/en/BlogList/blog.mdx +4 -71
  246. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  247. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  248. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  249. package/templates/content/en/BookCall/start-project.mdx +1 -17
  250. package/templates/content/en/ContactPage/contact.mdx +6 -63
  251. package/templates/content/en/HomePage/home.mdx +3 -21
  252. package/templates/database/CHANGELOG.md +51 -0
  253. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  254. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  255. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  256. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  257. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  258. package/templates/database/__tests__/seed.test.ts +63 -1
  259. package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
  260. package/templates/database/auth/schema.prisma +4 -0
  261. package/templates/database/config/schema.prisma +91 -0
  262. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  263. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  264. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  265. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  266. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  267. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
  268. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
  269. package/templates/database/package.json +1 -1
  270. package/templates/database/seed.ts +57 -2
  271. package/templates/tools/lint-architecture.ts +205 -1
  272. package/versions.json +35 -35
  273. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  274. package/templates/apps/api/src/routes/content/slug.ts +0 -9
  275. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  276. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  277. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  278. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  279. package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
  280. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
  281. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
  282. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
  283. package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
  284. package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
  285. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  286. package/templates/apps/web/lib/checkout.ts +0 -93
  287. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -50,7 +50,7 @@ jest.mock("../../../lib/prisma.js", () => ({
50
50
  },
51
51
  user: { findUnique: jest.fn() },
52
52
  projectListing: { findMany: jest.fn() },
53
- waitlistScore: { count: jest.fn() },
53
+ waitlistScore: { groupBy: jest.fn() },
54
54
  },
55
55
  }));
56
56
 
@@ -258,9 +258,16 @@ function installFixtureMocks(): void {
258
258
  },
259
259
  );
260
260
 
261
- (prisma.waitlistScore.count as jest.Mock).mockImplementation(
262
- ({ where }: { where: { siteId: string; experimentKey: string } }) =>
263
- Promise.resolve(WAITLIST_COUNTS[where.siteId]?.[where.experimentKey] ?? 0),
261
+ (prisma.waitlistScore.groupBy as jest.Mock).mockImplementation(
262
+ ({ where }: { where: { siteId: string; experimentKey: { in: string[] } } }) =>
263
+ Promise.resolve(
264
+ where.experimentKey.in
265
+ .filter((experimentKey) => (WAITLIST_COUNTS[where.siteId]?.[experimentKey] ?? 0) > 0)
266
+ .map((experimentKey) => ({
267
+ experimentKey,
268
+ _count: { _all: WAITLIST_COUNTS[where.siteId]![experimentKey] },
269
+ })),
270
+ ),
264
271
  );
265
272
  }
266
273
 
@@ -378,7 +385,7 @@ describe("GET /v1/project-listings/me — the empty case is not a refusal", () =
378
385
  it("no waitlist count is read when the caller owns nothing", async () => {
379
386
  await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_cleo") });
380
387
 
381
- expect(prisma.waitlistScore.count).not.toHaveBeenCalled();
388
+ expect(prisma.waitlistScore.groupBy).not.toHaveBeenCalled();
382
389
  });
383
390
  });
384
391
 
@@ -419,13 +426,17 @@ describe("GET /v1/project-listings/me — the response shape", () => {
419
426
  // One shared site id would have reported 0 for one of them.
420
427
  expect(byId["prjl_ada_one"]).toBe(7);
421
428
  expect(byId["prjl_ada_two"]).toBe(3);
422
- expect(prisma.waitlistScore.count).toHaveBeenCalledWith({
429
+ expect(prisma.waitlistScore.groupBy).toHaveBeenCalledWith({
430
+ by: ["experimentKey"],
423
431
  // #6258 AC-6 — the fenced read now filters the CONSTRAINED column beside
424
432
  // `siteId`; the two are equal by the workspace_site_agree CHECK.
425
- where: { siteId: SITE_A, workspaceId: SITE_A, experimentKey: "atlas-waitlist" },
433
+ where: { siteId: SITE_A, workspaceId: SITE_A, experimentKey: { in: ["atlas-waitlist"] } },
434
+ _count: { _all: true },
426
435
  });
427
- expect(prisma.waitlistScore.count).toHaveBeenCalledWith({
428
- where: { siteId: SITE_B, workspaceId: SITE_B, experimentKey: "atlas-two-waitlist" },
436
+ expect(prisma.waitlistScore.groupBy).toHaveBeenCalledWith({
437
+ by: ["experimentKey"],
438
+ where: { siteId: SITE_B, workspaceId: SITE_B, experimentKey: { in: ["atlas-two-waitlist"] } },
439
+ _count: { _all: true },
429
440
  });
430
441
  });
431
442
 
@@ -61,7 +61,7 @@ jest.mock("../../../lib/prisma.js", () => ({
61
61
  projectListing: { findMany: jest.fn() },
62
62
  siteListingConfig: { findUnique: jest.fn() },
63
63
  waitlistExperiment: { findMany: jest.fn() },
64
- waitlistScore: { count: jest.fn() },
64
+ waitlistScore: { groupBy: jest.fn() },
65
65
  },
66
66
  }));
67
67
 
@@ -236,9 +236,13 @@ function installFixtureMocks(): void {
236
236
  // count is keyed on the `experimentKey` the listing already holds, so the
237
237
  // id-to-key hop this route used to make is gone. A test below asserts it
238
238
  // stays gone.
239
- (prisma.waitlistScore.count as jest.Mock).mockImplementation(
240
- ({ where }: { where: { experimentKey: string } }) =>
241
- Promise.resolve(COUNTS_BY_KEY[where.experimentKey] ?? 0),
239
+ (prisma.waitlistScore.groupBy as jest.Mock).mockImplementation(
240
+ ({ where }: { where: { experimentKey: { in: string[] } } }) =>
241
+ Promise.resolve(
242
+ where.experimentKey.in
243
+ .filter((experimentKey) => (COUNTS_BY_KEY[experimentKey] ?? 0) > 0)
244
+ .map((experimentKey) => ({ experimentKey, _count: { _all: COUNTS_BY_KEY[experimentKey] } })),
245
+ ),
242
246
  );
243
247
  }
244
248
 
@@ -413,10 +417,17 @@ describe("GET /v1/project-listings/public", () => {
413
417
 
414
418
  it("counts the waitlist through the listing's own experimentKey, in one hop", async () => {
415
419
  await app.inject({ method: "GET", url: `/v1/project-listings/public?siteKey=${SITE_KEY}` });
416
- expect(prisma.waitlistScore.count).toHaveBeenCalledWith({
420
+ expect(prisma.waitlistScore.groupBy).toHaveBeenCalledTimes(1);
421
+ expect(prisma.waitlistScore.groupBy).toHaveBeenCalledWith({
422
+ by: ["experimentKey"],
417
423
  // #6258 AC-6 — the fenced read now filters the CONSTRAINED column
418
424
  // beside `siteId`; the two are equal by the workspace_site_agree CHECK.
419
- where: { siteId: SITE, workspaceId: SITE, experimentKey: "exp-newest" },
425
+ where: {
426
+ siteId: SITE,
427
+ workspaceId: SITE,
428
+ experimentKey: { in: expect.arrayContaining(["exp-newest"]) },
429
+ },
430
+ _count: { _all: true },
420
431
  });
421
432
  // The id-to-key translation read is gone with #5036 D6d.
422
433
  expect(prisma.waitlistExperiment.findMany).not.toHaveBeenCalled();
@@ -885,7 +885,7 @@ describe("a captured draft answers its listicle's Application fields (#7521 AC-2
885
885
  ({ where }: { where: Record<string, unknown> }) =>
886
886
  Promise.resolve(flows.filter((f) => matches(f, where))),
887
887
  );
888
- (prisma.waitlistScore.count as jest.Mock).mockResolvedValue(0);
888
+ (prisma.waitlistScore.groupBy as jest.Mock).mockResolvedValue([]);
889
889
  app = Fastify();
890
890
  await app.register(startProjectListingRoute);
891
891
  await app.register(patchProjectListingDraftRoute);
@@ -199,6 +199,72 @@ through `metadata.site`.
199
199
  site, which silences the hub's dunning record for that subscription — the
200
200
  same key can already cancel or re-price it outright.
201
201
 
202
+ ## Per-Organization endpoint (#8932)
203
+
204
+ `POST /v1/webhooks/stripe/:connectionToken` (`stripe-offerings.ts`) is the
205
+ receiver for one Organization's own Stripe connection — the Offerings money
206
+ path (spec `docs/specs/8932-the-per-organization-stripe-webhook-spec.md`,
207
+ contract `docs/specs/8879-the-offerings-security-contract-key-spec.md` W1,
208
+ W3–W11, G5, T8). The 43-character token selects one `StripeConnection` row
209
+ (`findStripeConnectionByWebhookToken`, #8907); that row's decrypted secret
210
+ verifies the signature through `verifyWebhookSignatureKeyless` (no env key),
211
+ and its `organizationId` is the only Organization the event can act for.
212
+ Registered beside the legacy route on every build, with its own 100/min
213
+ per-IP bucket; the commerce claim is read at call time.
214
+
215
+ **The ladder** — the first step that answers ends the request; every `200`
216
+ body is `{ received: true }` and the outcome is the log line's only:
217
+
218
+ | # | Step | Answer | Seen |
219
+ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------- |
220
+ | 1 | the token resolves to no row (or is malformed) | `404 not-found`, logs nothing | no |
221
+ | 1a | the token lookup rejects | `503 webhook-unavailable` | no |
222
+ | 2 | the row has no usable secret | `503 webhook-not-configured` | no |
223
+ | 3–6 | signature settled, then the claim (as the legacy route): `undetermined` → `503` + `Retry-After: 300`; `unclaimed` → signed `200 dropped_no_commerce_claim`, unsigned the module-disabled `404`; then the held `400` | as stated | no |
224
+ | 7 | `event.livemode` is not the row's mode | `200 livemode_mismatch` | no |
225
+ | 8 | not `checkout.session.completed` / `.async_payment_succeeded` on a session object | `200 ignored_type` | no |
226
+ | 9–11 | the hub's session record (#8930): rejects / unreadable / none | `503 record-unavailable` / `500 record-unreadable` / `200 unrecorded` | no |
227
+ | 12 | another Organization's record, or `metadata.site` is not the record's workspace | `200 foreign_organization` | no |
228
+ | 13 | the record's mode is not the row's | `200 livemode_mismatch` | no |
229
+ | 14 | already seen for this Organization | `200 duplicate` | — |
230
+ | 15 | a `completed` session neither `paid` nor `no_payment_required` | `200 unpaid` | yes |
231
+ | 16 | the connection's client or its `listLineItems` call fails | `503 stripe-unavailable` (never the env key) | no |
232
+ | 17 | not exactly one line item whose Price id is the record's | `200 offering_mismatch` | yes |
233
+ | 18 | `routeLead` (below) | `200 fulfilled` / `already_fulfilled` / `unfulfillable`, else `503 fulfilment-unavailable` | yes / no |
234
+
235
+ **The lead.** One per session: `routeLead` with the idempotency key
236
+ `"stripe-offerings " + sessionId` — a space, which no `Idempotency-Key`
237
+ header can carry, so no anonymous lead can take the key first — and
238
+ `offeringsCheckout: true`. Its offering is the record's FULL lookup key,
239
+ never `metadata.ailk_offering`. A live session's lead is marked
240
+ `spokeCheckout`, the hold #8793's `releaseHeldCheckoutPlan` releases at the
241
+ buyer's verified first link; no path here writes `Person.planOffering`. A
242
+ test-mode session's lead is written unmarked: no hold, purchase message,
243
+ email or consent grant. A replay or a conflict on that key is
244
+ `already_fulfilled`; a lead the database did not take is a `503`, so Stripe
245
+ retries.
246
+
247
+ **The legacy endpoint steps aside (W8).** `POST /v1/webhooks/stripe` acks
248
+ any event carrying an `ailk_offering` key — in a session's metadata or any
249
+ site-stamp source above — with `200 { received: true }` and
250
+ `outcome: "offerings_path"`, **before** `markSeen`, and writes nothing.
251
+ Every event without the key takes the legacy path unchanged.
252
+
253
+ **Two dedup stores.** The legacy route keeps `hasSeen` / `markSeen`; the
254
+ per-Organization route uses its own `hasSeenForOrganization` /
255
+ `markSeenForOrganization`, keyed `<organizationId>:<eventId>`. Neither
256
+ endpoint's store can swallow the other's event. Both are in-memory per
257
+ instance; the per-Organization route's correctness rests on the lead's
258
+ database key.
259
+
260
+ **Logs.** Each line carries `organizationId`, `eventType`, an 8-character
261
+ `eventIdPrefix` and `outcome` (fewer before an event is verified) — never
262
+ the token, the secret, a full Stripe id, an email, a lookup key, a metadata
263
+ value or Stripe product text. Request and error lines log the path with
264
+ the token replaced whole by `:connectionToken` (`redactLogPath`).
265
+
266
+ **W11.** No billing identity is bound from a connection's event.
267
+
202
268
  ## Dunning (#6034)
203
269
 
204
270
  Two arms record a per-subscriber lifecycle state so a failed renewal is no
@@ -179,6 +179,7 @@ describe("AC-1 — completed stamps the row once, guarded, beside the legacy tas
179
179
  });
180
180
  expect(prisma.user.findFirst).toHaveBeenCalledWith({
181
181
  where: { billingProvider: "stripe", billingCustomerId: CUSTOMER_ID },
182
+ select: { id: true },
182
183
  });
183
184
  expect(prisma.user.findUnique).not.toHaveBeenCalled();
184
185
  });