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
|
@@ -35,7 +35,7 @@ jest.mock("@working-theory/license", () => ({
|
|
|
35
35
|
jest.mock("../../lib/prisma.js", () => ({
|
|
36
36
|
prisma: {
|
|
37
37
|
projectListing: { findMany: jest.fn() },
|
|
38
|
-
waitlistScore: {
|
|
38
|
+
waitlistScore: { groupBy: jest.fn() },
|
|
39
39
|
},
|
|
40
40
|
}));
|
|
41
41
|
|
|
@@ -102,25 +102,25 @@ function listingRow(id: string, experimentKey: string | null): Record<string, un
|
|
|
102
102
|
|
|
103
103
|
beforeEach(() => {
|
|
104
104
|
jest.clearAllMocks();
|
|
105
|
-
(prisma.waitlistScore.
|
|
105
|
+
(prisma.waitlistScore.groupBy as jest.Mock).mockResolvedValue([]);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
describe("attachWaitlistCounts — the fenced count filters on workspaceId beside siteId (#6258 AC-6)", () => {
|
|
109
|
-
it("prisma.waitlistScore.
|
|
109
|
+
it("prisma.waitlistScore.groupBy's where carries workspaceId === the resolved scope id, by value", async () => {
|
|
110
110
|
(prisma.projectListing.findMany as jest.Mock).mockResolvedValue([
|
|
111
111
|
listingRow("plst_a", EXPERIMENT_A),
|
|
112
112
|
]);
|
|
113
113
|
|
|
114
114
|
await listProjectListings(SITE);
|
|
115
115
|
|
|
116
|
-
expect((prisma.waitlistScore.
|
|
116
|
+
expect((prisma.waitlistScore.groupBy as jest.Mock).mock.calls[0]![0]!.where).toEqual({
|
|
117
117
|
siteId: SITE,
|
|
118
118
|
workspaceId: SITE,
|
|
119
|
-
experimentKey: EXPERIMENT_A,
|
|
119
|
+
experimentKey: { in: [EXPERIMENT_A] },
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
it("every DISTINCT experimentKey
|
|
123
|
+
it("every DISTINCT experimentKey sits under the same two scope terms — one grouped read, no more (#9243)", async () => {
|
|
124
124
|
(prisma.projectListing.findMany as jest.Mock).mockResolvedValue([
|
|
125
125
|
listingRow("plst_a", EXPERIMENT_A),
|
|
126
126
|
listingRow("plst_b", EXPERIMENT_B),
|
|
@@ -129,17 +129,14 @@ describe("attachWaitlistCounts — the fenced count filters on workspaceId besid
|
|
|
129
129
|
|
|
130
130
|
await listProjectListings(SITE);
|
|
131
131
|
|
|
132
|
-
expect(prisma.waitlistScore.
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
expect(wheres.map((w) => w.experimentKey).sort()).toEqual(
|
|
141
|
-
[EXPERIMENT_A, EXPERIMENT_B].sort(),
|
|
142
|
-
);
|
|
132
|
+
expect(prisma.waitlistScore.groupBy).toHaveBeenCalledTimes(1);
|
|
133
|
+
const where = (prisma.waitlistScore.groupBy as jest.Mock).mock.calls[0]![0]!.where as Record<
|
|
134
|
+
string,
|
|
135
|
+
unknown
|
|
136
|
+
>;
|
|
137
|
+
expect(where.siteId).toBe(SITE);
|
|
138
|
+
expect(where.workspaceId).toBe(SITE);
|
|
139
|
+
expect(where.experimentKey).toEqual({ in: [EXPERIMENT_A, EXPERIMENT_B] });
|
|
143
140
|
});
|
|
144
141
|
|
|
145
142
|
it("no listing carries an experimentKey → no fenced read is issued at all", async () => {
|
|
@@ -149,6 +146,6 @@ describe("attachWaitlistCounts — the fenced count filters on workspaceId besid
|
|
|
149
146
|
|
|
150
147
|
await listProjectListings(SITE);
|
|
151
148
|
|
|
152
|
-
expect(prisma.waitlistScore.
|
|
149
|
+
expect(prisma.waitlistScore.groupBy).not.toHaveBeenCalled();
|
|
153
150
|
});
|
|
154
151
|
});
|
|
@@ -134,6 +134,16 @@ function buildResumeLink(
|
|
|
134
134
|
return `${resumeUrl}?fs=${sessionId}`;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// ─── Candidate paging (#9249) ───────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* How many candidates one run reads per flow definition. A flow with more
|
|
141
|
+
* past-delay sessions than this processes one page and leaves the rest —
|
|
142
|
+
* still `abandonEmailStatus: null` — for the next run. Same page as
|
|
143
|
+
* `STANDARD_SEED_PAGE` (lib/tenant-db.ts), this app's other background page.
|
|
144
|
+
*/
|
|
145
|
+
export const ABANDON_CANDIDATE_PAGE_SIZE = 500;
|
|
146
|
+
|
|
137
147
|
// ─── The sweep ──────────────────────────────────────────────────────────────────
|
|
138
148
|
|
|
139
149
|
/**
|
|
@@ -197,19 +207,21 @@ async function sweepAllDefinitions(
|
|
|
197
207
|
email: true,
|
|
198
208
|
locale: true,
|
|
199
209
|
},
|
|
210
|
+
// #9249 — oldest-updated first, so the page is deterministic (the same
|
|
211
|
+
// reason flow-aggregate.ts orders its capped read).
|
|
212
|
+
orderBy: { updatedAt: "asc" },
|
|
213
|
+
take: ABANDON_CANDIDATE_PAGE_SIZE,
|
|
200
214
|
})) as AbandonCandidateRow[];
|
|
201
215
|
|
|
202
216
|
summary.scanned += candidates.length;
|
|
203
217
|
|
|
218
|
+
const darkIds: string[] = [];
|
|
219
|
+
|
|
204
220
|
for (const session of candidates) {
|
|
205
221
|
summary.selected += 1;
|
|
206
222
|
|
|
207
223
|
if (policy.mode === "dark") {
|
|
208
|
-
|
|
209
|
-
where: { id: session.id },
|
|
210
|
-
data: { abandonEmailStatus: "dark", abandonEmailAt: now },
|
|
211
|
-
});
|
|
212
|
-
summary.dark += 1;
|
|
224
|
+
darkIds.push(session.id);
|
|
213
225
|
logOutcome(session, policy.mode, "dark");
|
|
214
226
|
continue;
|
|
215
227
|
}
|
|
@@ -265,6 +277,17 @@ async function sweepAllDefinitions(
|
|
|
265
277
|
logOutcome(session, policy.mode, "failed");
|
|
266
278
|
}
|
|
267
279
|
}
|
|
280
|
+
|
|
281
|
+
// #9249 — one stamp for the whole dark-mode page, not one per row. The
|
|
282
|
+
// `abandonEmailStatus: null` guard is the candidate read's own term, so a
|
|
283
|
+
// row an overlapping run stamped meanwhile keeps what that run wrote.
|
|
284
|
+
if (darkIds.length > 0) {
|
|
285
|
+
await prisma.flowSession.updateMany({
|
|
286
|
+
where: { id: { in: darkIds }, abandonEmailStatus: null },
|
|
287
|
+
data: { abandonEmailStatus: "dark", abandonEmailAt: now },
|
|
288
|
+
});
|
|
289
|
+
summary.dark += darkIds.length;
|
|
290
|
+
}
|
|
268
291
|
}
|
|
269
292
|
}
|
|
270
293
|
|
|
@@ -84,12 +84,6 @@ export type FulfillDeliverableInput = {
|
|
|
84
84
|
locale: string;
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
type DeliverableAssetRow = {
|
|
88
|
-
id: string;
|
|
89
|
-
siteId: string;
|
|
90
|
-
key: string;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
87
|
// Derived from the schema type (not hand-rolled) so it never drifts from
|
|
94
88
|
// `deliverableAssetMapEntrySchema`'s actual Zod-inferred optional shape.
|
|
95
89
|
type DeliverableEntry = NonNullable<
|
|
@@ -387,7 +381,7 @@ export async function sendCompletionEmail(
|
|
|
387
381
|
|
|
388
382
|
export async function fulfillDeliverable(
|
|
389
383
|
input: FulfillDeliverableInput,
|
|
390
|
-
): Promise<
|
|
384
|
+
): Promise<{ grantId: string } | undefined> {
|
|
391
385
|
const {
|
|
392
386
|
siteId,
|
|
393
387
|
flowSlug,
|
|
@@ -403,7 +397,7 @@ export async function fulfillDeliverable(
|
|
|
403
397
|
// the call on this being present; re-checking here is defense in depth,
|
|
404
398
|
// never load-bearing for the "no block → no call" AC4 regression floor.
|
|
405
399
|
const block = configSnapshot.deliverables;
|
|
406
|
-
if (!block) return;
|
|
400
|
+
if (!block) return undefined;
|
|
407
401
|
|
|
408
402
|
const logCtx = { siteId, flowSlug, sessionId };
|
|
409
403
|
|
|
@@ -412,7 +406,7 @@ export async function fulfillDeliverable(
|
|
|
412
406
|
const entry = resolveEntry(block.assets, segment);
|
|
413
407
|
if (!entry) {
|
|
414
408
|
logOutcome(logCtx, "no_entry");
|
|
415
|
-
return;
|
|
409
|
+
return undefined;
|
|
416
410
|
}
|
|
417
411
|
|
|
418
412
|
// FEAT-074 U4b (#4718) D4 — the `playbook:` virtual asset: a per-lead
|
|
@@ -439,7 +433,7 @@ export async function fulfillDeliverable(
|
|
|
439
433
|
});
|
|
440
434
|
if (!instance) {
|
|
441
435
|
logOutcome(logCtx, "playbook_missing");
|
|
442
|
-
return;
|
|
436
|
+
return undefined;
|
|
443
437
|
}
|
|
444
438
|
assetId = entry.assetKey;
|
|
445
439
|
} else {
|
|
@@ -464,6 +458,13 @@ export async function fulfillDeliverable(
|
|
|
464
458
|
assetId = resolvedAssetId;
|
|
465
459
|
grantWorkspaceId = workspaceId;
|
|
466
460
|
} else {
|
|
461
|
+
// #8848 D3 (SEC-8848-B) — a `deliverable` flow resolves against the
|
|
462
|
+
// Asset store only: it is new, so it has no legacy key to keep, and the
|
|
463
|
+
// legacy store also holds the founders' signup CSVs.
|
|
464
|
+
if (configSnapshot.deliverable !== undefined) {
|
|
465
|
+
logOutcome(logCtx, "asset_missing");
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
467
468
|
// #6310 D2/D3 — legacy fallback. A missing row (config references an
|
|
468
469
|
// un-uploaded key) → same skip-and-log posture as before.
|
|
469
470
|
// #6257 — the LEGACY store's read, scoped by the same resolved
|
|
@@ -472,11 +473,12 @@ export async function fulfillDeliverable(
|
|
|
472
473
|
// one row; `workspaceId` is what makes the tenant the QUERY's statement.
|
|
473
474
|
const asset = (await prisma.deliverableAsset.findUnique({
|
|
474
475
|
where: { siteId_key: { siteId, key: entry.assetKey }, workspaceId: siteId },
|
|
475
|
-
|
|
476
|
+
select: { id: true },
|
|
477
|
+
})) as { id: string } | null;
|
|
476
478
|
|
|
477
479
|
if (!asset) {
|
|
478
480
|
logOutcome(logCtx, "asset_missing");
|
|
479
|
-
return;
|
|
481
|
+
return undefined;
|
|
480
482
|
}
|
|
481
483
|
assetId = asset.id;
|
|
482
484
|
}
|
|
@@ -527,6 +529,7 @@ export async function fulfillDeliverable(
|
|
|
527
529
|
defaultLocale: configSnapshot.defaultLocale,
|
|
528
530
|
copy: block.copy,
|
|
529
531
|
});
|
|
532
|
+
return { grantId: grant.id };
|
|
530
533
|
}
|
|
531
534
|
|
|
532
535
|
// ─── #3998 D3 — resend: reuses the existing grant id, never mints ───────────────
|
|
@@ -15,15 +15,24 @@
|
|
|
15
15
|
* foundation fills `features` + `limits.static` (tier + seat count) and returns
|
|
16
16
|
* `limits.metered` as the reserved empty shape FEAT-047 (#3679) fills — this
|
|
17
17
|
* spec builds NO metering engine (spec D5).
|
|
18
|
+
*
|
|
19
|
+
* #9067 D5 — the Billing tab's plan card also reads here: the licence expiry,
|
|
20
|
+
* the Organization's one open plan Checkout, and the customer's billing
|
|
21
|
+
* summary. That summary is the one Stripe read this resolver makes, through
|
|
22
|
+
* `lib/stripe.ts`.
|
|
18
23
|
*/
|
|
19
|
-
import type
|
|
24
|
+
import { checkLicense, type Entitlements } from "@working-theory/license";
|
|
25
|
+
import { logger } from "@working-theory/observability";
|
|
20
26
|
import {
|
|
21
27
|
MAGNET_KIND_MIN_RANK,
|
|
28
|
+
orgBillingDetailsSchema,
|
|
22
29
|
planRank,
|
|
23
30
|
type FlowConfig,
|
|
31
|
+
type OrgBillingDetails,
|
|
24
32
|
} from "@working-theory/validation";
|
|
25
33
|
|
|
26
34
|
import { prisma } from "../lib/prisma.js";
|
|
35
|
+
import { readCustomerBillingSummary } from "../lib/stripe.js";
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* Feature flags unlocked per tier. Free unlocks none; every paid tier unlocks
|
|
@@ -180,3 +189,150 @@ export async function isBillingCustomerAnOrganization(
|
|
|
180
189
|
});
|
|
181
190
|
return org !== null;
|
|
182
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* #9067 D5 — the deployment licence's expiry, as an ISO string, or `null`
|
|
195
|
+
* for a keyless, expired or invalid licence. The trial state's date
|
|
196
|
+
* (C2). Never throws: a failed read is `null`.
|
|
197
|
+
*/
|
|
198
|
+
export async function resolveLicenseExpiresAt(): Promise<string | null> {
|
|
199
|
+
try {
|
|
200
|
+
const license = await checkLicense();
|
|
201
|
+
return license.expiresAt?.toISOString() ?? null;
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* #9067 D13 — the one plan Checkout this Organization has open, or `null`.
|
|
209
|
+
* Server-side only: the session id is never served. An unknown
|
|
210
|
+
* Organization reads `null`.
|
|
211
|
+
*/
|
|
212
|
+
export async function readOrgPlanCheckoutSession(
|
|
213
|
+
organizationId: string,
|
|
214
|
+
): Promise<string | null> {
|
|
215
|
+
const org = await prisma.organization.findUnique({
|
|
216
|
+
where: { id: organizationId },
|
|
217
|
+
select: { planCheckoutSessionId: true },
|
|
218
|
+
});
|
|
219
|
+
return org?.planCheckoutSessionId ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* #9067 D3 step 9b — record the plan Checkout the route just opened, as one
|
|
224
|
+
* compare-and-set: only while the column still holds `expected` and no
|
|
225
|
+
* customer is bound. Answers whether one row moved; throws on a database
|
|
226
|
+
* error.
|
|
227
|
+
*/
|
|
228
|
+
export async function setOrgPlanCheckoutSession(
|
|
229
|
+
organizationId: string,
|
|
230
|
+
expected: string | null,
|
|
231
|
+
next: string,
|
|
232
|
+
): Promise<boolean> {
|
|
233
|
+
const { count } = await prisma.organization.updateMany({
|
|
234
|
+
where: { id: organizationId, planCheckoutSessionId: expected, billingCustomerId: null },
|
|
235
|
+
data: { planCheckoutSessionId: next },
|
|
236
|
+
});
|
|
237
|
+
return count === 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* #9067 7b (code review) — bind the Organization to the customer a COMPLETED
|
|
242
|
+
* plan Checkout created, when the webhook's bind never landed (a lost event,
|
|
243
|
+
* a database error the webhook swallowed). The same guarded compare-and-set
|
|
244
|
+
* the webhook runs: only while the column still holds `sessionId` and no
|
|
245
|
+
* customer is bound, and it clears the session. Answers whether one row
|
|
246
|
+
* moved; throws on a database error.
|
|
247
|
+
*/
|
|
248
|
+
export async function bindOrgPlanCheckout(
|
|
249
|
+
organizationId: string,
|
|
250
|
+
sessionId: string,
|
|
251
|
+
customerId: string,
|
|
252
|
+
): Promise<boolean> {
|
|
253
|
+
const { count } = await prisma.organization.updateMany({
|
|
254
|
+
where: { id: organizationId, planCheckoutSessionId: sessionId, billingCustomerId: null },
|
|
255
|
+
data: { billingProvider: "stripe", billingCustomerId: customerId, planCheckoutSessionId: null },
|
|
256
|
+
});
|
|
257
|
+
return count === 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** How long one customer's summary is reused, a failure included (#9067 NFR-1). */
|
|
261
|
+
export const ORG_BILLING_SUMMARY_TTL_MS = 60_000;
|
|
262
|
+
/** The most customers held at once; the oldest entry is evicted at the cap. */
|
|
263
|
+
export const ORG_BILLING_SUMMARY_CACHE_MAX = 2_000;
|
|
264
|
+
/** The longest one summary read may take before the keys are left absent (NFR-2). */
|
|
265
|
+
export const ORG_BILLING_SUMMARY_TIMEOUT_MS = 3_000;
|
|
266
|
+
|
|
267
|
+
type OrgBillingSummaryKeys = Partial<Pick<OrgBillingDetails, "renewsAt" | "sites" | "paymentMethod">>;
|
|
268
|
+
|
|
269
|
+
/** Per customer: the settled keys and when they expire. Insertion order is age. */
|
|
270
|
+
const orgBillingSummaryCache = new Map<string, { keys: OrgBillingSummaryKeys; expiresAt: number }>();
|
|
271
|
+
/** Per customer: the one read in flight, joined by every concurrent miss. */
|
|
272
|
+
const orgBillingSummaryInFlight = new Map<string, Promise<OrgBillingSummaryKeys>>();
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* #9067 D5 — the Organization customer's renewal date, site count and card,
|
|
276
|
+
* from the cache when fresh, else one read shared by every concurrent caller
|
|
277
|
+
* of the same customer. The member read's wrapper (`services/subscription.ts`),
|
|
278
|
+
* rebuilt here because importing it would be a cycle. A throw or a timeout
|
|
279
|
+
* answers `{}` (every key absent) and is cached like a success; a value
|
|
280
|
+
* outside `orgBillingDetailsSchema`'s bounds is `null`.
|
|
281
|
+
*/
|
|
282
|
+
export async function readOrgBillingSummary(customerId: string): Promise<OrgBillingSummaryKeys> {
|
|
283
|
+
const cached = orgBillingSummaryCache.get(customerId);
|
|
284
|
+
if (cached && cached.expiresAt > Date.now()) return cached.keys;
|
|
285
|
+
const pending = orgBillingSummaryInFlight.get(customerId);
|
|
286
|
+
if (pending) return pending;
|
|
287
|
+
|
|
288
|
+
const read = settleOrgBillingSummary(customerId).then((keys) => {
|
|
289
|
+
orgBillingSummaryCache.delete(customerId);
|
|
290
|
+
if (orgBillingSummaryCache.size >= ORG_BILLING_SUMMARY_CACHE_MAX) {
|
|
291
|
+
const oldest = orgBillingSummaryCache.keys().next().value;
|
|
292
|
+
if (oldest !== undefined) orgBillingSummaryCache.delete(oldest);
|
|
293
|
+
}
|
|
294
|
+
orgBillingSummaryCache.set(customerId, {
|
|
295
|
+
keys,
|
|
296
|
+
expiresAt: Date.now() + ORG_BILLING_SUMMARY_TTL_MS,
|
|
297
|
+
});
|
|
298
|
+
return keys;
|
|
299
|
+
});
|
|
300
|
+
// Cleared however the read ends, so a rejection is never served forever.
|
|
301
|
+
void read.finally(() => orgBillingSummaryInFlight.delete(customerId)).catch(() => undefined);
|
|
302
|
+
orgBillingSummaryInFlight.set(customerId, read);
|
|
303
|
+
return read;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function settleOrgBillingSummary(customerId: string): Promise<OrgBillingSummaryKeys> {
|
|
307
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
308
|
+
try {
|
|
309
|
+
const timedOut = new Promise<"timeout">((resolve) => {
|
|
310
|
+
timer = setTimeout(() => resolve("timeout"), ORG_BILLING_SUMMARY_TIMEOUT_MS);
|
|
311
|
+
});
|
|
312
|
+
const summary = await Promise.race([readCustomerBillingSummary(customerId), timedOut]);
|
|
313
|
+
if (summary === "timeout") throw new Error("timeout");
|
|
314
|
+
const { shape } = orgBillingDetailsSchema;
|
|
315
|
+
const renewsAt = shape.renewsAt.safeParse(summary.renewsAt);
|
|
316
|
+
const sites = shape.sites.safeParse(summary.sites);
|
|
317
|
+
const paymentMethod = shape.paymentMethod.safeParse(summary.paymentMethod);
|
|
318
|
+
return {
|
|
319
|
+
renewsAt: renewsAt.success ? (renewsAt.data ?? null) : null,
|
|
320
|
+
sites: sites.success ? (sites.data ?? null) : null,
|
|
321
|
+
paymentMethod: paymentMethod.success ? (paymentMethod.data ?? null) : null,
|
|
322
|
+
};
|
|
323
|
+
} catch {
|
|
324
|
+
logger.warn(
|
|
325
|
+
{ route: "billing", outcome: "summary_unavailable" },
|
|
326
|
+
"[billing] organization billing summary unavailable",
|
|
327
|
+
);
|
|
328
|
+
return {};
|
|
329
|
+
} finally {
|
|
330
|
+
clearTimeout(timer);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Test seam: forget every cached and in-flight Organization summary. */
|
|
335
|
+
export function __resetOrgBillingSummaryCache(): void {
|
|
336
|
+
orgBillingSummaryCache.clear();
|
|
337
|
+
orgBillingSummaryInFlight.clear();
|
|
338
|
+
}
|