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.
Files changed (167) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +4 -1
  4. package/dist/category-profile.d.ts +10 -0
  5. package/dist/category-profile.js +21 -0
  6. package/dist/derive-architecture.d.ts +5 -1
  7. package/dist/derive-architecture.js +8 -3
  8. package/dist/lib/license-key-files.d.ts +6 -3
  9. package/dist/lib/license-key-files.js +8 -2
  10. package/dist/lib/scaffold-render-sweep.d.ts +9 -0
  11. package/dist/lib/scaffold-render-sweep.js +24 -3
  12. package/dist/license-set.d.ts +1 -1
  13. package/dist/license-set.js +13 -8
  14. package/dist/module-architecture.js +119 -0
  15. package/dist/programmatic.js +3 -0
  16. package/dist/spoke-checkout.d.ts +11 -3
  17. package/dist/spoke-checkout.js +62 -12
  18. package/dist/starter-grid.d.ts +1 -1
  19. package/dist/surfaces.js +140 -1
  20. package/dist/template-manifest.d.ts +4 -3
  21. package/dist/topology.d.ts +7 -2
  22. package/dist/topology.js +60 -3
  23. package/package.json +5 -4
  24. package/templates/.env.example +3 -3
  25. package/templates/.github/workflows/ci.yml +5 -0
  26. package/templates/apps/api/CLAUDE.md +25 -7
  27. package/templates/apps/api/LICENSE +6 -5
  28. package/templates/apps/api/NOTICE +2 -0
  29. package/templates/apps/api/jest.config.cjs +4 -29
  30. package/templates/apps/api/package.json +1 -0
  31. package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
  32. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  33. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  34. package/templates/apps/api/src/lib/stripe.ts +81 -0
  35. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
  36. package/templates/apps/api/src/middleware/auth.ts +206 -46
  37. package/templates/apps/api/src/routes/leads/index.ts +4 -0
  38. package/templates/apps/api/src/server.ts +107 -0
  39. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
  40. package/templates/apps/api/src/services/lead-routing.ts +30 -2
  41. package/templates/apps/api/vercel.json +2 -2
  42. package/templates/apps/mcp/.env.example +10 -0
  43. package/templates/apps/mcp/CLAUDE.md +9 -4
  44. package/templates/apps/mcp/LICENSE +6 -5
  45. package/templates/apps/mcp/NOTICE +2 -0
  46. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  47. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  48. package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
  49. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  50. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  51. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  52. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  53. package/templates/apps/mcp/jest.config.cjs +3 -44
  54. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  55. package/templates/apps/mcp/src/boot.ts +5 -23
  56. package/templates/apps/mcp/src/http-transport.ts +93 -73
  57. package/templates/apps/mcp/src/index.ts +2 -0
  58. package/templates/apps/mcp/src/request-context.ts +52 -0
  59. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  60. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  61. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  62. package/templates/apps/web/LICENSE +6 -5
  63. package/templates/apps/web/NOTICE +2 -0
  64. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  65. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
  66. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
  67. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
  68. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  69. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
  70. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  71. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  72. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
  73. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  74. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  75. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
  76. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
  77. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  78. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  79. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
  80. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
  81. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  82. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  83. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  84. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  85. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  86. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  87. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  88. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  89. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
  90. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  91. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  92. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  93. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  94. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  95. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  96. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  97. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
  98. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  99. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  100. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  101. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  102. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
  103. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  106. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  107. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  108. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  111. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  114. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  115. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  116. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  117. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  118. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  119. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  120. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  121. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  122. package/templates/apps/web/auth.ts +5 -1
  123. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  124. package/templates/apps/web/lib/account-config.ts +188 -52
  125. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  126. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  127. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  128. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  129. package/templates/apps/web/lib/topology.generated.ts +24 -0
  130. package/templates/apps/web/messages/en.json +156 -35
  131. package/templates/database/CHANGELOG.md +21 -0
  132. package/templates/database/LICENSE +6 -5
  133. package/templates/database/NOTICE +2 -0
  134. package/templates/database/README.md +1 -1
  135. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  136. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  137. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  138. package/templates/database/__tests__/seed.test.ts +54 -2
  139. package/templates/database/inbox/schema.prisma +90 -15
  140. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  141. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  142. package/templates/database/package.json +3 -2
  143. package/templates/database/seed.ts +34 -2
  144. package/versions.json +36 -36
  145. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  146. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  147. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  148. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  149. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  150. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  151. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  152. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  153. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  154. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  155. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  156. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  157. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  158. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  159. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  160. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  161. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  162. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  163. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  164. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  165. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  166. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  167. 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 three contribution arrays are exported; the base area ships no
7
- * Settings section (modules add those).
8
- * 2. The base Inbox kinds are exactly `result` and `purchase` — rows of
9
- * the Recents and All tabs; none fills Scheduled (the old Upcoming).
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 existing read: the session for
13
- * `email-unverified`, `waitlistSignups.me` per configured key for
14
- * `result`, `flowCheckouts.me` for `purchase`, and `subscription.me`'s
15
- * own `actionRequired` for `payment-due`.
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 CHECKOUT = {
83
- id: "flchk_1",
84
- flowSlug: "buy",
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
- completedAt: "2026-09-02T10:00:00.000Z",
92
- createdAt: "2026-09-02T09:00:00.000Z",
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 three contribution arrays", () => {
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 exactly the base Inbox kinds and the member kinds (#7920), all Recents rows (D6a, D9)", () => {
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", "updates"],
172
+ ["purchase", "purchases"],
114
173
  ["piece", "updates"],
115
174
  ["message", "updates"],
175
+ ["appointment", "upcoming"],
116
176
  ]);
117
- // Nothing in the base area fills Scheduled (the old Upcoming), and no
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/settings#general" },
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 — flowCheckouts.me", () => {
195
- it("is one item per checkout, at its completion time", async () => {
196
- mockCheckoutsMe.mockResolvedValue(ok([CHECKOUT]));
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(mockCheckoutsMe).toHaveBeenCalledWith(INIT);
199
- expect(items).toEqual([
200
- { id: "flchk_1", title: "pro", at: CHECKOUT.completedAt, data: CHECKOUT },
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
- mockCheckoutsMe.mockResolvedValue(fail(404));
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
- mockCheckoutsMe.mockResolvedValue(fail(status));
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
- mockCheckoutsMe.mockResolvedValue(fail(status));
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 failed read", async () => {
230
- mockCheckoutsMe.mockResolvedValue(fail(503));
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 checkout's fields", () => {
235
- const item = {
236
- id: "flchk_1",
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/settings#billing" },
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
+ });