@wirecat/cli-core 0.557.0 → 0.559.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.
- package/CHANGELOG.md +33 -0
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -1
- package/dist/domain/records.d.ts +161 -0
- package/dist/domain/records.js +234 -0
- package/dist/entities/fact.d.ts +1 -12
- package/dist/entities/fact.js +0 -13
- package/dist/entities/index.d.ts +2 -12
- package/dist/entities/meeting-findings.d.ts +1 -1
- package/dist/errors/reasons.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/meetings/findings-rules.d.ts +7 -4
- package/dist/meetings/findings-rules.js +11 -4
- package/dist/meetings/payment.d.ts +10 -6
- package/dist/meetings/payment.js +21 -6
- package/dist/runs/secrets.d.ts +2 -0
- package/dist/runs/secrets.js +2 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/ingest.d.ts +45 -0
- package/dist/services/ingest.js +80 -0
- package/dist/services/project-status/index.d.ts +1 -1
- package/dist/services/project-status/index.js +7 -3
- package/dist/services/project-status/signals/payments.d.ts +3 -3
- package/dist/services/project-status/signals/payments.js +43 -17
- package/dist/store/evidence-findings.js +16 -8
- package/dist/store/index.d.ts +2 -1
- package/dist/store/project-transfer/request.d.ts +1 -1
- package/dist/store/project-transfer/schema.d.ts +2 -2
- package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
- package/dist/store/sqlite/accounts.js +4 -0
- package/dist/store/sqlite/facts.d.ts +0 -13
- package/dist/store/sqlite/facts.js +5 -58
- package/dist/store/sqlite/finance.d.ts +4 -0
- package/dist/store/sqlite/finance.js +19 -4
- package/dist/store/sqlite/findings-core.js +5 -14
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/migrations.generated.js +18 -0
- package/dist/store/sqlite/open-store.js +2 -0
- package/dist/store/sqlite/payment-entries.d.ts +26 -0
- package/dist/store/sqlite/payment-entries.js +91 -0
- package/dist/store/sqlite/payment-facts.d.ts +12 -0
- package/dist/store/sqlite/payment-facts.js +92 -0
- package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
- package/dist/store/sqlite/records.d.ts +81 -0
- package/dist/store/sqlite/records.js +372 -0
- package/dist/store/sqlite/schema.d.ts +353 -0
- package/dist/store/sqlite/schema.js +37 -0
- package/dist/store/store.d.ts +3 -1
- package/dist/store/store.js +5 -0
- package/package.json +4 -2
- package/schemas/records-envelope.v1.schema.json +108 -0
- package/dist/cli/payment-query.d.ts +0 -8
- package/dist/cli/payment-query.js +0 -27
|
@@ -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",
|
|
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}
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,8 @@ 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 },
|
|
65
|
+
{ name: "20261102192959_version-47-records", version: 47, minCompatible: 34 },
|
|
64
66
|
];
|
|
65
67
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
66
68
|
const migrations = [];
|
|
@@ -924,5 +924,23 @@ 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
|
+
]
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
"name": "20261102192959_version-47-records",
|
|
938
|
+
"statements": [
|
|
939
|
+
"CREATE TABLE `record_relations` (\n\t`id` integer PRIMARY KEY,\n\t`record_id` integer NOT NULL,\n\t`role` text NOT NULL,\n\t`target_type` text NOT NULL,\n\t`target_external_id` text NOT NULL,\n\t`target_id` integer,\n\t`created_at` integer NOT NULL,\n\tCONSTRAINT `fk_record_relations_record_id_records_id_fk` FOREIGN KEY (`record_id`) REFERENCES `records`(`id`),\n\tCONSTRAINT `fk_record_relations_target_id_records_id_fk` FOREIGN KEY (`target_id`) REFERENCES `records`(`id`),\n\tCONSTRAINT `record_relations_record_id_role_target_type_target_external_id_unique` UNIQUE(`record_id`,`role`,`target_type`,`target_external_id`)\n);",
|
|
940
|
+
"CREATE TABLE `records` (\n\t`id` integer PRIMARY KEY,\n\t`account_id` integer NOT NULL,\n\t`type` text NOT NULL,\n\t`external_id` text NOT NULL,\n\t`fields` text NOT NULL,\n\t`revision` integer,\n\t`fingerprint` text NOT NULL,\n\t`entity_type` text,\n\t`entity_id` integer,\n\t`projected_fingerprint` text,\n\t`batch_id` text NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`deleted_at` integer,\n\tCONSTRAINT `fk_records_account_id_accounts_id_fk` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`),\n\tCONSTRAINT `records_account_id_type_external_id_unique` UNIQUE(`account_id`,`type`,`external_id`)\n);",
|
|
941
|
+
"CREATE INDEX `record_relations_by_target_type_target_external_id` ON `record_relations` (`target_type`,`target_external_id`);",
|
|
942
|
+
"CREATE INDEX `record_relations_by_target_id` ON `record_relations` (`target_id`);",
|
|
943
|
+
"CREATE INDEX `records_by_entity_type_entity_id` ON `records` (`entity_type`,`entity_id`);"
|
|
944
|
+
]
|
|
927
945
|
}
|
|
928
946
|
];
|
|
@@ -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" | "
|
|
8
|
+
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
9
9
|
statement: string;
|
|
10
10
|
status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
|
|
11
11
|
subject: string | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type EnvelopeRecord, type EnvelopeSource, type IngestError } from "../../domain/records.js";
|
|
2
|
+
import type { Author } from "./memories.js";
|
|
3
|
+
import type { StoreContext } from "./open.js";
|
|
4
|
+
export interface RecordKey {
|
|
5
|
+
provider: string;
|
|
6
|
+
account: string;
|
|
7
|
+
type: string;
|
|
8
|
+
externalId: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SourceRecord extends RecordKey {
|
|
11
|
+
fields: Record<string, unknown>;
|
|
12
|
+
revision: number | null;
|
|
13
|
+
fingerprint: string;
|
|
14
|
+
relations: {
|
|
15
|
+
role: string;
|
|
16
|
+
type: string;
|
|
17
|
+
externalId: string;
|
|
18
|
+
resolved: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
/** The entity made from it or mapped to it by the owner; `null` while it waits or for a type without a table. */
|
|
21
|
+
entity: string | null;
|
|
22
|
+
/** The source changed or deleted it after its entity was made: the owner should look at the entity. */
|
|
23
|
+
flagged: boolean;
|
|
24
|
+
batchId: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
deletedAt: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface RecordOutcome {
|
|
30
|
+
/** The record's place in the envelope's `records`. */
|
|
31
|
+
index: number;
|
|
32
|
+
key: string;
|
|
33
|
+
status: "created" | "updated" | "unchanged" | "deleted" | "refused";
|
|
34
|
+
error?: IngestError;
|
|
35
|
+
}
|
|
36
|
+
export interface RecordsWritten {
|
|
37
|
+
outcomes: RecordOutcome[];
|
|
38
|
+
/** `invoice:` and `payment:` refs this import proposed, also for records of earlier imports it completed. */
|
|
39
|
+
proposed: string[];
|
|
40
|
+
/** Kept, with no entity yet: the roles whose target is not imported or has no entity. */
|
|
41
|
+
waiting: {
|
|
42
|
+
index: number;
|
|
43
|
+
key: string;
|
|
44
|
+
missing: string[];
|
|
45
|
+
}[];
|
|
46
|
+
/** Changed or deleted at the source after its entity was made. */
|
|
47
|
+
flagged: {
|
|
48
|
+
index: number;
|
|
49
|
+
key: string;
|
|
50
|
+
entity: string;
|
|
51
|
+
}[];
|
|
52
|
+
/** A record of an earlier import this one completed, whose entity was still refused. */
|
|
53
|
+
laterErrors: IngestError[];
|
|
54
|
+
}
|
|
55
|
+
export interface RecordStore {
|
|
56
|
+
/** Writes validated records of one source, per chunk in one transaction; a dry run rolls everything back. */
|
|
57
|
+
write(source: EnvelopeSource, records: {
|
|
58
|
+
index: number;
|
|
59
|
+
record: EnvelopeRecord;
|
|
60
|
+
}[], options?: {
|
|
61
|
+
dryRun?: boolean;
|
|
62
|
+
chunk?: number;
|
|
63
|
+
}): Promise<RecordsWritten>;
|
|
64
|
+
get(key: RecordKey): Promise<SourceRecord | undefined>;
|
|
65
|
+
list(filter: {
|
|
66
|
+
provider: string;
|
|
67
|
+
account: string;
|
|
68
|
+
type?: string;
|
|
69
|
+
/** Only records whose entity waits for a reference. */
|
|
70
|
+
waiting?: boolean;
|
|
71
|
+
flagged?: boolean;
|
|
72
|
+
/** 1–500, 100 by default. */
|
|
73
|
+
limit?: number;
|
|
74
|
+
}): Promise<SourceRecord[]>;
|
|
75
|
+
/**
|
|
76
|
+
* The owner says which entity a record is: `person:4` for a payer a sheet names. Never done by name. Records that
|
|
77
|
+
* waited for it get their entity, as proposals.
|
|
78
|
+
*/
|
|
79
|
+
map(key: RecordKey, entity: string, by: Author): Promise<SourceRecord>;
|
|
80
|
+
}
|
|
81
|
+
export declare const recordStoreOver: (context: StoreContext) => RecordStore;
|