Public: anyone can read this; the crew can change it.

The samples include deals closed earlier this year, so the book line and the Closed list show a year of sales and commission earned instead of $0 #165

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/deal-clock:mason/158-the-samples-include-deals-closed-earlier → main 3 commits fork

  • Merged. mason/158-the-samples-include-deals-closed-earlier was merged into main as 5e0e4f2.

Review

Sign in to review this change.
16 files changed +211 −38
README.md +4 −5
@@ -9,8 +9,7 @@ is in [docs/TECHNICAL.md](docs/TECHNICAL.md).
99
1010 ## Try it on the sample deals
1111
12 Four sample deals are already in, so the first screen is never empty. Sign in
13 first (see below). You land on **the clock**: each deal is a lane, time runs
12 Seven sample deals are already in, so the first screen is never empty. Four are open. Three closed earlier this year: 35 Mill Pond Road, 602 Cedar Heights Blvd and 14 Saltmarsh Lane. The closed ones are off the clock, This week, reminders and the calendar. They sit in the Closed list with their prices, and they fill the book line under the clock's heading, which reads like "4 open · $1,693,400 under contract · 2 close in November · 3 closed this year, $1,375,000 · $45,328 commission pending · $37,395 earned this year". Sign in first (see below). You land on **the clock**: each deal is a lane, time runs
1413 left to right, and today is the dark line down the middle. Every date in a
1514 contract is a marker on its lane: red is overdue, amber is due within three
1615 days, teal is coming up, grey is done. The samples stay current: each time
@@ -177,7 +176,7 @@ Signed out, `/` shows what deal-clock does, three short points, a still picture
177176 terminal open the deal and press A; over MCP, the `update_address` tool.
178177 **Price and earnest money.** A deal may carry a sale price and an earnest
179178 money amount, in whole dollars (digits only, up to 100,000,000): set them
180 when you open the deal, or tap Edit price and earnest money on its page. Under the clock's heading one line adds up your book ("6 open · $2,410,000 under contract · 2 close in November · 3 closed this year, $1,180,000"; once any deal has a commission rate it ends "· $31,000 commission pending · $9,000 earned this year" (open deals with a price and a rate; deals closed this year; the sample deals show it; the figure is on this line only, never on This week or the printed sheet); deals without a price add no dollars and are noted, deals that fell through count in none), and each Closed line shows its price.
179 when you open the deal, or tap Edit price and earnest money on its page. Under the clock's heading one line adds up your book ("6 open · $2,410,000 under contract · 2 close in November · 3 closed this year, $1,180,000"; once any deal has a commission rate it ends "· $31,000 commission pending · $9,000 earned this year" (open deals with a price and a rate; deals closed this year; the three closed sample deals show it; the figure is on this line only, never on This week or the printed sheet); deals without a price add no dollars and are noted, deals that fell through count in none), and each Closed line shows its price.
181180 The deal's page and the client timeline show them as `$425,000`, a deal
182181 with none shows nothing, and the earnest money reminder says the amount
183182 ("Your $12,750 earnest money deposit is due Nov 7"). The sample deals carry
@@ -213,8 +212,8 @@ Signed out, `/` shows what deal-clock does, three short points, a still picture
213212 In the terminal, `n` asks the side last (Buyer's agent, Listing agent or Both; enter is Buyer's agent, any other word is refused), the clock, This week and the deal show the label, and Y on a deal changes it. A side that is not buyer, listing or both is refused with 422 on the page, the API and the terminal alike. `list_deals` takes `side` and `set_side` changes it; `GET /api/deals?side=listing` does the same.
214213 Each date also carries a short working note only you see, such as "Appraisal ordered, AMC says Thursday": write it in the date's sheet (Save note), or press T on a date in the terminal; it shows under the date, in This week and on the printed week, and never on a client page or in a message.
215214 15. **Put the samples back.** After the walk-through, an admin taps Sample
216 deals below the clock, then Restore the sample deals and confirms: the four
217 samples return as they were, dated from today, and the deals you opened are
215 deals below the clock, then Restore the sample deals and confirms: the seven
216 samples (the three closed ones too) return as they were, dated from today, and the deals you opened are
218217 left alone. Only an admin sees it. In the terminal press R; over MCP, the `restore_samples` tool.
219218 16. **Put it on your phone.** In your phone's browser, choose Add to Home Screen:
220219 the deal-clock mark sits beside the app's name, and it opens full screen
docs/TECHNICAL.md +2 −2
@@ -178,7 +178,7 @@ tools at `POST /mcp` (`list_deals`, `get_deal`, `client_timeline`, `reminders_to
178178 `remove_date` — custom dates only — `remove_deal` — its author or an admin —
179179 `mark_done` — with `done: false` to
180180 reopen a date — `waive_date` — with `waived: false` to un-waive it — `move_date`, `set_reminder_lead`, `amend_deal` (several dates, one reason), `send_reminder`, `restore_samples` — admin only: puts the
181 four sample deals back and leaves every other deal alone). `send_reminder` takes an
181 seven sample deals back and leaves every other deal alone). `send_reminder` takes an
182182 optional `subject` and `body` to send edited words; without them the draft
183183 goes as it is.
184184
@@ -232,7 +232,7 @@ decide nothing themselves. `AGENTS.md` says how to add to it.
232232 | `src/reminders.ts`, `src/ics.ts` | reminder drafts and `/tick`; the calendar file |
233233 | `src/csv.ts` | the spreadsheet file: rows shaped to CSV, formula guard |
234234 | `src/outbox.ts` | a sent reminder's mailto:/sms: link |
235 | `src/sample.ts` | the four sample deals a new database starts with |
235 | `src/sample.ts` | the seven sample deals a new database starts with |
236236 | `src/result.ts` | what an action answers, and the audit habit they share |
237237 | `src/actions.ts` | the audit trail, read |
238238 | `contract.json` | who may do what: roles, and the least role each verb needs |
src/actions.ts +1 −1
@@ -91,7 +91,7 @@ export const mayRestore = (who: Who): Result<null> =>
9191 : refuse(who, "Sign in to restore the sample deals.", "Only an admin may restore the sample deals.");
9292
9393 /**
94 * Put the four sample deals back as the README describes them, anchored to
94 * Put the seven sample deals back as the README describes them, anchored to
9595 * today, for the next demo. Only deals the sample identity wrote are removed;
9696 * every deal a person made is left as it is. Audited as a delete: the
9797 * samples are removed and written again.
src/book.closed.test.ts +1 −1
@@ -56,7 +56,7 @@ test("the terminal's clock view opens with the same book line", () => {
5656 const t = terminal(openDb(dir), alice, { title: "deal-clock", where: "test", agent: { name: "Dana", brokerage: "HP", phone: "555", email: "[email protected]" } });
5757 const screen = t.screen().map((l) => l.replace(/\x1b\[[0-9;]*m/g, ""));
5858 expect(screen).toContain(want);
59 expect(want).toContain("2 closed this year, $612,000 · ");
59 expect(want).toContain("5 closed this year, $1,987,000 · ");
6060 expect(want).toContain("(1 without a price)");
6161 const guest = terminal(openDb(dir), { role: "anon" }, { title: "deal-clock", where: "test", agent: { name: "Dana", brokerage: "HP", phone: "555", email: "[email protected]" } });
6262 expect(guest.screen().map((l) => l.replace(/\x1b\[[0-9;]*m/g, "")).some((l) => l.includes("under contract"))).toBe(false);
src/db.test.ts +1 −1
@@ -31,7 +31,7 @@ test("the sample deals are written once, however many processes open a new file"
3131 openDb(dir);
3232 const count = (db: typeof a) =>
3333 db.query<{ n: number }, []>("SELECT count(*) AS n FROM deal WHERE author_id = 'sample'").get()!.n;
34 expect([count(a), count(b)]).toEqual([4, 4]);
34 expect([count(a), count(b)]).toEqual([7, 7]);
3535 });
3636
3737 test("sample deals boot looking like today, and a changed one is left alone", () => {
src/deals.test.ts +2 −2
@@ -64,7 +64,7 @@ test("a new database carries four sample deals: one overdue, one closing this we
6464 const all = listDeals(db, bob);
6565 if (!all.ok) throw new Error(all.message);
6666 const samples = all.value.filter((d) => d.author_id === "sample");
67 expect(samples.length).toBe(4);
67 expect(samples.length).toBe(7);
6868 const on = today();
6969 expect(
7070 samples.some((d) => d.deadlines.some((x) => x.done_at === null && x.due < on)),
@@ -289,7 +289,7 @@ test("the book: every open date of every open deal, no done dates, no closed dea
289289 const all = listDeals(db, bob);
290290 if (!all.ok) throw new Error(all.message);
291291 const deals = all.value.filter((d) => d.author_id === "sample");
292 expect(deals.length).toBe(4);
292 expect(deals.length).toBe(7);
293293 const stamp = new Date("2026-10-12T08:00:00Z");
294294 const count = (ics: string) => ics.split("BEGIN:VEVENT").length - 1;
295295 const openDates = (list: readonly DealView[]) =>
src/earnest.test.ts +1 −1
@@ -66,7 +66,7 @@ test("the restored samples carry the deposit: done on three, ahead on Juniper Co
6666 "SELECT d.address, k.due, k.done_at FROM deadline k JOIN deal d ON d.id = k.deal_id WHERE k.kind = 'earnest' ORDER BY d.id",
6767 )
6868 .all();
69 expect(rows.length).toBe(4);
69 expect(rows.length).toBe(7);
7070 const by = (a: string) => rows.find((r) => r.address.includes(a))!;
7171 expect(by("Alder Lane").done_at).not.toBeNull();
7272 expect(by("Juniper").done_at).toBeNull();
src/figures.test.ts +1 −1
@@ -160,7 +160,7 @@ test("the sample deals carry plausible figures, and restoring them brings them b
160160 const rows = db.query<{ address: string; sale_price: number | null; earnest_amount: number | null }, [string]>("SELECT address, sale_price, earnest_amount FROM deal WHERE author_id = ?1 ORDER BY id").all("sample");
161161 return rows;
162162 };
163 expect(figures().length).toBe(4);
163 expect(figures().length).toBe(7);
164164 for (const r of figures()) {
165165 expect(r.sale_price).toBeGreaterThan(100_000);
166166 expect(r.earnest_amount).toBeGreaterThan(0);
src/mcp.ts +1 −1
@@ -524,7 +524,7 @@ export const tools: readonly Tool[] = [
524524 },
525525 {
526526 name: "restore_samples",
527 description: "Put the four sample deals back as the README describes them, anchored to today; deals people made are untouched.",
527 description: "Put the seven sample deals back as the README describes them, anchored to today; deals people made are untouched.",
528528 inputSchema: { type: "object", properties: {} },
529529 run: (db, who) => restoreSamples(db, who),
530530 },
src/pages/clock.ts +1 −1
@@ -221,7 +221,7 @@ function render(input: {
221221 ? ""
222222 : `<section id="samples" class="card">
223223 <h2>Sample deals</h2>
224 <p class="muted small">After a demo, put the four sample deals back as the README describes them. Deals you opened are not touched.</p>
224 <p class="muted small">After a demo, put the sample deals back as the README describes them. Deals you opened are not touched.</p>
225225 <details class="confirm">
226226 <summary class="button button-quiet">Restore the sample deals</summary>
227227 <p>Any change made to the sample deals is lost. Your own deals are kept.</p>
src/profile.test.ts +1 −1
@@ -123,7 +123,7 @@ test("the Profile page is prefilled, linked from the header, and saving changes
123123 expect(header).toContain('value="[email protected]"');
124124 expect(await page("/")).toContain("<b>Lee Park</b>");
125125 expect(await (await fetch(base + "/")).text()).toContain("<b>Lee Park</b>");
126 const deals = (await (await fetch(`${base}/api/deals`, { headers: root })).json()) as { id: number }[];
126 const deals = ((await (await fetch(`${base}/api/deals`, { headers: root })).json()) as { id: number; closed: boolean }[]).filter((d) => !d.closed);
127127 expect(await page(`/deals/${deals[0]!.id}/timeline`)).toContain("prepared by Lee Park, Bay Homes");
128128 expect(await page(`/deals/${deals[0]!.id}`)).toContain("Lee Park, Bay Homes");
129129 const cleared = await form("/profile/defaults", {});
src/restore.test.ts +3 −3
@@ -40,7 +40,7 @@ test("restoring brings the README walk-through back and leaves a person's deal b
4040 expect(date(sample("Harbor View"), "inspection").done_at).not.toBeNull();
4141 const restored = restoreSamples(db, root);
4242 expect(restored.ok).toBe(true);
43 expect(samples().length).toBe(4);
43 expect(samples().length).toBe(7);
4444 expect(JSON.stringify(all().find((d) => d.id === mine.value.id))).toBe(before);
4545
4646 const harbor = date(sample("Harbor View"), "inspection");
@@ -60,7 +60,7 @@ test("restoring twice leaves what once did, and the seeded guard is not tripped"
6060 const once = JSON.stringify(shape());
6161 restoreSamples(db, root);
6262 expect(JSON.stringify(shape())).toBe(once);
63 expect(samples().length).toBe(4);
63 expect(samples().length).toBe(7);
6464 expect(db.query("SELECT count(*) AS n FROM app_meta WHERE key = 'seeded'").get()).toEqual({ n: 1 });
6565 });
6666
@@ -91,6 +91,6 @@ test("a sample whose address was corrected is still that sample: it moves with t
9191 expect(moved.address).toBe("9 Orchard Row, Unit 2");
9292 expect(moved.closing).toBe(addDays(closing, 5));
9393 restoreSamples(db, root);
94 expect(samples().length).toBe(4);
94 expect(samples().length).toBe(7);
9595 expect(samples().filter((d) => d.address.includes("Orchard Row")).map((d) => d.address)).toEqual(["9 Orchard Row"]);
9696 });
src/sample.test.ts +108 −2
@@ -3,10 +3,16 @@ import { mkdtempSync } from "node:fs";
33 import { tmpdir } from "node:os";
44 import { join } from "node:path";
55 import { openDb } from "./db.ts";
6 import { listDeals } from "./deals.ts";
6 import { tally, bookLine } from "./book.ts";
7 import { book } from "./ics.ts";
8 import { dollars } from "./money.ts";
9 import { clock } from "./pages/clock.ts";
10 import { people } from "./people.ts";
11 import { thisWeek } from "./week.ts";
12 import { addDays, today } from "./dates.ts";
13 import { createDeal, listDeals, markDone } from "./deals.ts";
714 import { drafts, unsent } from "./reminders.ts";
815 import type { Who } from "./who.ts";
9 import { addDays } from "./dates.ts";
1016 import { isUntouched, refresh, restore, shiftSample, type SampleDates } from "./sample.ts";
1117
1218 const dates: SampleDates = {
@@ -62,6 +68,62 @@ test("the sample deals hold an overdue date with its follow-up waiting", () => {
6268 expect(late.length).toBeGreaterThan(0);
6369 });
6470
71 const agent = { name: "Dana", brokerage: "HP", phone: "555", email: "[email protected]" };
72 const closedSamples = (db: ReturnType<typeof openDb>) => {
73 const listed = listDeals(db, { role: "user", id: "u1", name: "alice" });
74 if (!listed.ok) throw new Error(listed.message);
75 return listed.value.filter((d) => d.author_id === "sample" && d.closed);
76 };
77
78 test("the samples include three deals closed earlier this year: priced, rated and in the book", () => {
79 const db = openDb(mkdtempSync(join(tmpdir(), "app-sample-closed-")));
80 const listed = listDeals(db, { role: "user", id: "u1", name: "alice" });
81 if (!listed.ok) throw new Error(listed.message);
82 const closed = closedSamples(db);
83 expect(closed.length).toBe(3);
84 for (const d of closed) {
85 expect(d.sale_price).not.toBeNull();
86 expect(d.commission_rate).not.toBeNull();
87 expect(d.closing < today()).toBe(true);
88 expect(d.closing.slice(0, 4)).toBe(today().slice(0, 4));
89 expect(d.deadlines.every((x) => x.done_at !== null || x.waived_at !== null)).toBe(true);
90 }
91 // No client party carries a contact, so not even a check-in is drafted.
92 expect(closed.flatMap((d) => drafts(d, agent))).toEqual([]);
93 const book = tally(listed.value, today());
94 expect(book.closedThisYear).toBe(3);
95 expect(book.closedVolume).toBe(closed.reduce((n, d) => n + (d.sale_price ?? 0), 0));
96 expect(book.commissionEarned).toBeGreaterThan(0);
97 // The open samples are the four the clock shows; the closed ones add to neither count.
98 expect(book.open).toBe(4);
99 expect(bookLine(book, today())).toContain("3 closed this year, $1,375,000");
100 });
101
102 test("a database seeded on 1 January still reads its closed samples as this year", () => {
103 const db = openDb(mkdtempSync(join(tmpdir(), "app-sample-jan-")));
104 restore(db, "2027-01-05");
105 const listed = listDeals(db, { role: "user", id: "u1", name: "alice" });
106 if (!listed.ok) throw new Error(listed.message);
107 const book = tally(listed.value, "2027-01-05");
108 expect(book.closedThisYear).toBe(3);
109 expect(book.commissionEarned).toBeGreaterThan(0);
110 });
111
112 test("restoring the samples brings the closed ones back and leaves a real deal alone", () => {
113 const db = openDb(mkdtempSync(join(tmpdir(), "app-sample-restore-closed-")));
114 const alice: Who = { role: "user", id: "u1", name: "alice" };
115 const mine = createDeal(db, alice, { openAnyway: true, address: "12 Elm St", buyer: "A", seller: "S", contractDate: today(), closingDate: addDays(today(), 40) });
116 if (!mine.ok) throw new Error(mine.message);
117 const closing = closedSamples(db)[0]!.deadlines.find((d) => d.kind === "closing")!;
118 markDone(db, alice, { id: closing.id, done: false });
119 expect(closedSamples(db).length).toBe(2);
120 restore(db);
121 expect(closedSamples(db).length).toBe(3);
122 const after = listDeals(db, alice);
123 if (!after.ok) throw new Error(after.message);
124 expect(after.value.filter((d) => d.author_id !== "sample").map((d) => d.address)).toEqual(["12 Elm St"]);
125 });
126
65127 function lateDone(db: ReturnType<typeof openDb>, on = new Date().toISOString().slice(0, 10)): string[] {
66128 return db
67129 .query<{ kind: string; due: string; done_at: string }, []>("SELECT kind, due, done_at FROM deadline WHERE done_at IS NOT NULL AND deal_id IN (SELECT id FROM deal WHERE author_id = 'sample')")
@@ -95,3 +157,47 @@ test("a real deal's finished date is left alone by a refresh", () => {
95157 const kept = db.query<{ done_at: string }, [number]>("SELECT done_at FROM deadline WHERE deal_id = ?1 AND done_at IS NOT NULL").all(real);
96158 expect(kept.every((d) => d.done_at === stamp)).toBe(true);
97159 });
160
161 test("the closed samples are off the clock, This week, People and the calendar, and listed under Closed with their price", () => {
162 const db = openDb(mkdtempSync(join(tmpdir(), "app-sample-off-")));
163 const alice: Who = { role: "user", id: "u1", name: "alice" };
164 const listed = listDeals(db, alice);
165 if (!listed.ok) throw new Error(listed.message);
166 const closed = listed.value.filter((d) => d.author_id === "sample" && d.closed);
167 expect(closed.length).toBe(3);
168 const html = clock({ who: alice, deals: listed.value, on: today() });
169 const cut = html.indexOf('id="closed"');
170 expect(cut).toBeGreaterThan(0);
171 const card = html.slice(cut, html.indexOf("</ul>", cut));
172 const rest = html.slice(0, cut) + html.slice(html.indexOf("</ul>", cut));
173 for (const d of closed) {
174 expect(card).toContain(`${d.address}</a>`);
175 expect(card).toContain(dollars(d.sale_price!));
176 expect(rest).not.toContain(d.address);
177 expect(thisWeek(listed.value, today()).some((i) => i.deal.id === d.id)).toBe(false);
178 expect(people(listed.value, today()).some((p) => p.owes.some((o) => o.address === d.address))).toBe(false);
179 expect(book(listed.value, "clock.example")).not.toContain(d.address);
180 }
181 // Left open, the same deal would show up in the lanes: the assertions above are not vacuous.
182 const open = listed.value.filter((d) => d.author_id === "sample" && !d.closed);
183 expect(clock({ who: alice, deals: open, on: today() })).toContain(open[0]!.address);
184 });
185
186 test("refresh keeps the closed samples in the past and in this year, across a New Year too", () => {
187 const db = openDb(mkdtempSync(join(tmpdir(), "app-sample-keep-")));
188 restore(db, "2026-12-20");
189 const alice: Who = { role: "user", id: "u1", name: "alice" };
190 const closings = (on: string) => {
191 const listed = listDeals(db, alice);
192 if (!listed.ok) throw new Error(listed.message);
193 const c = listed.value.filter((d) => d.author_id === "sample" && d.closed);
194 expect(c.length).toBe(3);
195 expect(tally(listed.value, on).closedThisYear).toBe(3);
196 return c.map((d) => d.closing);
197 };
198 for (const c of closings("2026-12-20")) expect(c < "2026-12-20" && c.startsWith("2026")).toBe(true);
199 refresh(db, "2026-12-30");
200 for (const c of closings("2026-12-30")) expect(c < "2026-12-30" && c.startsWith("2026")).toBe(true);
201 refresh(db, "2027-01-10");
202 for (const c of closings("2027-01-10")) expect(c <= "2027-01-10" && c.startsWith("2027")).toBe(true);
203 });
src/sample.ts +75 −9
@@ -1,8 +1,10 @@
11 /**
2 * Four sample deals, written once into a new database so the first screen
2 * Seven sample deals, written once into a new database so the first screen
33 * shows the job: one with a date gone overdue, one closing this week, one
4 * with a moved date, and one just signed. Dates are set relative to the
5 * day the database is created, so a fresh preview always looks like today.
4 * with a moved date, and one just signed; then three closed earlier this
5 * year, so the book line and the Closed list show a year of sales. Dates are
6 * set relative to the day the database is created, so a fresh preview always
7 * looks like today.
68 */
79
810 import type { Database } from "bun:sqlite";
@@ -124,8 +126,64 @@ const SAMPLES: readonly Sample[] = [
124126 ],
125127 waived: ["appraisal"],
126128 },
129 // Closed earlier this year: every date done or waived, so they are off the clock.
130 {
131 address: "35 Mill Pond Road",
132 buyer: "Hannah & Luis Ortega",
133 seller: "Fiona Calloway",
134 contract: -64,
135 closing: -21,
136 price: 465_000,
137 rate: 250,
138 earnest: 9_300,
139 parties: [
140 ["lender", "Priya Nair, Coastline Mortgage", "[email protected]"],
141 ["title", "Marcus Lee, Bayside Title", "[email protected]"],
142 ],
143 done: ["earnest", "inspection", "appraisal", "financing", "title", "walkthrough", "closing"],
144 },
145 {
146 address: "602 Cedar Heights Blvd",
147 side: "listing",
148 buyer: "Ines Park",
149 seller: "Douglas & Marian Hale",
150 contract: -52,
151 closing: -9,
152 price: 612_000,
153 rate: 275,
154 earnest: 12_000,
155 parties: [
156 ["lender", "Owen Brandt, First Keel Bank", "[email protected]"],
157 ["title", "Northgate Title & Escrow", "[email protected]"],
158 ["other_agent", "Ben Adeyemi, Lantern Realty", "[email protected]"],
159 ],
160 done: ["earnest", "inspection", "appraisal", "financing", "title", "walkthrough", "closing"],
161 },
162 {
163 address: "14 Saltmarsh Lane",
164 buyer: "Terrence Boyd",
165 seller: "Naomi Fisher",
166 contract: -240,
167 closing: -200,
168 price: 298_000,
169 rate: 300,
170 earnest: 6_000,
171 parties: [
172 ["lender", "Priya Nair, Coastline Mortgage", "[email protected]"],
173 ["title", "Bayside Title", "[email protected]"],
174 ["other_agent", "Alicia Moreno, Moreno & Co.", "[email protected]"],
175 ],
176 done: ["earnest", "inspection", "appraisal", "financing", "title", "walkthrough", "closing"],
177 },
127178 ];
128179
180 /** The sample's dates in days from `on`; a closing in the past is held inside the year `on` is in, so it still counts as closed this year in January. */
181 function placed(s: Sample, on: string): { contract: number; closing: number } {
182 if (s.closing >= 0) return s;
183 const closing = -Math.min(-s.closing, daysBetween(`${on.slice(0, 4)}-01-01`, on));
184 return { contract: closing - (s.closing - s.contract), closing };
185 }
186
129187 /** The time of day a sample's finished dates carry: what tells them from a person's click. */
130188 const DONE_TIME = "T17:00:00.000Z";
131189
@@ -234,7 +292,7 @@ export function refresh(db: Database, on = today()): void {
234292 moveAuthors: moves.map((m) => m.by_name),
235293 };
236294 if (!isUntouched(state, anchor)) continue;
237 const shifted = shiftSample(
295 let shifted = shiftSample(
238296 {
239297 contractDate: deal.contract_date,
240298 deadlines: deadlines.map((d) => ({ id: d.id, due: d.due, doneAt: d.done_at, waivedAt: d.waived_at })),
@@ -243,6 +301,13 @@ export function refresh(db: Database, on = today()): void {
243301 anchor,
244302 on,
245303 );
304 // A sample closed earlier this year stays in the year: crossing New Year, it is held at 1 January.
305 const closingRow = deadlines.find((d) => d.kind === "closing");
306 if (closingRow?.done_at != null) {
307 const closingNow = shifted.deadlines.find((d) => d.id === closingRow.id)!.due;
308 const newYear = `${on.slice(0, 4)}-01-01`;
309 if (closingNow < newYear) shifted = shiftSample(shifted, closingNow, newYear);
310 }
246311 // A database seeded before the fix stamped every finished date the day before the anchor: restamp it against its own due day.
247312 const legacy = `${addDays(anchor, -1)}${DONE_TIME}`;
248313 const restamped = shifted.deadlines.map((d, i) => (deadlines[i]!.done_at === legacy ? { ...d, doneAt: doneStamp(d.due, on) } : d));
@@ -258,15 +323,16 @@ export function refresh(db: Database, on = today()): void {
258323 }).immediate();
259324 }
260325
261 /** Write the four samples, anchored to `on`. The caller holds the write transaction. */
326 /** Write the samples, anchored to `on`. The caller holds the write transaction. */
262327 function writeSamples(db: Database, on: string): void {
263328 for (const s of SAMPLES) {
329 const at = placed(s, on);
264330 const deal = insertDeal(db, SAMPLE, {
265331 address: s.address,
266332 buyer: s.buyer,
267333 seller: s.seller,
268 contractDate: addDays(on, s.contract),
269 closingDate: addDays(on, s.closing),
334 contractDate: addDays(on, at.contract),
335 closingDate: addDays(on, at.closing),
270336 lengths: { ...BRIEF_LENGTHS, ...s.lengths },
271337 businessDays: false,
272338 salePrice: s.price,
@@ -322,8 +388,8 @@ export function seed(db: Database, on = today()): void {
322388 /**
323389 * Put the samples back as `seed` wrote them: every deal authored by the sample
324390 * identity (and nothing else) is removed with its dates, moves, parties and
325 * reminders, and the four are written again anchored to `on`. One immediate
326 * transaction, so two restores at once leave four, not eight. Answers the ids
391 * reminders, and the samples are written again anchored to `on`. One immediate
392 * transaction, so two restores at once leave seven, not fourteen. Answers the ids
327393 * of the deals written.
328394 */
329395 export function restore(db: Database, on = today()): number[] {
src/server.test.ts +5 −4
@@ -220,7 +220,7 @@ test("the MCP server lists the tools and calls them as the caller", async () =>
220220 );
221221 expect((await call("list_deals", {})).result.isError).toBe(true);
222222 const deals = JSON.parse((await call("list_deals", {}, alice)).result.content[0].text);
223 const open = deals[0].deadlines.find((d: { done_at: unknown }) => d.done_at === null);
223 const open = deals.find((d: { deadlines: { done_at: unknown }[] }) => d.deadlines.some((x) => x.done_at === null)).deadlines.find((d: { done_at: unknown }) => d.done_at === null);
224224 expect((await call("mark_done", { id: open.id })).result.isError).toBe(true);
225225 expect((await call("mark_done", { id: open.id }, root)).result.isError).toBeUndefined();
226226 expect((await call("mark_done", { id: open.id, done: false }, root)).result.isError).toBeUndefined();
@@ -402,14 +402,14 @@ test("restore_samples over MCP: an admin restores, anyone else is refused and no
402402 const sampleIds = async () =>
403403 ((await (await api("/api/deals", {}, root)).json()) as { id: number; author_id: string }[]).filter((d) => d.author_id === "sample").map((d) => d.id);
404404 const before = await sampleIds();
405 expect(before.length).toBe(4);
405 expect(before.length).toBe(7);
406406 expect((await call()).result.isError).toBe(true);
407407 expect((await call(rita)).result.isError).toBe(true);
408408 expect(await sampleIds()).toEqual(before);
409409 const restored = await call(root);
410410 expect(restored.result.isError).toBeUndefined();
411411 const after = await sampleIds();
412 expect(after.length).toBe(4);
412 expect(after.length).toBe(7);
413413 const trail = (await (await api("/api/audit", {}, root)).json()) as { verb: string; outcome: string }[];
414414 expect(trail.some((r) => r.verb === "delete" && r.outcome === "ok")).toBe(true);
415415 });
@@ -1046,7 +1046,8 @@ test("a deal whose Closing is marked done leaves the lanes for the Closed card,
10461046 expect((await post("0")).status).toBe(303);
10471047 const back = await (await api("/", {}, closer)).text();
10481048 expect(back).toContain(`<a href="/deals/${deal.id}">${deal.address}</a> <small`);
1049 expect(back).not.toContain('id="closed"');
1049 const card = back.slice(back.indexOf('id="closed"'));
1050 expect(card.slice(0, card.indexOf("</ul>"))).not.toContain(deal.address);
10501051 });
10511052
10521053 test("This week lists the sample's overdue inspection and this week's closing, and nothing done", async () => {
src/terminal.test.ts +4 −3
@@ -326,12 +326,13 @@ test("a closed deal's dates leave the date list and reminders, it is listed unde
326326 expect(lines(t).join("\n")).toContain("88 Shut Ave");
327327 t.press("d");
328328 expect(lines(t).join("\n")).toContain("88 Shut Ave");
329 expect(lines(t).join("\n")).not.toContain("Closed (");
329 expect(lines(t).join("\n")).toContain("Closed (3)");
330 expect(lines(t).join("\n")).not.toContain("88 Shut Ave — closed");
330331 markDone(db, alice, { id: closing.id, done: true });
331332 t.press("d");
332333 const dates = lines(t).join("\n");
333334 expect(dates).not.toContain("Inspection period ends — 88 Shut Ave");
334 expect(dates).toContain("Closed (1)");
335 expect(dates).toContain("Closed (4)");
335336 expect(dates).toContain(`88 Shut Ave — closed ${spoken(today())}`);
336337 t.press("r");
337338 expect(lines(t).join("\n")).not.toContain("88 Shut Ave");
@@ -342,7 +343,7 @@ test("a closed deal's dates leave the date list and reminders, it is listed unde
342343 goTo(t, "Closing — 88 Shut Ave");
343344 t.press("u");
344345 t.press("h");
345 expect(lines(t).join("\n")).not.toContain("Closed (");
346 expect(lines(t).join("\n")).not.toContain("88 Shut Ave — closed");
346347 expect(lines(t).join("\n")).toContain("Inspection period ends — 88 Shut Ave");
347348 });
348349

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs