@wirecat/cli-core 0.557.0 → 0.558.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/cli/index.d.ts +0 -1
  3. package/dist/cli/index.js +0 -1
  4. package/dist/entities/fact.d.ts +1 -12
  5. package/dist/entities/fact.js +0 -13
  6. package/dist/entities/index.d.ts +2 -12
  7. package/dist/entities/meeting-findings.d.ts +1 -1
  8. package/dist/errors/reasons.js +1 -0
  9. package/dist/meetings/findings-rules.d.ts +7 -4
  10. package/dist/meetings/findings-rules.js +11 -4
  11. package/dist/meetings/payment.d.ts +10 -6
  12. package/dist/meetings/payment.js +21 -6
  13. package/dist/services/project-status/index.d.ts +1 -1
  14. package/dist/services/project-status/index.js +7 -3
  15. package/dist/services/project-status/signals/payments.d.ts +3 -3
  16. package/dist/services/project-status/signals/payments.js +43 -17
  17. package/dist/store/evidence-findings.js +16 -8
  18. package/dist/store/index.d.ts +1 -1
  19. package/dist/store/project-transfer/request.d.ts +1 -1
  20. package/dist/store/project-transfer/schema.d.ts +2 -2
  21. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  22. package/dist/store/sqlite/facts.d.ts +0 -13
  23. package/dist/store/sqlite/facts.js +5 -58
  24. package/dist/store/sqlite/finance.d.ts +4 -0
  25. package/dist/store/sqlite/finance.js +19 -4
  26. package/dist/store/sqlite/findings-core.js +5 -14
  27. package/dist/store/sqlite/manifest.js +1 -0
  28. package/dist/store/sqlite/migrations.generated.js +8 -0
  29. package/dist/store/sqlite/open-store.js +2 -0
  30. package/dist/store/sqlite/payment-entries.d.ts +26 -0
  31. package/dist/store/sqlite/payment-entries.js +91 -0
  32. package/dist/store/sqlite/payment-facts.d.ts +12 -0
  33. package/dist/store/sqlite/payment-facts.js +92 -0
  34. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  35. package/dist/store/store.d.ts +0 -1
  36. package/package.json +1 -1
  37. package/dist/cli/payment-query.d.ts +0 -8
  38. package/dist/cli/payment-query.js +0 -27
package/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.558.0 — 02.11.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **A payment is no longer a fact (store version 46).** Money lives only in `store.finance`. `facts.add` refuses type
15
+ `payment` with reason `fact.payment-retired`, naming `store.finance.addPayment`, and a trigger refuses one from an
16
+ older build. `FACT_TYPES`, the `fact` entity and `Fact` lose `payment`; `PaymentFilter`, `facts.list({ payment })`,
17
+ `PAYMENT_QUERY_FIELDS` and `paymentFilterOf` are gone. The first writable open moves stored payment facts into rows.
18
+ - **An accepted payment finding writes a finance row, not a fact.** A paid one is a `payment:` row, an expected one
19
+ an `invoice:` of one line, proposed by the agent and confirmed by the owner's acceptance. `supersedes` now names
20
+ the `invoice:` a paid one settles or a cancelled one voids, not a `fact:`. An expected one needs a `person:` or
21
+ `organization:` counterparty, and the amount must fit the currency's decimals. New optional field `paid_at`.
22
+ - **`projects status` reads money from `store.finance`.** The `payments` signal (version 3) lists payments and open
23
+ invoices by their `payment:` and `invoice:` refs, `status` being `proposed` or `confirmed`; the open total counts
24
+ confirmed invoices only. Finance rows gain `evidence`, and `addInvoice` and `addPayment` take it.
25
+
10
26
  ## 0.557.0 — 02.11.2026
11
27
 
12
28
  ### Added
@@ -111,7 +111,6 @@ export { type Saving, stored } from "./messenger/stored.js";
111
111
  export { dateOrNone } from "./messenger/tasks-command.js";
112
112
  export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
113
113
  export { positiveCount, window, withPaging } from "./paging.js";
114
- export { PAYMENT_QUERY_FIELDS, paymentFilterOf } from "./payment-query.js";
115
114
  export { type CommandPermissions, commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, type PermissionKeyOf, type PermissionKeySource, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
116
115
  export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
117
116
  export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOptions, run } from "./program.js";
package/dist/cli/index.js CHANGED
@@ -101,7 +101,6 @@ export { stored } from "./messenger/stored.js";
101
101
  export { dateOrNone } from "./messenger/tasks-command.js";
102
102
  export { MODEL_CATALOGUE_QUERY } from "./model-catalogue-command.js";
103
103
  export { positiveCount, window, withPaging } from "./paging.js";
104
- export { PAYMENT_QUERY_FIELDS, paymentFilterOf } from "./payment-query.js";
105
104
  export { commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
106
105
  export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
107
106
  export { createProgram, run } from "./program.js";
@@ -2,7 +2,7 @@ import * as v from "valibot";
2
2
  export declare const fact: v.StrictObjectSchema<{
3
3
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
4
4
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
5
- readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
5
+ readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
6
6
  readonly text: v.StringSchema<undefined>;
7
7
  readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
8
8
  readonly project: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -29,17 +29,6 @@ export declare const fact: v.StrictObjectSchema<{
29
29
  /** Who recorded it: the owner, or an agent whose fact waits as proposed. */
30
30
  readonly origin: v.PicklistSchema<["owner", "agent"], undefined>;
31
31
  readonly evidence: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
32
- /** A `payment` fact's fields. */
33
- readonly payment: v.OptionalSchema<v.NullableSchema<v.StrictObjectSchema<{
34
- readonly amount: v.StringSchema<undefined>;
35
- readonly currency: v.StringSchema<undefined>;
36
- readonly direction: v.PicklistSchema<readonly ["to_us", "from_us"], undefined>;
37
- readonly counterparty: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
38
- readonly counterparty_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
39
- readonly due_at: v.NullableSchema<v.StringSchema<undefined>, undefined>;
40
- readonly state: v.PicklistSchema<readonly ["expected", "paid", "cancelled"], undefined>;
41
- readonly item: v.NullableSchema<v.StringSchema<undefined>, undefined>;
42
- }, undefined>, undefined>, undefined>;
43
32
  /** With `--sources`: each piece of evidence read back. */
44
33
  readonly sources: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
45
34
  readonly ref: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
@@ -1,17 +1,6 @@
1
1
  import * as v from "valibot";
2
2
  import { FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../domain/knowledge.js";
3
- import { PAYMENT_DIRECTIONS, PAYMENT_STATES } from "../meetings/index.js";
4
3
  import { author, evidenceSource, id, reference, text, time } from "./fields.js";
5
- const payment = v.strictObject({
6
- amount: text,
7
- currency: text,
8
- direction: v.picklist(PAYMENT_DIRECTIONS),
9
- counterparty: v.nullable(reference),
10
- counterparty_name: v.nullable(text),
11
- due_at: v.nullable(text),
12
- state: v.picklist(PAYMENT_STATES),
13
- item: v.nullable(text),
14
- });
15
4
  export const fact = v.strictObject({
16
5
  ref: reference,
17
6
  id,
@@ -35,8 +24,6 @@ export const fact = v.strictObject({
35
24
  /** Who recorded it: the owner, or an agent whose fact waits as proposed. */
36
25
  origin: v.picklist(["owner", "agent"]),
37
26
  evidence: v.array(reference),
38
- /** A `payment` fact's fields. */
39
- payment: v.optional(v.nullable(payment)),
40
27
  /** With `--sources`: each piece of evidence read back. */
41
28
  sources: v.optional(v.array(evidenceSource)),
42
29
  created_at: time,
@@ -5761,7 +5761,7 @@ export declare const ENTITIES: {
5761
5761
  readonly fact: v.StrictObjectSchema<{
5762
5762
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
5763
5763
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
5764
- readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
5764
+ readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
5765
5765
  readonly text: v.StringSchema<undefined>;
5766
5766
  readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
5767
5767
  readonly project: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -5784,16 +5784,6 @@ export declare const ENTITIES: {
5784
5784
  readonly scope: v.NullableSchema<v.PicklistSchema<readonly ["personal", "work"], undefined>, undefined>;
5785
5785
  readonly origin: v.PicklistSchema<["owner", "agent"], undefined>;
5786
5786
  readonly evidence: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5787
- readonly payment: v.OptionalSchema<v.NullableSchema<v.StrictObjectSchema<{
5788
- readonly amount: v.StringSchema<undefined>;
5789
- readonly currency: v.StringSchema<undefined>;
5790
- readonly direction: v.PicklistSchema<readonly ["to_us", "from_us"], undefined>;
5791
- readonly counterparty: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5792
- readonly counterparty_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
5793
- readonly due_at: v.NullableSchema<v.StringSchema<undefined>, undefined>;
5794
- readonly state: v.PicklistSchema<readonly ["expected", "paid", "cancelled"], undefined>;
5795
- readonly item: v.NullableSchema<v.StringSchema<undefined>, undefined>;
5796
- }, undefined>, undefined>, undefined>;
5797
5787
  readonly sources: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
5798
5788
  readonly ref: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5799
5789
  readonly kind: v.StringSchema<undefined>;
@@ -6589,7 +6579,7 @@ export declare const ENTITIES: {
6589
6579
  readonly fingerprint: v.StringSchema<undefined>;
6590
6580
  }, undefined>, undefined>;
6591
6581
  readonly kind: v.LiteralSchema<"fact", undefined>;
6592
- readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
6582
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
6593
6583
  readonly statement: v.StringSchema<undefined>;
6594
6584
  }, undefined>, v.StrictObjectSchema<{
6595
6585
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
@@ -17,7 +17,7 @@ export declare const meetingFindings: v.StrictObjectSchema<{
17
17
  readonly fingerprint: v.StringSchema<undefined>;
18
18
  }, undefined>, undefined>;
19
19
  readonly kind: v.LiteralSchema<"fact", undefined>;
20
- readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
20
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
21
21
  readonly statement: v.StringSchema<undefined>;
22
22
  }, undefined>, v.StrictObjectSchema<{
23
23
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
@@ -138,6 +138,7 @@ const OTHERS = [
138
138
  validation("input.required", "the command needs an answer and cannot ask for one"),
139
139
  validation("input.too-large", "the input is over its size limit"),
140
140
  ["meeting-project.stale", "configuration_error", "the meeting's project changed meanwhile"],
141
+ validation("fact.payment-retired", "a payment is a payments row in store.finance, not a fact"),
141
142
  validation("finance.allocate", "the invoice or payment cannot take an allocation in its state"),
142
143
  validation("finance.currency", "the payment and the invoice are in different currencies; nothing is converted"),
143
144
  validation("finance.field", "a field of the invoice or payment is not valid"),
@@ -14,16 +14,19 @@ interface FindingBase {
14
14
  }
15
15
  export interface FactFinding extends FindingBase {
16
16
  kind: "fact";
17
- type: Exclude<(typeof FACT_TYPES)[number], "payment">;
17
+ type: (typeof FACT_TYPES)[number];
18
18
  statement: string;
19
19
  }
20
- /** A payment an agent read; confirming it records a `payment` fact. A paid one supersedes the expected one. */
20
+ /**
21
+ * A payment an agent read. Confirming it writes money through `store.finance`, never a fact: a paid one a `payments`
22
+ * row, an expected one an invoice; a paid one that supersedes an invoice settles it, a cancelled one voids it.
23
+ */
21
24
  export interface PaymentFinding extends FindingBase {
22
25
  kind: "payment";
23
- /** The fact's full sentence; without one, the payment's title. */
26
+ /** The full sentence, kept as the proposal's reason; without one, the payment's title. */
24
27
  statement?: string;
25
28
  payment: Payment;
26
- /** The `fact:` of the expected payment this one replaces, once it is paid or cancelled. */
29
+ /** The `invoice:` of the expected payment this one settles (paid) or voids (cancelled). */
27
30
  supersedes?: string;
28
31
  }
29
32
  export interface TaskFinding extends FindingBase {
@@ -70,20 +70,27 @@ export const validateFindings = (input, rules) => {
70
70
  if (finding.kind === "fact") {
71
71
  fields(finding, ["id", "project", "subject", "evidence", "kind", "type", "statement"], rules.singular);
72
72
  if (finding.type === "payment")
73
- return invalid("a payment is a finding of kind payment");
73
+ return invalid("a payment is a finding of kind payment; money is a payments row, not a fact");
74
74
  if (!FACT_TYPES.includes(finding.type))
75
75
  return invalid("unknown fact finding type");
76
76
  return { ...base, kind: "fact", type: finding.type, statement: text(finding.statement, 2000) };
77
77
  }
78
78
  if (finding.kind === "payment" && rules.payments) {
79
79
  fields(finding, ["id", "project", "subject", "evidence", "kind", "statement", "payment", "supersedes"], rules.singular);
80
- if (finding.supersedes !== undefined && !/^fact:[1-9]\d*$/.test(String(finding.supersedes)))
81
- return invalid("a payment finding supersedes a fact: reference");
80
+ if (finding.supersedes !== undefined && !/^invoice:[1-9]\d*$/.test(String(finding.supersedes)))
81
+ return invalid("a payment finding supersedes an invoice: reference, the expected payment it settles or voids");
82
+ const payment = validatePayment(finding.payment);
83
+ if (payment.state === "expected" && finding.supersedes !== undefined)
84
+ return invalid("an expected payment supersedes nothing; a paid or cancelled one supersedes its invoice");
85
+ if (payment.state === "cancelled" && finding.supersedes === undefined)
86
+ return invalid("a cancelled payment names the invoice: it cancels in supersedes");
87
+ if (payment.state === "expected" && !/^(person|organization):/.test(payment.counterparty ?? ""))
88
+ return invalid("an expected payment becomes an invoice, which names its counterparty as a person: or organization:");
82
89
  return {
83
90
  ...base,
84
91
  kind: "payment",
85
92
  ...(finding.statement === undefined ? {} : { statement: text(finding.statement, 2000) }),
86
- payment: validatePayment(finding.payment),
93
+ payment,
87
94
  ...(finding.supersedes === undefined ? {} : { supersedes: finding.supersedes }),
88
95
  };
89
96
  }
@@ -1,8 +1,11 @@
1
- /** Every fact type the store takes; findings and meeting changes check against the same list. */
2
- export declare const FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise", "payment"];
1
+ /** Every fact type the store takes; findings and meeting changes check against the same list. Money is not a fact. */
2
+ export declare const FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise"];
3
3
  export declare const PAYMENT_DIRECTIONS: readonly ["to_us", "from_us"];
4
4
  export declare const PAYMENT_STATES: readonly ["expected", "paid", "cancelled"];
5
- /** A payment fact's fields, kept in `facts.metadata.payment`. */
5
+ /**
6
+ * A payment finding's fields, as an agent reads them from evidence. Confirmed by the owner, a paid one becomes a
7
+ * `payments` row, an expected one an invoice, a cancelled one voids the invoice it supersedes.
8
+ */
6
9
  export interface Payment {
7
10
  /** A decimal as text with two places, "50000.00": money is never a float. */
8
11
  amount: string;
@@ -14,14 +17,15 @@ export interface Payment {
14
17
  counterparty_name: string | null;
15
18
  /** An ISO date. */
16
19
  due_at: string | null;
17
- /** A change of state is a new fact that supersedes this one, so the history and its evidence stay. */
18
20
  state: (typeof PAYMENT_STATES)[number];
19
- /** What it is for, short: "support, September". The fact's statement is the full sentence. */
21
+ /** What it is for, short: "support, September". The finding's statement is the full sentence. */
20
22
  item: string | null;
23
+ /** When a paid one was paid, an ISO date; without it the due date, else the day the owner confirms it. */
24
+ paid_at?: string;
21
25
  }
22
26
  /** A payment as stored: the amount with two decimals, the currency upper case; refuses what is not one. */
23
27
  export declare const validatePayment: (input: unknown) => Payment;
24
- /** One line for a payment, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
28
+ /** One line for a payment finding, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
25
29
  export declare const paymentTitle: (fact: {
26
30
  payment: Payment | null;
27
31
  }) => string;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- /** Every fact type the store takes; findings and meeting changes check against the same list. */
3
- export const FACT_TYPES = ["fact", "statement", "question", "decision", "promise", "payment"];
2
+ /** Every fact type the store takes; findings and meeting changes check against the same list. Money is not a fact. */
3
+ export const FACT_TYPES = ["fact", "statement", "question", "decision", "promise"];
4
4
  export const PAYMENT_DIRECTIONS = ["to_us", "from_us"];
5
5
  export const PAYMENT_STATES = ["expected", "paid", "cancelled"];
6
6
  const invalid = (message) => {
@@ -21,7 +21,7 @@ const amountOf = (value) => {
21
21
  const dateOf = (value) => {
22
22
  const date = typeof value === "string" ? value.trim() : "";
23
23
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date)
24
- return invalid("a payment's due date is an ISO date, such as 2026-10-31");
24
+ return invalid("a payment's dates are ISO dates, such as 2026-10-31");
25
25
  return date;
26
26
  };
27
27
  const optionalText = (value, what, limit) => {
@@ -36,7 +36,17 @@ const oneOf = (value, allowed, what) => {
36
36
  return invalid(`a payment's ${what} is one of ${allowed.join(", ")}`);
37
37
  return value;
38
38
  };
39
- const FIELDS = ["amount", "currency", "direction", "counterparty", "counterparty_name", "due_at", "state", "item"];
39
+ const FIELDS = [
40
+ "amount",
41
+ "currency",
42
+ "direction",
43
+ "counterparty",
44
+ "counterparty_name",
45
+ "due_at",
46
+ "state",
47
+ "item",
48
+ "paid_at",
49
+ ];
40
50
  /** A payment as stored: the amount with two decimals, the currency upper case; refuses what is not one. */
41
51
  export const validatePayment = (input) => {
42
52
  if (!input || typeof input !== "object" || Array.isArray(input))
@@ -51,6 +61,9 @@ export const validatePayment = (input) => {
51
61
  const counterparty = optionalText(raw.counterparty, "counterparty", 80);
52
62
  if (counterparty !== null && !COUNTERPARTY.test(counterparty))
53
63
  return invalid("a payment's counterparty is a person:, organization: or project: reference");
64
+ const state = oneOf(raw.state, PAYMENT_STATES, "state");
65
+ if (raw.paid_at != null && state !== "paid")
66
+ return invalid("only a paid payment has paid_at");
54
67
  return {
55
68
  amount: amountOf(raw.amount),
56
69
  currency,
@@ -58,8 +71,10 @@ export const validatePayment = (input) => {
58
71
  counterparty,
59
72
  counterparty_name: optionalText(raw.counterparty_name, "counterparty name", 200),
60
73
  due_at: raw.due_at === undefined || raw.due_at === null ? null : dateOf(raw.due_at),
61
- state: oneOf(raw.state, PAYMENT_STATES, "state"),
74
+ state,
62
75
  item: optionalText(raw.item, "item", 200),
76
+ // Left out when absent, so a finding staged before the field keeps its fingerprint.
77
+ ...(raw.paid_at == null ? {} : { paid_at: dateOf(raw.paid_at) }),
63
78
  };
64
79
  };
65
80
  const grouped = (amount) => {
@@ -67,7 +82,7 @@ const grouped = (amount) => {
67
82
  const digits = whole.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
68
83
  return cents === "00" ? digits : `${digits}.${cents}`;
69
84
  };
70
- /** One line for a payment, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
85
+ /** One line for a payment finding, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
71
86
  export const paymentTitle = (fact) => {
72
87
  const payment = fact.payment;
73
88
  if (!payment)
@@ -60,7 +60,7 @@ export interface ProjectStatus {
60
60
  chats: StatusChatSummary[];
61
61
  /** Every signal's items, in signal order, then chat order. */
62
62
  items: StatusItem[];
63
- /** Open amounts and expected payment facts, summed per direction and currency. */
63
+ /** Open amounts and what confirmed open invoices still owe, summed per direction and currency. */
64
64
  money: {
65
65
  open: MoneyTotal[];
66
66
  };
@@ -53,11 +53,15 @@ const childrenOf = async (store, root, catalog) => {
53
53
  };
54
54
  const inAccounts = (accounts, provider, account) => accounts === undefined || accounts.some((one) => one.provider === provider && one.account === account);
55
55
  const failure = (error) => (error instanceof Error ? error.message : String(error));
56
- /** An open ask, or an expected payment fact; an ask that a payment fact cites is counted once, as the fact. */
56
+ /**
57
+ * An open ask, or a confirmed open invoice; an ask that a confirmed row cites is counted once, as the row. A proposed
58
+ * row promises nothing yet, so the ask it cites still counts.
59
+ */
57
60
  const counted = (items) => {
58
- const recorded = new Set(items.filter((item) => item.signal === "payments").flatMap((item) => item.evidence.slice(1).map((one) => one.ref)));
61
+ const confirmed = (item) => item.signal === "payments" && item.data.status === "confirmed";
62
+ const recorded = new Set(items.filter(confirmed).flatMap((item) => item.evidence.slice(1).map((one) => one.ref)));
59
63
  return (item, state) => item.signal === "payments"
60
- ? state === "expected"
64
+ ? state === "expected" && confirmed(item)
61
65
  : item.signal === "amounts" && state === "open" && !item.evidence.some((one) => recorded.has(one.ref));
62
66
  };
63
67
  const totalsOf = (items) => {
@@ -1,9 +1,9 @@
1
- import { type Payment } from "../../../meetings/index.js";
1
+ import type { Payment } from "../../../meetings/index.js";
2
2
  import type { StatusSignal } from "../types.js";
3
3
  export interface PaymentsData extends Payment {
4
4
  title: string;
5
- /** The fact's review status: proposed or accepted. */
5
+ /** The row's review: proposed or confirmed. */
6
6
  status: string;
7
7
  }
8
- /** Payment facts of the project and its children, proposed or accepted. Only what someone recorded. */
8
+ /** Payments and open invoices of the project and its children from `store.finance`, proposed or confirmed. */
9
9
  export declare const payments: StatusSignal<PaymentsData>;
@@ -1,24 +1,50 @@
1
- import { paymentTitle } from "../../../meetings/index.js";
2
- const HOLDING = new Set(["proposed", "accepted"]);
3
- /** Payment facts of the project and its children, proposed or accepted. Only what someone recorded. */
1
+ const holding = (row) => row.review !== "rejected";
2
+ const item = (row, at, data) => ({
3
+ signal: "payments",
4
+ chat: null,
5
+ at,
6
+ who: null,
7
+ // The row first, then what it cites, so an amount read from a cited message is not counted twice.
8
+ evidence: [row.ref, ...row.evidence].map((ref) => ({ ref, fingerprint: null })),
9
+ basis: "store",
10
+ data: { ...data, title: row.title, status: row.review },
11
+ });
12
+ const paid = (row) => {
13
+ const party = row.direction === "in" ? row.payer : row.payee;
14
+ return item(row, `${row.date}T00:00:00.000Z`, {
15
+ amount: row.amount,
16
+ currency: row.currency,
17
+ direction: row.direction === "in" ? "to_us" : "from_us",
18
+ counterparty: party?.ref ?? null,
19
+ counterparty_name: party?.name ?? null,
20
+ due_at: null,
21
+ state: "paid",
22
+ item: row.reference,
23
+ paid_at: row.date,
24
+ });
25
+ };
26
+ /** What an open invoice still owes is the expected payment. */
27
+ const expected = (row) => item(row, row.issueDate === null ? row.createdAt : `${row.issueDate}T00:00:00.000Z`, {
28
+ amount: row.open,
29
+ currency: row.currency,
30
+ direction: row.direction === "receivable" ? "to_us" : "from_us",
31
+ counterparty: row.counterparty.ref,
32
+ counterparty_name: row.counterparty.name,
33
+ due_at: row.dueDate,
34
+ state: "expected",
35
+ item: row.number,
36
+ });
37
+ /** Payments and open invoices of the project and its children from `store.finance`, proposed or confirmed. */
4
38
  export const payments = {
5
39
  id: "payments",
6
- version: 2,
40
+ version: 3,
7
41
  scope: "project",
8
42
  read: async (context) => {
9
43
  const projects = [context.project.ref, ...context.project.children.map((child) => child.ref)];
10
- const facts = (await Promise.all(projects.map((project) => context.store.facts.list({ project, type: "payment", limit: 500 })))).flat();
11
- return facts
12
- .filter((fact) => fact.payment !== null && HOLDING.has(fact.status))
13
- .map((fact) => ({
14
- signal: "payments",
15
- chat: null,
16
- at: fact.occurredAt ?? fact.createdAt,
17
- who: null,
18
- // The fact first, then what it cites, so an amount read from a cited message is not counted twice.
19
- evidence: [fact.ref, ...fact.evidence].map((ref) => ({ ref, fingerprint: null })),
20
- basis: "fact",
21
- data: { ...fact.payment, title: paymentTitle(fact), status: fact.status },
22
- }));
44
+ const rows = await Promise.all(projects.map(async (project) => [
45
+ ...(await context.store.finance.payments({ project, limit: 500 })).filter(holding).map(paid),
46
+ ...(await context.store.finance.invoices({ project, open: true, limit: 500 })).filter(holding).map(expected),
47
+ ]));
48
+ return rows.flat();
23
49
  },
24
50
  };
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { formatLocator, parseLocator } from "../domain/locator.js";
3
3
  import { validateFindings } from "../meetings/index.js";
4
+ import { entryAmount } from "./sqlite/payment-entries.js";
4
5
  export const EVIDENCE_FINDINGS_KIND = "evidence_findings";
5
6
  const reference = (raw) => {
6
7
  if (/^(document|note):[1-9]\d*$/.test(raw))
@@ -12,11 +13,18 @@ const reference = (raw) => {
12
13
  throw new CliError("validation_error", "finding evidence is a msg:, document: or note: reference");
13
14
  }
14
15
  };
15
- export const validateEvidenceFindings = (input) => validateFindings(input, {
16
- plural: "evidence findings",
17
- singular: "evidence finding",
18
- cited: "reference",
19
- reference,
20
- anySubject: true,
21
- payments: true,
22
- });
16
+ export const validateEvidenceFindings = (input) => {
17
+ const valid = validateFindings(input, {
18
+ plural: "evidence findings",
19
+ singular: "evidence finding",
20
+ cited: "reference",
21
+ reference,
22
+ anySubject: true,
23
+ payments: true,
24
+ });
25
+ // Refused at staging rather than when the owner accepts, which would fail the whole batch.
26
+ for (const finding of valid.findings)
27
+ if (finding.kind === "payment")
28
+ entryAmount(finding.payment);
29
+ return valid;
30
+ };
@@ -36,7 +36,7 @@ export type { ChatMetadata } from "./sqlite/chats.js";
36
36
  export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, PartialEmail, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
37
37
  export { EVENT_FIELDS, type EventChunkToEmbed, type EventHit, type EventQuery, type EventSearch, type NearestEvent, } from "./sqlite/event-search.js";
38
38
  export type { EvidenceSource } from "./sqlite/evidence-sources.js";
39
- export type { Fact, FactInput, FactsStore, PaymentFilter } from "./sqlite/facts.js";
39
+ export type { Fact, FactInput, FactsStore } from "./sqlite/facts.js";
40
40
  export type { FileListQuery, FilesStore, StoredFile } from "./sqlite/files.js";
41
41
  export type { Allocation, FinanceStore, Invoice, InvoiceDetail, InvoiceInput, InvoiceLine, InvoiceLineInput, OpenTotal, Party, Payment as FinancePayment, PaymentDetail as FinancePaymentDetail, PaymentInput as FinancePaymentInput, } from "./sqlite/finance.js";
42
42
  export { IMPORTED_DOCUMENT_QUERY, type ImportedDocuments } from "./sqlite/imported-documents.js";
@@ -4,7 +4,7 @@ export declare const projectAnnotationRequest: (request: ProjectAnnotationTarget
4
4
  format: "wirecat.project-annotations";
5
5
  facts: {
6
6
  ref: string;
7
- type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
7
+ type: "decision" | "fact" | "promise" | "question" | "statement";
8
8
  statement: string;
9
9
  status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
10
10
  subject: string | null;
@@ -23,7 +23,7 @@ export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
23
23
  readonly aliases: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>, v.MaxLengthAction<string[], 1000, undefined>]>;
24
24
  readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
25
25
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
26
- readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
26
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
27
27
  readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
28
28
  readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
29
29
  readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
@@ -38,7 +38,7 @@ export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
38
38
  readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
39
39
  }, undefined>, undefined>, v.MaxLengthAction<{
40
40
  ref: string;
41
- type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
41
+ type: "decision" | "fact" | "promise" | "question" | "statement";
42
42
  statement: string;
43
43
  status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
44
44
  subject: string | null;
@@ -4,7 +4,7 @@ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
4
4
  readonly format: v.LiteralSchema<"wirecat.project-annotations", undefined>;
5
5
  readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
6
6
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
7
- readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
7
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
8
8
  readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
9
9
  readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
10
10
  readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
@@ -19,7 +19,7 @@ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
19
19
  readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
20
20
  }, undefined>, undefined>, v.MaxLengthAction<{
21
21
  ref: string;
22
- type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
22
+ type: "decision" | "fact" | "promise" | "question" | "statement";
23
23
  statement: string;
24
24
  status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
25
25
  subject: string | null;
@@ -1,5 +1,4 @@
1
1
  import { type FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
2
- import { type PAYMENT_DIRECTIONS, type PAYMENT_STATES, type Payment } from "../../meetings/index.js";
3
2
  import { type Decider } from "../decisions.js";
4
3
  import { type Actor, type ProposedByFilter } from "./actors.js";
5
4
  import { type EvidenceSource } from "./evidence-sources.js";
@@ -35,8 +34,6 @@ export interface Fact {
35
34
  scope: (typeof MEMORY_SCOPES)[number] | null;
36
35
  source: string;
37
36
  evidence: string[];
38
- /** Set on a `payment` fact only. */
39
- payment: Payment | null;
40
37
  createdAt: string;
41
38
  updatedAt: string;
42
39
  }
@@ -55,14 +52,6 @@ export interface FactInput {
55
52
  supersedes?: string;
56
53
  /** The memory an agent drew it from: a `created-from` link. */
57
54
  createdFrom?: string;
58
- /** Required on a `payment` fact, refused on any other; checked as `validatePayment` does. */
59
- payment?: Payment;
60
- }
61
- /** `open` is an expected payment that still holds: not superseded, rejected or cancelled. */
62
- export interface PaymentFilter {
63
- direction?: (typeof PAYMENT_DIRECTIONS)[number];
64
- state?: (typeof PAYMENT_STATES)[number];
65
- open?: boolean;
66
55
  }
67
56
  export interface FactsStore {
68
57
  add(input: FactInput): Promise<Fact>;
@@ -73,8 +62,6 @@ export interface FactsStore {
73
62
  subject?: string;
74
63
  status?: Fact["status"];
75
64
  proposedBy?: ProposedByFilter;
76
- /** Narrows to payment facts by their fields. */
77
- payment?: PaymentFilter;
78
65
  /** `created` lists the newest written first; by default the newest by when it happened. */
79
66
  order?: "occurred" | "created";
80
67
  limit?: number;
@@ -1,7 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
3
3
  import { calendarMoment } from "../../domain/moment.js";
4
- import { validatePayment } from "../../meetings/index.js";
5
4
  import { assertMayDecide, assertMayWithdraw, deciderAuthor } from "../decisions.js";
6
5
  import { proposedBySql, proposerOf } from "./actors.js";
7
6
  import { evidenceSourcesOf } from "./evidence-sources.js";
@@ -36,7 +35,6 @@ export const factOperationsOver = (context) => {
36
35
  scope: row.scope == null ? null : row.scope,
37
36
  source: String(row.source),
38
37
  evidence: evidenceOf(context, { type: "fact", id: Number(row.id) }),
39
- payment: row.type === "payment" ? paymentOf(row.metadata) : null,
40
38
  createdAt: iso(row.created_at),
41
39
  updatedAt: iso(row.updated_at),
42
40
  });
@@ -54,7 +52,6 @@ export const factOperationsOver = (context) => {
54
52
  const decided = (row, by) => assertMayDecide({
55
53
  id: `fact:${row.id}`,
56
54
  proposedBy: proposerOf(database, row.author_type, row.author_id),
57
- ownerOnly: row.type === "payment",
58
55
  }, by, context.decisions);
59
56
  /** Keeps who decided for the owner and why in `metadata.decision`; the decider's actor is in `confirmed_by`. */
60
57
  const keepDecision = (id, by, reason) => {
@@ -83,6 +80,8 @@ export const factOperationsOver = (context) => {
83
80
  };
84
81
  return {
85
82
  add: (input) => {
83
+ if (input.type === "payment")
84
+ throw new CliError("validation_error", "a payment is not a fact: record it with store.finance.addPayment, a payment: row, or an invoice: for one expected", { reason: "fact.payment-retired" });
86
85
  const type = oneOf(input.type, FACT_TYPES, "a fact's type");
87
86
  const statement = input.statement?.trim() ?? "";
88
87
  if (!statement || statement.length > 2000)
@@ -91,7 +90,6 @@ export const factOperationsOver = (context) => {
91
90
  const occurredAt = input.occurredAt === undefined ? null : calendarMoment(input.occurredAt, "occurredAt");
92
91
  if (occurredAt !== null && !Number.isFinite(occurredAt))
93
92
  throw new CliError("validation_error", "occurredAt takes an ISO timestamp");
94
- const payment = paymentInput(type, input);
95
93
  const id = write(database, () => {
96
94
  const project = input.project === undefined ? undefined : requiredThing(database, input.project, "the project");
97
95
  if (project && project.type !== "project")
@@ -100,10 +98,6 @@ export const factOperationsOver = (context) => {
100
98
  const replaced = input.supersedes === undefined ? undefined : rowOf(input.supersedes);
101
99
  if (replaced && replaced.status !== "proposed" && replaced.status !== "accepted")
102
100
  throw new CliError("validation_error", `${input.supersedes} is ${String(replaced.status)}; only a proposed or accepted fact can be corrected`);
103
- if (replaced && (replaced.type === "payment") !== (type === "payment"))
104
- throw new CliError("validation_error", "a payment fact corrects a payment fact, and only one");
105
- if (payment?.counterparty)
106
- requiredThing(database, payment.counterparty, "the counterparty");
107
101
  const owner = input.by === "owner";
108
102
  const { actor, model } = authorActor(context, input.by);
109
103
  const at = now();
@@ -111,7 +105,7 @@ export const factOperationsOver = (context) => {
111
105
  .prepare("INSERT INTO facts (id, type, statement, status, project_id, subject_type, subject_id, occurred_at, supersedes_id, " +
112
106
  "author_type, author_id, model, scope, source, metadata, created_at, updated_at) " +
113
107
  "VALUES (?, ?, ?, 'proposed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
114
- .get(nextId(database, "facts"), type, statement, project?.id ?? null, subject?.type ?? null, subject?.id ?? null, occurredAt, replaced === undefined ? null : Number(replaced.id), actor.type, actor.id, model, scope, owner ? "owner" : "agent", payment ? JSON.stringify({ payment }) : null, at, at)?.id);
108
+ .get(nextId(database, "facts"), type, statement, project?.id ?? null, subject?.type ?? null, subject?.id ?? null, occurredAt, replaced === undefined ? null : Number(replaced.id), actor.type, actor.id, model, scope, owner ? "owner" : "agent", null, at, at)?.id);
115
109
  linkEvidence(context, { type: "fact", id: made }, input.evidence ?? [], owner ? "owner" : "agent");
116
110
  if (input.createdFrom !== undefined) {
117
111
  const from = requiredThing(database, input.createdFrom, "the source");
@@ -132,12 +126,11 @@ export const factOperationsOver = (context) => {
132
126
  const subject = options.subject === undefined ? undefined : requiredThing(database, options.subject);
133
127
  const by = proposedBySql("f.author_type", "f.author_id", options.proposedBy);
134
128
  const order = options.order === "created" ? "f.created_at" : "coalesce(f.occurred_at, f.created_at)";
135
- const paid = paymentSql(options.payment);
136
129
  return database
137
130
  .prepare(`${NAMED} WHERE f.deleted_at IS NULL AND (? IS NULL OR f.type = ?) AND (? IS NULL OR f.project_id = ?) ` +
138
131
  "AND (? IS NULL OR (f.subject_type = ? AND f.subject_id = ?)) AND (? IS NULL OR f.status = ?) " +
139
- `AND ${by.sql}${paid.sql} ORDER BY ${order} DESC, f.id DESC LIMIT ?`)
140
- .all(options.type ?? null, options.type ?? null, project?.id ?? null, project?.id ?? null, subject?.type ?? null, subject?.type ?? null, subject?.id ?? null, options.status ?? null, options.status ?? null, ...by.params, ...paid.params, Math.min(Math.max(options.limit ?? 100, 1), 500))
132
+ `AND ${by.sql} ORDER BY ${order} DESC, f.id DESC LIMIT ?`)
133
+ .all(options.type ?? null, options.type ?? null, project?.id ?? null, project?.id ?? null, subject?.type ?? null, subject?.type ?? null, subject?.id ?? null, options.status ?? null, options.status ?? null, ...by.params, Math.min(Math.max(options.limit ?? 100, 1), 500))
141
134
  .map(factOf);
142
135
  },
143
136
  accept: (reference, by, reason) => {
@@ -195,52 +188,6 @@ export const factOperationsOver = (context) => {
195
188
  sources: (reference) => evidenceSourcesOf(database, { type: "fact", id: Number(rowOf(reference).id) }),
196
189
  };
197
190
  };
198
- const paymentInput = (type, input) => {
199
- if (type !== "payment") {
200
- if (input.payment !== undefined)
201
- throw new CliError("validation_error", "only a payment fact takes a payment");
202
- return undefined;
203
- }
204
- if (input.payment === undefined)
205
- throw new CliError("validation_error", "a payment fact needs its payment: amount, currency, direction and state");
206
- const evidence = input.evidence?.length ?? 0;
207
- if (evidence < 1 || evidence > 8)
208
- throw new CliError("validation_error", "a payment fact cites 1–8 references: the ask, the transfer or a sheet row");
209
- return validatePayment(input.payment);
210
- };
211
- const paymentOf = (metadata) => {
212
- if (metadata == null)
213
- return null;
214
- try {
215
- const { payment } = JSON.parse(String(metadata));
216
- return payment ?? null;
217
- }
218
- catch {
219
- return null;
220
- }
221
- };
222
- const paymentSql = (filter) => {
223
- if (filter === undefined)
224
- return { sql: "", params: [] };
225
- const parts = ["f.type = 'payment'"];
226
- const params = [];
227
- const state = filter.open ? "expected" : filter.state;
228
- if (filter.open && filter.state !== undefined && filter.state !== "expected")
229
- parts.push("0");
230
- if (filter.direction !== undefined) {
231
- parts.push("json_extract(f.metadata, '$.payment.direction') = ?");
232
- params.push(filter.direction);
233
- }
234
- if (state !== undefined) {
235
- parts.push("json_extract(f.metadata, '$.payment.state') = ?");
236
- params.push(state);
237
- }
238
- if (filter.open)
239
- parts.push("f.status IN ('proposed', 'accepted')");
240
- if (filter.open === false)
241
- parts.push("NOT (json_extract(f.metadata, '$.payment.state') = 'expected' AND f.status IN ('proposed', 'accepted'))");
242
- return { sql: ` AND ${parts.join(" AND ")}`, params };
243
- };
244
191
  const migratedFrom = (metadata) => {
245
192
  try {
246
193
  const { memory } = JSON.parse(metadata);
@@ -28,6 +28,8 @@ interface Reviewed {
28
28
  source: string;
29
29
  /** Why an agent or an import proposed it. */
30
30
  reason: string | null;
31
+ /** What it was read from: the messages, documents or notes an agent or the owner cited. */
32
+ evidence: string[];
31
33
  project: string | null;
32
34
  projectName: string | null;
33
35
  createdAt: string;
@@ -97,6 +99,8 @@ interface Written {
97
99
  source?: string;
98
100
  /** Why a bot proposes it, kept for the reviewer. */
99
101
  reason?: string;
102
+ /** References it was read from, linked as `evidence`. */
103
+ evidence?: string[];
100
104
  }
101
105
  export interface InvoiceLineInput {
102
106
  description: string;
@@ -3,7 +3,7 @@ import { FINANCE_REVIEWS, INVOICE_DIRECTIONS, INVOICE_STATUSES, invoiceTitle, MO
3
3
  import { currencyOf, formatDecimal, lineUnits, minorUnits, moneyLabel, moneyOf, quantityOf, scaleOf, } from "../../domain/money.js";
4
4
  import { assertMayDecide, botKey, deciderAuthor } from "../decisions.js";
5
5
  import { proposedBySql, proposerOf } from "./actors.js";
6
- import { authorActor, authorOf, projectId, projectOf, subjectOf } from "./memories.js";
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
9
  const refuse = (message, reason, code = "validation_error") => {
@@ -30,6 +30,13 @@ const isoDate = (value, what) => {
30
30
  return date;
31
31
  };
32
32
  const iso = (value) => new Date(Number(value)).toISOString();
33
+ const evidenceList = (value) => {
34
+ if (value === undefined)
35
+ return [];
36
+ if (!Array.isArray(value) || value.length > 8 || value.some((one) => typeof one !== "string" || !one.trim()))
37
+ return refuse("evidence is a list of at most 8 references", "finance.field");
38
+ return [...new Set(value.map((one) => one.trim()))];
39
+ };
33
40
  const MAX_LINES = 500;
34
41
  const reasonOf = (metadata) => {
35
42
  if (metadata == null)
@@ -65,7 +72,7 @@ export const financeOperationsOver = (context) => {
65
72
  return refuse(`${what} is a person: or organization: reference, not ${reference}`, "finance.party");
66
73
  return thing;
67
74
  };
68
- const reviewedOf = (row) => ({
75
+ const reviewedOf = (row, type) => ({
69
76
  review: row.review,
70
77
  author: authorOf(row),
71
78
  model: row.model == null ? null : String(row.model),
@@ -74,6 +81,7 @@ export const financeOperationsOver = (context) => {
74
81
  : { type: row.confirmed_by_type, id: String(row.confirmed_by_id) },
75
82
  source: String(row.source),
76
83
  reason: reasonOf(row.metadata),
84
+ evidence: evidenceOf(context, { type, id: Number(row.id) }),
77
85
  ...projectOf(database, row),
78
86
  createdAt: iso(row.created_at),
79
87
  updatedAt: iso(row.updated_at),
@@ -109,7 +117,7 @@ export const financeOperationsOver = (context) => {
109
117
  issueDate: row.issue_date == null ? null : String(row.issue_date),
110
118
  dueDate: fields.dueDate,
111
119
  status: fields.status,
112
- ...reviewedOf(row),
120
+ ...reviewedOf(row, "invoice"),
113
121
  };
114
122
  };
115
123
  const paymentOf = (row) => {
@@ -140,7 +148,7 @@ export const financeOperationsOver = (context) => {
140
148
  reference: row.reference == null ? null : String(row.reference),
141
149
  allocated: formatDecimal(allocated, scale),
142
150
  unallocated: formatDecimal(amount - allocated, scale),
143
- ...reviewedOf(row),
151
+ ...reviewedOf(row, "payment"),
144
152
  };
145
153
  };
146
154
  const allocationsWhere = (column, id) => database
@@ -236,8 +244,13 @@ export const financeOperationsOver = (context) => {
236
244
  confirmedId: owner ? actor.id : null,
237
245
  metadata: reason === null ? null : JSON.stringify({ reason }),
238
246
  project: input.project === undefined ? null : projectId(database, input.project),
247
+ evidence: evidenceList(input.evidence),
239
248
  };
240
249
  };
250
+ const keepEvidence = (type, id, by) => {
251
+ if (by.evidence.length)
252
+ linkEvidence(context, { type, id }, by.evidence, by.owner ? "owner" : "agent");
253
+ };
241
254
  const ownerOnly = (by, what) => {
242
255
  if (by !== "owner")
243
256
  refuse(`only the owner ${what}`, "finance.owner-only", "permission_error");
@@ -361,6 +374,7 @@ export const financeOperationsOver = (context) => {
361
374
  "VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
362
375
  for (const [index, line] of lines.entries())
363
376
  insert.run(made, index + 1, line.description, line.quantity, line.unitPrice, line.amount, at, at);
377
+ keepEvidence("invoice", made, by);
364
378
  return made;
365
379
  });
366
380
  return invoiceDetail(`invoice:${id}`);
@@ -436,6 +450,7 @@ export const financeOperationsOver = (context) => {
436
450
  "reference, project_id, review, author_type, author_id, model, confirmed_by_type, confirmed_by_id, source, " +
437
451
  "metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *")
438
452
  .get(direction, payer?.type ?? null, payer?.id ?? null, payee?.type ?? null, payee?.id ?? null, amount, currency, date, reference, by.project, by.review, by.actor.type, by.actor.id, by.model, by.confirmedType, by.confirmedId, by.source, by.metadata, at, at);
453
+ keepEvidence("payment", Number(made.id), by);
439
454
  const touched = allocations.map(({ invoice, units }) => allocateRow(made, invoice, units, by.actor));
440
455
  for (const invoice of touched)
441
456
  settle(invoice);
@@ -1,8 +1,9 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { findingFingerprint, findingHash, paymentTitle, } from "../../meetings/index.js";
2
+ import { findingFingerprint, findingHash, } from "../../meetings/index.js";
3
3
  import { factOperationsOver } from "./facts.js";
4
4
  import { linkEvidence } from "./memories.js";
5
5
  import { requiredThing } from "./notes.js";
6
+ import { paymentEntriesOver } from "./payment-entries.js";
6
7
  import { taskRowOf } from "./things.js";
7
8
  import { workTaskOperationsOver } from "./work-tasks.js";
8
9
  const invalid = (message) => {
@@ -27,6 +28,7 @@ export const findingsCoreOver = (context, kind) => {
27
28
  const { database } = context;
28
29
  const facts = factOperationsOver(context);
29
30
  const tasks = workTaskOperationsOver(context);
31
+ const recordPayment = paymentEntriesOver(context);
30
32
  const cited = (input, cite) => {
31
33
  const seen = new Map();
32
34
  for (const finding of input.findings) {
@@ -81,19 +83,8 @@ export const findingsCoreOver = (context, kind) => {
81
83
  });
82
84
  return facts.acceptChecked(fact.ref, decidedBy).ref;
83
85
  }
84
- if (finding.kind === "payment") {
85
- const fact = facts.add({
86
- type: "payment",
87
- statement: finding.statement ?? paymentTitle(finding),
88
- payment: finding.payment,
89
- ...(finding.project === undefined ? {} : { project: finding.project }),
90
- ...(finding.subject === undefined ? {} : { subject: finding.subject }),
91
- ...(finding.supersedes === undefined ? {} : { supersedes: finding.supersedes }),
92
- evidence,
93
- by: agent,
94
- });
95
- return facts.acceptChecked(fact.ref, decidedBy).ref;
96
- }
86
+ if (finding.kind === "payment")
87
+ return recordPayment({ ...finding, evidence }, agent, { confirm: true, by: decidedBy });
97
88
  if (finding.project === undefined)
98
89
  return invalid("a task finding names the project the task goes in");
99
90
  const task = tasks.create({
@@ -61,6 +61,7 @@ export const MANIFEST = [
61
61
  { name: "20261102050534_version-44-suggested-links-move", version: 44, minCompatible: 34 },
62
62
  { name: "20261102173102_version-45-finance", version: 45, minCompatible: 34 },
63
63
  { name: "20261102173113_version-45-finance-rules", version: 45, minCompatible: 34 },
64
+ { name: "20261102185954_version-46-payment-facts", version: 46, minCompatible: 34 },
64
65
  ];
65
66
  export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
66
67
  const migrations = [];
@@ -924,5 +924,13 @@ export const GENERATED = [
924
924
  "CREATE TRIGGER payment_allocations_valid_bi BEFORE INSERT ON payment_allocations\n WHEN new.amount <= 0\n OR (SELECT currency FROM payments WHERE id = new.payment_id)\n IS NOT (SELECT currency FROM invoices WHERE id = new.invoice_id)\n OR (SELECT coalesce(sum(amount), 0) FROM payment_allocations WHERE payment_id = new.payment_id) + new.amount\n > (SELECT amount FROM payments WHERE id = new.payment_id)\n OR (SELECT coalesce(sum(a.amount), 0) FROM payment_allocations a JOIN payments p ON p.id = a.payment_id\n WHERE a.invoice_id = new.invoice_id\n AND ((p.review = 'confirmed' AND p.deleted_at IS NULL) OR a.payment_id = new.payment_id)) + new.amount\n > (SELECT total FROM invoices WHERE id = new.invoice_id) BEGIN\n SELECT RAISE(ABORT, 'payment_allocations: above 0, one currency, within the payment and within the invoice');\nEND;",
925
925
  "CREATE TRIGGER payment_allocations_valid_bu BEFORE UPDATE ON payment_allocations\n WHEN new.amount <= 0 OR new.payment_id IS NOT old.payment_id OR new.invoice_id IS NOT old.invoice_id\n OR (SELECT coalesce(sum(amount), 0) FROM payment_allocations WHERE payment_id = new.payment_id AND id <> old.id)\n + new.amount > (SELECT amount FROM payments WHERE id = new.payment_id)\n OR (SELECT coalesce(sum(a.amount), 0) FROM payment_allocations a JOIN payments p ON p.id = a.payment_id\n WHERE a.invoice_id = new.invoice_id AND a.id <> old.id\n AND ((p.review = 'confirmed' AND p.deleted_at IS NULL) OR a.payment_id = new.payment_id)) + new.amount\n > (SELECT total FROM invoices WHERE id = new.invoice_id) BEGIN\n SELECT RAISE(ABORT, 'payment_allocations: above 0, within the payment and within the invoice, sides unchanged');\nEND;"
926
926
  ]
927
+ },
928
+ {
929
+ "name": "20261102185954_version-46-payment-facts",
930
+ "statements": [
931
+ "-- Money lives in `payments` and `invoices`, not facts. An older build still takes a payment fact, so the triggers\n-- refuse one; the facts already stored need the finance code's minor units and rules, so the next writable open moves\n-- them and deletes this key (movePaymentFacts).\nCREATE TRIGGER facts_no_payment_bi BEFORE INSERT ON facts WHEN new.type = 'payment' BEGIN\n SELECT RAISE(ABORT, 'facts: a payment is a payments row in store.finance, not a fact; upgrade the CLI');\nEND;",
932
+ "CREATE TRIGGER facts_no_payment_bu BEFORE UPDATE OF type ON facts WHEN new.type = 'payment' BEGIN\n SELECT RAISE(ABORT, 'facts: a payment is a payments row in store.finance, not a fact; upgrade the CLI');\nEND;",
933
+ "INSERT OR REPLACE INTO store_settings (key, value, at) VALUES ('payment_facts_to_move', '1', unixepoch() * 1000);"
934
+ ]
927
935
  }
928
936
  ];
@@ -12,6 +12,7 @@ import { fillFolded } from "./folded.js";
12
12
  import { fillJoinKeys } from "./join-keys.js";
13
13
  import { logPruner } from "./log-pruning.js";
14
14
  import { openSqlite } from "./open.js";
15
+ import { movePaymentFacts } from "./payment-facts.js";
15
16
  import { loadProviders } from "./providers.js";
16
17
  import { assertReadCompatible } from "./read-compatibility.js";
17
18
  import { stemsState } from "./stems.js";
@@ -57,6 +58,7 @@ export const openStoreContext = async ({ path, env, now = Date.now, command, ver
57
58
  fillFolded(database);
58
59
  fillJoinKeys(database);
59
60
  moveSuggestedLinks(database, now);
61
+ movePaymentFacts({ ...opened, now, decisions });
60
62
  const pending = pendingNormalization(database);
61
63
  if (pending > 0 && pending <= BACKFILL_ON_OPEN)
62
64
  backfillNormalized(database);
@@ -0,0 +1,26 @@
1
+ import { type Payment } from "../../meetings/index.js";
2
+ import type { Decider } from "../decisions.js";
3
+ import type { Author } from "./memories.js";
4
+ import type { StoreContext } from "./open.js";
5
+ /** A payment as an agent's finding or a retired payment fact holds it. */
6
+ export interface PaymentEntry {
7
+ payment: Payment;
8
+ statement?: string;
9
+ project?: string;
10
+ evidence: string[];
11
+ /** The `invoice:` a paid entry settles or a cancelled one voids. */
12
+ supersedes?: string;
13
+ }
14
+ /** What becomes of the proposal: confirmed or rejected by the decider, or left proposed. */
15
+ export type PaymentOutcome = {
16
+ confirm: boolean;
17
+ by: Decider;
18
+ } | "proposed";
19
+ /** The finding's two-decimal amount in the currency's own scale: "1200.00" JPY is "1200", "1200.50" JPY is refused. */
20
+ export declare const entryAmount: (payment: Payment) => string;
21
+ /**
22
+ * Money from a payment finding goes to `store.finance`, never into a fact: a paid one is a payment, allocated to the
23
+ * invoice it supersedes as far as that is open; an expected one is an invoice of one line; a cancelled one voids its
24
+ * invoice, which only the owner does. The author proposes; `outcome` is the owner's decision on it.
25
+ */
26
+ export declare const paymentEntriesOver: (context: StoreContext) => (entry: PaymentEntry, by: Author, outcome: PaymentOutcome) => string;
@@ -0,0 +1,91 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { formatDecimal, minorUnits, parseDecimal, scaleOf } from "../../domain/money.js";
3
+ import { paymentTitle } from "../../meetings/index.js";
4
+ import { financeOperationsOver } from "./finance.js";
5
+ const refuse = (message, reason) => {
6
+ throw new CliError("validation_error", message, { reason });
7
+ };
8
+ /** The finding's two-decimal amount in the currency's own scale: "1200.00" JPY is "1200", "1200.50" JPY is refused. */
9
+ export const entryAmount = (payment) => {
10
+ const scale = scaleOf(payment.currency);
11
+ const cents = parseDecimal(payment.amount, 2);
12
+ if (cents === undefined)
13
+ return refuse(`${payment.amount} is not an amount`, "money.amount");
14
+ const shift = 10n ** BigInt(Math.abs(scale - 2));
15
+ if (scale >= 2)
16
+ return formatDecimal(cents * shift, scale);
17
+ if (cents % shift !== 0n)
18
+ return refuse(`${payment.currency} has ${scale} decimals; ${payment.amount} does not fit`, "money.amount");
19
+ return formatDecimal(cents / shift, scale);
20
+ };
21
+ const party = (payment) => /^(person|organization):/.test(payment.counterparty ?? "") ? payment.counterparty : null;
22
+ /**
23
+ * Money from a payment finding goes to `store.finance`, never into a fact: a paid one is a payment, allocated to the
24
+ * invoice it supersedes as far as that is open; an expected one is an invoice of one line; a cancelled one voids its
25
+ * invoice, which only the owner does. The author proposes; `outcome` is the owner's decision on it.
26
+ */
27
+ export const paymentEntriesOver = (context) => {
28
+ const finance = financeOperationsOver(context);
29
+ const today = () => new Date(context.now()).toISOString().slice(0, 10);
30
+ const decided = (ref, outcome) => {
31
+ if (outcome === "proposed")
32
+ return ref;
33
+ return (outcome.confirm ? finance.confirm(ref, outcome.by) : finance.reject(ref, outcome.by)).ref;
34
+ };
35
+ /** As much of the payment as the invoice still has open; nothing when it is already covered. */
36
+ const settled = (reference, amount, currency) => {
37
+ const invoice = finance.invoice(reference);
38
+ const units = Math.min(minorUnits(amount, currency), minorUnits(invoice.open, invoice.currency));
39
+ return units > 0 ? [{ invoice: invoice.ref, amount: formatDecimal(units, scaleOf(currency)) }] : [];
40
+ };
41
+ return (entry, by, outcome) => {
42
+ const { payment } = entry;
43
+ const amount = entryAmount(payment);
44
+ const project = entry.project ?? (payment.counterparty?.startsWith("project:") ? payment.counterparty : undefined);
45
+ const written = {
46
+ by,
47
+ ...(project === undefined ? {} : { project }),
48
+ ...(by === "owner" ? {} : { source: "agent", reason: entry.statement ?? paymentTitle(entry) }),
49
+ evidence: entry.evidence,
50
+ };
51
+ const counterparty = party(payment);
52
+ if (payment.state === "cancelled") {
53
+ if (entry.supersedes === undefined)
54
+ return refuse("a cancelled payment names the invoice it cancels", "finance.ref");
55
+ if (outcome === "proposed" || !outcome.confirm)
56
+ return entry.supersedes;
57
+ if (outcome.by !== "owner")
58
+ throw new CliError("permission_error", "only the owner voids an invoice", { reason: "finance.owner-only" });
59
+ return finance.voidInvoice(entry.supersedes, "owner").ref;
60
+ }
61
+ if (payment.state === "expected") {
62
+ if (counterparty === null)
63
+ return refuse("an expected payment is an invoice, which names a person: or organization:", "finance.party");
64
+ const invoice = finance.addInvoice({
65
+ ...written,
66
+ direction: payment.direction === "to_us" ? "receivable" : "payable",
67
+ counterparty,
68
+ currency: payment.currency,
69
+ ...(payment.due_at === null ? {} : { dueDate: payment.due_at }),
70
+ lines: [{ description: (payment.item ?? entry.statement ?? paymentTitle(entry)).slice(0, 500), amount }],
71
+ });
72
+ return decided(invoice.ref, outcome);
73
+ }
74
+ const settles = entry.supersedes === undefined ? [] : settled(entry.supersedes, amount, payment.currency);
75
+ const made = finance.addPayment({
76
+ ...written,
77
+ direction: payment.direction === "to_us" ? "in" : "out",
78
+ ...(counterparty === null
79
+ ? {}
80
+ : payment.direction === "to_us"
81
+ ? { payer: counterparty }
82
+ : { payee: counterparty }),
83
+ amount,
84
+ currency: payment.currency,
85
+ date: payment.paid_at ?? payment.due_at ?? today(),
86
+ ...(payment.item === null ? {} : { reference: payment.item }),
87
+ allocations: settles,
88
+ });
89
+ return decided(made.ref, outcome);
90
+ };
91
+ };
@@ -0,0 +1,12 @@
1
+ import type { StoreContext } from "./open.js";
2
+ /** Set by store version 46 and deleted once the move ran. */
3
+ export declare const PAYMENT_FACTS_TO_MOVE = "payment_facts_to_move";
4
+ /**
5
+ * Moves the payment facts store versions 44–45 took into `store.finance`, as a payment finding is recorded now: a
6
+ * paid one a payment, an expected one an invoice, a cancelled one voids the invoice it supersedes. An accepted fact
7
+ * becomes a confirmed row and a proposed one stays proposed; a rejected one, or one a correction superseded, is
8
+ * rejected. An expected fact that a payment or a cancellation superseded held until then, so its invoice is
9
+ * confirmed and the payment settles it. What cannot be a row (no party for an invoice, a pending cancellation, an
10
+ * owner's fact that no longer holds) stays a `statement` fact with the fields under `metadata.retired_payment`.
11
+ */
12
+ export declare const movePaymentFacts: (context: StoreContext) => number;
@@ -0,0 +1,92 @@
1
+ import { validatePayment } from "../../meetings/index.js";
2
+ import { dropLinksOf, evidenceOf, keepHighestId } from "./memories.js";
3
+ import { paymentEntriesOver } from "./payment-entries.js";
4
+ import { write } from "./transaction.js";
5
+ /** Set by store version 46 and deleted once the move ran. */
6
+ export const PAYMENT_FACTS_TO_MOVE = "payment_facts_to_move";
7
+ const paymentOf = (metadata) => {
8
+ try {
9
+ return validatePayment(JSON.parse(String(metadata)).payment);
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ };
15
+ /**
16
+ * Moves the payment facts store versions 44–45 took into `store.finance`, as a payment finding is recorded now: a
17
+ * paid one a payment, an expected one an invoice, a cancelled one voids the invoice it supersedes. An accepted fact
18
+ * becomes a confirmed row and a proposed one stays proposed; a rejected one, or one a correction superseded, is
19
+ * rejected. An expected fact that a payment or a cancellation superseded held until then, so its invoice is
20
+ * confirmed and the payment settles it. What cannot be a row (no party for an invoice, a pending cancellation, an
21
+ * owner's fact that no longer holds) stays a `statement` fact with the fields under `metadata.retired_payment`.
22
+ */
23
+ export const movePaymentFacts = (context) => {
24
+ const { database, now } = context;
25
+ if (!database.prepare("SELECT 1 AS found FROM store_settings WHERE key = ?").get(PAYMENT_FACTS_TO_MOVE))
26
+ return 0;
27
+ const record = paymentEntriesOver(context);
28
+ return write(database, () => {
29
+ const rows = database
30
+ .prepare("SELECT f.*, b.name AS bot_name, b.kind AS bot_kind FROM facts f " +
31
+ "LEFT JOIN bots b ON f.author_type = 'bot' AND b.id = f.author_id WHERE f.type = 'payment' ORDER BY f.id")
32
+ .all();
33
+ const nextState = (id) => paymentOf(database
34
+ .prepare("SELECT metadata FROM facts WHERE supersedes_id = ? AND type = 'payment' ORDER BY id DESC LIMIT 1")
35
+ .get(id)?.metadata)?.state;
36
+ const outcomeOf = (row, payment) => {
37
+ if (row.status === "proposed")
38
+ return "proposed";
39
+ if (row.status === "accepted")
40
+ return { confirm: true, by: "owner" };
41
+ const settled = row.status === "superseded" && payment.state === "expected" && nextState(Number(row.id));
42
+ return { confirm: settled === "paid" || settled === "cancelled", by: "owner" };
43
+ };
44
+ const moved = new Map();
45
+ for (const row of rows) {
46
+ const id = Number(row.id);
47
+ const payment = paymentOf(row.metadata);
48
+ if (!payment)
49
+ continue;
50
+ const by = row.author_type === "bot" && row.bot_name != null
51
+ ? {
52
+ bot: String(row.bot_name),
53
+ kind: row.bot_kind === "rule" ? "rule" : "agent",
54
+ ...(row.model == null ? {} : { model: String(row.model) }),
55
+ }
56
+ : "owner";
57
+ const outcome = outcomeOf(row, payment);
58
+ const holds = outcome !== "proposed" && outcome.confirm;
59
+ if ((by === "owner" && !holds) || (payment.state === "cancelled" && !holds))
60
+ continue;
61
+ const target = row.supersedes_id == null ? undefined : moved.get(Number(row.supersedes_id));
62
+ const entry = {
63
+ payment,
64
+ statement: String(row.statement),
65
+ ...(row.project_id == null ? {} : { project: `project:${row.project_id}` }),
66
+ evidence: evidenceOf(context, { type: "fact", id }),
67
+ ...(payment.state !== "expected" && target?.startsWith("invoice:") ? { supersedes: target } : {}),
68
+ };
69
+ try {
70
+ moved.set(id, write(database, () => record(entry, by, by === "owner" ? "proposed" : outcome)));
71
+ }
72
+ catch {
73
+ // Kept below as a statement: an unknown currency or a party that is gone is no reason to lose the fact.
74
+ }
75
+ }
76
+ if (moved.size)
77
+ keepHighestId(database, "facts", now());
78
+ for (const id of moved.keys()) {
79
+ database.prepare("UPDATE facts SET supersedes_id = NULL WHERE supersedes_id = ?").run(id);
80
+ dropLinksOf(database, { type: "fact", id });
81
+ database.prepare("DELETE FROM links WHERE from_type = 'fact' AND from_id = ?").run(id);
82
+ database.prepare("DELETE FROM taggings WHERE taggable_type = 'fact' AND taggable_id = ?").run(id);
83
+ database.prepare("DELETE FROM facts WHERE id = ?").run(id);
84
+ }
85
+ database
86
+ .prepare("UPDATE facts SET type = 'statement', updated_at = ?, metadata = json_remove(json_set(coalesce(metadata, '{}'), " +
87
+ "'$.retired_payment', json(json_extract(metadata, '$.payment'))), '$.payment') WHERE type = 'payment'")
88
+ .run(now());
89
+ database.prepare("DELETE FROM store_settings WHERE key = ?").run(PAYMENT_FACTS_TO_MOVE);
90
+ return moved.size;
91
+ });
92
+ };
@@ -5,7 +5,7 @@ export declare const projectAnnotationSnapshot: (database: CacheDatabase, projec
5
5
  format: "wirecat.project-annotations";
6
6
  facts: {
7
7
  ref: string;
8
- type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
8
+ type: "decision" | "fact" | "promise" | "question" | "statement";
9
9
  statement: string;
10
10
  status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
11
11
  subject: string | null;
@@ -115,7 +115,6 @@ export interface MessageStore {
115
115
  forAccount(key: AccountKey): AccountStore;
116
116
  close(): Promise<void>;
117
117
  }
118
- export type { PaymentFilter } from "./sqlite/facts.js";
119
118
  export type { ChatHistoryShape, StoredHistorySkip } from "./sqlite/history-skips.js";
120
119
  export { BACKFILL_ON_OPEN, type StoreOptions } from "./sqlite/open-store.js";
121
120
  export { fileFacts, readStoreTables, type TableKind } from "./sqlite/store-tables.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.557.0",
3
+ "version": "0.558.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
@@ -1,8 +0,0 @@
1
- import type { PaymentFilter } from "../store/store.js";
2
- /**
3
- * The terms a facts list adds for payments. `state` is the payment's, not `status`, which stays the review status
4
- * (proposed, accepted).
5
- */
6
- export declare const PAYMENT_QUERY_FIELDS: import("@wirecat/cli-base/query").FieldSpec[];
7
- /** The store's payment filter from `queryTerms`' answer; `undefined` when no payment term was given. */
8
- export declare const paymentFilterOf: (terms: Map<string, string>) => PaymentFilter | undefined;
@@ -1,27 +0,0 @@
1
- import { CliError } from "@wirecat/cli-base";
2
- import { PAYMENT_DIRECTIONS, PAYMENT_STATES } from "../meetings/index.js";
3
- import { termField } from "./list-query.js";
4
- /**
5
- * The terms a facts list adds for payments. `state` is the payment's, not `status`, which stays the review status
6
- * (proposed, accepted).
7
- */
8
- export const PAYMENT_QUERY_FIELDS = [
9
- termField("open", "bool", "open:true"),
10
- termField("direction", "enum", "direction:to_us", { values: PAYMENT_DIRECTIONS }),
11
- termField("state", "enum", "state:paid", { values: PAYMENT_STATES }),
12
- ];
13
- /** The store's payment filter from `queryTerms`' answer; `undefined` when no payment term was given. */
14
- export const paymentFilterOf = (terms) => {
15
- const open = terms.get("open");
16
- const direction = terms.get("direction");
17
- const state = terms.get("state");
18
- if (open === undefined && direction === undefined && state === undefined)
19
- return undefined;
20
- if (open !== undefined && open !== "true" && open !== "false")
21
- throw new CliError("validation_error", "open takes true or false");
22
- return {
23
- ...(open === undefined ? {} : { open: open === "true" }),
24
- ...(direction === undefined ? {} : { direction }),
25
- ...(state === undefined ? {} : { state }),
26
- };
27
- };