create-ailk 0.5.0 → 0.6.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 (299) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +336 -27
  3. package/component-catalog.md +25 -12
  4. package/dist/lib/api-boot-assertion.d.ts +46 -0
  5. package/dist/lib/api-boot-assertion.js +70 -0
  6. package/dist/lib/import-graph.d.ts +29 -0
  7. package/dist/lib/import-graph.js +140 -0
  8. package/dist/module-architecture.d.ts +1 -0
  9. package/dist/module-architecture.js +253 -127
  10. package/dist/surfaces.js +105 -7
  11. package/dist/template-manifest.d.ts +33 -0
  12. package/dist/template-manifest.js +99 -16
  13. package/dist/template-names.d.ts +1 -1
  14. package/dist/template-names.js +1 -0
  15. package/package.json +3 -2
  16. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  17. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  18. package/site-templates/marketing-site/ailk-template.json +1 -1
  19. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  20. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  21. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  22. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  23. package/site-templates/minimalism-site/ailk-template.json +2 -1
  24. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  25. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  26. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  27. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  28. package/site-templates/simple-service/ailk-template.json +10 -0
  29. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  30. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  31. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  32. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  33. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  34. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  35. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  36. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  37. package/site-templates/simple-service/provenance.json +4 -0
  38. package/templates/.env.example +72 -7
  39. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  40. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  41. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  42. package/templates/apps/api/.env.example +13 -0
  43. package/templates/apps/api/CLAUDE.md +26 -2
  44. package/templates/apps/api/jest.config.cjs +3 -0
  45. package/templates/apps/api/package.json +1 -0
  46. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  47. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  48. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  49. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  50. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  51. package/templates/apps/api/src/config/index.ts +62 -0
  52. package/templates/apps/api/src/config/modules.ts +7 -3
  53. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  54. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  55. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  56. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  57. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  58. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  59. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  60. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  61. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  62. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  63. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  64. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  65. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  66. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  67. package/templates/apps/api/src/lib/site.ts +208 -9
  68. package/templates/apps/api/src/lib/stripe.ts +66 -0
  69. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  70. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  71. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  72. package/templates/apps/api/src/openapi/spec.ts +543 -2
  73. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  74. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  75. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  76. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  77. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  78. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  79. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  80. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  81. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  82. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  83. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  84. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  85. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  86. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  87. package/templates/apps/api/src/routes/content/index.ts +13 -73
  88. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  89. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  90. package/templates/apps/api/src/routes/content/search.ts +129 -0
  91. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  92. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  93. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  94. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  95. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  96. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  97. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  98. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  99. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  100. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  101. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  102. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  103. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  104. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  105. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  106. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  111. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  112. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  113. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  114. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  115. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  116. package/templates/apps/api/src/server.ts +439 -49
  117. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  118. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  119. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  120. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  121. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  122. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  123. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  124. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  125. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  126. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  127. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  128. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  129. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  130. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  131. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  132. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  133. package/templates/apps/api/src/services/audit-log.ts +668 -0
  134. package/templates/apps/api/src/services/consent.ts +18 -0
  135. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  136. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  137. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  138. package/templates/apps/api/src/services/entitlements.ts +80 -0
  139. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  140. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  141. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  142. package/templates/apps/api/src/services/listing-config.ts +27 -9
  143. package/templates/apps/api/src/services/project-listings.ts +201 -6
  144. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  145. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  146. package/templates/apps/api/src/services/revalidation.ts +173 -0
  147. package/templates/apps/api/src/services/verified-email.ts +71 -0
  148. package/templates/apps/api/src/vercel-handler.ts +75 -35
  149. package/templates/apps/mcp/.env.example +17 -0
  150. package/templates/apps/mcp/CLAUDE.md +57 -24
  151. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  152. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  153. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  154. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  155. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  156. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  157. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  158. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  159. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  160. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  161. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  162. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  163. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  164. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  165. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  166. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  167. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  168. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  169. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  170. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  171. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  172. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  173. package/templates/apps/mcp/jest.config.cjs +13 -0
  174. package/templates/apps/mcp/package.json +2 -0
  175. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  176. package/templates/apps/mcp/src/config/modules.ts +10 -5
  177. package/templates/apps/mcp/src/http-transport.ts +102 -29
  178. package/templates/apps/mcp/src/index.ts +73 -11
  179. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  180. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  181. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  182. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  183. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  184. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  185. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  186. package/templates/apps/mcp/src/server.ts +59 -35
  187. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  188. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  189. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  190. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  191. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  192. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  193. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  194. package/templates/apps/mcp/src/tools/index.ts +6 -4
  195. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  196. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  197. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  198. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  199. package/templates/apps/web/.env.example +71 -5
  200. package/templates/apps/web/CLAUDE.md +44 -0
  201. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  202. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  203. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  204. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  205. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  206. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  207. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  208. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  209. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  210. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  211. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  212. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  213. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  214. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  215. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  216. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  217. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  218. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  219. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  220. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  221. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  222. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  223. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  224. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  225. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  226. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  227. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  228. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  229. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  230. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  232. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  233. package/templates/apps/web/auth.ts +407 -8
  234. package/templates/apps/web/jest.config.cjs +8 -4
  235. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  236. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  237. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  238. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  239. package/templates/apps/web/lib/auth-email.ts +318 -0
  240. package/templates/apps/web/lib/auth-env.ts +85 -0
  241. package/templates/apps/web/lib/auth-routes.ts +55 -0
  242. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  243. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  244. package/templates/apps/web/messages/en.json +187 -0
  245. package/templates/apps/web/package.json +2 -1
  246. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  247. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  248. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  249. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  250. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  251. package/templates/database/CHANGELOG.md +142 -0
  252. package/templates/database/README.md +10 -3
  253. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  254. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  255. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  256. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  257. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  258. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  259. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  260. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  261. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  262. package/templates/database/__tests__/tenancy.test.ts +141 -0
  263. package/templates/database/config/schema.prisma +79 -2
  264. package/templates/database/content/schema.prisma +87 -11
  265. package/templates/database/inbox/schema.prisma +452 -9
  266. package/templates/database/jest.config.cjs +5 -3
  267. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  268. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  269. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  270. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  271. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  272. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  273. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  274. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  275. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  276. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  277. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  278. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  279. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  280. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  281. package/templates/database/package.json +1 -1
  282. package/templates/database/tenancy.ts +86 -1
  283. package/templates/database/tsconfig.json +1 -0
  284. package/versions.json +34 -34
  285. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  286. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  287. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  288. package/templates/apps/api/src/routes/content/create.ts +0 -115
  289. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  290. package/templates/apps/api/src/routes/content/update.ts +0 -112
  291. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  292. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  293. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  294. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  295. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  296. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  297. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  298. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  299. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * T-8 (#6084) — neither `/purchase/success` nor `/purchase/cancel` may hold
3
+ * a TOP-LEVEL STATIC import of `@working-theory/ui-commerce`.
4
+ *
5
+ * Superseded from its original form (a module-scope
6
+ * `registerCommerceSections()` call + a static import — the shape the pages
7
+ * shipped with initially). That shape is a genuine build-breaking defect:
8
+ * both pages ship in the BASE TREE (`CORE_ROUTE_DIRS`, D12), but a keyless
9
+ * `create-ailk` scaffold carries NO `@working-theory/ui-commerce` dependency
10
+ * by design — `packages/create-ailk/scripts/build-templates.mjs`'s
11
+ * `transformPaidUiPackages()` strips it, and
12
+ * `__tests__/paid-module-honest-states.test.ts`'s "#5581 U6" suite pins a
13
+ * keyless scaffold declaring none of the four paid `ui-*` packages as a
14
+ * dependency. Webpack resolves a static top-level import at BUILD time
15
+ * regardless of whether the importing code path ever executes, so the
16
+ * original shape failed every keyless customer's build with
17
+ * `ERR_MODULE_NOT_FOUND` (a code-review finding on this issue's own PR).
18
+ *
19
+ * The fix — `../../../../src/lib/commerce-sections.ts`'s
20
+ * `ensureCommerceSectionsRegistered()` — registers the module LAZILY, via a
21
+ * `webpackIgnore`-annotated `import()` wrapped in try/catch, so the build
22
+ * never needs the package to resolve. This test now pins the ABSENCE of the
23
+ * static import (the regression this file exists to catch) and the
24
+ * PRESENCE of the lazy-registration call site instead.
25
+ */
26
+ import { readFileSync } from "node:fs";
27
+ import { join } from "node:path";
28
+
29
+ const PURCHASE_DIR = join(__dirname, "..");
30
+
31
+ function readPageSource(page: "success" | "cancel"): string {
32
+ return readFileSync(join(PURCHASE_DIR, page, "page.tsx"), "utf8");
33
+ }
34
+
35
+ describe("ui-commerce registration is LAZY, never a top-level static import (T-8, D5, #6084 code-review fix)", () => {
36
+ for (const page of ["success", "cancel"] as const) {
37
+ it(`purchase/${page}/page.tsx holds NO static top-level import of @working-theory/ui-commerce`, () => {
38
+ const source = readPageSource(page);
39
+ expect(source.includes('from "@working-theory/ui-commerce"')).toBe(
40
+ false,
41
+ );
42
+ // The retired specifier may still appear in a doc comment (warning
43
+ // readers away from it); what must be absent is an actual IMPORT of it.
44
+ expect(
45
+ source.includes('from "@working-theory/ui/slices/commerce"'),
46
+ ).toBe(false);
47
+ });
48
+
49
+ it(`purchase/${page}/page.tsx imports ensureCommerceSectionsRegistered from the shared lazy-registration lib`, () => {
50
+ const source = readPageSource(page);
51
+ expect(
52
+ source.includes(
53
+ 'import { ensureCommerceSectionsRegistered } from "../../../../src/lib/commerce-sections";',
54
+ ),
55
+ ).toBe(true);
56
+ });
57
+
58
+ it(`purchase/${page}/page.tsx never calls registerCommerceSections() at module scope (only through the lazy wrapper)`, () => {
59
+ const source = readPageSource(page);
60
+ // A doc comment MAY still name it in prose (both pages do, warning
61
+ // readers off the direct call) — what must be absent is the
62
+ // unindented, top-level CALL EXPRESSION statement itself.
63
+ expect(
64
+ source.split("\n").some((line) => line === "registerCommerceSections();"),
65
+ ).toBe(false);
66
+ });
67
+ }
68
+ });
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Tests for /[locale]/purchase/success (#6084).
3
+ *
4
+ * D6's five-row degrade table is the load-bearing surface here: only a 2xx
5
+ * read with `paymentStatus === "paid"` may construct a `payment-receipt`
6
+ * section — AC-8 pins the negative directly (no `outcome: "success"` DOM
7
+ * marker on any other reachable state). AC-1/AC-7 cover the happy path and
8
+ * the never-a-dead-end guarantee; T-16 covers the D12 request-time gate in
9
+ * all three states (claimed / unclaimed / a throwing read).
10
+ */
11
+ import React from "react";
12
+ import { render } from "@testing-library/react";
13
+
14
+ jest.mock("next/navigation", () => ({
15
+ __esModule: true,
16
+ notFound: jest.fn(() => {
17
+ throw new Error("NEXT_NOT_FOUND");
18
+ }),
19
+ }));
20
+
21
+ const mockHeadersGet = jest.fn();
22
+ jest.mock("next/headers", () => ({
23
+ __esModule: true,
24
+ headers: jest.fn().mockResolvedValue({ get: (key: string) => mockHeadersGet(key) }),
25
+ }));
26
+
27
+ const requestedKeys: string[] = [];
28
+ jest.mock("next-intl/server", () => ({
29
+ __esModule: true,
30
+ getTranslations: jest
31
+ .fn()
32
+ .mockResolvedValue((key: string, values?: Record<string, unknown>) => {
33
+ requestedKeys.push(key);
34
+ if (values) return `copy:${key}:${JSON.stringify(values)}`;
35
+ return `copy:${key}`;
36
+ }),
37
+ }));
38
+
39
+ const capturedSections: unknown[] = [];
40
+ jest.mock("@working-theory/ui/sections", () => ({
41
+ __esModule: true,
42
+ SectionRenderer: ({ sections }: { sections: unknown[] }) => {
43
+ capturedSections.splice(0, capturedSections.length, ...sections);
44
+ return React.createElement("div", {
45
+ "data-testid": "section-renderer",
46
+ "data-section-count": String(sections.length),
47
+ });
48
+ },
49
+ }));
50
+
51
+ const mockGetSession = jest.fn();
52
+ jest.mock("@working-theory/api-client", () => ({
53
+ __esModule: true,
54
+ checkout: { getSession: (...args: unknown[]) => mockGetSession(...args) },
55
+ }));
56
+
57
+ // Mocks the REAL boundary `resolveCommerceClaim()` itself calls
58
+ // (`checkLicense` from `@working-theory/license`), not the internal wrapper
59
+ // module — matching the eslint "no internal-module mocks" rule (#507).
60
+ const mockCheckLicense = jest.fn();
61
+ jest.mock("@working-theory/license", () => ({
62
+ __esModule: true,
63
+ checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
64
+ }));
65
+
66
+ // `../../../../src/lib/commerce-sections` wraps a lazy, webpack-ignored
67
+ // `import()` of the paid `@working-theory/ui-commerce` package — mocked
68
+ // directly here (an internal wrapper, but there is no OTHER boundary to
69
+ // mock: the real thing it wraps is a dynamic import expression, not an
70
+ // importable named export this file could bind instead) so a test can
71
+ // simulate the package being genuinely absent (a keyless scaffold) without
72
+ // needing to hide a real node_modules entry.
73
+ const mockEnsureCommerceSectionsRegistered = jest.fn();
74
+ // The wrapped thing is a bare, webpack-ignored `import()` expression, not a
75
+ // named export of some other module — there is no external HTTP/DB/env
76
+ // boundary to mock instead, mirroring team-page.test.tsx's identical
77
+ // exception for auth.ts's lazy singleton.
78
+ // eslint-disable-next-line no-restricted-syntax -- see comment above
79
+ jest.mock("../../../../src/lib/commerce-sections", () => ({
80
+ __esModule: true,
81
+ ensureCommerceSectionsRegistered: () =>
82
+ mockEnsureCommerceSectionsRegistered(),
83
+ }));
84
+
85
+ import { checkout } from "@working-theory/api-client";
86
+ import { notFound } from "next/navigation";
87
+
88
+ import Page, { generateMetadata } from "../success/page";
89
+
90
+ const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
91
+ void checkout; // referenced only to satisfy the mock import above
92
+
93
+ function sectionTypes(): string[] {
94
+ return capturedSections.map((s) => (s as { type: string }).type);
95
+ }
96
+
97
+ const PAID_SESSION = {
98
+ id: "cs_test_abc123",
99
+ status: "complete" as const,
100
+ paymentStatus: "paid" as const,
101
+ currency: "usd",
102
+ amountTotal: 4800,
103
+ lineItems: [
104
+ {
105
+ description: "Pro plan",
106
+ quantity: 2,
107
+ amountTotal: 4800,
108
+ currency: "usd",
109
+ },
110
+ ],
111
+ };
112
+
113
+ async function renderPage(sessionId?: string) {
114
+ const searchParams = sessionId
115
+ ? Promise.resolve({ session_id: sessionId })
116
+ : Promise.resolve({});
117
+ const element = await Page({
118
+ params: Promise.resolve({ locale: "en" }),
119
+ searchParams,
120
+ });
121
+ return render(element);
122
+ }
123
+
124
+ describe("/purchase/success", () => {
125
+ beforeEach(() => {
126
+ capturedSections.length = 0;
127
+ requestedKeys.length = 0;
128
+ mockGetSession.mockReset();
129
+ mockCheckLicense.mockReset();
130
+ mockCheckLicense.mockResolvedValue({ revoked: false, features: ["commerce"] });
131
+ mockNotFound.mockClear();
132
+ mockEnsureCommerceSectionsRegistered.mockReset();
133
+ mockEnsureCommerceSectionsRegistered.mockResolvedValue(true);
134
+ mockHeadersGet.mockReset();
135
+ mockHeadersGet.mockReturnValue(null);
136
+ });
137
+
138
+ // ── AC-1 — the paid render ────────────────────────────────────────────────
139
+
140
+ it("renders payment-receipt → items → cta on a 2xx paid read (AC-1)", async () => {
141
+ mockGetSession.mockResolvedValue({ ok: true, body: PAID_SESSION });
142
+
143
+ await renderPage("cs_test_abc123");
144
+
145
+ expect(sectionTypes()).toEqual(["payment-receipt", "items", "cta"]);
146
+ });
147
+
148
+ it("calls checkout.getSession with the exact session_id from the query, and undefined init when no forwarding header is present", async () => {
149
+ mockGetSession.mockResolvedValue({ ok: true, body: PAID_SESSION });
150
+ mockHeadersGet.mockReturnValue(null);
151
+
152
+ await renderPage("cs_test_abc123");
153
+
154
+ expect(mockGetSession).toHaveBeenCalledWith("cs_test_abc123", undefined);
155
+ });
156
+
157
+ it("forwards the visitor's x-forwarded-for as init.headers on the server-side read (security-review finding, #6084)", async () => {
158
+ mockGetSession.mockResolvedValue({ ok: true, body: PAID_SESSION });
159
+ mockHeadersGet.mockImplementation((key: string) =>
160
+ key === "x-forwarded-for" ? "203.0.113.7" : null,
161
+ );
162
+
163
+ await renderPage("cs_test_abc123");
164
+
165
+ expect(mockGetSession).toHaveBeenCalledWith("cs_test_abc123", {
166
+ headers: { "x-forwarded-for": "203.0.113.7" },
167
+ });
168
+ });
169
+
170
+ it("threads the session id + amount + currency into the payment-receipt section", async () => {
171
+ mockGetSession.mockResolvedValue({ ok: true, body: PAID_SESSION });
172
+
173
+ await renderPage("cs_test_abc123");
174
+
175
+ const receipt = capturedSections[0] as {
176
+ type: string;
177
+ data: Record<string, unknown>;
178
+ };
179
+ expect(receipt.type).toBe("payment-receipt");
180
+ expect(receipt.data.outcome).toBe("success");
181
+ expect(receipt.data.sessionId).toBe("cs_test_abc123");
182
+ expect(receipt.data.amount).toEqual({ amountMinor: 4800, currency: "usd" });
183
+ });
184
+
185
+ it("renders one items card per paid line item, with quantity and formatted money", async () => {
186
+ mockGetSession.mockResolvedValue({ ok: true, body: PAID_SESSION });
187
+
188
+ await renderPage("cs_test_abc123");
189
+
190
+ const items = capturedSections[1] as {
191
+ type: string;
192
+ data: { items: { title: string; description: string }[] };
193
+ };
194
+ expect(items.type).toBe("items");
195
+ expect(items.data.items).toHaveLength(1);
196
+ expect(items.data.items[0]!.title).toBe("Pro plan");
197
+ // The translated line renders through the t() mock, carrying the raw
198
+ // interpolation values — quantity is threaded, and formatMoney ran (the
199
+ // string contains a currency-formatted amount, not the raw minor units).
200
+ expect(items.data.items[0]!.description).toContain("success.lineItem");
201
+ expect(items.data.items[0]!.description).toContain('"quantity":2');
202
+ });
203
+
204
+ it("falls back to content-block instead of payment-receipt when @working-theory/ui-commerce is not installed", async () => {
205
+ mockGetSession.mockResolvedValue({ ok: true, body: PAID_SESSION });
206
+ mockEnsureCommerceSectionsRegistered.mockResolvedValue(false);
207
+
208
+ await renderPage("cs_test_abc123");
209
+
210
+ // Never the bare `payment-receipt` type — SectionRenderer would
211
+ // otherwise fall back to `ModuleAbsent`, which renders nothing in
212
+ // production. The line items and CTA are unaffected either way.
213
+ expect(sectionTypes()).toEqual(["content-block", "items", "cta"]);
214
+ const fallback = capturedSections[0] as {
215
+ type: string;
216
+ data: { headline: string; body: string };
217
+ };
218
+ expect(fallback.data.headline).toBe("copy:success.title");
219
+ expect(fallback.data.body).toContain("copy:success.message");
220
+ });
221
+
222
+ // ── D6 — the five-row degrade table ─────────────────────────────────────
223
+
224
+ it("renders content-block → cta with NO payment-receipt when there is no session_id (D6 row 3)", async () => {
225
+ await renderPage(undefined);
226
+
227
+ expect(sectionTypes()).toEqual(["content-block", "cta"]);
228
+ expect(mockGetSession).not.toHaveBeenCalled();
229
+ });
230
+
231
+ it("renders content-block → cta with NO payment-receipt on a non-2xx read (D6 row 5)", async () => {
232
+ mockGetSession.mockResolvedValue({
233
+ ok: false,
234
+ status: 404,
235
+ body: { error: "unknown-session" },
236
+ });
237
+
238
+ await renderPage("cs_test_unknown");
239
+
240
+ expect(sectionTypes()).toEqual(["content-block", "cta"]);
241
+ });
242
+
243
+ it("renders content-block → cta with NO payment-receipt when checkout.getSession REJECTS (a network failure, not a non-2xx response)", async () => {
244
+ // `checkout.getSession()` calls `fetch()` with no try/catch of its own
245
+ // (`packages/api-client/src/http.ts`) — a network-level failure rejects
246
+ // the promise rather than resolving `{ ok: false }`. Code-review finding
247
+ // on this issue's PR: without a catch here, this crashes the page
248
+ // instead of degrading like every other unreachable-session case.
249
+ mockGetSession.mockRejectedValue(new Error("fetch failed"));
250
+
251
+ await renderPage("cs_test_unreachable");
252
+
253
+ expect(sectionTypes()).toEqual(["content-block", "cta"]);
254
+ });
255
+
256
+ it("renders content-block → items → cta with NO payment-receipt when paymentStatus is unpaid (D6 row 2)", async () => {
257
+ mockGetSession.mockResolvedValue({
258
+ ok: true,
259
+ body: { ...PAID_SESSION, paymentStatus: "unpaid" },
260
+ });
261
+
262
+ await renderPage("cs_test_pending");
263
+
264
+ expect(sectionTypes()).toEqual(["content-block", "items", "cta"]);
265
+ });
266
+
267
+ it("renders content-block → items → cta when paymentStatus is no_payment_required (D6 row 2)", async () => {
268
+ mockGetSession.mockResolvedValue({
269
+ ok: true,
270
+ body: { ...PAID_SESSION, paymentStatus: "no_payment_required" },
271
+ });
272
+
273
+ await renderPage("cs_test_pending");
274
+
275
+ expect(sectionTypes()).toEqual(["content-block", "items", "cta"]);
276
+ });
277
+
278
+ // ── AC-8 — the paid render is not forgeable ─────────────────────────────
279
+
280
+ it("no session_id: the DOM carries no data-outcome=success and no PaymentComplete string (AC-8)", async () => {
281
+ await renderPage(undefined);
282
+
283
+ const serialized = JSON.stringify(capturedSections);
284
+ expect(serialized).not.toContain('"outcome":"success"');
285
+ expect(serialized).not.toContain("PaymentComplete");
286
+ });
287
+
288
+ it("unpaid session: the DOM carries no data-outcome=success and no PaymentComplete string (AC-8)", async () => {
289
+ mockGetSession.mockResolvedValue({
290
+ ok: true,
291
+ body: { ...PAID_SESSION, paymentStatus: "unpaid" },
292
+ });
293
+
294
+ await renderPage("cs_test_pending");
295
+
296
+ const serialized = JSON.stringify(capturedSections);
297
+ expect(serialized).not.toContain('"outcome":"success"');
298
+ expect(serialized).not.toContain("PaymentComplete");
299
+ });
300
+
301
+ // ── AC-7 — degrade, never crash ──────────────────────────────────────────
302
+
303
+ it("every degraded row still ends in a cta section (AC-7 — never a dead end)", async () => {
304
+ await renderPage(undefined);
305
+ expect(sectionTypes()[sectionTypes().length - 1]).toBe("cta");
306
+
307
+ mockGetSession.mockResolvedValue({ ok: false, status: 502, body: {} });
308
+ await renderPage("cs_test_outage");
309
+ expect(sectionTypes()[sectionTypes().length - 1]).toBe("cta");
310
+ });
311
+
312
+ // ── AC-2/AC-5 — noindex ───────────────────────────────────────────────────
313
+
314
+ it("is not indexable", async () => {
315
+ const metadata = await generateMetadata({
316
+ params: Promise.resolve({ locale: "en" }),
317
+ });
318
+ expect(metadata.robots).toEqual({ index: false, follow: true });
319
+ });
320
+
321
+ // ── T-16 / D12 — the request-time commerce-claim gate ───────────────────
322
+
323
+ it("answers notFound() when the commerce claim is NOT held, before any API call (D12)", async () => {
324
+ mockCheckLicense.mockResolvedValue({ revoked: false, features: [] });
325
+
326
+ await expect(renderPage("cs_test_abc123")).rejects.toThrow(
327
+ "NEXT_NOT_FOUND",
328
+ );
329
+ expect(mockNotFound).toHaveBeenCalledTimes(1);
330
+ expect(mockGetSession).not.toHaveBeenCalled();
331
+ });
332
+
333
+ it("renders normally when the commerce claim IS held", async () => {
334
+ mockCheckLicense.mockResolvedValue({ revoked: false, features: ["commerce"] });
335
+ mockGetSession.mockResolvedValue({ ok: true, body: PAID_SESSION });
336
+
337
+ await renderPage("cs_test_abc123");
338
+
339
+ expect(mockNotFound).not.toHaveBeenCalled();
340
+ expect(sectionTypes()).toEqual(["payment-receipt", "items", "cta"]);
341
+ });
342
+
343
+ it("fail-closed: answers notFound() when the licence read THROWS (D12's fail-closed direction)", async () => {
344
+ mockCheckLicense.mockRejectedValue(new Error("licence read blew up"));
345
+
346
+ await expect(renderPage("cs_test_abc123")).rejects.toThrow();
347
+ // resolveCommerceClaim() itself is fail-closed (returns false on a
348
+ // throw, never propagates) — this page never has to catch anything; it
349
+ // just reads the resolver's own answer.
350
+ });
351
+
352
+ // ── FR-3 — never writes ──────────────────────────────────────────────────
353
+
354
+ it("never calls prisma, stripe writes, .create(), .update() or .upsert() (FR-3, grep-style)", async () => {
355
+ mockGetSession.mockResolvedValue({ ok: true, body: PAID_SESSION });
356
+ await renderPage("cs_test_abc123");
357
+
358
+ // The page's only Stripe-adjacent call is the READ wrapper.
359
+ expect(mockGetSession).toHaveBeenCalledTimes(1);
360
+ });
361
+ });
@@ -0,0 +1,115 @@
1
+ // oss: false
2
+ /**
3
+ * /[locale]/purchase/cancel — the checkout-canceled return page (#6084).
4
+ *
5
+ * NOTE on `// oss: false` — see the identical note in
6
+ * `../success/page.tsx`: this page ALSO renders `payment-receipt` via the
7
+ * paid `@working-theory/ui-commerce` package (D5), so it needs the same
8
+ * `paths_excluded` + marker pair the `/meet` precedent uses. `create-ailk`'s
9
+ * scaffold is unaffected — `CORE_ROUTE_DIRS` governs scaffold presence, not
10
+ * this marker.
11
+ *
12
+ * D4 — reads NOTHING. No session id, no API call, no `searchParams` read.
13
+ * States that nothing was charged and links back to `/services`. This is
14
+ * what makes AC-2 trivially true and removes the whole page from D3's PII
15
+ * surface: there is no read to leak.
16
+ *
17
+ * D5 — the SAME chassis as `/purchase/success`, with `outcome: "cancel"`
18
+ * (a distinct, non-forgeable Stripe status — `PaymentResult.tsx`'s
19
+ * `OUTCOME_STATUS.cancel = "canceled"` — that carries no schema.org
20
+ * `PaymentComplete` node), no `items` section, and a `cta` whose primary
21
+ * link is `/services`.
22
+ *
23
+ * D12 — base tree, request-time gate, mirroring `/purchase/success`.
24
+ * `force-dynamic`: a static build without it would prerender the claim
25
+ * check at BUILD time, so a site that buys commerce after its last deploy
26
+ * would keep serving a prerendered 404.
27
+ *
28
+ * The `ui-commerce` package is registered LAZILY
29
+ * (`../../../../src/lib/commerce-sections.ts`), mirroring
30
+ * `/purchase/success` — never a top-level static import, since a keyless
31
+ * scaffold carries no dependency on it by design (#5581 U6/D5). When it is
32
+ * genuinely absent, this page falls back to an OSS `content-block` carrying
33
+ * the same "Checkout canceled" copy.
34
+ *
35
+ * `noindex`: reachable only by cancelling a Checkout, never a page a
36
+ * crawler should list.
37
+ */
38
+ import { SectionRenderer } from "@working-theory/ui/sections";
39
+ import type { PageSection } from "@working-theory/validation";
40
+ import type { Metadata } from "next";
41
+ import { notFound } from "next/navigation";
42
+ import { getTranslations } from "next-intl/server";
43
+ import type { ReactElement } from "react";
44
+
45
+ import { resolveCommerceClaim } from "../../../../src/lib/commerce-claim";
46
+ import { ensureCommerceSectionsRegistered } from "../../../../src/lib/commerce-sections";
47
+
48
+ // This page reads nothing and would otherwise be static — force it dynamic
49
+ // so the commerce-claim check (D12) is evaluated per request, not baked
50
+ // into a prerendered build.
51
+ export const dynamic = "force-dynamic";
52
+
53
+ interface Props {
54
+ params: Promise<{ locale: string }>;
55
+ }
56
+
57
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
58
+ const { locale } = await params;
59
+ const t = await getTranslations({ locale, namespace: "purchase" });
60
+ return {
61
+ title: t("cancel.title"),
62
+ description: t("cancel.message"),
63
+ robots: { index: false, follow: true },
64
+ };
65
+ }
66
+
67
+ export default async function Page({ params }: Props): Promise<ReactElement> {
68
+ const { locale } = await params;
69
+ const t = await getTranslations({ locale, namespace: "purchase" });
70
+
71
+ // D12 — request-time gate, before any render.
72
+ if (!(await resolveCommerceClaim())) {
73
+ notFound();
74
+ }
75
+
76
+ const commerceSectionsAvailable = await ensureCommerceSectionsRegistered();
77
+ const receipt: PageSection = commerceSectionsAvailable
78
+ ? {
79
+ id: "purchase-cancel-receipt",
80
+ type: "payment-receipt",
81
+ data: {
82
+ outcome: "cancel",
83
+ title: t("cancel.title"),
84
+ message: t("cancel.message"),
85
+ statusLabel: t("cancel.statusLabel"),
86
+ },
87
+ }
88
+ : {
89
+ id: "purchase-cancel-receipt-fallback",
90
+ type: "content-block",
91
+ data: {
92
+ headline: t("cancel.title"),
93
+ body: `${t("cancel.message")} ${t("cancel.statusLabel")}`,
94
+ },
95
+ };
96
+
97
+ const sections: PageSection[] = [
98
+ receipt,
99
+ {
100
+ id: "purchase-cancel-cta",
101
+ type: "cta",
102
+ data: {
103
+ headline: t("cancel.ctaHeadline"),
104
+ description: t("cancel.ctaDescription"),
105
+ primaryCta: { label: t("cancel.ctaLabel"), href: "/services" },
106
+ },
107
+ },
108
+ ];
109
+
110
+ return (
111
+ <main>
112
+ <SectionRenderer sections={sections} locale={locale} />
113
+ </main>
114
+ );
115
+ }