create-ailk 0.18.0 → 0.19.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/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +4 -1
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +9 -0
- package/dist/lib/scaffold-render-sweep.js +24 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +119 -0
- package/dist/programmatic.js +3 -0
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.js +140 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +60 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/CLAUDE.md +25 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +81 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
- package/templates/apps/api/src/middleware/auth.ts +206 -46
- package/templates/apps/api/src/routes/leads/index.ts +4 -0
- package/templates/apps/api/src/server.ts +107 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
- package/templates/apps/api/src/services/lead-routing.ts +30 -2
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +5 -1
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +24 -0
- package/templates/apps/web/messages/en.json +156 -35
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +90 -15
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -1,31 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for lib/account-config.ts — the base account area's contributions
|
|
3
|
-
* (#7429, spec 7423 U3 · AC-6; amended by #7448, U5 · D6a/D8a/D9
|
|
3
|
+
* (#7429, spec 7423 U3 · AC-6; amended by #7448, U5 · D6a/D8a/D9; by #8231,
|
|
4
|
+
* spec 8231 D4–D7).
|
|
4
5
|
*
|
|
5
6
|
* Covers:
|
|
6
|
-
* 1. The
|
|
7
|
-
* Settings section (modules add those)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* 1. The five contribution arrays are exported; the base area ships no
|
|
8
|
+
* Settings section or Billing section (modules add those), and the
|
|
9
|
+
* three New actions (#8233 D2).
|
|
10
|
+
* 2. The Inbox kinds: `result`, `piece` and `message` in `updates` (All
|
|
11
|
+
* only), `purchase` in `purchases`, `appointment` in `upcoming`.
|
|
10
12
|
* 3. `email-unverified` and `payment-due` are For-you sources — needs-you
|
|
11
13
|
* items with an Open action to User settings, never Inbox rows.
|
|
12
|
-
* 4. Each contribution maps its
|
|
13
|
-
* `
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
14
|
+
* 4. Each contribution maps its read: the session for `email-unverified`,
|
|
15
|
+
* `waitlistSignups.me` per configured key for `result`,
|
|
16
|
+
* `member.purchases` for `purchase` (never `flowCheckouts.me`),
|
|
17
|
+
* `member.appointments` for `appointment`, and `subscription.me`'s own
|
|
18
|
+
* `actionRequired` for `payment-due`.
|
|
16
19
|
* 5. A 404 — or a 401/403 while the email is unverified — is "nothing for
|
|
17
20
|
* this caller" (`[]`); any other failed read rejects, a 401/403 for a
|
|
18
21
|
* verified email included, so the Inbox can show an error row (D12).
|
|
22
|
+
* 6. The member kinds call on a cookie-session site — no hub-credential
|
|
23
|
+
* gate (#8231 D7).
|
|
24
|
+
* 7. #8233 D1, D2 — each New action is offered while its member read
|
|
25
|
+
* answers: a 404 (or a switched-off request form, or an unverified
|
|
26
|
+
* 401/403) is `false`, any other failure rejects. The popups are bound
|
|
27
|
+
* in the route (`account/new-popups.tsx`, tested beside it): this file
|
|
28
|
+
* imports nothing under `(authed)`.
|
|
19
29
|
*/
|
|
20
30
|
|
|
21
31
|
const mockSignupMe = jest.fn();
|
|
22
32
|
const mockCheckoutsMe = jest.fn();
|
|
23
33
|
const mockSubscriptionMe = jest.fn();
|
|
34
|
+
const mockMemberPieces = jest.fn();
|
|
35
|
+
const mockMemberMessages = jest.fn();
|
|
36
|
+
const mockMemberPurchases = jest.fn();
|
|
37
|
+
const mockMemberAppointments = jest.fn();
|
|
38
|
+
const mockMemberMeetingTypes = jest.fn();
|
|
39
|
+
const mockMemberRequestForm = jest.fn();
|
|
24
40
|
jest.mock("@working-theory/api-client", () => ({
|
|
25
41
|
__esModule: true,
|
|
26
42
|
waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
|
|
27
43
|
flowCheckouts: { me: (...args: unknown[]) => mockCheckoutsMe(...args) },
|
|
28
44
|
subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
|
|
45
|
+
member: {
|
|
46
|
+
pieces: (...args: unknown[]) => mockMemberPieces(...args),
|
|
47
|
+
messages: (...args: unknown[]) => mockMemberMessages(...args),
|
|
48
|
+
purchases: (...args: unknown[]) => mockMemberPurchases(...args),
|
|
49
|
+
appointments: (...args: unknown[]) => mockMemberAppointments(...args),
|
|
50
|
+
meetingTypes: (...args: unknown[]) => mockMemberMeetingTypes(...args),
|
|
51
|
+
requestForm: (...args: unknown[]) => mockMemberRequestForm(...args),
|
|
52
|
+
},
|
|
29
53
|
}));
|
|
30
54
|
|
|
31
55
|
import type {
|
|
@@ -33,12 +57,16 @@ import type {
|
|
|
33
57
|
AccountForYouSource,
|
|
34
58
|
AccountInboxKind,
|
|
35
59
|
} from "@working-theory/templates/account";
|
|
60
|
+
import { requestQuoteFlowConfig } from "@working-theory/validation";
|
|
36
61
|
|
|
37
62
|
import {
|
|
63
|
+
accountBillingSections,
|
|
38
64
|
accountExperimentKeys,
|
|
39
65
|
accountForYouSources,
|
|
40
66
|
accountInboxKinds,
|
|
67
|
+
accountNewActions,
|
|
41
68
|
accountSettingsSections,
|
|
69
|
+
type AccountNewRead,
|
|
42
70
|
} from "../account-config";
|
|
43
71
|
|
|
44
72
|
const INIT = { headers: { cookie: "session=abc" } };
|
|
@@ -79,17 +107,39 @@ const SIGNUP = {
|
|
|
79
107
|
answers: { q3: null },
|
|
80
108
|
};
|
|
81
109
|
|
|
82
|
-
const
|
|
83
|
-
id: "
|
|
84
|
-
|
|
85
|
-
experimentKey: "diag",
|
|
86
|
-
offerKey: "pro",
|
|
87
|
-
status: "completed",
|
|
110
|
+
const PURCHASE = {
|
|
111
|
+
id: "pur_1",
|
|
112
|
+
offering: "Pro plan",
|
|
88
113
|
amountMinor: 4900,
|
|
89
114
|
currency: "USD",
|
|
90
115
|
period: "month",
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
purchasedAt: "2026-09-02T10:00:00.000Z",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const APPOINTMENT = {
|
|
120
|
+
id: "appt_1",
|
|
121
|
+
title: "Kickoff",
|
|
122
|
+
startTime: "2026-10-20T15:00:00.000Z",
|
|
123
|
+
endTime: "2026-10-20T15:30:00.000Z",
|
|
124
|
+
status: "confirmed" as const,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const PIECE = {
|
|
128
|
+
id: "piece_1",
|
|
129
|
+
title: "Brand guide",
|
|
130
|
+
stage: "in_progress",
|
|
131
|
+
needsClient: true,
|
|
132
|
+
createdAt: "2026-09-01T10:00:00.000Z",
|
|
133
|
+
updatedAt: "2026-09-03T10:00:00.000Z",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const MESSAGE = {
|
|
137
|
+
id: "intx_1",
|
|
138
|
+
subject: "Your draft",
|
|
139
|
+
body: "Attached.",
|
|
140
|
+
createdAt: "2026-09-04T10:00:00.000Z",
|
|
141
|
+
file: null,
|
|
142
|
+
readAt: null,
|
|
93
143
|
};
|
|
94
144
|
|
|
95
145
|
const ok = <T>(body: T) => ({ ok: true as const, body });
|
|
@@ -101,22 +151,30 @@ afterEach(() => {
|
|
|
101
151
|
});
|
|
102
152
|
|
|
103
153
|
describe("account-config exports (AC-6)", () => {
|
|
104
|
-
it("exports the
|
|
154
|
+
it("exports the five contribution arrays", () => {
|
|
105
155
|
expect(Array.isArray(accountInboxKinds)).toBe(true);
|
|
106
156
|
expect(accountSettingsSections).toEqual([]);
|
|
107
157
|
expect(Array.isArray(accountForYouSources)).toBe(true);
|
|
108
158
|
});
|
|
109
159
|
|
|
110
|
-
it("ships
|
|
160
|
+
it("ships the three New actions, by message key, and no Billing section (#8233 D2)", () => {
|
|
161
|
+
expect(accountNewActions.map((a) => [a.id, a.label])).toEqual([
|
|
162
|
+
["schedule-service", "new.actions.scheduleService"],
|
|
163
|
+
["request-quote", "new.actions.requestQuote"],
|
|
164
|
+
["send-message", "new.actions.sendMessage"],
|
|
165
|
+
]);
|
|
166
|
+
expect(accountBillingSections).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("ships the base kinds and the member kinds in their sections (#8231 D4–D6)", () => {
|
|
111
170
|
expect(accountInboxKinds.map((k) => [k.kind, k.section])).toEqual([
|
|
112
171
|
["result", "updates"],
|
|
113
|
-
["purchase", "
|
|
172
|
+
["purchase", "purchases"],
|
|
114
173
|
["piece", "updates"],
|
|
115
174
|
["message", "updates"],
|
|
175
|
+
["appointment", "upcoming"],
|
|
116
176
|
]);
|
|
117
|
-
//
|
|
118
|
-
// Inbox row asks for a decision — that is the For-you panel's job.
|
|
119
|
-
expect(accountInboxKinds.some((k) => k.section === "upcoming")).toBe(false);
|
|
177
|
+
// No Inbox row asks for a decision — that is the For-you panel's job.
|
|
120
178
|
expect(accountInboxKinds.some((k) => k.section === "needs-you")).toBe(false);
|
|
121
179
|
});
|
|
122
180
|
|
|
@@ -152,7 +210,7 @@ describe("email-unverified — a For-you item from the session", () => {
|
|
|
152
210
|
id: "email-unverified",
|
|
153
211
|
type: { id: "email", label: "t:profile.email", tone: "needs-you" },
|
|
154
212
|
title: "t:profile.unverified",
|
|
155
|
-
action: { kind: "open", href: "/account/
|
|
213
|
+
action: { kind: "open", href: "/account/general" },
|
|
156
214
|
dismissible: true,
|
|
157
215
|
});
|
|
158
216
|
});
|
|
@@ -191,25 +249,30 @@ describe("result — waitlistSignups.me per accountExperimentKeys", () => {
|
|
|
191
249
|
});
|
|
192
250
|
});
|
|
193
251
|
|
|
194
|
-
describe("purchase —
|
|
195
|
-
|
|
196
|
-
|
|
252
|
+
describe("purchase — member.purchases (#8231 D5)", () => {
|
|
253
|
+
const ITEM = { id: "pur_1", title: "Pro plan", at: PURCHASE.purchasedAt, data: PURCHASE };
|
|
254
|
+
|
|
255
|
+
it("sits in the purchases section", () => {
|
|
256
|
+
expect(kind("purchase").section).toBe("purchases");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("is one item per purchase, at its purchase time, and never reads flowCheckouts.me", async () => {
|
|
260
|
+
mockMemberPurchases.mockResolvedValue(ok({ items: [PURCHASE] }));
|
|
197
261
|
const items = await kind("purchase").load(ctx());
|
|
198
|
-
expect(
|
|
199
|
-
expect(
|
|
200
|
-
|
|
201
|
-
]);
|
|
262
|
+
expect(mockMemberPurchases).toHaveBeenCalledWith(INIT);
|
|
263
|
+
expect(mockCheckoutsMe).not.toHaveBeenCalled();
|
|
264
|
+
expect(items).toEqual([ITEM]);
|
|
202
265
|
});
|
|
203
266
|
|
|
204
267
|
it("is empty on a 404", async () => {
|
|
205
|
-
|
|
268
|
+
mockMemberPurchases.mockResolvedValue(fail(404));
|
|
206
269
|
await expect(kind("purchase").load(ctx())).resolves.toEqual([]);
|
|
207
270
|
});
|
|
208
271
|
|
|
209
272
|
it.each([401, 403])(
|
|
210
273
|
"is empty on a %i while the email is unverified",
|
|
211
274
|
async (status) => {
|
|
212
|
-
|
|
275
|
+
mockMemberPurchases.mockResolvedValue(fail(status));
|
|
213
276
|
await expect(
|
|
214
277
|
kind("purchase").load(ctx({ emailVerified: false })),
|
|
215
278
|
).resolves.toEqual([]);
|
|
@@ -219,33 +282,114 @@ describe("purchase — flowCheckouts.me", () => {
|
|
|
219
282
|
it.each([401, 403])(
|
|
220
283
|
"rejects on a %i for a verified email",
|
|
221
284
|
async (status) => {
|
|
222
|
-
|
|
285
|
+
mockMemberPurchases.mockResolvedValue(fail(status));
|
|
223
286
|
await expect(kind("purchase").load(ctx())).rejects.toMatchObject({
|
|
224
287
|
status,
|
|
225
288
|
});
|
|
226
289
|
},
|
|
227
290
|
);
|
|
228
291
|
|
|
229
|
-
it("rejects on a
|
|
230
|
-
|
|
292
|
+
it("rejects on a 503", async () => {
|
|
293
|
+
mockMemberPurchases.mockResolvedValue(fail(503));
|
|
231
294
|
await expect(kind("purchase").load(ctx())).rejects.toThrow();
|
|
232
295
|
});
|
|
233
296
|
|
|
234
|
-
it("details the
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
title: "pro",
|
|
238
|
-
at: CHECKOUT.completedAt,
|
|
239
|
-
data: CHECKOUT,
|
|
240
|
-
};
|
|
241
|
-
const detail = kind("purchase").renderDetail(item, ctx());
|
|
242
|
-
expect(detail.status).toBe("completed");
|
|
297
|
+
it("details the purchase's fields, with no status", () => {
|
|
298
|
+
const detail = kind("purchase").renderDetail(ITEM, ctx());
|
|
299
|
+
expect(detail.status).toBeUndefined();
|
|
243
300
|
expect(detail.fields?.items.map((i) => i.label)).toEqual([
|
|
244
301
|
"t:purchases.offer",
|
|
245
302
|
"t:purchases.amount",
|
|
246
303
|
"t:purchases.period",
|
|
247
304
|
"t:purchases.date",
|
|
248
305
|
]);
|
|
306
|
+
expect(detail.fields?.items[0]?.value).toBe("Pro plan");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("shows Not recorded for a null amount or period", () => {
|
|
310
|
+
const data = { ...PURCHASE, amountMinor: null, currency: null, period: null };
|
|
311
|
+
const detail = kind("purchase").renderDetail({ ...ITEM, data }, ctx());
|
|
312
|
+
const values = detail.fields?.items.map((i) => i.value);
|
|
313
|
+
expect(values?.[1]).toBe("t:purchases.unavailable");
|
|
314
|
+
expect(values?.[2]).toBe("t:purchases.unavailable");
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("appointment — member.appointments (#8231 D6)", () => {
|
|
319
|
+
const PAST = {
|
|
320
|
+
...APPOINTMENT,
|
|
321
|
+
id: "appt_0",
|
|
322
|
+
title: null,
|
|
323
|
+
startTime: "2026-09-10T15:00:00.000Z",
|
|
324
|
+
endTime: "2026-09-10T15:30:00.000Z",
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
it("sits in the upcoming section, named by its message key", () => {
|
|
328
|
+
expect(kind("appointment").section).toBe("upcoming");
|
|
329
|
+
expect(kind("appointment").label).toBe("appointments.heading");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("returns the upcoming items then the past, flagged, at their start time", async () => {
|
|
333
|
+
mockMemberAppointments.mockResolvedValue(ok({ upcoming: [APPOINTMENT], past: [PAST] }));
|
|
334
|
+
const items = await kind("appointment").load(ctx());
|
|
335
|
+
expect(mockMemberAppointments).toHaveBeenCalledWith(INIT);
|
|
336
|
+
expect(items).toEqual([
|
|
337
|
+
{ id: "appt_1", title: "Kickoff", at: APPOINTMENT.startTime, upcoming: true, data: APPOINTMENT },
|
|
338
|
+
{ id: "appt_0", title: "t:appointments.untitled", at: PAST.startTime, upcoming: false, data: PAST },
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("is empty on a 404", async () => {
|
|
343
|
+
mockMemberAppointments.mockResolvedValue(fail(404));
|
|
344
|
+
await expect(kind("appointment").load(ctx())).resolves.toEqual([]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("rejects on a 503", async () => {
|
|
348
|
+
mockMemberAppointments.mockResolvedValue(fail(503));
|
|
349
|
+
await expect(kind("appointment").load(ctx())).rejects.toThrow();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("details the status and the start and end", () => {
|
|
353
|
+
const item = { id: "appt_1", title: "Kickoff", at: APPOINTMENT.startTime, upcoming: true, data: APPOINTMENT };
|
|
354
|
+
const detail = kind("appointment").renderDetail(item, ctx());
|
|
355
|
+
expect(detail.status).toBe("t:appointments.statuses.confirmed");
|
|
356
|
+
expect(detail.fields?.items.map((i) => i.label)).toEqual([
|
|
357
|
+
"t:appointments.starts",
|
|
358
|
+
"t:appointments.ends",
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("member kinds on a cookie-session site — no gate (#8231 D7)", () => {
|
|
364
|
+
it("piece reads member.pieces with the cookie init and returns its rows", async () => {
|
|
365
|
+
mockMemberPieces.mockResolvedValue(ok({ items: [PIECE] }));
|
|
366
|
+
const items = await kind("piece").load(ctx());
|
|
367
|
+
expect(mockMemberPieces).toHaveBeenCalledWith(INIT);
|
|
368
|
+
expect(items.map((i) => i.id)).toEqual(["piece_1"]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("message reads member.messages with the cookie init and returns its rows", async () => {
|
|
372
|
+
mockMemberMessages.mockResolvedValue(ok({ items: [MESSAGE] }));
|
|
373
|
+
const items = await kind("message").load(ctx());
|
|
374
|
+
expect(mockMemberMessages).toHaveBeenCalledWith(INIT);
|
|
375
|
+
expect(items.map((i) => i.id)).toEqual(["intx_1"]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("piece-needs-client reads member.pieces with the cookie init", async () => {
|
|
379
|
+
mockMemberPieces.mockResolvedValue(ok({ items: [PIECE] }));
|
|
380
|
+
const items = await source("piece-needs-client").load(ctx());
|
|
381
|
+
expect(mockMemberPieces).toHaveBeenCalledWith(INIT);
|
|
382
|
+
expect(items.map((i) => i.id)).toEqual(["piece:piece_1"]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it.each([
|
|
386
|
+
["piece", mockMemberPieces],
|
|
387
|
+
["message", mockMemberMessages],
|
|
388
|
+
] as const)("%s loads [] on a 404 and rejects on a 500", async (id, mock) => {
|
|
389
|
+
mock.mockResolvedValue(fail(404));
|
|
390
|
+
await expect(kind(id).load(ctx())).resolves.toEqual([]);
|
|
391
|
+
mock.mockResolvedValue(fail(500));
|
|
392
|
+
await expect(kind(id).load(ctx())).rejects.toThrow();
|
|
249
393
|
});
|
|
250
394
|
});
|
|
251
395
|
|
|
@@ -266,7 +410,7 @@ describe("payment-due — a For-you item from subscription.me's actionRequired",
|
|
|
266
410
|
id: "payment-due",
|
|
267
411
|
type: { id: "payment", label: "t:subscription.action", tone: "needs-you" },
|
|
268
412
|
title: "t:subscription.headline",
|
|
269
|
-
action: { kind: "open", href: "/account/
|
|
413
|
+
action: { kind: "open", href: "/account/billing" },
|
|
270
414
|
dismissible: true,
|
|
271
415
|
});
|
|
272
416
|
});
|
|
@@ -286,3 +430,71 @@ describe("payment-due — a For-you item from subscription.me's actionRequired",
|
|
|
286
430
|
await expect(source("payment-due").load(ctx())).rejects.toThrow();
|
|
287
431
|
});
|
|
288
432
|
});
|
|
433
|
+
|
|
434
|
+
// ── #8233 — the New actions ──────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
describe("New actions — offered while their member read answers (#8233 D1, D2)", () => {
|
|
437
|
+
function action(id: string): AccountNewRead {
|
|
438
|
+
const found = accountNewActions.find((a) => a.id === id);
|
|
439
|
+
if (!found) throw new Error(`no New action ${id}`);
|
|
440
|
+
return found;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const TYPE = { slug: "consult", title: "Consultation", durationMinutes: 30 };
|
|
444
|
+
const FORM = {
|
|
445
|
+
form: {
|
|
446
|
+
title: "Request a quote",
|
|
447
|
+
defaultLocale: "en",
|
|
448
|
+
steps: requestQuoteFlowConfig.steps,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
const failure = (status: number) => ({
|
|
452
|
+
ok: false as const,
|
|
453
|
+
status,
|
|
454
|
+
body: { ok: false, error: { code: "failed", message: "x" } },
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it.each([
|
|
458
|
+
["{ items: [] }", true, ok({ items: [] })],
|
|
459
|
+
["one type", true, ok({ items: [TYPE] })],
|
|
460
|
+
["404", false, fail(404)],
|
|
461
|
+
])("schedule-service: meetingTypes %s → %s", async (_name, offered, answer) => {
|
|
462
|
+
mockMemberMeetingTypes.mockResolvedValue(answer);
|
|
463
|
+
await expect(action("schedule-service").available(ctx())).resolves.toBe(offered);
|
|
464
|
+
expect(mockMemberMeetingTypes).toHaveBeenCalledWith(INIT);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it.each([
|
|
468
|
+
["a form", true, ok(FORM)],
|
|
469
|
+
["{ form: null }", false, ok({ form: null })],
|
|
470
|
+
["404", false, fail(404)],
|
|
471
|
+
])("request-quote: requestForm %s → %s", async (_name, offered, answer) => {
|
|
472
|
+
mockMemberRequestForm.mockResolvedValue(answer);
|
|
473
|
+
await expect(action("request-quote").available(ctx())).resolves.toBe(offered);
|
|
474
|
+
expect(mockMemberRequestForm).toHaveBeenCalledWith(INIT);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("send-message: a messages 200 → true, read with ctx.init", async () => {
|
|
478
|
+
mockMemberMessages.mockResolvedValue(ok({ items: [] }));
|
|
479
|
+
await expect(action("send-message").available(ctx())).resolves.toBe(true);
|
|
480
|
+
expect(mockMemberMessages).toHaveBeenCalledWith(INIT);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("send-message: 404 → false; 403 while the email is unverified → false", async () => {
|
|
484
|
+
mockMemberMessages.mockResolvedValue(fail(404));
|
|
485
|
+
await expect(action("send-message").available(ctx())).resolves.toBe(false);
|
|
486
|
+
mockMemberMessages.mockResolvedValue(fail(403));
|
|
487
|
+
await expect(action("send-message").available(ctx({ emailVerified: false }))).resolves.toBe(
|
|
488
|
+
false,
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it.each([
|
|
493
|
+
["schedule-service", 503, mockMemberMeetingTypes],
|
|
494
|
+
["request-quote", 500, mockMemberRequestForm],
|
|
495
|
+
["send-message", 503, mockMemberMessages],
|
|
496
|
+
] as const)("%s rejects on a %i — a failed read is never reported as absent", async (id, status, mock) => {
|
|
497
|
+
mock.mockResolvedValue(failure(status));
|
|
498
|
+
await expect(action(id).available(ctx())).rejects.toMatchObject({ status });
|
|
499
|
+
});
|
|
500
|
+
});
|