create-ailk 0.22.0 → 0.23.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/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +11 -9
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +294 -12
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +9 -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 +3 -2
- package/dist/lib/scaffold-render-sweep.js +5 -8
- package/dist/module-architecture.js +98 -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 +38 -1
- package/dist/sync-routes.js +184 -11
- 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 +9 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +27 -0
- package/templates/apps/api/CLAUDE.md +77 -16
- 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 +3 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/stripe.ts +74 -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/spec.ts +10 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -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 +1300 -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/index.ts +16 -2
- 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/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +17 -14
- package/templates/apps/api/src/routes/webhooks/README.md +66 -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 +5 -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 +91 -17
- package/templates/apps/api/src/server.ts +140 -51
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -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 +232 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
- 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 +295 -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 +868 -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 +396 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1003 -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 +124 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +22 -14
- 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 +4 -4
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- 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__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- 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/purchase.ts +30 -2
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +315 -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__/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 +6 -2
- 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 +352 -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 +57 -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 +42 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +202 -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 +31 -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 +31 -1
- 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/package.json +1 -1
- package/templates/database/seed.ts +30 -2
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- 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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/offerings/checkout-session-record.ts — the Checkout session record
|
|
3
|
+
* (#8930, spec docs/specs/8930-the-offerings-checkout-session-record-spec.md
|
|
4
|
+
* D4, D5, D7, AC-4, AC-5; contract A11): a record is written once, for a
|
|
5
|
+
* workspace of the named Organization, and read back exactly; a read answers
|
|
6
|
+
* `null` only when the database holds no row, never for an error or a corrupt
|
|
7
|
+
* row; and no error or log line carries a session, Price or key value.
|
|
8
|
+
*
|
|
9
|
+
* The world is the in-memory Prisma double, which declares no unique key for
|
|
10
|
+
* this model (spec §1 fact 7), so the duplicate write is driven through the
|
|
11
|
+
* `create` spy.
|
|
12
|
+
*/
|
|
13
|
+
// eslint-disable-next-line no-restricted-syntax -- lib/prisma.ts wraps the external DB boundary (PrismaClient requires a live connection); the in-memory double is the repo-standard where-honouring test double for it.
|
|
14
|
+
jest.mock("../../../lib/prisma.js", () =>
|
|
15
|
+
jest
|
|
16
|
+
.requireActual<
|
|
17
|
+
typeof import("../../../__tests__/factories/in-memory-prisma")
|
|
18
|
+
>("../../../__tests__/factories/in-memory-prisma")
|
|
19
|
+
.createInMemoryPrisma(),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const mockLogger = {
|
|
23
|
+
error: jest.fn(),
|
|
24
|
+
warn: jest.fn(),
|
|
25
|
+
info: jest.fn(),
|
|
26
|
+
debug: jest.fn(),
|
|
27
|
+
};
|
|
28
|
+
jest.mock("@working-theory/observability", () => ({
|
|
29
|
+
logger: {
|
|
30
|
+
error: (...args: unknown[]) => mockLogger.error(...args),
|
|
31
|
+
warn: (...args: unknown[]) => mockLogger.warn(...args),
|
|
32
|
+
info: (...args: unknown[]) => mockLogger.info(...args),
|
|
33
|
+
debug: (...args: unknown[]) => mockLogger.debug(...args),
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
import { prisma } from "../../../lib/prisma.js";
|
|
38
|
+
import {
|
|
39
|
+
CheckoutSessionRecordError,
|
|
40
|
+
readCheckoutSessionRecord,
|
|
41
|
+
recordCheckoutSession,
|
|
42
|
+
type CheckoutSessionRecordInput,
|
|
43
|
+
} from "../checkout-session-record.js";
|
|
44
|
+
|
|
45
|
+
type Row = Record<string, unknown>;
|
|
46
|
+
// The in-memory double exposes its store and recorded delegates, which the
|
|
47
|
+
// PrismaClient type the import carries does not name, so it is named here once.
|
|
48
|
+
// eslint-disable-next-line no-restricted-syntax -- justified above
|
|
49
|
+
const db = prisma as unknown as {
|
|
50
|
+
__db: Record<string, Row[]>;
|
|
51
|
+
workspace: { findUnique: jest.Mock };
|
|
52
|
+
offeringsCheckoutSession: { create: jest.Mock; findUnique: jest.Mock };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const ORG_A = "org_01920000-8930-7000-8000-0000000000a1";
|
|
56
|
+
const ORG_B = "org_01920000-8930-7000-8000-0000000000a2";
|
|
57
|
+
const WS_A1 = "wksp_01920000-8930-7000-8000-0000000000b1";
|
|
58
|
+
const WS_A2 = "wksp_01920000-8930-7000-8000-0000000000b2";
|
|
59
|
+
const WS_B1 = "wksp_01920000-8930-7000-8000-0000000000c1";
|
|
60
|
+
const WS_NONE = "wksp_01920000-8930-7000-8000-0000000000d1";
|
|
61
|
+
|
|
62
|
+
const SESSION = "cs_test_a1Recorded8930";
|
|
63
|
+
const PRICE = "price_1Secret8930";
|
|
64
|
+
const KEY = "growth8930_monthly";
|
|
65
|
+
|
|
66
|
+
const VALID: CheckoutSessionRecordInput = {
|
|
67
|
+
sessionId: SESSION,
|
|
68
|
+
workspaceId: WS_A1,
|
|
69
|
+
organizationId: ORG_A,
|
|
70
|
+
priceId: PRICE,
|
|
71
|
+
lookupKey: KEY,
|
|
72
|
+
mode: "test",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const RECORD_KEYS = [
|
|
76
|
+
"createdAt",
|
|
77
|
+
"lookupKey",
|
|
78
|
+
"mode",
|
|
79
|
+
"organizationId",
|
|
80
|
+
"priceId",
|
|
81
|
+
"sessionId",
|
|
82
|
+
"workspaceId",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/** The reason a call rejected with, failing when it rejected with anything else. */
|
|
86
|
+
async function reasonOf(call: Promise<unknown>): Promise<string> {
|
|
87
|
+
const err = await call.then(
|
|
88
|
+
() => {
|
|
89
|
+
throw new Error("expected a rejection");
|
|
90
|
+
},
|
|
91
|
+
(e: unknown) => e,
|
|
92
|
+
);
|
|
93
|
+
expect(err).toBeInstanceOf(CheckoutSessionRecordError);
|
|
94
|
+
const recordError = err as CheckoutSessionRecordError;
|
|
95
|
+
// No input value and no database message reaches the error (D7).
|
|
96
|
+
const text = `${recordError.message} ${JSON.stringify(Object.entries(recordError))}`;
|
|
97
|
+
for (const secret of [SESSION, PRICE, KEY])
|
|
98
|
+
expect(text).not.toContain(secret);
|
|
99
|
+
expect(recordError.cause).toBeUndefined();
|
|
100
|
+
expect(Object.keys(recordError)).not.toContain("cause");
|
|
101
|
+
return recordError.reason;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
for (const key of Object.keys(db.__db)) delete db.__db[key];
|
|
106
|
+
db.__db.workspace = [
|
|
107
|
+
{ id: WS_A1, organizationId: ORG_A, name: "A1" },
|
|
108
|
+
{ id: WS_A2, organizationId: ORG_A, name: "A2" },
|
|
109
|
+
{ id: WS_B1, organizationId: ORG_B, name: "B1" },
|
|
110
|
+
];
|
|
111
|
+
db.workspace.findUnique.mockClear();
|
|
112
|
+
db.offeringsCheckoutSession.create.mockClear();
|
|
113
|
+
db.offeringsCheckoutSession.findUnique.mockClear();
|
|
114
|
+
for (const fn of Object.values(mockLogger)) fn.mockClear();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
// Across the whole file, no log line carries a session, Price or key value.
|
|
119
|
+
const logged = JSON.stringify(
|
|
120
|
+
Object.values(mockLogger).flatMap((fn) => fn.mock.calls),
|
|
121
|
+
);
|
|
122
|
+
for (const secret of [SESSION, PRICE, KEY])
|
|
123
|
+
expect(logged).not.toContain(secret);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("recordCheckoutSession + readCheckoutSessionRecord (#8930 D4, D5, AC-4)", () => {
|
|
127
|
+
it("writes a record once and reads it back exactly", async () => {
|
|
128
|
+
await expect(recordCheckoutSession(VALID)).resolves.toBeUndefined();
|
|
129
|
+
expect(db.offeringsCheckoutSession.create).toHaveBeenCalledTimes(1);
|
|
130
|
+
|
|
131
|
+
const record = await readCheckoutSessionRecord(SESSION);
|
|
132
|
+
expect(record).toEqual({ ...VALID, createdAt: expect.any(Date) });
|
|
133
|
+
expect(Object.keys(record as object).sort()).toEqual(RECORD_KEYS);
|
|
134
|
+
|
|
135
|
+
const [[args]] = db.offeringsCheckoutSession.findUnique.mock.calls as [
|
|
136
|
+
[Row],
|
|
137
|
+
];
|
|
138
|
+
expect(args.where).toEqual({ sessionId: SESSION });
|
|
139
|
+
expect(Object.keys(args.select as Row).sort()).toEqual(RECORD_KEYS);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("writes a live record of a live session", async () => {
|
|
143
|
+
const live = {
|
|
144
|
+
...VALID,
|
|
145
|
+
sessionId: "cs_live_a1Recorded8930",
|
|
146
|
+
mode: "live" as const,
|
|
147
|
+
};
|
|
148
|
+
await recordCheckoutSession(live);
|
|
149
|
+
expect(await readCheckoutSessionRecord(live.sessionId)).toEqual({
|
|
150
|
+
...live,
|
|
151
|
+
createdAt: expect.any(Date),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("answers another session's id with null, never the recorded one", async () => {
|
|
156
|
+
await recordCheckoutSession(VALID);
|
|
157
|
+
expect(await readCheckoutSessionRecord("cs_test_neverRecorded")).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it.each([["ch_1"], [""], [undefined], [42]])(
|
|
161
|
+
"answers %p with null and no database read",
|
|
162
|
+
async (sessionId) => {
|
|
163
|
+
expect(await readCheckoutSessionRecord(sessionId)).toBeNull();
|
|
164
|
+
expect(db.offeringsCheckoutSession.findUnique).not.toHaveBeenCalled();
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
it("rejects read_failed, never null, when the read rejects", async () => {
|
|
169
|
+
db.offeringsCheckoutSession.findUnique.mockRejectedValueOnce(
|
|
170
|
+
new Error(`connection reset reading ${SESSION}`),
|
|
171
|
+
);
|
|
172
|
+
expect(await reasonOf(readCheckoutSessionRecord(SESSION))).toBe(
|
|
173
|
+
"read_failed",
|
|
174
|
+
);
|
|
175
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
176
|
+
{ reason: "checkout_record_read_failed" },
|
|
177
|
+
expect.any(String),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("answers a corrupt row as unreadable, never null, and logs the reason alone", async () => {
|
|
182
|
+
db.__db.offeringsCheckoutSession = [
|
|
183
|
+
{
|
|
184
|
+
...VALID,
|
|
185
|
+
id: "ocsess_01920000-8930-7000-8000-000000000001",
|
|
186
|
+
mode: "prod",
|
|
187
|
+
createdAt: new Date(),
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
expect(await readCheckoutSessionRecord(SESSION)).toBe("unreadable");
|
|
191
|
+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
|
|
192
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
193
|
+
{ reason: "checkout_record_unreadable" },
|
|
194
|
+
expect.any(String),
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("recordCheckoutSession refuses before it writes (#8930 D4, AC-5)", () => {
|
|
200
|
+
// Not a literal at the call, so the type allows the key the parse refuses.
|
|
201
|
+
const withExtraKey = { ...VALID, email: "buyer@example.com" };
|
|
202
|
+
it.each<[string, CheckoutSessionRecordInput]>([
|
|
203
|
+
["an unknown extra key", withExtraKey],
|
|
204
|
+
[
|
|
205
|
+
"a lookup key the parser refuses",
|
|
206
|
+
{ ...VALID, lookupKey: "growth_quarterly" },
|
|
207
|
+
],
|
|
208
|
+
["a live mode on a test session", { ...VALID, mode: "live" }],
|
|
209
|
+
["a malformed organization id", { ...VALID, organizationId: "org_1" }],
|
|
210
|
+
])("refuses %s as invalid", async (_label, input) => {
|
|
211
|
+
expect(await reasonOf(recordCheckoutSession(input))).toBe("invalid");
|
|
212
|
+
expect(db.offeringsCheckoutSession.create).not.toHaveBeenCalled();
|
|
213
|
+
expect(db.workspace.findUnique).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("refuses another Organization's workspace as workspace_mismatch", async () => {
|
|
217
|
+
expect(
|
|
218
|
+
await reasonOf(recordCheckoutSession({ ...VALID, workspaceId: WS_B1 })),
|
|
219
|
+
).toBe("workspace_mismatch");
|
|
220
|
+
expect(db.offeringsCheckoutSession.create).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("refuses a workspace with no row as workspace_mismatch", async () => {
|
|
224
|
+
expect(
|
|
225
|
+
await reasonOf(recordCheckoutSession({ ...VALID, workspaceId: WS_NONE })),
|
|
226
|
+
).toBe("workspace_mismatch");
|
|
227
|
+
expect(db.offeringsCheckoutSession.create).not.toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("refuses a second record of one session as conflict and keeps the first", async () => {
|
|
231
|
+
await recordCheckoutSession(VALID);
|
|
232
|
+
db.offeringsCheckoutSession.create.mockRejectedValueOnce(
|
|
233
|
+
Object.assign(new Error("Unique constraint failed"), { code: "P2002" }),
|
|
234
|
+
);
|
|
235
|
+
expect(
|
|
236
|
+
await reasonOf(recordCheckoutSession({ ...VALID, workspaceId: WS_A2 })),
|
|
237
|
+
).toBe("conflict");
|
|
238
|
+
expect(await readCheckoutSessionRecord(SESSION)).toEqual({
|
|
239
|
+
...VALID,
|
|
240
|
+
createdAt: expect.any(Date),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("answers any other write error as write_failed, with no database message", async () => {
|
|
245
|
+
db.offeringsCheckoutSession.create.mockRejectedValueOnce(
|
|
246
|
+
new Error(
|
|
247
|
+
`new row for relation violates check constraint: (${SESSION}, ${PRICE}, ${KEY})`,
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
expect(await reasonOf(recordCheckoutSession(VALID))).toBe("write_failed");
|
|
251
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
252
|
+
{ workspaceId: WS_A1, reason: "checkout_record_write_failed" },
|
|
253
|
+
expect.any(String),
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("answers a failed workspace lookup as write_failed, with no write", async () => {
|
|
258
|
+
db.workspace.findUnique.mockRejectedValueOnce(
|
|
259
|
+
new Error("connection reset"),
|
|
260
|
+
);
|
|
261
|
+
expect(await reasonOf(recordCheckoutSession(VALID))).toBe("write_failed");
|
|
262
|
+
expect(db.offeringsCheckoutSession.create).not.toHaveBeenCalled();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// "One file names the delegate" (#8930 D3, AC-6) runs `git grep`, which a
|
|
267
|
+
// scaffold cannot: it lives in delegate-confinement.test.ts, kept out of the
|
|
268
|
+
// template (#9179).
|