@wirecat/cli-core 0.574.0 → 0.576.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 (50) hide show
  1. package/CHANGELOG.md +33 -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/cli/bulk-settings.d.ts +6 -0
  16. package/dist/mail/cli/bulk-settings.js +65 -0
  17. package/dist/mail/cli/index.d.ts +4 -0
  18. package/dist/mail/cli/index.js +1 -0
  19. package/dist/mail/cli/review.js +34 -13
  20. package/dist/mail/findings.d.ts +73 -1
  21. package/dist/mail/findings.js +164 -1
  22. package/dist/mail/index.d.ts +2 -1
  23. package/dist/mail/index.js +1 -0
  24. package/dist/mail/marks.d.ts +19 -7
  25. package/dist/mail/marks.js +72 -16
  26. package/dist/mail/newsletter.d.ts +52 -0
  27. package/dist/mail/newsletter.js +189 -0
  28. package/dist/mail/review.d.ts +2 -0
  29. package/dist/services/proposals/kinds/mail-findings.js +9 -0
  30. package/dist/services/proposals/kinds/orders.d.ts +4 -0
  31. package/dist/services/proposals/kinds/orders.js +32 -0
  32. package/dist/services/proposals/registry.js +2 -0
  33. package/dist/store/index.d.ts +1 -0
  34. package/dist/store/sqlite/entity-types.d.ts +1 -0
  35. package/dist/store/sqlite/entity-types.js +1 -0
  36. package/dist/store/sqlite/finance.d.ts +40 -2
  37. package/dist/store/sqlite/finance.js +48 -42
  38. package/dist/store/sqlite/knowledge.js +2 -1
  39. package/dist/store/sqlite/mail-findings.js +81 -1
  40. package/dist/store/sqlite/manifest.js +2 -0
  41. package/dist/store/sqlite/migrations.generated.js +20 -0
  42. package/dist/store/sqlite/orders.d.ts +84 -0
  43. package/dist/store/sqlite/orders.js +186 -0
  44. package/dist/store/sqlite/ref-summaries.js +10 -0
  45. package/dist/store/sqlite/schema.d.ts +440 -0
  46. package/dist/store/sqlite/schema.js +38 -0
  47. package/dist/store/sqlite/things.js +1 -0
  48. package/dist/store/store.d.ts +3 -0
  49. package/dist/store/store.js +5 -0
  50. package/package.json +1 -1
@@ -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 = [];
@@ -954,5 +954,25 @@ export const GENERATED = [
954
954
  "statements": [
955
955
  "ALTER TABLE `record_relations` ADD `amount` text;"
956
956
  ]
957
+ },
958
+ {
959
+ "name": "20261105125314_version-50-orders",
960
+ "statements": [
961
+ "CREATE TABLE `orders` (\n\t`id` integer PRIMARY KEY,\n\t`customer_type` text NOT NULL,\n\t`customer_id` integer NOT NULL,\n\t`purpose` text NOT NULL,\n\t`supplier_type` text,\n\t`supplier_id` integer,\n\t`supplier_name` text NOT NULL,\n\t`number` text,\n\t`currency` text,\n\t`total` integer,\n\t`status` text NOT NULL,\n\t`order_date` text,\n\t`fulfilled_date` text,\n\t`invoice_id` integer,\n\t`payment_id` integer,\n\t`project_id` integer,\n\t`review` text NOT NULL,\n\t`author_type` text NOT NULL,\n\t`author_id` integer NOT NULL,\n\t`model` text,\n\t`confirmed_by_type` text,\n\t`confirmed_by_id` integer,\n\t`source` text NOT NULL,\n\t`metadata` text,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`deleted_at` integer,\n\tCONSTRAINT `fk_orders_invoice_id_invoices_id_fk` FOREIGN KEY (`invoice_id`) REFERENCES `invoices`(`id`),\n\tCONSTRAINT `fk_orders_payment_id_payments_id_fk` FOREIGN KEY (`payment_id`) REFERENCES `payments`(`id`),\n\tCONSTRAINT `fk_orders_project_id_projects_id_fk` FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`)\n);",
962
+ "CREATE INDEX `orders_by_customer_type_customer_id` ON `orders` (`customer_type`,`customer_id`);",
963
+ "CREATE INDEX `orders_by_supplier_type_supplier_id` ON `orders` (`supplier_type`,`supplier_id`);",
964
+ "CREATE INDEX `orders_by_invoice_id` ON `orders` (`invoice_id`);",
965
+ "CREATE INDEX `orders_by_payment_id` ON `orders` (`payment_id`);",
966
+ "CREATE INDEX `orders_by_project_id` ON `orders` (`project_id`);",
967
+ "CREATE INDEX `orders_by_author_type_author_id` ON `orders` (`author_type`,`author_id`);",
968
+ "CREATE INDEX `orders_by_confirmed_by_type_confirmed_by_id` ON `orders` (`confirmed_by_type`,`confirmed_by_id`);"
969
+ ]
970
+ },
971
+ {
972
+ "name": "20261105125315_version-50-orders-rules",
973
+ "statements": [
974
+ "-- The store's code checks these first and says why; the triggers keep a stray write from another build or a hand\n-- edit from storing an order with an unknown status or purpose, or an amount without its currency.\nCREATE TRIGGER orders_valid_bi BEFORE INSERT ON orders\n WHEN new.customer_type NOT IN ('person', 'organization')\n OR new.purpose NOT IN ('personal', 'business', 'client', 'other')\n OR new.status NOT IN ('placed', 'shipped', 'delivered', 'cancelled', 'returned')\n OR new.review NOT IN ('proposed', 'confirmed', 'rejected')\n OR (new.total IS NULL) <> (new.currency IS NULL) OR new.total < 0 BEGIN\n SELECT RAISE(ABORT, 'orders: a person or organization customer, a known purpose, status and review, an amount with its currency');\nEND;",
975
+ "CREATE TRIGGER orders_valid_bu BEFORE UPDATE ON orders\n WHEN new.customer_type NOT IN ('person', 'organization')\n OR new.purpose NOT IN ('personal', 'business', 'client', 'other')\n OR new.status NOT IN ('placed', 'shipped', 'delivered', 'cancelled', 'returned')\n OR new.review NOT IN ('proposed', 'confirmed', 'rejected')\n OR (new.total IS NULL) <> (new.currency IS NULL) OR new.total < 0 BEGIN\n SELECT RAISE(ABORT, 'orders: a person or organization customer, a known purpose, status and review, an amount with its currency');\nEND;"
976
+ ]
957
977
  }
958
978
  ];
@@ -0,0 +1,84 @@
1
+ import { type FinanceReview } from "../../domain/finance.js";
2
+ import { type OrderPurpose, type OrderStatus } from "../../domain/orders.js";
3
+ import { type Decider } from "../decisions.js";
4
+ import { type ProposedByFilter } from "./actors.js";
5
+ import { type Party, type Reviewed, type Written } from "./finance.js";
6
+ import { type Author } from "./memories.js";
7
+ import type { StoreContext } from "./open.js";
8
+ export interface Order extends Reviewed {
9
+ id: string;
10
+ ref: string;
11
+ /** One line, as a list or the review inbox prints it. */
12
+ title: string;
13
+ /** Whom it is for: the owner's own person for his own purchase. */
14
+ customer: Party;
15
+ purpose: OrderPurpose;
16
+ supplier: Party | null;
17
+ /** The shop or supplier as its mail names it. */
18
+ supplierName: string;
19
+ number: string | null;
20
+ /** Both `null` when the mail gave no amount. */
21
+ currency: string | null;
22
+ total: string | null;
23
+ status: OrderStatus;
24
+ /** `YYYY-MM-DD`. */
25
+ orderDate: string | null;
26
+ fulfilledDate: string | null;
27
+ invoice: string | null;
28
+ payment: string | null;
29
+ }
30
+ export interface OrderInput extends Written {
31
+ /** `person:` or `organization:`; the owner's own person when left out. */
32
+ customer?: string;
33
+ purpose: OrderPurpose;
34
+ supplier?: string;
35
+ supplierName: string;
36
+ number?: string;
37
+ /** A decimal string in `currency`; give both or neither. */
38
+ total?: string;
39
+ currency?: string;
40
+ /** `placed` when left out. */
41
+ status?: OrderStatus;
42
+ orderDate?: string;
43
+ fulfilledDate?: string;
44
+ invoice?: string;
45
+ payment?: string;
46
+ }
47
+ export interface OrderFilter {
48
+ status?: OrderStatus;
49
+ purpose?: OrderPurpose;
50
+ customer?: string;
51
+ /** The shop's name in any letter case; with `number`, how a later mail finds its order. */
52
+ supplierName?: string;
53
+ number?: string;
54
+ project?: string;
55
+ review?: FinanceReview;
56
+ proposedBy?: ProposedByFilter;
57
+ /** 1–500, 100 by default; newest written first. */
58
+ limit?: number;
59
+ }
60
+ /**
61
+ * What the owner, his business or a client ordered, from whom, and how far delivery got. The owner's entry is
62
+ * confirmed at once; an agent's waits as proposed for him, and only he decides it.
63
+ */
64
+ export interface OrdersStore {
65
+ add(input: OrderInput): Promise<Order>;
66
+ get(reference: string): Promise<Order>;
67
+ list(filter?: OrderFilter): Promise<Order[]>;
68
+ /** The owner moves an order on, such as a later mail saying it shipped; `date` is the delivery day. */
69
+ setStatus(reference: string, input: {
70
+ status: OrderStatus;
71
+ date?: string;
72
+ by: Author;
73
+ }): Promise<Order>;
74
+ confirm(reference: string, by: Decider, reason?: string): Promise<Order>;
75
+ reject(reference: string, by: Decider, reason?: string): Promise<Order>;
76
+ }
77
+ type OrderOperations = {
78
+ [K in keyof OrdersStore]: OrdersStore[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R : never;
79
+ };
80
+ export declare const orderOperationsOver: (context: StoreContext) => OrderOperations;
81
+ /** The one stored order of a shop with a number, as a later mail names it; refuses none and more than one. */
82
+ export declare const orderOfShop: (context: StoreContext, supplierName: string, number: string) => string;
83
+ export declare const ordersStoreOver: (context: StoreContext) => OrdersStore;
84
+ export {};
@@ -0,0 +1,186 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { FINANCE_REVIEWS } from "../../domain/finance.js";
3
+ import { currencyOf, formatDecimal, minorUnits, scaleOf } from "../../domain/money.js";
4
+ import { ORDER_PURPOSES, ORDER_STATUSES, orderTitle } from "../../domain/orders.js";
5
+ import { assertMayDecide, deciderAuthor } from "../decisions.js";
6
+ import { fold } from "../normalize.js";
7
+ import { ownerPerson, proposedBySql, proposerOf } from "./actors.js";
8
+ import { isoDate, oneOf, refuse, reviewedRowsOver, text } from "./finance.js";
9
+ import { authorActor, projectId } from "./memories.js";
10
+ import { requiredThing } from "./notes.js";
11
+ import { write } from "./transaction.js";
12
+ const ORDERS = "SELECT o.*, b.name AS author_name, b.kind AS author_kind FROM orders o " +
13
+ "LEFT JOIN bots b ON o.author_type = 'bot' AND b.id = o.author_id";
14
+ export const orderOperationsOver = (context) => {
15
+ const { database, now } = context;
16
+ const { partyOf, partyThing, reviewedOf, written, keepEvidence, ownerOnly, keepDecision } = reviewedRowsOver(context);
17
+ const orderOf = (row) => {
18
+ const currency = row.currency == null ? null : String(row.currency);
19
+ const total = row.total == null || currency === null ? null : Number(row.total);
20
+ const fields = {
21
+ id: Number(row.id),
22
+ number: row.number == null ? null : String(row.number),
23
+ supplierName: String(row.supplier_name),
24
+ total,
25
+ currency,
26
+ status: row.status,
27
+ review: row.review,
28
+ };
29
+ return {
30
+ id: String(row.id),
31
+ ref: `order:${row.id}`,
32
+ title: orderTitle(fields),
33
+ customer: partyOf(row.customer_type, row.customer_id),
34
+ purpose: row.purpose,
35
+ supplier: partyOf(row.supplier_type, row.supplier_id),
36
+ supplierName: fields.supplierName,
37
+ number: fields.number,
38
+ currency,
39
+ total: total === null || currency === null ? null : formatDecimal(total, scaleOf(currency)),
40
+ status: fields.status,
41
+ orderDate: row.order_date == null ? null : String(row.order_date),
42
+ fulfilledDate: row.fulfilled_date == null ? null : String(row.fulfilled_date),
43
+ invoice: row.invoice_id == null ? null : `invoice:${row.invoice_id}`,
44
+ payment: row.payment_id == null ? null : `payment:${row.payment_id}`,
45
+ ...reviewedOf(row, "order"),
46
+ };
47
+ };
48
+ const rowOf = (reference) => {
49
+ const thing = requiredThing(database, reference.includes(":") ? reference : `order:${reference}`, "the order");
50
+ if (thing.type !== "order")
51
+ return refuse(`${reference} is not an order`, "order.ref");
52
+ return database.prepare(`${ORDERS} WHERE o.id = ?`).get(thing.id);
53
+ };
54
+ const linked = (reference, type) => {
55
+ if (reference === undefined)
56
+ return null;
57
+ const thing = requiredThing(database, reference, `the ${type}`);
58
+ if (thing.type !== type)
59
+ return refuse(`${reference} is not an ${type}: reference`, "order.ref");
60
+ return thing.id;
61
+ };
62
+ const statusOf = (value) => oneOf(value, ORDER_STATUSES, "an order's status");
63
+ const decide = (reference, by, reason, confirm) => {
64
+ const row = rowOf(reference);
65
+ const ref = `order:${row.id}`;
66
+ assertMayDecide({ id: ref, proposedBy: proposerOf(database, row.author_type, row.author_id), ownerOnly: true }, by, context.decisions);
67
+ if (row.review !== "proposed")
68
+ refuse(`${ref} is already ${String(row.review)}`, "proposal.wrong-state");
69
+ write(database, () => {
70
+ if (confirm) {
71
+ const { actor } = authorActor(context, deciderAuthor(by));
72
+ database
73
+ .prepare("UPDATE orders SET review = 'confirmed', confirmed_by_type = ?, confirmed_by_id = ?, updated_at = ? WHERE id = ?")
74
+ .run(actor.type, actor.id, now(), Number(row.id));
75
+ }
76
+ else
77
+ database
78
+ .prepare("UPDATE orders SET review = 'rejected', updated_at = ? WHERE id = ?")
79
+ .run(now(), Number(row.id));
80
+ keepDecision("orders", Number(row.id), by, reason);
81
+ });
82
+ return orderOf(rowOf(ref));
83
+ };
84
+ return {
85
+ add: (input) => {
86
+ const purpose = oneOf(input.purpose, ORDER_PURPOSES, "an order's purpose");
87
+ const status = input.status === undefined ? "placed" : statusOf(input.status);
88
+ const supplierName = text(input.supplierName, "an order's supplier name", 200) ??
89
+ refuse("an order names its shop or supplier", "order.field");
90
+ const number = text(input.number, "an order's number", 80);
91
+ if ((input.total === undefined) !== (input.currency === undefined))
92
+ refuse("an order's amount and currency come together", "order.field");
93
+ const currency = input.currency === undefined ? null : currencyOf(input.currency);
94
+ const total = currency === null ? null : minorUnits(input.total, currency, "an order's amount");
95
+ if (total !== null && total < 0)
96
+ refuse("an order's amount is 0 or more", "money.amount");
97
+ const orderDate = isoDate(input.orderDate, "an order's date");
98
+ const fulfilledDate = isoDate(input.fulfilledDate, "an order's delivery date");
99
+ const id = write(database, () => {
100
+ if (number !== null && ordersOfShop(database, supplierName, number).length)
101
+ refuse(`order ${number} from ${supplierName} is already stored; change its status instead`, "order.duplicate");
102
+ const customer = partyThing(input.customer, "the customer") ?? ownerPerson(database);
103
+ const supplier = partyThing(input.supplier, "the supplier");
104
+ const invoice = linked(input.invoice, "invoice");
105
+ const payment = linked(input.payment, "payment");
106
+ const by = written(input);
107
+ const at = now();
108
+ const made = Number(database
109
+ .prepare("INSERT INTO orders (customer_type, customer_id, purpose, supplier_type, supplier_id, supplier_name, number, " +
110
+ "currency, total, status, order_date, fulfilled_date, invoice_id, payment_id, project_id, review, " +
111
+ "author_type, author_id, model, confirmed_by_type, confirmed_by_id, source, metadata, created_at, updated_at) " +
112
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
113
+ .get(customer.type, customer.id, purpose, supplier?.type ?? null, supplier?.id ?? null, supplierName, number, currency, total, status, orderDate, fulfilledDate, invoice, payment, by.project, by.review, by.actor.type, by.actor.id, by.model, by.confirmedType, by.confirmedId, by.source, by.metadata, at, at)?.id);
114
+ keepEvidence("order", made, by);
115
+ return made;
116
+ });
117
+ return orderOf(rowOf(`order:${id}`));
118
+ },
119
+ get: (reference) => orderOf(rowOf(reference)),
120
+ list: (filter = {}) => {
121
+ const project = filter.project === undefined ? null : projectId(database, filter.project);
122
+ const customer = partyThing(filter.customer, "the customer");
123
+ const by = proposedBySql("o.author_type", "o.author_id", filter.proposedBy);
124
+ const status = filter.status === undefined ? null : statusOf(filter.status);
125
+ const purpose = filter.purpose === undefined ? null : oneOf(filter.purpose, ORDER_PURPOSES, "a purpose");
126
+ const review = filter.review === undefined ? null : oneOf(filter.review, FINANCE_REVIEWS, "a review");
127
+ const number = filter.number === undefined ? null : filter.number.trim();
128
+ const shop = filter.supplierName === undefined ? null : fold(filter.supplierName);
129
+ const limit = Math.min(Math.max(filter.limit ?? 100, 1), 500);
130
+ const rows = database
131
+ .prepare(`${ORDERS} WHERE o.deleted_at IS NULL AND (? IS NULL OR o.status = ?) AND (? IS NULL OR o.purpose = ?) ` +
132
+ "AND (? IS NULL OR o.review = ?) AND (? IS NULL OR o.project_id = ?) AND (? IS NULL OR o.number = ?) " +
133
+ "AND (? IS NULL OR (o.customer_type = ? AND o.customer_id = ?)) " +
134
+ `AND ${by.sql} ORDER BY o.created_at DESC, o.id DESC`)
135
+ .all(status, status, purpose, purpose, review, review, project, project, number, number, customer?.type ?? null, customer?.type ?? null, customer?.id ?? null, ...by.params);
136
+ return rows
137
+ .filter((row) => shop === null || fold(String(row.supplier_name)) === shop)
138
+ .slice(0, limit)
139
+ .map(orderOf);
140
+ },
141
+ setStatus: (reference, { status, date, by }) => {
142
+ ownerOnly(by, "moves an order on");
143
+ const next = statusOf(status);
144
+ const day = isoDate(date, "an order's delivery date");
145
+ const row = rowOf(reference);
146
+ if (row.review === "rejected" || row.deleted_at != null)
147
+ refuse(`order:${row.id} is ${row.deleted_at != null ? "deleted" : "rejected"}`, "order.status");
148
+ write(database, () => {
149
+ database
150
+ .prepare("UPDATE orders SET status = ?, fulfilled_date = coalesce(?, fulfilled_date), updated_at = ? WHERE id = ?")
151
+ .run(next, next === "delivered" ? day : null, now(), Number(row.id));
152
+ });
153
+ return orderOf(rowOf(`order:${row.id}`));
154
+ },
155
+ confirm: (reference, by, reason) => decide(reference, by, reason, true),
156
+ reject: (reference, by, reason) => decide(reference, by, reason, false),
157
+ };
158
+ };
159
+ /** Orders of one shop with one number that still count: neither rejected nor deleted, the name in any letter case. */
160
+ const ordersOfShop = (database, supplierName, number) => {
161
+ const name = fold(supplierName);
162
+ return database
163
+ .prepare("SELECT id, supplier_name FROM orders WHERE number = ? AND deleted_at IS NULL AND review <> 'rejected'")
164
+ .all(number.trim())
165
+ .filter((row) => fold(String(row.supplier_name)) === name);
166
+ };
167
+ /** The one stored order of a shop with a number, as a later mail names it; refuses none and more than one. */
168
+ export const orderOfShop = (context, supplierName, number) => {
169
+ const rows = ordersOfShop(context.database, supplierName, number);
170
+ if (rows.length === 0)
171
+ throw new CliError("not_found", `no stored order ${number} from ${supplierName}`, { reason: "order.not-found" });
172
+ if (rows.length > 1)
173
+ return refuse(`more than one order ${number} from ${supplierName}`, "order.ref");
174
+ return `order:${rows[0]?.id}`;
175
+ };
176
+ export const ordersStoreOver = (context) => {
177
+ const operations = orderOperationsOver(context);
178
+ return {
179
+ add: async (input) => operations.add(input),
180
+ get: async (reference) => operations.get(reference),
181
+ list: async (filter) => operations.list(filter),
182
+ setStatus: async (reference, input) => operations.setStatus(reference, input),
183
+ confirm: async (reference, by, reason) => operations.confirm(reference, by, reason),
184
+ reject: async (reference, by, reason) => operations.reject(reference, by, reason),
185
+ };
186
+ };
@@ -1,4 +1,5 @@
1
1
  import { invoiceLine, paymentLine } from "../../domain/finance.js";
2
+ import { orderLine } from "../../domain/orders.js";
2
3
  import { filesAccountSql, providerLabel, providerNamed } from "../../domain/providers.js";
3
4
  import { oneLine } from "../../domain/ref-summaries.js";
4
5
  import { formatReference, parseReference } from "../../domain/references.js";
@@ -199,6 +200,13 @@ const READERS = {
199
200
  at: "x.created_at",
200
201
  deleted: "x.deleted_at",
201
202
  })),
203
+ order: byId("order", "orders", () => ({
204
+ title: "json_object('id', x.id, 'number', x.number, 'supplierName', x.supplier_name, 'total', x.total, " +
205
+ "'currency', x.currency, 'status', x.status, 'review', x.review)",
206
+ holder: "(SELECT name FROM projects WHERE id = x.project_id)",
207
+ at: "x.created_at",
208
+ deleted: "x.deleted_at",
209
+ })),
202
210
  };
203
211
  /** Mail saved before the mail tables existed is still messages, so a miss there is read again as a message. */
204
212
  const FALLBACK = { email: "message", email_thread: "chat" };
@@ -304,6 +312,8 @@ const titleOf = (kind, reference, found) => {
304
312
  return fieldsOf(found.title, invoiceLine);
305
313
  case "payment":
306
314
  return fieldsOf(found.title, paymentLine);
315
+ case "order":
316
+ return fieldsOf(found.title, orderLine);
307
317
  case "folder":
308
318
  return reference.type === "folder" && reference.path !== null && found.title !== null
309
319
  ? `${found.title}/${reference.path}`