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.
- package/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +142 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /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
|
+
}
|