@wirecat/cli-core 0.574.0 → 0.575.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 (38) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/calendar/mcp/index.d.ts +12 -12
  3. package/dist/cli/findings-input.js +6 -0
  4. package/dist/domain/orders.d.ts +19 -0
  5. package/dist/domain/orders.js +12 -0
  6. package/dist/domain/ref-summaries.js +1 -0
  7. package/dist/domain/references.d.ts +1 -1
  8. package/dist/domain/references.js +1 -0
  9. package/dist/entities/index.d.ts +75 -0
  10. package/dist/entities/mail-findings.d.ts +75 -0
  11. package/dist/entities/mail-findings.js +62 -0
  12. package/dist/errors/reasons.js +5 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/mail/findings.d.ts +73 -1
  16. package/dist/mail/findings.js +164 -1
  17. package/dist/services/proposals/kinds/mail-findings.js +9 -0
  18. package/dist/services/proposals/kinds/orders.d.ts +4 -0
  19. package/dist/services/proposals/kinds/orders.js +32 -0
  20. package/dist/services/proposals/registry.js +2 -0
  21. package/dist/store/index.d.ts +1 -0
  22. package/dist/store/sqlite/entity-types.d.ts +1 -0
  23. package/dist/store/sqlite/entity-types.js +1 -0
  24. package/dist/store/sqlite/finance.d.ts +40 -2
  25. package/dist/store/sqlite/finance.js +48 -42
  26. package/dist/store/sqlite/knowledge.js +2 -1
  27. package/dist/store/sqlite/mail-findings.js +81 -1
  28. package/dist/store/sqlite/manifest.js +2 -0
  29. package/dist/store/sqlite/migrations.generated.js +20 -0
  30. package/dist/store/sqlite/orders.d.ts +84 -0
  31. package/dist/store/sqlite/orders.js +186 -0
  32. package/dist/store/sqlite/ref-summaries.js +10 -0
  33. package/dist/store/sqlite/schema.d.ts +440 -0
  34. package/dist/store/sqlite/schema.js +38 -0
  35. package/dist/store/sqlite/things.js +1 -0
  36. package/dist/store/store.d.ts +3 -0
  37. package/dist/store/store.js +5 -0
  38. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { INVOICE_DIRECTIONS, MONEY_DIRECTIONS } from "../domain/finance.js";
2
3
  import { MEMORY_KINDS, MEMORY_SCOPES } from "../domain/knowledge.js";
3
4
  import { formatLocator, parseLocator } from "../domain/locator.js";
5
+ import { ORDER_PURPOSES, ORDER_STATUSES } from "../domain/orders.js";
4
6
  import { parseReference } from "../domain/references.js";
5
7
  import { findingFields, findingHash, findingId, findingText, meetingTimestamp, validateBatch, validateFinding, validEvidence, } from "../meetings/index.js";
6
8
  export { MAIL_FINDINGS_KIND } from "../store/mail-findings.js";
@@ -42,6 +44,18 @@ const LINKS = {
42
44
  "chat:email": "project",
43
45
  };
44
46
  const ADDRESS = /^[^\s@<>]+@[^\s@<>]+$/;
47
+ const PARTY = /^(person|organization):[1-9]\d*$/;
48
+ const INVOICE = /^invoice:[1-9]\d*$/;
49
+ const PAYMENT = /^payment:[1-9]\d*$/;
50
+ const PROJECT = /^project:[1-9]\d*$/;
51
+ const oneOf = (value, allowed, what) => {
52
+ if (typeof value !== "string" || !allowed.includes(value))
53
+ return invalid(`${what} is ${allowed.join(", ")}`);
54
+ return value;
55
+ };
56
+ /** Money and dates stay text here: the store's finance and order services check them as they check the owner's. */
57
+ const word = (value, limit) => findingText(value, limit);
58
+ const optional = (key, value, read) => (value === undefined ? {} : { [key]: read(value) });
45
59
  const one = (raw, ids) => {
46
60
  if (!raw || typeof raw !== "object")
47
61
  return invalid("a finding is an object");
@@ -123,8 +137,157 @@ const one = (raw, ids) => {
123
137
  evidence,
124
138
  };
125
139
  }
140
+ case "invoice": {
141
+ findingFields(raw, [
142
+ "id",
143
+ "kind",
144
+ "direction",
145
+ "counterparty",
146
+ "currency",
147
+ "lines",
148
+ "total",
149
+ "number",
150
+ "issueDate",
151
+ "dueDate",
152
+ "project",
153
+ "reason",
154
+ "evidence",
155
+ ], RULES.singular);
156
+ if (!Array.isArray(raw.lines) || raw.lines.length < 1 || raw.lines.length > 50)
157
+ return invalid("an invoice finding has 1–50 lines");
158
+ const lines = raw.lines.map((line) => {
159
+ if (!line || typeof line !== "object")
160
+ return invalid("an invoice line is an object");
161
+ const fields = line;
162
+ findingFields(fields, ["description", "quantity", "unitPrice", "amount"], "invoice line");
163
+ return {
164
+ description: word(fields.description, 500),
165
+ ...optional("quantity", fields.quantity, (value) => word(value, 20)),
166
+ ...optional("unitPrice", fields.unitPrice, (value) => word(value, 30)),
167
+ ...optional("amount", fields.amount, (value) => word(value, 30)),
168
+ };
169
+ });
170
+ return {
171
+ id,
172
+ kind: "invoice",
173
+ direction: oneOf(raw.direction, INVOICE_DIRECTIONS, "an invoice's direction"),
174
+ counterparty: reference(raw.counterparty, PARTY, "an invoice's counterparty"),
175
+ currency: word(raw.currency, 3),
176
+ lines,
177
+ ...optional("total", raw.total, (value) => word(value, 30)),
178
+ ...optional("number", raw.number, (value) => word(value, 80)),
179
+ ...optional("issueDate", raw.issueDate, (value) => word(value, 10)),
180
+ ...optional("dueDate", raw.dueDate, (value) => word(value, 10)),
181
+ ...optional("project", raw.project, (value) => reference(value, PROJECT, "a project")),
182
+ reason: findingText(raw.reason, 2000),
183
+ evidence,
184
+ };
185
+ }
186
+ case "payment": {
187
+ findingFields(raw, [
188
+ "id",
189
+ "kind",
190
+ "direction",
191
+ "payer",
192
+ "payee",
193
+ "amount",
194
+ "currency",
195
+ "date",
196
+ "reference",
197
+ "allocations",
198
+ "project",
199
+ "reason",
200
+ "evidence",
201
+ ], RULES.singular);
202
+ let allocations;
203
+ if (raw.allocations !== undefined) {
204
+ if (!Array.isArray(raw.allocations) || raw.allocations.length < 1 || raw.allocations.length > 8)
205
+ return invalid("a payment finding's allocations are 1–8 invoices it settles");
206
+ allocations = raw.allocations.map((share) => {
207
+ if (!share || typeof share !== "object")
208
+ return invalid("an allocation is an invoice and an amount");
209
+ const fields = share;
210
+ findingFields(fields, ["invoice", "amount"], "allocation");
211
+ return {
212
+ invoice: reference(fields.invoice, INVOICE, "an allocation's invoice"),
213
+ amount: word(fields.amount, 30),
214
+ };
215
+ });
216
+ }
217
+ return {
218
+ id,
219
+ kind: "payment",
220
+ direction: oneOf(raw.direction, MONEY_DIRECTIONS, "a payment's direction"),
221
+ ...optional("payer", raw.payer, (value) => reference(value, PARTY, "a payment's payer")),
222
+ ...optional("payee", raw.payee, (value) => reference(value, PARTY, "a payment's payee")),
223
+ amount: word(raw.amount, 30),
224
+ currency: word(raw.currency, 3),
225
+ date: word(raw.date, 10),
226
+ ...optional("reference", raw.reference, (value) => word(value, 500)),
227
+ ...(allocations === undefined ? {} : { allocations }),
228
+ ...optional("project", raw.project, (value) => reference(value, PROJECT, "a project")),
229
+ reason: findingText(raw.reason, 2000),
230
+ evidence,
231
+ };
232
+ }
233
+ case "order": {
234
+ findingFields(raw, [
235
+ "id",
236
+ "kind",
237
+ "purpose",
238
+ "customer",
239
+ "supplier",
240
+ "supplierName",
241
+ "number",
242
+ "total",
243
+ "currency",
244
+ "status",
245
+ "orderDate",
246
+ "fulfilledDate",
247
+ "invoice",
248
+ "payment",
249
+ "project",
250
+ "reason",
251
+ "evidence",
252
+ ], RULES.singular);
253
+ return {
254
+ id,
255
+ kind: "order",
256
+ purpose: oneOf(raw.purpose, ORDER_PURPOSES, "an order's purpose"),
257
+ ...optional("customer", raw.customer, (value) => reference(value, PARTY, "an order's customer")),
258
+ ...optional("supplier", raw.supplier, (value) => reference(value, PARTY, "an order's supplier")),
259
+ supplierName: word(raw.supplierName, 200),
260
+ ...optional("number", raw.number, (value) => word(value, 80)),
261
+ ...optional("total", raw.total, (value) => word(value, 30)),
262
+ ...optional("currency", raw.currency, (value) => word(value, 3)),
263
+ ...optional("status", raw.status, (value) => oneOf(value, ORDER_STATUSES, "an order's status")),
264
+ ...optional("orderDate", raw.orderDate, (value) => word(value, 10)),
265
+ ...optional("fulfilledDate", raw.fulfilledDate, (value) => word(value, 10)),
266
+ ...optional("invoice", raw.invoice, (value) => reference(value, INVOICE, "an order's invoice")),
267
+ ...optional("payment", raw.payment, (value) => reference(value, PAYMENT, "an order's payment")),
268
+ ...optional("project", raw.project, (value) => reference(value, PROJECT, "a project")),
269
+ reason: findingText(raw.reason, 2000),
270
+ evidence,
271
+ };
272
+ }
273
+ case "order_update": {
274
+ findingFields(raw, ["id", "kind", "supplierName", "number", "status", "date", "reason", "evidence"], RULES.singular);
275
+ const status = oneOf(raw.status, ORDER_STATUSES, "an order update's status");
276
+ if (raw.date !== undefined && status !== "delivered")
277
+ return invalid("only a delivered order update gives date");
278
+ return {
279
+ id,
280
+ kind: "order_update",
281
+ supplierName: word(raw.supplierName, 200),
282
+ number: word(raw.number, 80),
283
+ status,
284
+ ...optional("date", raw.date, (value) => word(value, 10)),
285
+ reason: findingText(raw.reason, 2000),
286
+ evidence,
287
+ };
288
+ }
126
289
  default:
127
- return invalid("a mail finding is a task, task_update, fact, memory, link or new_contact");
290
+ return invalid("a mail finding is a task, task_update, fact, memory, link, new_contact, invoice, payment, order or order_update");
128
291
  }
129
292
  };
130
293
  /**
@@ -1,5 +1,6 @@
1
1
  import { MAIL_FINDINGS_KIND } from "../../../store/index.js";
2
2
  import { kept, proposerOfAuthor } from "./shared.js";
3
+ const words = (...parts) => parts.filter(Boolean).join(" ");
3
4
  /** A finding's one line in the inbox; never the email's text, only what the agent wrote. */
4
5
  const summaryOf = (finding) => {
5
6
  switch (finding.kind) {
@@ -15,6 +16,14 @@ const summaryOf = (finding) => {
15
16
  return `${finding.from} → ${finding.to}`;
16
17
  case "new_contact":
17
18
  return `${finding.name} <${finding.address}>`;
19
+ case "invoice":
20
+ return words("invoice", finding.number, finding.total, finding.currency, `· ${finding.reason}`);
21
+ case "payment":
22
+ return words("payment", finding.amount, finding.currency, finding.date, `· ${finding.reason}`);
23
+ case "order":
24
+ return words("order", finding.number, finding.supplierName, finding.status ?? "placed", `· ${finding.reason}`);
25
+ case "order_update":
26
+ return words("order", finding.number, finding.supplierName, finding.status, `· ${finding.reason}`);
18
27
  }
19
28
  };
20
29
  const rowOf = (batch, createdAt) => {
@@ -0,0 +1,4 @@
1
+ import type { MessageStore } from "../../../store/index.js";
2
+ import type { ProposalKind } from "../types.js";
3
+ /** An agent's order, by its own ref (`order:12`) as invoices are; only the owner decides it. */
4
+ export declare const orderKind: (store: Pick<MessageStore, "orders">) => ProposalKind;
@@ -0,0 +1,32 @@
1
+ import { kept, proposerOfAuthor, pushed } from "./shared.js";
2
+ const rowOf = (order) => ({
3
+ id: order.ref,
4
+ kind: "order",
5
+ status: order.review,
6
+ project: order.project,
7
+ subject: order.ref,
8
+ object: order.supplier?.ref ?? null,
9
+ summary: order.title,
10
+ proposedBy: proposerOfAuthor(order.author),
11
+ reason: order.reason,
12
+ evidence: [],
13
+ score: null,
14
+ signals: null,
15
+ parts: null,
16
+ createdAt: order.createdAt,
17
+ });
18
+ /** An agent's order, by its own ref (`order:12`) as invoices are; only the owner decides it. */
19
+ export const orderKind = (store) => ({
20
+ kind: "order",
21
+ prefix: "order",
22
+ list: async (filter) => {
23
+ const status = filter.status ?? "proposed";
24
+ const orders = await store.orders.list({ ...pushed(filter), ...(status === "all" ? {} : { review: status }) });
25
+ return kept(orders.map(rowOf), filter);
26
+ },
27
+ get: async (id) => rowOf(await store.orders.get(id)),
28
+ decide: async (id, { confirm, by, reason }) => {
29
+ const order = confirm ? await store.orders.confirm(id, by, reason) : await store.orders.reject(id, by, reason);
30
+ return { id, kind: "order", status: confirm ? "confirmed" : "rejected", made: confirm ? [order.ref] : [] };
31
+ },
32
+ });
@@ -8,6 +8,7 @@ import { keyDocumentKind } from "./kinds/key-document.js";
8
8
  import { mailFindingsKind } from "./kinds/mail-findings.js";
9
9
  import { meetingFindingsKind } from "./kinds/meeting-findings.js";
10
10
  import { memoryKind } from "./kinds/memory.js";
11
+ import { orderKind } from "./kinds/orders.js";
11
12
  import { relationshipKind } from "./kinds/relationship.js";
12
13
  import { relationshipUpdateKind } from "./kinds/relationship-update.js";
13
14
  import { taskCloseKind } from "./kinds/task-close.js";
@@ -132,6 +133,7 @@ export const proposalRegistry = (store) => {
132
133
  factKind(store),
133
134
  invoiceKind(store),
134
135
  paymentKind(store),
136
+ orderKind(store),
135
137
  memoryKind(store),
136
138
  findingsKind(store),
137
139
  mailFindingsKind(store),
@@ -53,6 +53,7 @@ export type { Author, MemoriesStore, Memory, MemoryInput } from "./sqlite/memori
53
53
  export type { NoteIndexState } from "./sqlite/note-index.js";
54
54
  export { type NearestNote, NOTE_FIELDS, type NoteChunkToEmbed, type NoteHit, type NoteQuery, type NoteSearch, } from "./sqlite/note-search.js";
55
55
  export type { FileNoteInput, Link, LinkInput, LinkListFilter, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
56
+ export type { Order, OrderFilter, OrderInput, OrdersStore } from "./sqlite/orders.js";
56
57
  export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
57
58
  export { assertGenericAction, PROPOSAL_STATUSES, type ProposalInput, type ProposedAction, type ProposedActionsStore, proposerOfAction, } from "./sqlite/proposed-actions.js";
58
59
  export type { RecordKey, RecordOutcome, RecordStore, RecordsWritten, SourceRecord, } from "./sqlite/records.js";
@@ -27,6 +27,7 @@ export declare const ENTITY_TABLES: {
27
27
  readonly memory: "memories";
28
28
  readonly message: "messages";
29
29
  readonly note: "notes";
30
+ readonly order: "orders";
30
31
  readonly organization: "organizations";
31
32
  readonly payment: "payments";
32
33
  readonly person: "persons";
@@ -26,6 +26,7 @@ export const ENTITY_TABLES = {
26
26
  memory: "memories",
27
27
  message: "messages",
28
28
  note: "notes",
29
+ order: "orders",
29
30
  organization: "organizations",
30
31
  payment: "payments",
31
32
  person: "persons",
@@ -11,7 +11,7 @@ export interface Party {
11
11
  /** The owner's own person row. */
12
12
  owner: boolean;
13
13
  }
14
- interface Reviewed {
14
+ export interface Reviewed {
15
15
  review: FinanceReview;
16
16
  /** `name` is a bot's handle and `kind` whether it is an agent or a rule; both `null` for a person. */
17
17
  author: {
@@ -91,7 +91,7 @@ export interface Payment extends Reviewed {
91
91
  export interface PaymentDetail extends Payment {
92
92
  allocations: Allocation[];
93
93
  }
94
- interface Written {
94
+ export interface Written {
95
95
  /** The owner's entry is confirmed at once; a bot's waits as proposed for the owner. */
96
96
  by: Author;
97
97
  project?: string;
@@ -204,6 +204,44 @@ export interface FinanceStore {
204
204
  payable: OpenTotal[];
205
205
  }>;
206
206
  }
207
+ type Row = Record<string, unknown>;
208
+ export declare const refuse: (message: string, reason: string, code?: "validation_error" | "permission_error") => never;
209
+ export declare const oneOf: <T extends string>(value: unknown, allowed: readonly T[], what: string) => T;
210
+ export declare const text: (value: unknown, what: string, limit: number) => string | null;
211
+ export declare const isoDate: (value: unknown, what: string) => string | null;
212
+ export declare const iso: (value: unknown) => string;
213
+ /** What invoices, payments and orders share: their parties, who wrote them, and how the owner decides them. */
214
+ export declare const reviewedRowsOver: (context: StoreContext) => {
215
+ partyOf: (type: unknown, id: unknown) => Party | null;
216
+ partyThing: (reference: string | undefined, what: string) => import("./things.js").Thing | undefined;
217
+ reviewedOf: (row: Row, type: "invoice" | "payment" | "order") => Reviewed;
218
+ written: (input: Written) => {
219
+ owner: boolean;
220
+ actor: Actor;
221
+ model: string | null;
222
+ source: string;
223
+ review: string;
224
+ confirmedType: "bot" | "person" | null;
225
+ confirmedId: number | null;
226
+ metadata: string | null;
227
+ project: number | null;
228
+ evidence: string[];
229
+ };
230
+ keepEvidence: (type: "invoice" | "payment" | "order", id: number, by: ReturnType<(input: Written) => {
231
+ owner: boolean;
232
+ actor: Actor;
233
+ model: string | null;
234
+ source: string;
235
+ review: string;
236
+ confirmedType: "bot" | "person" | null;
237
+ confirmedId: number | null;
238
+ metadata: string | null;
239
+ project: number | null;
240
+ evidence: string[];
241
+ }>) => void;
242
+ ownerOnly: (by: Author, what: string) => void;
243
+ keepDecision: (table: "invoices" | "payments" | "orders", id: number, by: Decider, reason?: string) => void;
244
+ };
207
245
  type FinanceOperations = {
208
246
  [K in keyof FinanceStore]: FinanceStore[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R : never;
209
247
  };
@@ -6,22 +6,22 @@ import { proposedBySql, proposerOf } from "./actors.js";
6
6
  import { authorActor, authorOf, evidenceOf, linkEvidence, projectId, projectOf, subjectOf, } from "./memories.js";
7
7
  import { requiredThing } from "./notes.js";
8
8
  import { write } from "./transaction.js";
9
- const refuse = (message, reason, code = "validation_error") => {
9
+ export const refuse = (message, reason, code = "validation_error") => {
10
10
  throw new CliError(code, message, { reason });
11
11
  };
12
- const oneOf = (value, allowed, what) => {
12
+ export const oneOf = (value, allowed, what) => {
13
13
  if (typeof value !== "string" || !allowed.includes(value))
14
14
  return refuse(`${what} is one of ${allowed.join(", ")}`, "finance.field");
15
15
  return value;
16
16
  };
17
- const text = (value, what, limit) => {
17
+ export const text = (value, what, limit) => {
18
18
  if (value === undefined || value === null)
19
19
  return null;
20
20
  if (typeof value !== "string" || !value.trim() || value.length > limit || value.includes("\0"))
21
21
  return refuse(`${what} takes 1–${limit} characters`, "finance.field");
22
22
  return value.trim();
23
23
  };
24
- const isoDate = (value, what) => {
24
+ export const isoDate = (value, what) => {
25
25
  if (value === undefined || value === null)
26
26
  return null;
27
27
  const date = typeof value === "string" ? value.trim() : "";
@@ -29,7 +29,7 @@ const isoDate = (value, what) => {
29
29
  return refuse(`${what} is a date such as 2026-11-15`, "finance.field");
30
30
  return date;
31
31
  };
32
- const iso = (value) => new Date(Number(value)).toISOString();
32
+ export const iso = (value) => new Date(Number(value)).toISOString();
33
33
  const evidenceList = (value) => {
34
34
  if (value === undefined)
35
35
  return [];
@@ -56,8 +56,9 @@ const INVOICES = `SELECT i.*, ${PAID} AS paid, b.name AS author_name, b.kind AS
56
56
  "LEFT JOIN bots b ON i.author_type = 'bot' AND b.id = i.author_id";
57
57
  const PAYMENTS = "SELECT p.*, (SELECT coalesce(sum(amount), 0) FROM payment_allocations WHERE payment_id = p.id) AS allocated, " +
58
58
  "b.name AS author_name, b.kind AS author_kind FROM payments p LEFT JOIN bots b ON p.author_type = 'bot' AND b.id = p.author_id";
59
- export const financeOperationsOver = (context) => {
60
- const { database, now } = context;
59
+ /** What invoices, payments and orders share: their parties, who wrote them, and how the owner decides them. */
60
+ export const reviewedRowsOver = (context) => {
61
+ const { database } = context;
61
62
  const partyOf = (type, id) => {
62
63
  if (type == null)
63
64
  return null;
@@ -86,6 +87,46 @@ export const financeOperationsOver = (context) => {
86
87
  createdAt: iso(row.created_at),
87
88
  updatedAt: iso(row.updated_at),
88
89
  });
90
+ const written = (input) => {
91
+ const owner = input.by === "owner";
92
+ const { actor, model } = authorActor(context, input.by);
93
+ const source = owner ? "owner" : (text(input.source, "a source", 80) ?? "agent");
94
+ const reason = owner ? null : text(input.reason, "a reason", 2000);
95
+ return {
96
+ owner,
97
+ actor,
98
+ model,
99
+ source,
100
+ review: owner ? "confirmed" : "proposed",
101
+ confirmedType: owner ? actor.type : null,
102
+ confirmedId: owner ? actor.id : null,
103
+ metadata: reason === null ? null : JSON.stringify({ reason }),
104
+ project: input.project === undefined ? null : projectId(database, input.project),
105
+ evidence: evidenceList(input.evidence),
106
+ };
107
+ };
108
+ const keepEvidence = (type, id, by) => {
109
+ if (by.evidence.length)
110
+ linkEvidence(context, { type, id }, by.evidence, by.owner ? "owner" : "agent");
111
+ };
112
+ const ownerOnly = (by, what) => {
113
+ if (by !== "owner")
114
+ refuse(`only the owner ${what}`, "finance.owner-only", "permission_error");
115
+ };
116
+ const keepDecision = (table, id, by, reason) => {
117
+ const why = [by === "owner" ? undefined : by.reason.trim(), reason?.trim() || undefined].filter(Boolean).join(": ");
118
+ if (by === "owner" && !why)
119
+ return;
120
+ const decision = by === "owner" ? { by: "owner", reason: why } : { by: "agent", bot: by.bot, reason: why };
121
+ database
122
+ .prepare(`UPDATE ${table} SET metadata = json_set(coalesce(metadata, '{}'), '$.decision', json(?)) WHERE id = ?`)
123
+ .run(JSON.stringify(decision), id);
124
+ };
125
+ return { partyOf, partyThing, reviewedOf, written, keepEvidence, ownerOnly, keepDecision };
126
+ };
127
+ export const financeOperationsOver = (context) => {
128
+ const { database, now } = context;
129
+ const { partyOf, partyThing, reviewedOf, written, keepEvidence, ownerOnly, keepDecision } = reviewedRowsOver(context);
89
130
  const invoiceOf = (row) => {
90
131
  const currency = String(row.currency);
91
132
  const scale = scaleOf(currency);
@@ -229,41 +270,6 @@ export const financeOperationsOver = (context) => {
229
270
  .run(Number(payment.id), Number(invoice.id), units, actor.type, actor.id, at, at);
230
271
  return Number(invoice.id);
231
272
  };
232
- const written = (input) => {
233
- const owner = input.by === "owner";
234
- const { actor, model } = authorActor(context, input.by);
235
- const source = owner ? "owner" : (text(input.source, "a source", 80) ?? "agent");
236
- const reason = owner ? null : text(input.reason, "a reason", 2000);
237
- return {
238
- owner,
239
- actor,
240
- model,
241
- source,
242
- review: owner ? "confirmed" : "proposed",
243
- confirmedType: owner ? actor.type : null,
244
- confirmedId: owner ? actor.id : null,
245
- metadata: reason === null ? null : JSON.stringify({ reason }),
246
- project: input.project === undefined ? null : projectId(database, input.project),
247
- evidence: evidenceList(input.evidence),
248
- };
249
- };
250
- const keepEvidence = (type, id, by) => {
251
- if (by.evidence.length)
252
- linkEvidence(context, { type, id }, by.evidence, by.owner ? "owner" : "agent");
253
- };
254
- const ownerOnly = (by, what) => {
255
- if (by !== "owner")
256
- refuse(`only the owner ${what}`, "finance.owner-only", "permission_error");
257
- };
258
- const keepDecision = (table, id, by, reason) => {
259
- const why = [by === "owner" ? undefined : by.reason.trim(), reason?.trim() || undefined].filter(Boolean).join(": ");
260
- if (by === "owner" && !why)
261
- return;
262
- const decision = by === "owner" ? { by: "owner", reason: why } : { by: "agent", bot: by.bot, reason: why };
263
- database
264
- .prepare(`UPDATE ${table} SET metadata = json_set(coalesce(metadata, '{}'), '$.decision', json(?)) WHERE id = ?`)
265
- .run(JSON.stringify(decision), id);
266
- };
267
273
  /** A proposed payment's allocations count once confirmed; by then other confirmed payments may cover the invoice. */
268
274
  const coveredElsewhere = (paymentId) => {
269
275
  const over = database
@@ -150,7 +150,8 @@ export const knowledgeStoreOver = (context) => {
150
150
  parsed.type === "event" ||
151
151
  parsed.type === "meeting" ||
152
152
  parsed.type === "invoice" ||
153
- parsed.type === "payment")
153
+ parsed.type === "payment" ||
154
+ parsed.type === "order")
154
155
  return undefined;
155
156
  return { type: parsed.type, id: parsed.id };
156
157
  };
@@ -5,9 +5,11 @@ import { MAIL_FINDINGS_PER_SENDER, mailFindingFingerprint, validateMailFindings,
5
5
  import { findingHash } from "../../meetings/index.js";
6
6
  import { assertMayDecide } from "../decisions.js";
7
7
  import { MAIL_FINDINGS_KIND } from "../mail-findings.js";
8
+ import { financeOperationsOver } from "./finance.js";
8
9
  import { findingsAgent, findingsCoreOver } from "./findings-core.js";
9
- import { authorActor, memoriesStoreOver } from "./memories.js";
10
+ import { authorActor, linkEvidence, memoriesStoreOver } from "./memories.js";
10
11
  import { insertLink, requiredThing } from "./notes.js";
12
+ import { orderOfShop, orderOperationsOver } from "./orders.js";
11
13
  import { linkIdentityToPerson } from "./person-links.js";
12
14
  import { proposalOperationsOver } from "./proposed-actions.js";
13
15
  import { addComment } from "./task-comments.js";
@@ -35,6 +37,8 @@ export const mailFindingsStoreOver = (context) => {
35
37
  const core = findingsCoreOver(context, MAIL_FINDINGS_KIND);
36
38
  const memories = memoriesStoreOver(context);
37
39
  const tasks = workTaskOperationsOver(context);
40
+ const finance = financeOperationsOver(context);
41
+ const orders = orderOperationsOver(context);
38
42
  const mailbox = (key) => {
39
43
  if (key?.provider !== "email" || typeof key.account !== "string" || !key.account.trim())
40
44
  return invalid("mail findings belong to one email account");
@@ -135,6 +139,60 @@ export const mailFindingsStoreOver = (context) => {
135
139
  }
136
140
  return read;
137
141
  };
142
+ /** The invoice, payment or order an accepted finding records: the owner's own entry, citing the mail it came from. */
143
+ const entry = (finding, evidence) => {
144
+ const by = "owner";
145
+ switch (finding.kind) {
146
+ case "invoice":
147
+ return () => finance.addInvoice({
148
+ by,
149
+ evidence,
150
+ direction: finding.direction,
151
+ counterparty: finding.counterparty,
152
+ currency: finding.currency,
153
+ lines: finding.lines,
154
+ ...(finding.total === undefined ? {} : { total: finding.total }),
155
+ ...(finding.number === undefined ? {} : { number: finding.number }),
156
+ ...(finding.issueDate === undefined ? {} : { issueDate: finding.issueDate }),
157
+ ...(finding.dueDate === undefined ? {} : { dueDate: finding.dueDate }),
158
+ ...(finding.project === undefined ? {} : { project: finding.project }),
159
+ });
160
+ case "payment":
161
+ return () => finance.addPayment({
162
+ by,
163
+ evidence,
164
+ direction: finding.direction,
165
+ amount: finding.amount,
166
+ currency: finding.currency,
167
+ date: finding.date,
168
+ ...(finding.payer === undefined ? {} : { payer: finding.payer }),
169
+ ...(finding.payee === undefined ? {} : { payee: finding.payee }),
170
+ ...(finding.reference === undefined ? {} : { reference: finding.reference }),
171
+ ...(finding.allocations === undefined ? {} : { allocations: finding.allocations }),
172
+ ...(finding.project === undefined ? {} : { project: finding.project }),
173
+ });
174
+ case "order": {
175
+ const { id: _id, kind: _kind, evidence: _cited, reason: _reason, ...fields } = finding;
176
+ return () => orders.add({ ...fields, by, evidence });
177
+ }
178
+ default:
179
+ return undefined;
180
+ }
181
+ };
182
+ /** Runs what accepting would write and rolls it back, so staging refuses what the owner's accept would refuse. */
183
+ const ROLLBACK = Symbol("rollback");
184
+ const wouldWrite = (make) => {
185
+ try {
186
+ write(database, () => {
187
+ make();
188
+ throw ROLLBACK;
189
+ });
190
+ }
191
+ catch (error) {
192
+ if (error !== ROLLBACK)
193
+ throw error;
194
+ }
195
+ };
138
196
  const identityOfAddress = (address) => database
139
197
  .prepare("SELECT i.id, l.person_id FROM identities i LEFT JOIN identity_links l ON l.identity_id = i.id " +
140
198
  "WHERE i.provider = 'email' AND lower(i.external_id) = ?")
@@ -163,6 +221,14 @@ export const mailFindingsStoreOver = (context) => {
163
221
  thing(finding.from, "the link's source");
164
222
  thing(finding.to, "the link's target");
165
223
  return;
224
+ case "invoice":
225
+ case "payment":
226
+ case "order":
227
+ wouldWrite(entry(finding, finding.evidence.map((one) => one.reference)));
228
+ return;
229
+ case "order_update":
230
+ orderOfShop(context, finding.supplierName, finding.number);
231
+ return;
166
232
  case "new_contact": {
167
233
  const ids = finding.evidence.map(({ reference }) => Number(read.get(reference)?.id));
168
234
  const marks = ids.map(() => "?").join(",");
@@ -230,6 +296,20 @@ export const mailFindingsStoreOver = (context) => {
230
296
  database.prepare("UPDATE links SET confirmed = 1, updated_at = ? WHERE id = ?").run(now(), Number(link.id));
231
297
  return `link:${link.id}`;
232
298
  }
299
+ case "invoice":
300
+ case "payment":
301
+ case "order":
302
+ return entry(finding, evidence)().ref;
303
+ case "order_update": {
304
+ const order = orderOfShop(context, finding.supplierName, finding.number);
305
+ orders.setStatus(order, {
306
+ status: finding.status,
307
+ by: "owner",
308
+ ...(finding.date === undefined ? {} : { date: finding.date }),
309
+ });
310
+ linkEvidence(context, { type: "order", id: Number(order.slice("order:".length)) }, evidence, "owner");
311
+ return order;
312
+ }
233
313
  case "new_contact": {
234
314
  const person = Number(identityOfAddress(finding.address)?.person_id);
235
315
  database.prepare("UPDATE persons SET name = ?, updated_at = ? WHERE id = ?").run(finding.name, now(), person);
@@ -65,6 +65,8 @@ export const MANIFEST = [
65
65
  { name: "20261102192959_version-47-records", version: 47, minCompatible: 34 },
66
66
  { name: "20261102222340_version-48-test-accounts", version: 48, minCompatible: 34 },
67
67
  { name: "20261104232153_version-49-relation-amounts", version: 49, minCompatible: 34 },
68
+ { name: "20261105125314_version-50-orders", version: 50, minCompatible: 34 },
69
+ { name: "20261105125315_version-50-orders-rules", version: 50, minCompatible: 34 },
68
70
  ];
69
71
  export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
70
72
  const migrations = [];