create-ailk 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -14
- package/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +17 -10
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +301 -13
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +26 -32
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +0 -4
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +36 -2
- package/dist/lib/scaffold-render-sweep.js +50 -8
- package/dist/module-architecture.js +115 -10
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/spoke-checkout.d.ts +38 -35
- package/dist/spoke-checkout.js +66 -229
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +33 -12
- package/dist/sync-routes.d.ts +68 -5
- package/dist/sync-routes.js +455 -29
- package/dist/template-record.d.ts +1 -1
- package/dist/template-record.js +3 -2
- package/dist/update.js +10 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +13 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +34 -0
- package/templates/apps/api/CLAUDE.md +94 -19
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/site.ts +4 -1
- package/templates/apps/api/src/lib/stripe.ts +77 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
- package/templates/apps/api/src/openapi/spec.ts +343 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +4 -2
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +34 -119
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/search.ts +16 -45
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +377 -308
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
- package/templates/apps/api/src/server.ts +165 -51
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
- package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
- package/templates/apps/api/src/services/abandon-email.ts +28 -5
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +113 -58
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +147 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/project-listings.ts +43 -31
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +32 -18
- package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
- package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
- package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
- package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
- package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/package.json +1 -0
- package/templates/apps/mcp/src/abuse/index.ts +18 -11
- package/templates/apps/mcp/src/http-transport.ts +47 -9
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/lib/site-config.ts +53 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/index.ts +22 -21
- package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/tools/read_content.ts +46 -40
- package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
- package/templates/apps/mcp/src/tools/search_content.ts +20 -24
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +324 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +9 -20
- package/templates/apps/web/lib/deck-page.ts +1 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +44 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +205 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
- package/templates/apps/web/messages/en.json +49 -15
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +51 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +63 -1
- package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +57 -2
- package/templates/tools/lint-architecture.ts +205 -1
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
- package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/checkout.ts +0 -93
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* `resolveMemberPersonId` and `setPersonPlanOffering` (#7877 D8; AC-3, AC-5),
|
|
8
8
|
* and the hub's buyer read, `resolveCallerBillingCustomerId`, with the
|
|
9
9
|
* stamped-Person guard on the customer-id write (#7961, spec
|
|
10
|
-
* docs/specs/7894-the-hub-signs-in-its-spec.md D13, SEC-7894-6; AC-8)
|
|
10
|
+
* docs/specs/7894-the-hub-signs-in-its-spec.md D13, SEC-7894-6; AC-8),
|
|
11
|
+
* and the held checkout plan's release on a verified first link
|
|
12
|
+
* (#8793, spec docs/specs/8793-the-hub-s-checkout-plan-spec.md D2–D5;
|
|
13
|
+
* AC-1, AC-2, AC-6).
|
|
11
14
|
*
|
|
12
15
|
* Runs over `createInMemoryPrisma()`, whose `person` delegate enforces the
|
|
13
16
|
* declared `(workspaceId, email)` and `(workspaceId, userId)` keys the way
|
|
@@ -36,6 +39,7 @@ import { prisma } from "../../lib/prisma.js";
|
|
|
36
39
|
import { personContactPatchSchema } from "@working-theory/validation";
|
|
37
40
|
|
|
38
41
|
import {
|
|
42
|
+
checkoutPlanKey,
|
|
39
43
|
contactConfirmedFromRow,
|
|
40
44
|
ensureMemberPerson,
|
|
41
45
|
findOrCreatePerson,
|
|
@@ -45,11 +49,13 @@ import {
|
|
|
45
49
|
normalizeTelephone,
|
|
46
50
|
personContactFromRow,
|
|
47
51
|
recordCheckoutPerson,
|
|
52
|
+
releaseHeldCheckoutPlan,
|
|
48
53
|
resolveCallerBillingCustomerId,
|
|
49
54
|
resolveMemberPersonId,
|
|
50
55
|
setOwnPersonContact,
|
|
51
56
|
setPersonPlanOffering,
|
|
52
57
|
stampPersonUser,
|
|
58
|
+
stripeCustomerDashboardUrl,
|
|
53
59
|
updatePersonContact,
|
|
54
60
|
} from "../person";
|
|
55
61
|
|
|
@@ -61,6 +67,7 @@ const store = prisma as unknown as {
|
|
|
61
67
|
__db: Record<string, Row[]>;
|
|
62
68
|
__calls: Array<{ model: string; op: string }>;
|
|
63
69
|
person: Record<string, jest.Mock>;
|
|
70
|
+
lead: Record<string, jest.Mock>;
|
|
64
71
|
};
|
|
65
72
|
|
|
66
73
|
const WS_A1 = "wksp_00000000-0000-7000-8000-0000000000a1";
|
|
@@ -943,3 +950,255 @@ describe("setPersonPlanOffering (#7877 D8, AC-4, AC-5)", () => {
|
|
|
943
950
|
expect(await setPersonPlanOffering(WS_A1, "person_x", "retainer")).toBe("internal_error");
|
|
944
951
|
});
|
|
945
952
|
});
|
|
953
|
+
|
|
954
|
+
// ─── The held checkout plan (#8793) ──────────────────────────────────────────
|
|
955
|
+
|
|
956
|
+
const leads = () => (store.__db["lead"] ??= []);
|
|
957
|
+
const leadCalls = () => store.__calls.filter((c) => c.model === "lead");
|
|
958
|
+
|
|
959
|
+
/** A spoke checkout lead on a Person, newest last unless `createdAt` says otherwise. */
|
|
960
|
+
function seedMarkedLead(personId: string, row: Row = {}): Row {
|
|
961
|
+
const full: Row = {
|
|
962
|
+
id: `lead_00000000-0000-7000-8000-${String(leads().length + 700).padStart(12, "0")}`,
|
|
963
|
+
workspaceId: WS_A1,
|
|
964
|
+
personId,
|
|
965
|
+
email: "Ada@Example.com",
|
|
966
|
+
source: "web",
|
|
967
|
+
spokeCheckout: true,
|
|
968
|
+
payload: { site: "wendi", page: "checkout", offering: "wendi_retainer" },
|
|
969
|
+
createdAt: new Date(Date.UTC(2026, 1, 1 + leads().length)),
|
|
970
|
+
...row,
|
|
971
|
+
};
|
|
972
|
+
leads().push(full);
|
|
973
|
+
return full;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
describe("checkoutPlanKey (#8793 D4)", () => {
|
|
977
|
+
it("strips a leading <site>_ and answers a plan key, else null", () => {
|
|
978
|
+
expect(checkoutPlanKey("wendi_retainer", "wendi")).toBe("retainer");
|
|
979
|
+
expect(checkoutPlanKey("retainer", "wendi")).toBe("retainer");
|
|
980
|
+
expect(checkoutPlanKey("wendi_Not A Key!", "wendi")).toBeNull();
|
|
981
|
+
expect(checkoutPlanKey(42, "wendi")).toBeNull();
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
describe("setPersonPlanOffering's link guard (#8793 D5, AC-1, AC-6)", () => {
|
|
986
|
+
const U = "user_00000000-0000-7000-8000-000000000031";
|
|
987
|
+
const V = "user_00000000-0000-7000-8000-000000000032";
|
|
988
|
+
|
|
989
|
+
it("a guarded write never overwrites a set plan", async () => {
|
|
990
|
+
const p = seedPerson({ email: "ada@example.com", userId: U, planOffering: "premium" });
|
|
991
|
+
expect(
|
|
992
|
+
await setPersonPlanOffering(WS_A1, p["id"] as string, "x", { linkedUserId: U }),
|
|
993
|
+
).toBe("not_found");
|
|
994
|
+
expect(people()[0]!["planOffering"]).toBe("premium");
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it("a guarded write never reaches a Person linked to another user", async () => {
|
|
998
|
+
const p = seedPerson({ email: "ada@example.com", userId: U });
|
|
999
|
+
expect(
|
|
1000
|
+
await setPersonPlanOffering(WS_A1, p["id"] as string, "x", { linkedUserId: V }),
|
|
1001
|
+
).toBe("not_found");
|
|
1002
|
+
expect(people()[0]!["planOffering"]).toBeNull();
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it("the operator's three-argument write still overwrites a set plan", async () => {
|
|
1006
|
+
const p = seedPerson({ email: "ada@example.com", userId: U, planOffering: "premium" });
|
|
1007
|
+
expect(await setPersonPlanOffering(WS_A1, p["id"] as string, "basic")).toBe("set");
|
|
1008
|
+
expect(people()[0]!["planOffering"]).toBe("basic");
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
describe("releaseHeldCheckoutPlan (#8793 D3, AC-1)", () => {
|
|
1013
|
+
const U = "user_00000000-0000-7000-8000-000000000041";
|
|
1014
|
+
const EMAIL = "ada@example.com";
|
|
1015
|
+
let personId: string;
|
|
1016
|
+
|
|
1017
|
+
const release = () => releaseHeldCheckoutPlan(WS_A1, personId, U, EMAIL);
|
|
1018
|
+
const plan = () => people()[0]!["planOffering"];
|
|
1019
|
+
|
|
1020
|
+
beforeEach(() => {
|
|
1021
|
+
personId = seedPerson({ email: EMAIL, userId: U })["id"] as string;
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it("one marked lead sets its key", async () => {
|
|
1025
|
+
seedMarkedLead(personId);
|
|
1026
|
+
expect(await release()).toBe("set");
|
|
1027
|
+
expect(plan()).toBe("retainer");
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it("two marked leads naming one key, with and without the site prefix, set it", async () => {
|
|
1031
|
+
seedMarkedLead(personId);
|
|
1032
|
+
seedMarkedLead(personId, {
|
|
1033
|
+
payload: { site: "wendi", page: "checkout", offering: "retainer" },
|
|
1034
|
+
});
|
|
1035
|
+
expect(await release()).toBe("set");
|
|
1036
|
+
expect(plan()).toBe("retainer");
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it.each([
|
|
1040
|
+
["the basic purchase is newer", 1, 2],
|
|
1041
|
+
["the retainer purchase is newer", 2, 1],
|
|
1042
|
+
])("two marked leads naming different keys are ambiguous whatever their order (%s)", async (_label, retainerDay, basicDay) => {
|
|
1043
|
+
seedMarkedLead(personId, { createdAt: new Date(Date.UTC(2026, 2, retainerDay)) });
|
|
1044
|
+
seedMarkedLead(personId, {
|
|
1045
|
+
createdAt: new Date(Date.UTC(2026, 2, basicDay)),
|
|
1046
|
+
payload: { site: "wendi", page: "checkout", offering: "wendi_basic" },
|
|
1047
|
+
});
|
|
1048
|
+
expect(await release()).toBe("ambiguous");
|
|
1049
|
+
expect(plan()).toBeNull();
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
it("a marked lead under another email is ignored", async () => {
|
|
1053
|
+
seedMarkedLead(personId);
|
|
1054
|
+
seedMarkedLead(personId, {
|
|
1055
|
+
email: "other@example.com",
|
|
1056
|
+
payload: { site: "wendi", page: "checkout", offering: "wendi_basic" },
|
|
1057
|
+
});
|
|
1058
|
+
expect(await release()).toBe("set");
|
|
1059
|
+
expect(plan()).toBe("retainer");
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it("only unmarked leads are none", async () => {
|
|
1063
|
+
seedMarkedLead(personId, { spokeCheckout: false });
|
|
1064
|
+
expect(await release()).toBe("none");
|
|
1065
|
+
expect(plan()).toBeNull();
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("only an offering that is no plan key is none", async () => {
|
|
1069
|
+
seedMarkedLead(personId, {
|
|
1070
|
+
payload: { site: "wendi", page: "checkout", offering: "wendi_Not A Key!" },
|
|
1071
|
+
});
|
|
1072
|
+
expect(await release()).toBe("none");
|
|
1073
|
+
expect(plan()).toBeNull();
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it("51 marked leads are ambiguous, even when they all agree", async () => {
|
|
1077
|
+
for (let i = 0; i < 51; i += 1) seedMarkedLead(personId);
|
|
1078
|
+
expect(await release()).toBe("ambiguous");
|
|
1079
|
+
expect(plan()).toBeNull();
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
it("a plan already set is kept", async () => {
|
|
1083
|
+
people()[0]!["planOffering"] = "premium";
|
|
1084
|
+
seedMarkedLead(personId);
|
|
1085
|
+
expect(await release()).toBe("kept");
|
|
1086
|
+
expect(plan()).toBe("premium");
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it("a failed lead read is internal_error, with no write", async () => {
|
|
1090
|
+
seedMarkedLead(personId);
|
|
1091
|
+
store.lead["findMany"]!.mockRejectedValueOnce(new Error("down"));
|
|
1092
|
+
expect(await release()).toBe("internal_error");
|
|
1093
|
+
expect(personCalls().filter((c) => c.op === "updateMany")).toHaveLength(0);
|
|
1094
|
+
expect(plan()).toBeNull();
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
describe("resolveMemberPersonId releases a held plan on a fresh link only (#8793 D2, AC-2)", () => {
|
|
1099
|
+
const users = () => (store.__db["user"] ??= []);
|
|
1100
|
+
const U = "user_00000000-0000-7000-8000-000000000051";
|
|
1101
|
+
const EMAIL = "ada@example.com";
|
|
1102
|
+
const log = { warn: jest.fn() };
|
|
1103
|
+
let personId: string;
|
|
1104
|
+
|
|
1105
|
+
const plan = () => people()[0]!["planOffering"];
|
|
1106
|
+
const releaseLines = () =>
|
|
1107
|
+
log.warn.mock.calls
|
|
1108
|
+
.map(([fields]) => fields as Record<string, unknown>)
|
|
1109
|
+
.filter((fields) => fields["event"] === "checkout-plan-release");
|
|
1110
|
+
|
|
1111
|
+
beforeEach(() => {
|
|
1112
|
+
log.warn.mockClear();
|
|
1113
|
+
personId = seedPerson({ email: EMAIL })["id"] as string;
|
|
1114
|
+
seedMarkedLead(personId);
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
it("a verified caller's first link releases the held plan, logging nothing", async () => {
|
|
1118
|
+
users().push({ id: U, email: EMAIL, emailVerified: true });
|
|
1119
|
+
expect(await resolveMemberPersonId(WS_A1, U, EMAIL, log)).toBe(personId);
|
|
1120
|
+
expect(people()[0]!["userId"]).toBe(U);
|
|
1121
|
+
expect(plan()).toBe("retainer");
|
|
1122
|
+
expect(releaseLines()).toEqual([]);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it("an unverified caller links nothing and releases nothing", async () => {
|
|
1126
|
+
users().push({ id: U, email: EMAIL, emailVerified: false });
|
|
1127
|
+
expect(await resolveMemberPersonId(WS_A1, U, EMAIL, log)).toBeNull();
|
|
1128
|
+
expect(plan()).toBeNull();
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it("a Person linked before the call is answered with no release and no lead read", async () => {
|
|
1132
|
+
people()[0]!["userId"] = U;
|
|
1133
|
+
expect(await resolveMemberPersonId(WS_A1, U, EMAIL, log)).toBe(personId);
|
|
1134
|
+
expect(plan()).toBeNull();
|
|
1135
|
+
expect(leadCalls()).toHaveLength(0);
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it("a direct stamp (the inbox accept's and the consents route's path) releases nothing", async () => {
|
|
1139
|
+
users().push({ id: U, email: EMAIL, emailVerified: true });
|
|
1140
|
+
expect(await stampPersonUser(WS_A1, personId, U)).toBe("stamped");
|
|
1141
|
+
expect(plan()).toBeNull();
|
|
1142
|
+
expect(leadCalls()).toHaveLength(0);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
it("a failed release still answers the Person and logs its kind only", async () => {
|
|
1146
|
+
users().push({ id: U, email: EMAIL, emailVerified: true });
|
|
1147
|
+
store.lead["findMany"]!.mockRejectedValueOnce(new Error("down"));
|
|
1148
|
+
expect(await resolveMemberPersonId(WS_A1, U, EMAIL, log)).toBe(personId);
|
|
1149
|
+
expect(plan()).toBeNull();
|
|
1150
|
+
expect(releaseLines()).toEqual([
|
|
1151
|
+
{ event: "checkout-plan-release", outcome: "internal_error" },
|
|
1152
|
+
]);
|
|
1153
|
+
const logged = JSON.stringify(log.warn.mock.calls);
|
|
1154
|
+
expect(logged).not.toContain(EMAIL);
|
|
1155
|
+
expect(logged).not.toContain("retainer");
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it("disagreeing purchases release nothing and log ambiguous", async () => {
|
|
1159
|
+
users().push({ id: U, email: EMAIL, emailVerified: true });
|
|
1160
|
+
seedMarkedLead(personId, {
|
|
1161
|
+
payload: { site: "wendi", page: "checkout", offering: "wendi_basic" },
|
|
1162
|
+
});
|
|
1163
|
+
expect(await resolveMemberPersonId(WS_A1, U, EMAIL, log)).toBe(personId);
|
|
1164
|
+
expect(plan()).toBeNull();
|
|
1165
|
+
expect(releaseLines()).toEqual([
|
|
1166
|
+
{ event: "checkout-plan-release", outcome: "ambiguous" },
|
|
1167
|
+
]);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it("a release that finds nothing to set logs nothing", async () => {
|
|
1171
|
+
leads().length = 0;
|
|
1172
|
+
users().push({ id: U, email: EMAIL, emailVerified: true });
|
|
1173
|
+
expect(await resolveMemberPersonId(WS_A1, U, EMAIL, log)).toBe(personId);
|
|
1174
|
+
expect(plan()).toBeNull();
|
|
1175
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
describe("stripeCustomerDashboardUrl (#9349 DD5, AC-9)", () => {
|
|
1180
|
+
it("builds the live and the test Dashboard links", () => {
|
|
1181
|
+
expect(stripeCustomerDashboardUrl("cus_ABC123", "live")).toBe(
|
|
1182
|
+
"https://dashboard.stripe.com/customers/cus_ABC123",
|
|
1183
|
+
);
|
|
1184
|
+
expect(stripeCustomerDashboardUrl("cus_ABC123", "test")).toBe(
|
|
1185
|
+
"https://dashboard.stripe.com/test/customers/cus_ABC123",
|
|
1186
|
+
);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it("refuses an id outside the cus_ grammar or over 255 characters", () => {
|
|
1190
|
+
for (const id of [
|
|
1191
|
+
"cus_a/../../settings",
|
|
1192
|
+
"cus_",
|
|
1193
|
+
"acct_1",
|
|
1194
|
+
"cus_a?x=1",
|
|
1195
|
+
"cus_a#x",
|
|
1196
|
+
`cus_${"a".repeat(252)}`,
|
|
1197
|
+
]) {
|
|
1198
|
+
expect(stripeCustomerDashboardUrl(id, "live")).toBeNull();
|
|
1199
|
+
}
|
|
1200
|
+
expect(
|
|
1201
|
+
stripeCustomerDashboardUrl(`cus_${"a".repeat(251)}`, "live"),
|
|
1202
|
+
).not.toBeNull();
|
|
1203
|
+
});
|
|
1204
|
+
});
|