create-ailk 0.1.0 → 0.2.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 (620) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +456 -0
  3. package/dist/cli.js +92 -16
  4. package/dist/component-catalog.d.ts +11 -0
  5. package/dist/component-catalog.js +39 -0
  6. package/dist/copy.d.ts +7 -0
  7. package/dist/copy.js +24 -0
  8. package/dist/derive-architecture.d.ts +13 -0
  9. package/dist/derive-architecture.js +238 -0
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +19 -2
  12. package/dist/lib/extract-module-bundle.d.ts +16 -0
  13. package/dist/lib/extract-module-bundle.js +184 -0
  14. package/dist/lib/fetch-module.d.ts +55 -0
  15. package/dist/lib/fetch-module.js +125 -0
  16. package/dist/lib/module-license-gate.d.ts +48 -0
  17. package/dist/lib/module-license-gate.js +67 -0
  18. package/dist/lib/module-tier.d.ts +35 -0
  19. package/dist/lib/module-tier.js +37 -0
  20. package/dist/module-architecture.d.ts +175 -0
  21. package/dist/module-architecture.js +663 -0
  22. package/dist/parse-args.d.ts +47 -1
  23. package/dist/parse-args.js +143 -2
  24. package/dist/programmatic.d.ts +160 -3
  25. package/dist/programmatic.js +196 -7
  26. package/dist/surfaces.d.ts +211 -0
  27. package/dist/surfaces.js +532 -0
  28. package/dist/sync-routes.d.ts +92 -0
  29. package/dist/sync-routes.js +350 -0
  30. package/package.json +12 -5
  31. package/templates/.claude/rules/architecture.md +6 -2
  32. package/templates/.claude/skills/README.md +9 -9
  33. package/templates/.claude/skills/add-schema/SKILL.md +14 -7
  34. package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
  35. package/templates/.claude/skills/provision-config/SKILL.md +1 -1
  36. package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
  37. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
  38. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
  39. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
  40. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
  41. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
  42. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
  43. package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
  44. package/templates/.env.example +126 -2
  45. package/templates/CLAUDE.md +11 -5
  46. package/templates/README.md +1 -1
  47. package/templates/apps/api/.env.example +38 -0
  48. package/templates/apps/api/CLAUDE.md +78 -1
  49. package/templates/apps/api/LICENSE +201 -0
  50. package/templates/apps/api/api/index.ts +6 -141
  51. package/templates/apps/api/jest.config.cjs +16 -0
  52. package/templates/apps/api/package.json +22 -6
  53. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
  54. package/templates/apps/api/src/__tests__/server.test.ts +46 -0
  55. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
  56. package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
  57. package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
  58. package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
  59. package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
  60. package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
  61. package/templates/apps/api/src/bin/seed-presets.ts +58 -0
  62. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
  63. package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
  64. package/templates/apps/api/src/cli.ts +5 -11
  65. package/templates/apps/api/src/config/index.ts +35 -0
  66. package/templates/apps/api/src/config/modules.ts +43 -0
  67. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
  68. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
  69. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
  70. package/templates/apps/api/src/lib/site.ts +16 -0
  71. package/templates/apps/api/src/lib/stripe.ts +22 -0
  72. package/templates/apps/api/src/lib/tenant-db.ts +225 -0
  73. package/templates/apps/api/src/lib/ws-token.ts +91 -0
  74. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
  75. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
  77. package/templates/apps/api/src/middleware/auth.ts +43 -8
  78. package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
  79. package/templates/apps/api/src/middleware/tenant.ts +99 -0
  80. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
  81. package/templates/apps/api/src/openapi/spec.ts +312 -3
  82. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
  83. package/templates/apps/api/src/routes/aeo/index.ts +107 -0
  84. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
  85. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
  86. package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
  87. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
  88. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
  89. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
  90. package/templates/apps/api/src/routes/billing/index.ts +36 -0
  91. package/templates/apps/api/src/routes/billing/portal.ts +182 -0
  92. package/templates/apps/api/src/routes/billing/read.ts +75 -0
  93. package/templates/apps/api/src/routes/billing/usage.ts +153 -0
  94. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
  95. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
  96. package/templates/apps/api/src/routes/content/create.ts +1 -0
  97. package/templates/apps/api/src/routes/content/delete.ts +1 -0
  98. package/templates/apps/api/src/routes/content/index.ts +42 -6
  99. package/templates/apps/api/src/routes/content/update.ts +1 -0
  100. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
  101. package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
  102. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
  103. package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
  104. package/templates/apps/api/src/routes/flows/README.md +147 -0
  105. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
  106. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
  107. package/templates/apps/api/src/routes/flows/index.ts +202 -0
  108. package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
  109. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
  110. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
  111. package/templates/apps/api/src/routes/leads/index.ts +43 -9
  112. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
  113. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
  114. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
  120. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
  121. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  122. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  123. package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
  124. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  125. package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
  126. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  127. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  128. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  129. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  130. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  131. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  132. package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
  133. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  134. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  135. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  136. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  137. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  138. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  140. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  141. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  143. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  144. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  145. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  146. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  147. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  148. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  149. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  150. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  151. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  152. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  153. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  154. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  155. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  156. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  157. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  158. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  159. package/templates/apps/api/src/server.ts +307 -7
  160. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  161. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  162. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  163. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  164. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  165. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  166. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  167. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  168. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  169. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  170. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  171. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  172. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  173. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
  174. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  175. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  176. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  177. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  178. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  179. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  180. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  181. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  182. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  183. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  184. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  185. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  186. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  187. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  188. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  189. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  190. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  191. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  192. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  193. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  194. package/templates/apps/api/src/services/consent.ts +236 -0
  195. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  196. package/templates/apps/api/src/services/entitlements.ts +102 -0
  197. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  198. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  199. package/templates/apps/api/src/services/flow-engine.ts +1296 -0
  200. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  201. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  202. package/templates/apps/api/src/services/listing-config.ts +102 -0
  203. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  204. package/templates/apps/api/src/services/listing-promotion.ts +342 -0
  205. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  206. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  207. package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
  208. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  209. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  210. package/templates/apps/api/src/services/project-listings.ts +1472 -0
  211. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  212. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  213. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  214. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  215. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  216. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  217. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  218. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  219. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  220. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  221. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  222. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  223. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  224. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  225. package/templates/apps/api/src/vercel-handler.ts +165 -0
  226. package/templates/apps/api/tsconfig.json +0 -12
  227. package/templates/apps/mcp/CLAUDE.md +19 -9
  228. package/templates/apps/mcp/LICENSE +201 -0
  229. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  230. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  231. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  232. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  233. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  234. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  235. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  236. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  237. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  238. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  239. package/templates/apps/mcp/package.json +6 -4
  240. package/templates/apps/mcp/src/config/modules.ts +37 -0
  241. package/templates/apps/mcp/src/server.ts +109 -18
  242. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  243. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  244. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  245. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  246. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  247. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  248. package/templates/apps/mcp/src/tools/index.ts +25 -0
  249. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  250. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  252. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  253. package/templates/apps/mcp/tsconfig.json +1 -9
  254. package/templates/apps/web/.env.example +44 -0
  255. package/templates/apps/web/CLAUDE.md +28 -0
  256. package/templates/apps/web/LICENSE +201 -0
  257. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  258. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  259. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  260. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  261. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  262. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  263. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  264. package/templates/apps/web/__tests__/setup.ts +22 -32
  265. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  266. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  267. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  268. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  269. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  270. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  271. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  279. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  280. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  281. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  282. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  283. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  284. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  285. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  286. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  287. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  288. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  289. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  290. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  291. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  292. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  293. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  294. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  295. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  296. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  297. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  298. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  299. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  300. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  301. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  302. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  303. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  304. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  305. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  306. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  307. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  308. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  309. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  310. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  311. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  312. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  313. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  314. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  315. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  316. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  317. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  318. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  323. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  324. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  325. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  326. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  327. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  328. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  329. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  330. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  331. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  332. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  333. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  334. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  335. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  336. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  337. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  339. package/templates/apps/web/app/[locale]/layout.tsx +127 -16
  340. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  341. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  342. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  343. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  344. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  345. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  346. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  347. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  348. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  349. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  350. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  351. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  352. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  353. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  354. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  355. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  356. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  357. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  358. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  359. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  360. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  361. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  362. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  363. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  364. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  365. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  366. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  367. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  370. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  371. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  372. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  373. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  374. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  375. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  376. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  377. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  378. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  379. package/templates/apps/web/app/globals.css +35 -0
  380. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  381. package/templates/apps/web/app/og/route.tsx +10 -2
  382. package/templates/apps/web/auth.ts +8 -0
  383. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  384. package/templates/apps/web/content/en/blog/README.txt +25 -0
  385. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  386. package/templates/apps/web/jest.config.cjs +90 -27
  387. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  388. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  389. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  390. package/templates/apps/web/lib/account-config.ts +13 -0
  391. package/templates/apps/web/lib/server-api.ts +85 -0
  392. package/templates/apps/web/lib/site-brand.tsx +121 -0
  393. package/templates/apps/web/lib/site-theme.ts +74 -0
  394. package/templates/apps/web/messages/en.json +275 -7
  395. package/templates/apps/web/middleware.ts +11 -3
  396. package/templates/apps/web/next.config.mjs +8 -2
  397. package/templates/apps/web/package.json +17 -7
  398. package/templates/apps/web/postcss.config.mjs +23 -0
  399. package/templates/apps/web/public/.well-known/security.txt +3 -3
  400. package/templates/apps/web/src/lib/routes.ts +15 -2
  401. package/templates/apps/web/tsconfig.json +1 -16
  402. package/templates/content/_site.mdx +36 -25
  403. package/templates/content/en/AboutPage/about.mdx +24 -14
  404. package/templates/content/en/ContactPage/contact.mdx +23 -14
  405. package/templates/content/en/FAQPage/faq.mdx +10 -392
  406. package/templates/content/en/HomePage/home.mdx +14 -285
  407. package/templates/database/CHANGELOG.md +443 -0
  408. package/templates/database/LICENSE +201 -0
  409. package/templates/database/README.md +3 -1
  410. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  411. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  412. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  413. package/templates/database/__tests__/seed.test.ts +88 -0
  414. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  415. package/templates/database/auth/schema.prisma +521 -0
  416. package/templates/database/config/schema.prisma +234 -0
  417. package/templates/database/content/schema.prisma +263 -0
  418. package/templates/database/flows/listing-diagnostic.ts +182 -0
  419. package/templates/database/flows/presets.ts +315 -0
  420. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  421. package/templates/database/inbox/schema.prisma +574 -3
  422. package/templates/database/jest.config.cjs +8 -0
  423. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  424. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  425. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  426. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  427. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  428. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  429. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  430. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  431. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  432. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  433. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  434. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  435. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  436. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  437. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  438. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  439. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  440. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  441. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  442. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  443. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  444. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  445. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  446. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  447. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  448. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  449. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  450. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  451. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  452. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  453. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  454. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  455. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  456. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  457. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  458. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  459. package/templates/database/ops/schema.prisma +27 -0
  460. package/templates/database/package.json +42 -4
  461. package/templates/database/prisma.config.ts +1 -1
  462. package/templates/database/schema.prisma +19 -435
  463. package/templates/database/seed-test.ts +14 -4
  464. package/templates/database/seed.ts +148 -0
  465. package/templates/database/tenancy.ts +45 -0
  466. package/templates/database/tsconfig.flows.json +15 -0
  467. package/templates/database/tsconfig.json +7 -1
  468. package/templates/database/tsconfig.tenancy.json +14 -0
  469. package/templates/package.json +30 -55
  470. package/templates/pnpm-workspace.yaml +0 -3
  471. package/templates/project.yaml +1 -1
  472. package/templates/tsconfig.json +1 -37
  473. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  474. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  475. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  476. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  477. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  478. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  479. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  480. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  481. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  482. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  483. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  484. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  485. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  486. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  487. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  488. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  489. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  490. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  491. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  492. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  493. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  494. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  495. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  496. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  497. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  498. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  499. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  500. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  501. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  502. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  503. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  504. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  505. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  506. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  507. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  508. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  509. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  510. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  511. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  512. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  513. package/templates/content/.gitkeep +0 -0
  514. package/templates/content/CLAUDE.md +0 -85
  515. package/templates/content/ar/HomePage/home.mdx +0 -123
  516. package/templates/content/ar/_site.mdx +0 -35
  517. package/templates/content/de/_site.mdx +0 -29
  518. package/templates/content/en/.gitkeep +0 -0
  519. package/templates/content/en/AboutPage/about.schema.json +0 -82
  520. package/templates/content/en/BookCall/book-call.mdx +0 -19
  521. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  522. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  523. package/templates/content/en/CareersList/careers.mdx +0 -24
  524. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  525. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  526. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  527. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  528. package/templates/content/en/ClientsList/clients.mdx +0 -38
  529. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  530. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  531. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  532. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  533. package/templates/content/en/CoursesList/courses.mdx +0 -22
  534. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  535. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  536. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  537. package/templates/content/en/EventsList/events.mdx +0 -26
  538. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  539. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  540. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  541. package/templates/content/en/HomePage/home.schema.json +0 -88
  542. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  543. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  544. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  545. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  546. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  547. package/templates/content/en/Integration/github-actions.mdx +0 -31
  548. package/templates/content/en/Integration/openai-api.mdx +0 -26
  549. package/templates/content/en/Integration/vercel.mdx +0 -32
  550. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  551. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  552. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  553. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  554. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  555. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  556. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  557. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  558. package/templates/content/en/LandingPage/developers.mdx +0 -19
  559. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  560. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  561. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  562. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  563. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  564. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  565. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  566. package/templates/content/en/LandingPage/platform.mdx +0 -369
  567. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  568. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  569. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  570. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  571. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  572. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  573. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  574. package/templates/content/en/Organization/site-owner.mdx +0 -16
  575. package/templates/content/en/Partner/prisma.mdx +0 -31
  576. package/templates/content/en/Partner/supabase.mdx +0 -30
  577. package/templates/content/en/Partner/vercel.mdx +0 -28
  578. package/templates/content/en/PartnersList/partners.mdx +0 -22
  579. package/templates/content/en/Person/founder.mdx +0 -18
  580. package/templates/content/en/Pricing/pricing.mdx +0 -538
  581. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  582. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  583. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  584. package/templates/content/en/Product/ailk.mdx +0 -19
  585. package/templates/content/en/Product/ailk.schema.json +0 -94
  586. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  587. package/templates/content/en/Product/launch-kit.mdx +0 -28
  588. package/templates/content/en/ProductsList/products.mdx +0 -26
  589. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  590. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  591. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  592. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  593. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  594. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  595. package/templates/content/en/Service/consulting.mdx +0 -22
  596. package/templates/content/en/Service/implementation.mdx +0 -30
  597. package/templates/content/en/Service/launch-service.mdx +0 -232
  598. package/templates/content/en/Service/launch.mdx +0 -228
  599. package/templates/content/en/ServicesList/services.mdx +0 -84
  600. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  601. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  602. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  603. package/templates/content/en/TeamList/team.mdx +0 -63
  604. package/templates/content/en/TeamMember/founder.mdx +0 -72
  605. package/templates/content/en/TermsPage/terms.mdx +0 -19
  606. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  607. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  608. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  609. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  610. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  611. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  612. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  613. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  614. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  615. package/templates/content/es/_site.mdx +0 -30
  616. package/templates/content/fr/_site.mdx +0 -30
  617. package/templates/content/ja/_site.mdx +0 -30
  618. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  619. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  620. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -0,0 +1,768 @@
1
+ // oss: false
2
+ /**
3
+ * @file usage-metering.test.ts
4
+ * @description Unit tests for the usage-metering service (ADR 0009 D3
5
+ * service-layer pattern; spec docs/specs/3705-s1-usage-metering-service-spec.md §5).
6
+ *
7
+ * External dependency boundary mocked:
8
+ * - apps/api/src/lib/prisma — the repo-standard `__mocks__/prisma.ts`
9
+ * manual mock (`jest.mock("../../lib/prisma.js")`), extended with the
10
+ * FEAT-047 P1 model namespaces (usageEvent/usagePeriod/llmModel/
11
+ * llmModelPricing/aiMarkupConfig — #3709/#3710, not yet in the generated
12
+ * Prisma client this package compiles against) plus `$transaction`.
13
+ * `usage-metering.ts` casts `prisma` to a local structural delegate type
14
+ * (`UsageMeteringPrismaDelegates`); this test does the same so the mock's
15
+ * `jest.fn()` methods type-check the same way the service's calls do —
16
+ * this test harness stands in for the not-yet-existing live AI call
17
+ * (AC-UB-02 service-level, per the spec's §1 scope-reality note).
18
+ *
19
+ * `aiMarkupConfig.findFirst` is given a REALISTIC filtering implementation
20
+ * over an in-memory fixture "table" (mirroring lead-promotion.test.ts's
21
+ * `seedLeadTable` pattern) so the hierarchy tests (AC-2) exercise the actual
22
+ * precedence the service resolves, not just a static per-call stub.
23
+ *
24
+ * `$transaction` is likewise given REALISTIC commit/rollback semantics over an
25
+ * in-memory `committedEvents` store (`installAtomicDb`), while the BASE client's
26
+ * `usageEvent.create` models an AUTO-COMMIT write. That asymmetry is what makes
27
+ * the H3 atomicity test load-bearing: a non-transactional implementation (write
28
+ * the event, THEN upsert) commits the event immediately, so a failing upsert
29
+ * strands an orphan row in `committedEvents` and the test fails. Only writes
30
+ * routed through `$transaction` roll back.
31
+ *
32
+ * Security-review coverage (PR #3727): every High/Medium finding carries a test
33
+ * that fails without its fix. H1/H2 (per-event `ceil` to whole cents) are
34
+ * deliberately NOT addressed — a spec-level billing-fairness decision escalated
35
+ * to Sam — so the 360¢ expectation below still encodes the current rounding.
36
+ */
37
+
38
+ // eslint-disable-next-line no-restricted-syntax -- lib/prisma.ts wraps the external DB boundary (PrismaClient requires a live connection); __mocks__/prisma.ts is the repo-standard test double for this boundary.
39
+ jest.mock("../../lib/prisma.js");
40
+
41
+ import type { Id } from "@working-theory/ids";
42
+
43
+ import { prisma } from "../../lib/prisma.js";
44
+ import type { TenantContext } from "../tenant-context.js";
45
+ import {
46
+ calculateProviderCost,
47
+ recordUsageEvent,
48
+ type ModelPricing,
49
+ type RecordUsageEventInput,
50
+ type RecordUsageResult,
51
+ type UsageMeteringPrismaDelegates,
52
+ type UsageWriteClient,
53
+ } from "../usage-metering";
54
+
55
+ // The generated Prisma client doesn't carry the FEAT-047 P1 models yet
56
+ // (#3709/#3710 unmerged) — see the file docblock. Same structural cast the
57
+ // service source uses.
58
+ // eslint-disable-next-line no-restricted-syntax -- prisma's TS type resolves to the real (not-yet-augmented) PrismaClient; the FEAT-047 P1 model delegates (usageEvent/usagePeriod/llmModel/llmModelPricing/aiMarkupConfig, #3709/#3710) don't exist on it yet, so the intermediate `unknown` widening is required, not gratuitous — mirrors the identical cast in usage-metering.ts.
59
+ const db = prisma as unknown as UsageMeteringPrismaDelegates;
60
+
61
+ /** The delegate methods are typed as plain functions; the mock behind them is a jest.Mock. */
62
+ const mockOf = (fn: unknown): jest.Mock => fn as jest.Mock;
63
+
64
+ const ORG_ID = "org_00000000-0000-7000-8000-0000000000a1" as Id<"organization">;
65
+ const WORKSPACE_ID = "wksp_00000000-0000-7000-8000-0000000000b1" as Id<"workspace">;
66
+ const USER_ID = "user_00000000-0000-7000-8000-0000000000c1" as Id<"user">;
67
+ const PROVIDER = "anthropic";
68
+ const MODEL = "claude-fable";
69
+
70
+ /** The resolved tenant grain — what `resolveActiveTenant` hands the service (H5). */
71
+ const TENANT: TenantContext = {
72
+ userId: USER_ID,
73
+ organizationId: ORG_ID,
74
+ workspaceId: WORKSPACE_ID,
75
+ role: "owner",
76
+ };
77
+
78
+ function baseInput(overrides?: Partial<RecordUsageEventInput>): RecordUsageEventInput {
79
+ return {
80
+ tenant: TENANT,
81
+ provider: PROVIDER,
82
+ model: MODEL,
83
+ inputTokensNoCacheWrite: 1_000_000,
84
+ cacheReadTokens: 0,
85
+ outputTokens: 0,
86
+ ...overrides,
87
+ };
88
+ }
89
+
90
+ /** The in-memory "committed rows" store the atomicity assertions read (H3). */
91
+ let committedEvents: unknown[];
92
+
93
+ /**
94
+ * Forges a tenant grain that did NOT come from `resolveActiveTenant` — the H5
95
+ * bypass (a caller casting around the branded `Id<>` types, or a route stuffing
96
+ * request-supplied strings into the grain). The double assertion is the point of
97
+ * the test, not an accident: it reproduces exactly the shape the service's
98
+ * runtime id-grammar check must reject. "Fixing the type at the boundary" would
99
+ * delete the test.
100
+ */
101
+ function forgedTenant(overrides: Record<string, string>): TenantContext {
102
+ // eslint-disable-next-line no-restricted-syntax -- see the docblock above: the unbranded cast IS the attack under test (H5).
103
+ return { ...TENANT, ...overrides } as unknown as TenantContext;
104
+ }
105
+
106
+ /**
107
+ * Installs a DB double with REAL transactional semantics:
108
+ * - `$transaction(fn)` stages writes and commits them only if `fn` resolves;
109
+ * a throw discards the staged rows (ROLLBACK).
110
+ * - the BASE client's `usageEvent.create` commits IMMEDIATELY (auto-commit) —
111
+ * modelling what a non-transactional implementation would do.
112
+ * Both base delegates keep recording their calls, so `toHaveBeenCalled*`
113
+ * assertions work regardless of which path a write took.
114
+ */
115
+ function installAtomicDb(options?: { upsertError?: Error }) {
116
+ mockOf(db.usageEvent.create).mockImplementation(
117
+ async (args: { data: Record<string, unknown> }) => {
118
+ committedEvents.push(args.data); // auto-commit — nothing can roll this back
119
+ return { id: "evt_1" };
120
+ },
121
+ );
122
+ mockOf(db.usagePeriod.upsert).mockImplementation(async () => {
123
+ if (options?.upsertError) {
124
+ throw options.upsertError;
125
+ }
126
+ return {};
127
+ });
128
+
129
+ mockOf(db.$transaction).mockImplementation(
130
+ async (fn: (tx: UsageWriteClient) => Promise<unknown>) => {
131
+ const staged: unknown[] = [];
132
+ const tx: UsageWriteClient = {
133
+ usageEvent: {
134
+ create: async (args) => {
135
+ // Record the call on the base mock (so call assertions still work),
136
+ // but STAGE the row — it is only committed if the callback resolves.
137
+ mockOf(db.usageEvent.create).mock.calls.push([args]);
138
+ staged.push(args.data);
139
+ return { id: "evt_1" };
140
+ },
141
+ },
142
+ usagePeriod: {
143
+ upsert: async (args) =>
144
+ mockOf(db.usagePeriod.upsert)(args) as Promise<unknown>,
145
+ },
146
+ };
147
+ const result = await fn(tx);
148
+ committedEvents.push(...staged); // COMMIT (a throw above skips this → ROLLBACK)
149
+ return result;
150
+ },
151
+ );
152
+ }
153
+
154
+ /** Seeds aiMarkupConfig.findFirst with a realistic hierarchy-precedence filter. */
155
+ function seedMarkupTable(
156
+ rows: Array<{
157
+ provider: string | null;
158
+ model: string | null;
159
+ markupPercent: unknown;
160
+ }>,
161
+ ) {
162
+ mockOf(db.aiMarkupConfig.findFirst).mockImplementation(
163
+ async (args: {
164
+ where: { organizationId: string; provider: string | null; model: string | null };
165
+ }) => {
166
+ const { organizationId, provider, model } = args.where;
167
+ if (organizationId !== ORG_ID) return null;
168
+ const row = rows.find((r) => r.provider === provider && r.model === model);
169
+ return row ? { markupPercent: row.markupPercent } : null;
170
+ },
171
+ );
172
+ }
173
+
174
+ function seedModel(overrides?: Record<string, unknown>) {
175
+ mockOf(db.llmModel.findFirst).mockResolvedValue({
176
+ tier: "BASE",
177
+ isAutoModel: false,
178
+ chargeExtendedContextAt2x: false,
179
+ consumptionMultiplier: 1,
180
+ ...overrides,
181
+ });
182
+ }
183
+
184
+ function seedPricing(overrides?: Record<string, unknown>) {
185
+ mockOf(db.llmModelPricing.findFirst).mockResolvedValue({
186
+ inputPricePerMillion: 1,
187
+ outputPricePerMillion: 1,
188
+ cachedInputPricePerMillion: null,
189
+ ...overrides,
190
+ });
191
+ }
192
+
193
+ /** The persisted `UsageEvent` data of the Nth create call. */
194
+ function createdEventData(call = 0): Record<string, unknown> {
195
+ return mockOf(db.usageEvent.create).mock.calls[call]?.[0]?.data as Record<
196
+ string,
197
+ unknown
198
+ >;
199
+ }
200
+
201
+ /** The `usagePeriod.upsert` args of the Nth upsert call. */
202
+ function upsertArgs(call = 0): {
203
+ create: Record<string, unknown>;
204
+ update: Record<string, unknown>;
205
+ } {
206
+ return mockOf(db.usagePeriod.upsert).mock.calls[call]?.[0] as {
207
+ create: Record<string, unknown>;
208
+ update: Record<string, unknown>;
209
+ };
210
+ }
211
+
212
+ function expectInvalidInput(result: RecordUsageResult) {
213
+ expect(result.ok).toBe(false);
214
+ const err = result as Extract<RecordUsageResult, { ok: false }>;
215
+ expect(err.error.kind).toBe("invalid_input");
216
+ expect(err.error.persisted).toBe(false);
217
+ }
218
+
219
+ describe("calculateProviderCost", () => {
220
+ // ── AC-1: standard path ────────────────────────────────────────────────────
221
+ it("standard path — no context window / at-or-below 200K", () => {
222
+ const pricing: ModelPricing = {
223
+ inputPricePerMillion: 3,
224
+ outputPricePerMillion: 15,
225
+ cachedInputPricePerMillion: 0.3,
226
+ };
227
+ const cost = calculateProviderCost(1_000_000, 500_000, 200_000, pricing);
228
+ // inputCost=1*3=3, cachedInputCost=0.5*0.3=0.15, outputCost=0.2*15=3
229
+ expect(cost).toBeCloseTo(6.15, 6);
230
+ });
231
+
232
+ it("cached input defaults to half the input rate when unset", () => {
233
+ const pricing: ModelPricing = { inputPricePerMillion: 4, outputPricePerMillion: 20 };
234
+ const cost = calculateProviderCost(1_000_000, 1_000_000, 0, pricing);
235
+ // inputCost=1*4=4, cachedInputCost=1*(4*0.5)=2
236
+ expect(cost).toBeCloseTo(6, 6);
237
+ });
238
+
239
+ // ── AC-1: extended-context 2x split ────────────────────────────────────────
240
+ it("extended-context path — only when chargeExtendedAt2x AND contextWindowSize>200_000", () => {
241
+ const pricing: ModelPricing = {
242
+ inputPricePerMillion: 1,
243
+ outputPricePerMillion: 1,
244
+ cachedInputPricePerMillion: 1,
245
+ };
246
+ // 800K context: standardRatio=200K/800K=0.25, extendedRatio=0.75
247
+ // factor = 0.25*1 + 0.75*2 = 1.75
248
+ const cost = calculateProviderCost(1_000_000, 0, 0, pricing, 800_000, true);
249
+ expect(cost).toBeCloseTo(1.75, 6);
250
+ });
251
+
252
+ it("chargeExtendedAt2x=false skips the surcharge regardless of contextWindowSize", () => {
253
+ const pricing: ModelPricing = { inputPricePerMillion: 1, outputPricePerMillion: 1 };
254
+ const cost = calculateProviderCost(1_000_000, 0, 0, pricing, 800_000, false);
255
+ expect(cost).toBeCloseTo(1, 6);
256
+ });
257
+
258
+ it("contextWindowSize <= 200_000 skips the surcharge even when chargeExtendedAt2x=true", () => {
259
+ const pricing: ModelPricing = { inputPricePerMillion: 1, outputPricePerMillion: 1 };
260
+ const cost = calculateProviderCost(1_000_000, 0, 0, pricing, 200_000, true);
261
+ expect(cost).toBeCloseTo(1, 6);
262
+ });
263
+
264
+ it("contextWindowSize omitted defaults to 0 — extended pricing skipped (don't guess)", () => {
265
+ const pricing: ModelPricing = { inputPricePerMillion: 1, outputPricePerMillion: 1 };
266
+ const cost = calculateProviderCost(1_000_000, 0, 0, pricing, undefined, true);
267
+ expect(cost).toBeCloseTo(1, 6);
268
+ });
269
+ });
270
+
271
+ describe("recordUsageEvent", () => {
272
+ beforeEach(() => {
273
+ jest.clearAllMocks();
274
+ jest.spyOn(console, "warn").mockImplementation(() => undefined);
275
+ jest.spyOn(console, "error").mockImplementation(() => undefined);
276
+ committedEvents = [];
277
+ installAtomicDb();
278
+ seedModel();
279
+ seedPricing();
280
+ seedMarkupTable([]);
281
+ });
282
+
283
+ afterEach(() => {
284
+ jest.restoreAllMocks();
285
+ });
286
+
287
+ // ── AC-3 (AC-UB-02 service-level): write UsageEvent then upsert UsagePeriod ─
288
+ it("AC-3: writes the UsageEvent (grain + costs + attribution=0) then upserts the UsagePeriod", async () => {
289
+ seedModel({ tier: "PREMIUM" });
290
+ seedPricing({ inputPricePerMillion: 3, outputPricePerMillion: 15 });
291
+
292
+ const result = await recordUsageEvent(baseInput());
293
+
294
+ // providerCost = 1M/1e6 * 3 = 3; billedCost = 3 * 1.2 = 3.6
295
+ expect(result.ok).toBe(true);
296
+ const okResult = result as Extract<RecordUsageResult, { ok: true }>;
297
+ expect(okResult.eventId).toBe("evt_1");
298
+ expect(okResult.billedCostUsd).toBeCloseTo(3.6, 6);
299
+
300
+ expect(createdEventData()).toEqual(
301
+ expect.objectContaining({
302
+ organizationId: ORG_ID,
303
+ workspaceId: WORKSPACE_ID,
304
+ userId: USER_ID,
305
+ provider: PROVIDER,
306
+ model: MODEL,
307
+ providerCostUsd: 3,
308
+ markupPercent: 20,
309
+ billedCostUsd: expect.closeTo(3.6, 6),
310
+ modelTier: "PREMIUM",
311
+ wasAutoModel: false,
312
+ includedPortionCents: 0,
313
+ creditBalancePortionCents: 0,
314
+ onDemandPortionCents: 0,
315
+ completionStatus: "success",
316
+ }),
317
+ );
318
+
319
+ // H1/H2 (per-event ceil to whole cents) are DELIBERATELY unchanged — this
320
+ // 360¢ expectation encodes the current, spec-mandated rounding, pending Sam's
321
+ // billing-fairness decision.
322
+ expect(upsertArgs().create).toEqual(
323
+ expect.objectContaining({
324
+ organizationId: ORG_ID,
325
+ tokensConsumed: 1_000_000,
326
+ usageSpendCents: 360, // ceil(3.6 * 100)
327
+ premiumTokensConsumed: 1_000_000,
328
+ }),
329
+ );
330
+ expect(upsertArgs().update).toEqual(
331
+ expect.objectContaining({
332
+ tokensConsumed: { increment: 1_000_000 },
333
+ usageSpendCents: { increment: 360 },
334
+ premiumTokensConsumed: { increment: 1_000_000 },
335
+ }),
336
+ );
337
+ });
338
+
339
+ // ── Tier-column selection (BASE / FRONTIER) ────────────────────────────────
340
+ it.each([
341
+ ["BASE", "baseTokensConsumed"],
342
+ ["FRONTIER", "frontierTokensConsumed"],
343
+ ] as const)("upserts the %s tier column (%s)", async (tier, column) => {
344
+ seedModel({ tier });
345
+
346
+ await recordUsageEvent(baseInput());
347
+
348
+ expect(upsertArgs().create).toEqual(
349
+ expect.objectContaining({ [column]: 1_000_000 }),
350
+ );
351
+ expect(upsertArgs().update).toEqual(
352
+ expect.objectContaining({ [column]: { increment: 1_000_000 } }),
353
+ );
354
+ });
355
+
356
+ // ── AC-2: markup hierarchy ─────────────────────────────────────────────────
357
+ it("AC-2: markup resolves the model-specific row first", async () => {
358
+ seedMarkupTable([
359
+ { provider: PROVIDER, model: MODEL, markupPercent: 5 },
360
+ { provider: PROVIDER, model: null, markupPercent: 10 },
361
+ { provider: null, model: null, markupPercent: 8 },
362
+ ]);
363
+
364
+ const result = (await recordUsageEvent(baseInput())) as Extract<
365
+ RecordUsageResult,
366
+ { ok: true }
367
+ >;
368
+
369
+ expect(result.billedCostUsd).toBeCloseTo(1.05, 6); // providerCost=1 * (1+5/100)
370
+ });
371
+
372
+ it("AC-2: markup falls back to the provider-wide row", async () => {
373
+ seedMarkupTable([
374
+ { provider: PROVIDER, model: null, markupPercent: 10 },
375
+ { provider: null, model: null, markupPercent: 8 },
376
+ ]);
377
+
378
+ const result = (await recordUsageEvent(baseInput())) as Extract<
379
+ RecordUsageResult,
380
+ { ok: true }
381
+ >;
382
+
383
+ expect(result.billedCostUsd).toBeCloseTo(1.1, 6);
384
+ });
385
+
386
+ it("AC-2: markup falls back to the organization-default row", async () => {
387
+ seedMarkupTable([{ provider: null, model: null, markupPercent: 8 }]);
388
+
389
+ const result = (await recordUsageEvent(baseInput())) as Extract<
390
+ RecordUsageResult,
391
+ { ok: true }
392
+ >;
393
+
394
+ expect(result.billedCostUsd).toBeCloseTo(1.08, 6);
395
+ });
396
+
397
+ it("AC-2: markup falls back to the 20% system default", async () => {
398
+ seedMarkupTable([]);
399
+
400
+ const result = (await recordUsageEvent(baseInput())) as Extract<
401
+ RecordUsageResult,
402
+ { ok: true }
403
+ >;
404
+
405
+ expect(result.billedCostUsd).toBeCloseTo(1.2, 6);
406
+ });
407
+
408
+ // ── Unknown model → safe defaults, no crash ────────────────────────────────
409
+ it("an unresolvable model defaults to BASE / non-auto / no extended surcharge", async () => {
410
+ mockOf(db.llmModel.findFirst).mockResolvedValue(null);
411
+
412
+ // A large context window would trigger the surcharge IF chargeExtendedAt2x
413
+ // resolved true — for an unknown model it must default false.
414
+ const result = (await recordUsageEvent(
415
+ baseInput({ contextWindowSize: 800_000 }),
416
+ )) as Extract<RecordUsageResult, { ok: true }>;
417
+
418
+ expect(result.billedCostUsd).toBeCloseTo(1.2, 6); // no 2x surcharge
419
+ expect(createdEventData()).toEqual(
420
+ expect.objectContaining({ modelTier: "BASE", wasAutoModel: false }),
421
+ );
422
+ });
423
+
424
+ // ── PII non-leak — the result never carries token content ──────────────────
425
+ it("PII/content non-leak — the result carries only ids and computed costs", async () => {
426
+ const result = await recordUsageEvent(baseInput());
427
+
428
+ expect(Object.keys(result)).toEqual(
429
+ expect.arrayContaining(["ok", "eventId", "billedCostUsd"]),
430
+ );
431
+ expect(JSON.stringify(result).length).toBeLessThan(200);
432
+ });
433
+
434
+ // ══ H3 — atomicity: the event + the aggregate are ONE transaction ══════════
435
+ describe("H3 — atomic write (no double-billing on retry)", () => {
436
+ it("a failed UsagePeriod upsert persists NO UsageEvent — a retry cannot double-bill", async () => {
437
+ // The P2002 upsert race: two concurrent calls create the same
438
+ // (organizationId, periodStart) row; the loser throws.
439
+ installAtomicDb({
440
+ upsertError: new Error(
441
+ "Unique constraint failed on the fields: (`organizationId`,`periodStart`)",
442
+ ),
443
+ });
444
+
445
+ const result = await recordUsageEvent(baseInput());
446
+
447
+ expect(result.ok).toBe(false);
448
+ // THE assertion: a non-transactional implementation commits the event
449
+ // before the upsert runs, stranding an orphan row here — and the caller's
450
+ // retry then writes a SECOND event, billing the org twice for one AI call.
451
+ expect(committedEvents).toHaveLength(0);
452
+ });
453
+
454
+ it("a write failure reports persisted:false — a retry is always safe", async () => {
455
+ installAtomicDb({ upsertError: new Error("connection reset") });
456
+
457
+ const result = await recordUsageEvent(baseInput());
458
+
459
+ const err = result as Extract<RecordUsageResult, { ok: false }>;
460
+ expect(err.error.kind).toBe("write_failed");
461
+ expect(err.error.persisted).toBe(false);
462
+ });
463
+
464
+ it("both writes go through $transaction — neither is reachable off the base client", async () => {
465
+ // A transaction that cannot even be opened must produce ZERO writes:
466
+ // proof that the writes live only inside the transaction callback.
467
+ mockOf(db.$transaction).mockImplementation(async () => {
468
+ throw new Error("could not start transaction");
469
+ });
470
+
471
+ const result = await recordUsageEvent(baseInput());
472
+
473
+ expect(result.ok).toBe(false);
474
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
475
+ expect(db.usagePeriod.upsert).not.toHaveBeenCalled();
476
+ expect(committedEvents).toHaveLength(0);
477
+ expect(db.$transaction).toHaveBeenCalledTimes(1);
478
+ });
479
+
480
+ it("the happy path commits exactly one event, through the transaction", async () => {
481
+ const result = await recordUsageEvent(baseInput());
482
+
483
+ expect(result.ok).toBe(true);
484
+ expect(db.$transaction).toHaveBeenCalledTimes(1);
485
+ expect(committedEvents).toHaveLength(1);
486
+ });
487
+ });
488
+
489
+ // ══ H4 — boundary validation: no negative/NaN/Infinity reaches cost or DB ══
490
+ describe("H4 — input validation (invalid_input)", () => {
491
+ it("a NEGATIVE token count is rejected — it cannot decrement the org's spend", async () => {
492
+ const result = await recordUsageEvent(
493
+ baseInput({ inputTokensNoCacheWrite: -1_000_000 }),
494
+ );
495
+
496
+ expectInvalidInput(result);
497
+ // The attack: a negative cost → a negative usageSpendCents increment →
498
+ // the org's metered spend is DECREMENTED (its bill zeroed out).
499
+ expect(db.$transaction).not.toHaveBeenCalled();
500
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
501
+ expect(db.usagePeriod.upsert).not.toHaveBeenCalled();
502
+ expect(committedEvents).toHaveLength(0);
503
+ });
504
+
505
+ it("NaN is rejected — Postgres double precision accepts it and the aggregate is poisoned forever", async () => {
506
+ const result = await recordUsageEvent(baseInput({ outputTokens: Number.NaN }));
507
+
508
+ expectInvalidInput(result);
509
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
510
+ expect(db.usagePeriod.upsert).not.toHaveBeenCalled();
511
+ });
512
+
513
+ it("Infinity is rejected", async () => {
514
+ const result = await recordUsageEvent(
515
+ baseInput({ outputTokens: Number.POSITIVE_INFINITY }),
516
+ );
517
+
518
+ expectInvalidInput(result);
519
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
520
+ });
521
+
522
+ it("a non-integer token count is rejected", async () => {
523
+ const result = await recordUsageEvent(baseInput({ outputTokens: 1.5 }));
524
+
525
+ expectInvalidInput(result);
526
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
527
+ });
528
+
529
+ it("an absurd token magnitude is rejected (overflow guard)", async () => {
530
+ const result = await recordUsageEvent(
531
+ baseInput({ inputTokensNoCacheWrite: 1e15 }),
532
+ );
533
+
534
+ expectInvalidInput(result);
535
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
536
+ });
537
+
538
+ it("a negative contextWindowSize is rejected", async () => {
539
+ const result = await recordUsageEvent(baseInput({ contextWindowSize: -1 }));
540
+
541
+ expectInvalidInput(result);
542
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
543
+ });
544
+
545
+ it("the error names the offending field", async () => {
546
+ const result = await recordUsageEvent(baseInput({ outputTokens: -5 }));
547
+
548
+ const err = result as Extract<RecordUsageResult, { ok: false }>;
549
+ expect(err.error.message).toContain("outputTokens");
550
+ });
551
+ });
552
+
553
+ // ══ H5 — tenant grain: branded ids, validated at the boundary ══════════════
554
+ describe("H5 — tenant grain", () => {
555
+ it("rejects a MISMATCHED grain — a workspace id smuggled into the organization slot", async () => {
556
+ // organizationId keys BOTH the markup lookup and the spend increment, so a
557
+ // wrong id here bills the wrong tenant (and leaks its markup back).
558
+ const badTenant = forgedTenant({ organizationId: WORKSPACE_ID });
559
+
560
+ const result = await recordUsageEvent(baseInput({ tenant: badTenant }));
561
+
562
+ expectInvalidInput(result);
563
+ expect(db.$transaction).not.toHaveBeenCalled();
564
+ expect(db.aiMarkupConfig.findFirst).not.toHaveBeenCalled();
565
+ });
566
+
567
+ it("rejects an arbitrary caller-supplied string as an organization id", async () => {
568
+ const badTenant = forgedTenant({ organizationId: "victim-org" });
569
+
570
+ const result = await recordUsageEvent(baseInput({ tenant: badTenant }));
571
+
572
+ expectInvalidInput(result);
573
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
574
+ });
575
+
576
+ it("rejects a malformed workspace id", async () => {
577
+ const badTenant = forgedTenant({ workspaceId: "wksp_not-a-uuid" });
578
+
579
+ const result = await recordUsageEvent(baseInput({ tenant: badTenant }));
580
+
581
+ expectInvalidInput(result);
582
+ expect(db.usageEvent.create).not.toHaveBeenCalled();
583
+ });
584
+
585
+ it("persists the tenant grain exactly as resolved", async () => {
586
+ await recordUsageEvent(baseInput());
587
+
588
+ expect(createdEventData()).toEqual(
589
+ expect.objectContaining({
590
+ organizationId: ORG_ID,
591
+ workspaceId: WORKSPACE_ID,
592
+ userId: USER_ID,
593
+ }),
594
+ );
595
+ });
596
+ });
597
+
598
+ // ══ M1 — totalTokens is DERIVED, never caller-supplied ═════════════════════
599
+ describe("M1 — totalTokens derived server-side", () => {
600
+ it("derives totalTokens from the buckets — a caller cannot bill without burning quota", async () => {
601
+ await recordUsageEvent(
602
+ baseInput({
603
+ inputTokensCacheWrite: 1_000,
604
+ inputTokensNoCacheWrite: 10_000,
605
+ cacheReadTokens: 2_000,
606
+ outputTokens: 500,
607
+ thinkingTokens: 100,
608
+ }),
609
+ );
610
+
611
+ const total = 1_000 + 10_000 + 2_000 + 500 + 100; // 13_600
612
+ expect(createdEventData()).toEqual(
613
+ expect.objectContaining({ totalTokens: total }),
614
+ );
615
+ // The quota surface: tokensConsumed + the tier column track the derived sum.
616
+ expect(upsertArgs().create).toEqual(
617
+ expect.objectContaining({
618
+ tokensConsumed: total,
619
+ baseTokensConsumed: total,
620
+ }),
621
+ );
622
+ expect(upsertArgs().update).toEqual(
623
+ expect.objectContaining({
624
+ tokensConsumed: { increment: total },
625
+ baseTokensConsumed: { increment: total },
626
+ }),
627
+ );
628
+ });
629
+ });
630
+
631
+ // ══ M2 — markup + pricing range validation ═════════════════════════════════
632
+ describe("M2 — markup/pricing range validation", () => {
633
+ it("a NULL markup column does not become a silent 0% (lost revenue) — falls through to 20%", async () => {
634
+ seedMarkupTable([{ provider: PROVIDER, model: MODEL, markupPercent: null }]);
635
+
636
+ const result = (await recordUsageEvent(baseInput())) as Extract<
637
+ RecordUsageResult,
638
+ { ok: true }
639
+ >;
640
+
641
+ // Number(null) === 0 would bill at cost. The system default must win.
642
+ expect(result.billedCostUsd).toBeCloseTo(1.2, 6);
643
+ expect(createdEventData()).toEqual(
644
+ expect.objectContaining({ markupPercent: 20 }),
645
+ );
646
+ });
647
+
648
+ it("a NEGATIVE markup row never bills below provider cost", async () => {
649
+ seedMarkupTable([{ provider: PROVIDER, model: MODEL, markupPercent: -50 }]);
650
+
651
+ const result = (await recordUsageEvent(baseInput())) as Extract<
652
+ RecordUsageResult,
653
+ { ok: true }
654
+ >;
655
+
656
+ expect(result.billedCostUsd).toBeCloseTo(1.2, 6);
657
+ });
658
+
659
+ it("a non-numeric markup row falls through to the default", async () => {
660
+ seedMarkupTable([
661
+ { provider: PROVIDER, model: MODEL, markupPercent: "not-a-number" },
662
+ ]);
663
+
664
+ const result = (await recordUsageEvent(baseInput())) as Extract<
665
+ RecordUsageResult,
666
+ { ok: true }
667
+ >;
668
+
669
+ expect(result.billedCostUsd).toBeCloseTo(1.2, 6);
670
+ expect(Number.isFinite(createdEventData().markupPercent as number)).toBe(true);
671
+ });
672
+
673
+ it("an unusable model-specific row falls through the chain to the provider-wide row", async () => {
674
+ seedMarkupTable([
675
+ { provider: PROVIDER, model: MODEL, markupPercent: null },
676
+ { provider: PROVIDER, model: null, markupPercent: 10 },
677
+ ]);
678
+
679
+ const result = (await recordUsageEvent(baseInput())) as Extract<
680
+ RecordUsageResult,
681
+ { ok: true }
682
+ >;
683
+
684
+ expect(result.billedCostUsd).toBeCloseTo(1.1, 6);
685
+ });
686
+
687
+ it("a corrupt pricing rate never reaches the cost math (no NaN in the DB)", async () => {
688
+ seedPricing({ inputPricePerMillion: "corrupt" });
689
+
690
+ const result = (await recordUsageEvent(baseInput())) as Extract<
691
+ RecordUsageResult,
692
+ { ok: true }
693
+ >;
694
+
695
+ expect(result.billedCostUsd).toBe(0);
696
+ expect(Number.isFinite(createdEventData().providerCostUsd as number)).toBe(true);
697
+ expect(Number.isFinite(upsertArgs().create.usageSpendCents as number)).toBe(true);
698
+ });
699
+ });
700
+
701
+ // ══ M3 — unvalidated tier → an "undefined" column key ══════════════════════
702
+ describe("M3 — tier validation", () => {
703
+ it("an out-of-union tier defaults to BASE — never an 'undefined' upsert key", async () => {
704
+ seedModel({ tier: "LEGENDARY" }); // a tier the catalog should never hold
705
+
706
+ const result = await recordUsageEvent(baseInput());
707
+
708
+ expect(result.ok).toBe(true);
709
+ expect(upsertArgs().create).toEqual(
710
+ expect.objectContaining({ baseTokensConsumed: 1_000_000 }),
711
+ );
712
+ expect(upsertArgs().create).not.toHaveProperty("undefined");
713
+ expect(upsertArgs().update).not.toHaveProperty("undefined");
714
+ expect(createdEventData()).toEqual(
715
+ expect.objectContaining({ modelTier: "BASE" }),
716
+ );
717
+ });
718
+ });
719
+
720
+ // ══ M4 — missing pricing is MARKED, not silent ═════════════════════════════
721
+ describe("M4 — missing pricing", () => {
722
+ it("meters at $0 without blocking, and MARKS the event so it can be re-priced", async () => {
723
+ mockOf(db.llmModelPricing.findFirst).mockResolvedValue(null);
724
+
725
+ const result = await recordUsageEvent(baseInput());
726
+
727
+ expect(result).toEqual<RecordUsageResult>({
728
+ ok: true,
729
+ eventId: "evt_1",
730
+ billedCostUsd: 0,
731
+ });
732
+ const data = createdEventData();
733
+ expect(data).toEqual(
734
+ expect.objectContaining({ providerCostUsd: 0, billedCostUsd: 0 }),
735
+ );
736
+ // Distinguishable from a genuine $0 call — and the caller's status survives.
737
+ expect(data.completionStatus).toBe("pricing_missing:success");
738
+ });
739
+
740
+ it("a priced call carries the caller's status unmarked", async () => {
741
+ await recordUsageEvent(baseInput({ completionStatus: "error" }));
742
+
743
+ expect(createdEventData().completionStatus).toBe("error");
744
+ });
745
+ });
746
+
747
+ // ══ M5 — raw DB error text is never returned ═══════════════════════════════
748
+ describe("M5 — error propagation", () => {
749
+ it("does not leak the raw Prisma message (which embeds ids + token counts)", async () => {
750
+ // A Prisma validation error embeds the whole `data` argument.
751
+ const leaky = new Error(
752
+ `Invalid \`prisma.usageEvent.create()\` invocation: { organizationId: "${ORG_ID}", userId: "${USER_ID}", inputTokensNoCacheWrite: 1000000 }`,
753
+ );
754
+ installAtomicDb({ upsertError: leaky });
755
+
756
+ const result = await recordUsageEvent(baseInput());
757
+
758
+ const err = result as Extract<RecordUsageResult, { ok: false }>;
759
+ expect(err.error.kind).toBe("write_failed");
760
+ expect(err.error.message).toBe("usage event write failed");
761
+ expect(err.error.message).not.toContain(ORG_ID);
762
+ expect(err.error.message).not.toContain(USER_ID);
763
+ expect(err.error.message).not.toContain("prisma");
764
+ // The detail is still available to operators — server-side only.
765
+ expect(console.error).toHaveBeenCalled();
766
+ });
767
+ });
768
+ });