@ekwo-ai/core 0.4.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 (66) hide show
  1. package/README.md +76 -0
  2. package/dist/books/backend.d.ts +118 -0
  3. package/dist/books/backend.d.ts.map +1 -0
  4. package/dist/books/backend.js +69 -0
  5. package/dist/books/backend.js.map +1 -0
  6. package/dist/books/columns.d.ts +68 -0
  7. package/dist/books/columns.d.ts.map +1 -0
  8. package/dist/books/columns.js +466 -0
  9. package/dist/books/columns.js.map +1 -0
  10. package/dist/books/contacts.d.ts +43 -0
  11. package/dist/books/contacts.d.ts.map +1 -0
  12. package/dist/books/contacts.js +67 -0
  13. package/dist/books/contacts.js.map +1 -0
  14. package/dist/books/documents.d.ts +87 -0
  15. package/dist/books/documents.d.ts.map +1 -0
  16. package/dist/books/documents.js +413 -0
  17. package/dist/books/documents.js.map +1 -0
  18. package/dist/books/format.d.ts +21 -0
  19. package/dist/books/format.d.ts.map +1 -0
  20. package/dist/books/format.js +49 -0
  21. package/dist/books/format.js.map +1 -0
  22. package/dist/books/index.d.ts +18 -0
  23. package/dist/books/index.d.ts.map +1 -0
  24. package/dist/books/index.js +18 -0
  25. package/dist/books/index.js.map +1 -0
  26. package/dist/books/payments.d.ts +57 -0
  27. package/dist/books/payments.d.ts.map +1 -0
  28. package/dist/books/payments.js +255 -0
  29. package/dist/books/payments.js.map +1 -0
  30. package/dist/books/rounding.d.ts +25 -0
  31. package/dist/books/rounding.d.ts.map +1 -0
  32. package/dist/books/rounding.js +35 -0
  33. package/dist/books/rounding.js.map +1 -0
  34. package/dist/books/shared.d.ts +33 -0
  35. package/dist/books/shared.d.ts.map +1 -0
  36. package/dist/books/shared.js +98 -0
  37. package/dist/books/shared.js.map +1 -0
  38. package/dist/client.d.ts +73 -0
  39. package/dist/client.d.ts.map +1 -0
  40. package/dist/client.js +138 -0
  41. package/dist/client.js.map +1 -0
  42. package/dist/fec.d.ts +14 -0
  43. package/dist/fec.d.ts.map +1 -0
  44. package/dist/fec.js +13 -0
  45. package/dist/fec.js.map +1 -0
  46. package/dist/identity.d.ts +46 -0
  47. package/dist/identity.d.ts.map +1 -0
  48. package/dist/identity.js +62 -0
  49. package/dist/identity.js.map +1 -0
  50. package/dist/index.d.ts +12 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +12 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/refusal.d.ts +27 -0
  55. package/dist/refusal.d.ts.map +1 -0
  56. package/dist/refusal.js +38 -0
  57. package/dist/refusal.js.map +1 -0
  58. package/dist/schema.d.ts +23 -0
  59. package/dist/schema.d.ts.map +1 -0
  60. package/dist/schema.js +42 -0
  61. package/dist/schema.js.map +1 -0
  62. package/dist/types.d.ts +466 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +170 -0
  65. package/dist/types.js.map +1 -0
  66. package/package.json +34 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Payments and matching.
3
+ *
4
+ * `post_payment`, `reconcile` and `unreconcile` carry the rules and this file
5
+ * calls them. The one thing decided here is which open items a payment is
6
+ * offered to, oldest first — never how much is owed, which the ledger says.
7
+ */
8
+ import { BooksError } from './backend.js';
9
+ import * as columns from './columns.js';
10
+ import { amountIn, moneyFields } from './format.js';
11
+ import { companyCurrency, createdBefore, idsByCode, only } from './shared.js';
12
+ /** What is still open on one document, as `open_items()` reports it. */
13
+ async function openItemsOf(backend, company, documentId) {
14
+ const items = await backend.rpc('open_items', { p_company_id: company });
15
+ const open = items.filter((item) => item.document_id === documentId);
16
+ if (open.length === 0) {
17
+ throw new BooksError(`nothing_open: document ${documentId} has no open item. Either it is not posted yet, or it is already settled, or it is not visible to you.`);
18
+ }
19
+ return open;
20
+ }
21
+ export async function recordPayment(backend, args) {
22
+ const before = await createdBefore(backend, 'payments', args.company_id, args.client_ref, ['id', 'entry_id']);
23
+ // A payment of a document: who and which way are read off its open item,
24
+ // which the ledger holds, rather than deduced from the kind of document.
25
+ // Recorded before under this reference, the document may well have nothing
26
+ // open any more — that is what the first call was for — so nothing open is
27
+ // then not a refusal.
28
+ let paid;
29
+ if (args.document_id !== undefined) {
30
+ try {
31
+ paid = await openItemsOf(backend, args.company_id, args.document_id);
32
+ }
33
+ catch (error) {
34
+ if (before === undefined)
35
+ throw error;
36
+ }
37
+ }
38
+ const contactId = args.contact_id ?? paid?.[0]?.contact_id ?? undefined;
39
+ const direction = args.direction ?? (paid === undefined ? undefined : paid[0]?.side === 'debit' ? 'inbound' : 'outbound');
40
+ if (direction === undefined && before === undefined) {
41
+ throw new BooksError('missing_direction: a payment is inbound or outbound — say which, or give document_id and the open item says it.');
42
+ }
43
+ let journalId = args.journal_id;
44
+ if (journalId === undefined && args.journal_code !== undefined) {
45
+ journalId = (await idsByCode(backend, 'journals', args.company_id, [args.journal_code])).get(args.journal_code);
46
+ }
47
+ if (journalId === undefined && args.bank_account_id !== undefined) {
48
+ // A bank account knows the journal it moves through; asking the caller
49
+ // to repeat it was the first thing the real test tripped over.
50
+ const account = await backend.select({
51
+ table: 'bank_accounts',
52
+ columns: ['journal_id'],
53
+ where: [
54
+ { column: 'id', op: 'eq', value: args.bank_account_id },
55
+ { column: 'company_id', op: 'eq', value: args.company_id },
56
+ ],
57
+ });
58
+ journalId = account[0]?.journal_id ?? undefined;
59
+ }
60
+ if (journalId === undefined && before === undefined) {
61
+ throw new BooksError('missing_journal: a payment needs the bank or cash book it goes through — give bank_account_id (list_bank_accounts), or journal_id / journal_code (get_company lists the journals).');
62
+ }
63
+ // Recorded before under this reference: nothing is inserted, and what a
64
+ // dropped connection left undone — the booking, the matching — is finished.
65
+ const payment = before ?? only(await backend.insert('payments', [
66
+ {
67
+ company_id: args.company_id,
68
+ direction,
69
+ payment_date: args.payment_date,
70
+ amount: amountIn(args.amount),
71
+ // Never a literal, for the reason `create_document` gives: a company
72
+ // that keeps its books in another currency would get a euro payment
73
+ // out of a euro written here.
74
+ currency_code: args.currency_code ?? (await companyCurrency(backend, args.company_id)),
75
+ exchange_rate: args.exchange_rate === undefined ? 1 : String(args.exchange_rate),
76
+ contact_id: contactId ?? null,
77
+ journal_id: journalId,
78
+ bank_account_id: args.bank_account_id ?? null,
79
+ reference: args.reference ?? null,
80
+ memo: args.memo ?? null,
81
+ client_ref: args.client_ref ?? null,
82
+ },
83
+ ], ['id']), 'the payment could not be created');
84
+ const booked = typeof payment['entry_id'] === 'string' ? payment['entry_id'] : undefined;
85
+ const entry = only(booked === undefined
86
+ ? moneyFields(await backend.rpc('post_payment', { p_payment_id: payment['id'] }), [
87
+ 'total_debit',
88
+ 'total_credit',
89
+ ])
90
+ : await backend.select({
91
+ table: 'entries',
92
+ columns: columns.ENTRY,
93
+ where: [{ column: 'id', op: 'eq', value: booked }],
94
+ }), 'the payment produced no entry');
95
+ const lines = await backend.select({
96
+ table: 'entry_lines',
97
+ columns: columns.ENTRY_LINE,
98
+ where: [{ column: 'entry_id', op: 'eq', value: entry['id'] }],
99
+ order: [{ column: 'sequence' }],
100
+ });
101
+ const matched = args.match_open_items === false || contactId === undefined || (args.document_id !== undefined && paid === undefined)
102
+ ? []
103
+ : await matchOpenItems(backend, args.company_id, contactId, lines, paid === undefined ? undefined : new Set(paid.map((item) => item.line_id)));
104
+ const settled = await backend.select({
105
+ table: 'payments',
106
+ columns: columns.PAYMENT,
107
+ where: [{ column: 'id', op: 'eq', value: payment['id'] }],
108
+ });
109
+ return {
110
+ payment: settled[0] ?? null,
111
+ entry,
112
+ entry_lines: lines,
113
+ matched,
114
+ ...(before === undefined ? {} : { replayed: true }),
115
+ note: contactId === undefined
116
+ ? 'No contact was given, so the payment landed on the company default third-party account and nothing was matched.'
117
+ : 'Matching is what makes a document paid: documents.amount_paid is derived from it.',
118
+ };
119
+ }
120
+ /**
121
+ * Matches the payment against the open items of a contact, oldest first.
122
+ *
123
+ * Only lines on the same third-party account, on the other side, still
124
+ * carrying a residual. Each pairing is a call to `reconcile`, which is what
125
+ * draws the letter and keeps the residual honest; this function decides
126
+ * nothing about amounts beyond "the smaller of what is left on either side".
127
+ */
128
+ async function matchOpenItems(backend, company, contact, paymentLines, among) {
129
+ const accounts = await backend.select({
130
+ table: 'accounts',
131
+ columns: ['id', 'reconcilable'],
132
+ where: [
133
+ {
134
+ column: 'id',
135
+ op: 'in',
136
+ value: paymentLines
137
+ .map((line) => line['account_id'])
138
+ .filter((id) => typeof id === 'string'),
139
+ },
140
+ ],
141
+ });
142
+ const reconcilable = new Set(accounts.filter((account) => account.reconcilable).map((a) => a.id));
143
+ const paymentLine = paymentLines.find((line) => typeof line['account_id'] === 'string' && reconcilable.has(line['account_id']));
144
+ if (paymentLine === undefined)
145
+ return [];
146
+ const accountId = paymentLine['account_id'];
147
+ const paymentIsDebit = Number(paymentLine['debit'] ?? 0) > 0;
148
+ const candidates = await backend.select({
149
+ table: 'entry_lines',
150
+ columns: [...columns.ENTRY_LINE, 'company_id'],
151
+ where: [
152
+ { column: 'company_id', op: 'eq', value: company },
153
+ { column: 'contact_id', op: 'eq', value: contact },
154
+ { column: 'account_id', op: 'eq', value: accountId },
155
+ ],
156
+ limit: 500,
157
+ });
158
+ const entryIds = [...new Set(candidates.map((line) => line['entry_id']))];
159
+ const entries = await backend.select({
160
+ table: 'entries',
161
+ columns: ['id', 'state', 'entry_date::text'],
162
+ where: [{ column: 'id', op: 'in', value: entryIds }],
163
+ });
164
+ const posted = new Map(entries.filter((entry) => entry.state === 'posted').map((e) => [e.id, e]));
165
+ // Which scale the allocation is worked out on. `reconcile` matches two
166
+ // lines in the same foreign currency in *that* currency, because that is
167
+ // where they are equal, and reads `p_amount` the same way. So when the
168
+ // payment is foreign the amounts below are its currency's, and a candidate
169
+ // in another currency is left alone rather than matched at the wrong scale.
170
+ const home = await companyCurrency(backend, company);
171
+ const currency = paymentLine['currency_code'];
172
+ const foreign = currency !== null && currency !== home && paymentLine['amount_currency'] !== null;
173
+ /** What is still open on a line, on the scale the matching uses. */
174
+ const openOf = (line) => {
175
+ const ledger = Math.abs(Number(line['balance'] ?? 0));
176
+ const left = ledger - Number(line['matched_amount'] ?? 0);
177
+ if (!foreign)
178
+ return left;
179
+ const inCurrency = Math.abs(Number(line['amount_currency'] ?? 0));
180
+ return ledger === 0 ? 0 : Math.round((inCurrency * left * 100) / ledger) / 100;
181
+ };
182
+ const open = candidates
183
+ .filter((line) => line['id'] !== paymentLine['id'])
184
+ .filter((line) => among === undefined || among.has(line['id']))
185
+ .filter((line) => posted.has(line['entry_id']))
186
+ .filter((line) => (Number(line['debit'] ?? 0) > 0) !== paymentIsDebit)
187
+ .filter((line) => !foreign || line['currency_code'] === currency)
188
+ .map((line) => ({
189
+ line,
190
+ residual: openOf(line),
191
+ due: String(line['date_maturity'] ?? posted.get(line['entry_id'])?.entry_date ?? ''),
192
+ }))
193
+ .filter((item) => item.residual > 0.005)
194
+ .sort((a, b) => a.due.localeCompare(b.due));
195
+ let remaining = openOf(paymentLine);
196
+ const done = [];
197
+ for (const item of open) {
198
+ if (remaining <= 0.005)
199
+ break;
200
+ const amount = Math.min(remaining, item.residual).toFixed(2);
201
+ const reconciliation = moneyFields(await backend.rpc('reconcile', {
202
+ p_line_a: paymentLine['id'],
203
+ p_line_b: item.line['id'],
204
+ p_amount: amount,
205
+ }), ['amount']);
206
+ done.push(reconciliation[0] ?? null);
207
+ remaining -= Number(amount);
208
+ }
209
+ return done;
210
+ }
211
+ export async function reconcile(backend, args) {
212
+ const rows = moneyFields(await backend.rpc('reconcile', {
213
+ p_line_a: args.line_a,
214
+ p_line_b: args.line_b,
215
+ p_amount: args.amount === undefined ? null : amountIn(args.amount),
216
+ }), ['amount']);
217
+ return { reconciliation: rows[0] ?? null };
218
+ }
219
+ export async function unreconcile(backend, args) {
220
+ await backend.rpcVoid('unreconcile', { p_reconciliation_id: args.reconciliation_id });
221
+ return {
222
+ unreconciled: args.reconciliation_id,
223
+ note: 'The matching is undone. The entries themselves are untouched: matching changes no account.',
224
+ };
225
+ }
226
+ /**
227
+ * A statement line pays a document: `settle_from_statement()` books the
228
+ * payment the line is and matches it against the open items named. Naming a
229
+ * document names what is open on it; nothing here chooses between candidates.
230
+ */
231
+ export async function settleFromStatement(backend, args) {
232
+ let lineIds = args.line_ids;
233
+ if (lineIds === undefined || lineIds.length === 0) {
234
+ if (args.document_id === undefined) {
235
+ throw new BooksError('no_lines_named: settling needs the document this money pays, or the open items themselves.');
236
+ }
237
+ const transaction = only(await backend.select({
238
+ table: 'bank_transactions',
239
+ columns: ['company_id'],
240
+ where: [{ column: 'id', op: 'eq', value: args.transaction_id }],
241
+ }), `bank transaction ${args.transaction_id}`);
242
+ lineIds = (await openItemsOf(backend, transaction.company_id, args.document_id)).map((item) => item.line_id);
243
+ }
244
+ await backend.rpc('settle_from_statement', {
245
+ p_transaction_id: args.transaction_id,
246
+ p_line_ids: lineIds,
247
+ });
248
+ const settled = await backend.select({
249
+ table: 'bank_transactions',
250
+ columns: columns.BANK_TRANSACTION,
251
+ where: [{ column: 'id', op: 'eq', value: args.transaction_id }],
252
+ });
253
+ return { transaction: settled[0] ?? null, settled_lines: lineIds };
254
+ }
255
+ //# sourceMappingURL=payments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payments.js","sourceRoot":"","sources":["../../src/books/payments.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAuC,MAAM,cAAc,CAAC;AAC/E,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AA0C9E,wEAAwE;AACxE,KAAK,UAAU,WAAW,CAAC,OAAgB,EAAE,OAAe,EAAE,UAAkB;IAC9E,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAW,YAAY,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;IACnF,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,KAAK,UAAU,CAAC,CAAC;IACrE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,UAAU,CAClB,0BAA0B,UAAU,wGAAwG,CAC7I,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAYD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAgB,EAChB,IAAuB;IAEvB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;IAE9G,yEAAyE;IACzE,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,sBAAsB;IACtB,IAAI,IAA4B,CAAC;IACjC,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACnC,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QACvE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,KAAK,CAAC;QACxC,CAAC;IACH,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,IAAI,SAAS,CAAC;IACxE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAC1H,IAAI,SAAS,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACpD,MAAM,IAAI,UAAU,CAAC,iHAAiH,CAAC,CAAC;IAC1I,CAAC;IAED,IAAI,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;IAChC,IAAI,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/D,SAAS,GAAG,CAAC,MAAM,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG,CAC1F,IAAI,CAAC,YAAY,CACR,CAAC;IACd,CAAC;IACD,IAAI,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QAClE,uEAAuE;QACvE,+DAA+D;QAC/D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAgC;YAClE,KAAK,EAAE,eAAe;YACtB,OAAO,EAAE,CAAC,YAAY,CAAC;YACvB,KAAK,EAAE;gBACL,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE;gBACvD,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE;aAC3D;SACF,CAAC,CAAC;QACH,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,IAAI,SAAS,CAAC;IAClD,CAAC;IACD,IAAI,SAAS,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACpD,MAAM,IAAI,UAAU,CAClB,oLAAoL,CACrL,CAAC;IACJ,CAAC;IAED,wEAAwE;IACxE,4EAA4E;IAC5E,MAAM,OAAO,GAAG,MAAM,IAAI,IAAI,CAC5B,MAAM,OAAO,CAAC,MAAM,CAClB,UAAU,EACV;QACE;YACE,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,SAAS;YACT,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;YAC7B,qEAAqE;YACrE,oEAAoE;YACpE,8BAA8B;YAC9B,aAAa,EACX,IAAI,CAAC,aAAa,IAAI,CAAC,MAAM,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;YACzE,aAAa,EAAE,IAAI,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC;YAChF,UAAU,EAAE,SAAS,IAAI,IAAI;YAC7B,UAAU,EAAE,SAAmB;YAC/B,eAAe,EAAE,IAAI,CAAC,eAAe,IAAI,IAAI;YAC7C,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;YACjC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;YACvB,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,IAAI;SACpC;KACF,EACD,CAAC,IAAI,CAAC,CACP,EACD,kCAAkC,CACnC,CAAC;IAEF,MAAM,MAAM,GAAG,OAAO,OAAO,CAAC,UAAU,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,OAAO,CAAC,UAAU,CAAY,CAAC,CAAC,CAAC,SAAS,CAAC;IACrG,MAAM,KAAK,GAAG,IAAI,CAChB,MAAM,KAAK,SAAS;QAClB,CAAC,CAAC,WAAW,CAAC,MAAM,OAAO,CAAC,GAAG,CAAM,cAAc,EAAE,EAAE,YAAY,EAAE,OAAO,CAAC,IAAI,CAAW,EAAE,CAAC,EAAE;YAC7F,aAAa;YACb,cAAc;SACf,CAAC;QACJ,CAAC,CAAC,MAAM,OAAO,CAAC,MAAM,CAAM;YACxB,KAAK,EAAE,SAAS;YAChB,OAAO,EAAE,OAAO,CAAC,KAAK;YACtB,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;SACnD,CAAC,EACN,+BAA+B,CAChC,CAAC;IAEF,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,MAAM,CAAM;QACtC,KAAK,EAAE,aAAa;QACpB,OAAO,EAAE,OAAO,CAAC,UAAU;QAC3B,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAW,EAAE,CAAC;QACvE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;KAChC,CAAC,CAAC;IAEH,MAAM,OAAO,GACX,IAAI,CAAC,gBAAgB,KAAK,KAAK,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,CAAC;QAClH,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,MAAM,cAAc,CAClB,OAAO,EACP,IAAI,CAAC,UAAU,EACf,SAAS,EACT,KAAK,EACL,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAC3E,CAAC;IAER,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAM;QACxC,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAW,EAAE,CAAC;KACpE,CAAC,CAAC;IAEH,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI;QAC3B,KAAK;QACL,WAAW,EAAE,KAAK;QAClB,OAAO;QACP,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QACnD,IAAI,EACF,SAAS,KAAK,SAAS;YACrB,CAAC,CAAC,iHAAiH;YACnH,CAAC,CAAC,mFAAmF;KAC1F,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,cAAc,CAC3B,OAAgB,EAChB,OAAe,EACf,OAAe,EACf,YAAmB,EACnB,KAAmB;IAEnB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAwC;QAC3E,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC;QAC/B,KAAK,EAAE;YACL;gBACE,MAAM,EAAE,IAAI;gBACZ,EAAE,EAAE,IAAI;gBACR,KAAK,EAAE,YAAY;qBAChB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;qBACjC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC;aACxD;SACF;KACF,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClG,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CACnC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,QAAQ,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CACzF,CAAC;IACF,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAEzC,MAAM,SAAS,GAAG,WAAW,CAAC,YAAY,CAAW,CAAC;IACtD,MAAM,cAAc,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IAE7D,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,MAAM,CAAM;QAC3C,KAAK,EAAE,aAAa;QACpB,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC;QAC9C,KAAK,EAAE;YACL,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;YAClD,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;YAClD,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE;SAClC;QACpB,KAAK,EAAE,GAAG;KACX,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAW,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAoD;QACtF,KAAK,EAAE,SAAS;QAChB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,kBAAkB,CAAC;QAC5C,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;KACrD,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAElG,uEAAuE;IACvE,yEAAyE;IACzE,uEAAuE;IACvE,2EAA2E;IAC3E,4EAA4E;IAC5E,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,WAAW,CAAC,eAAe,CAAkB,CAAC;IAC/D,MAAM,OAAO,GACX,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,IAAI,WAAW,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAEpF,oEAAoE;IACpE,MAAM,MAAM,GAAG,CAAC,IAAS,EAAU,EAAE;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1D,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClE,OAAO,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,IAAI,GAAG,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,GAAG,CAAC;IACjF,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,UAAU;SACpB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,WAAW,CAAC,IAAI,CAAC,CAAC;SAClD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAW,CAAC,CAAC;SACxE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAW,CAAC,CAAC;SACxD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,cAAc,CAAC;SACrE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,eAAe,CAAC,KAAK,QAAQ,CAAC;SAChE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACd,IAAI;QACJ,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC;QACtB,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAW,CAAC,EAAE,UAAU,IAAI,EAAE,CAAC;KAC/F,CAAC,CAAC;SACF,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;SACvC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAE9C,IAAI,SAAS,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,IAAI,GAAc,EAAE,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,SAAS,IAAI,KAAK;YAAE,MAAM;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC7D,MAAM,cAAc,GAAG,WAAW,CAChC,MAAM,OAAO,CAAC,GAAG,CAAM,WAAW,EAAE;YAClC,QAAQ,EAAE,WAAW,CAAC,IAAI,CAAC;YAC3B,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YACzB,QAAQ,EAAE,MAAM;SACjB,CAAC,EACF,CAAC,QAAQ,CAAC,CACX,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;QACrC,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,OAAgB,EAChB,IAAmB;IAEnB,MAAM,IAAI,GAAG,WAAW,CACtB,MAAM,OAAO,CAAC,GAAG,CAAM,WAAW,EAAE;QAClC,QAAQ,EAAE,IAAI,CAAC,MAAM;QACrB,QAAQ,EAAE,IAAI,CAAC,MAAM;QACrB,QAAQ,EAAE,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;KACnE,CAAC,EACF,CAAC,QAAQ,CAAC,CACX,CAAC;IACF,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAgB,EAChB,IAAqB;IAErB,MAAM,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,mBAAmB,EAAE,IAAI,CAAC,iBAAiB,EAAE,CAAC,CAAC;IACtF,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,iBAAiB;QACpC,IAAI,EAAE,4FAA4F;KACnG,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAgB,EAChB,IAA6B;IAE7B,IAAI,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC5B,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,IAAI,UAAU,CAAC,4FAA4F,CAAC,CAAC;QACrH,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CACtB,MAAM,OAAO,CAAC,MAAM,CAAyB;YAC3C,KAAK,EAAE,mBAAmB;YAC1B,OAAO,EAAE,CAAC,YAAY,CAAC;YACvB,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC;SAChE,CAAC,EACF,oBAAoB,IAAI,CAAC,cAAc,EAAE,CAC1C,CAAC;QACF,OAAO,GAAG,CAAC,MAAM,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/G,CAAC;IAED,MAAM,OAAO,CAAC,GAAG,CAAM,uBAAuB,EAAE;QAC9C,gBAAgB,EAAE,IAAI,CAAC,cAAc;QACrC,UAAU,EAAE,OAAO;KACpB,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAM;QACxC,KAAK,EAAE,mBAAmB;QAC1B,OAAO,EAAE,OAAO,CAAC,gBAAgB;QACjC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC;KAChE,CAAC,CAAC;IACH,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;AACrE,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The one rounding rule of Ekwo: **half up, on the absolute value, at the
3
+ * currency's decimals**.
4
+ *
5
+ * Three packages used to answer this differently — `Math.round((v + EPSILON) *
6
+ * 100) / 100`, `Math.round(v * 100) / 100` and `Number.prototype.toFixed(2)` —
7
+ * and the three disagreed on the two cases that matter: a negative half, where
8
+ * `Math.round` goes towards positive infinity and turns -0.005 into -0.00, and
9
+ * a value a binary float cannot hold, where 2.675 becomes 2.67 because
10
+ * `2.675 * 100` is really 267.49999999999994. A credit note is a sale with the
11
+ * sign flipped; a rule that treats the two differently puts a cent between an
12
+ * invoice and its credit note, and the cent is found by the person filing the
13
+ * return.
14
+ *
15
+ * Half up on the absolute value is what an invoice, a VAT return and a set of
16
+ * annual accounts are written with, and it is symmetric by construction: the
17
+ * rounding of -x is the rounding of x with the sign put back.
18
+ *
19
+ * The file is deliberately identical in every package that holds a copy. A
20
+ * format brick may not import the core or another brick, so the rule is
21
+ * duplicated rather than shared — and `tests/rounding.test.ts` reads the
22
+ * copies and refuses one that has drifted. See `docs/decisions.md`.
23
+ */
24
+ export declare function roundCurrency(value: number, decimals?: number): number;
25
+ //# sourceMappingURL=rounding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rounding.d.ts","sourceRoot":"","sources":["../../src/books/rounding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAUjE"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The one rounding rule of Ekwo: **half up, on the absolute value, at the
3
+ * currency's decimals**.
4
+ *
5
+ * Three packages used to answer this differently — `Math.round((v + EPSILON) *
6
+ * 100) / 100`, `Math.round(v * 100) / 100` and `Number.prototype.toFixed(2)` —
7
+ * and the three disagreed on the two cases that matter: a negative half, where
8
+ * `Math.round` goes towards positive infinity and turns -0.005 into -0.00, and
9
+ * a value a binary float cannot hold, where 2.675 becomes 2.67 because
10
+ * `2.675 * 100` is really 267.49999999999994. A credit note is a sale with the
11
+ * sign flipped; a rule that treats the two differently puts a cent between an
12
+ * invoice and its credit note, and the cent is found by the person filing the
13
+ * return.
14
+ *
15
+ * Half up on the absolute value is what an invoice, a VAT return and a set of
16
+ * annual accounts are written with, and it is symmetric by construction: the
17
+ * rounding of -x is the rounding of x with the sign put back.
18
+ *
19
+ * The file is deliberately identical in every package that holds a copy. A
20
+ * format brick may not import the core or another brick, so the rule is
21
+ * duplicated rather than shared — and `tests/rounding.test.ts` reads the
22
+ * copies and refuses one that has drifted. See `docs/decisions.md`.
23
+ */
24
+ export function roundCurrency(value, decimals = 2) {
25
+ const factor = 10 ** decimals;
26
+ const scaled = Math.abs(value) * factor;
27
+ // The nudge is proportional to the number, which is what makes it work at
28
+ // 1.005 and at 1000000.005 alike, and far too small to move a value that is
29
+ // not already within a bit of the halfway point.
30
+ const rounded = Math.round(scaled + Number.EPSILON * scaled);
31
+ // `-0` is a value of its own in JavaScript and it prints as "-0.00".
32
+ // Adding zero turns it back into `0`.
33
+ return (value < 0 ? -rounded : rounded) / factor + 0;
34
+ }
35
+ //# sourceMappingURL=rounding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rounding.js","sourceRoot":"","sources":["../../src/books/rounding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,QAAQ,GAAG,CAAC;IACvD,MAAM,MAAM,GAAG,EAAE,IAAI,QAAQ,CAAC;IAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;IACxC,0EAA0E;IAC1E,4EAA4E;IAC5E,iDAAiD;IACjD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC;IAC7D,qEAAqE;IACrE,sCAAsC;IACtC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;AACvD,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What the bookkeeping functions share: reading one row or saying why not,
3
+ * turning codes into ids, and the currency a company keeps its books in.
4
+ */
5
+ import { type Backend } from './backend.js';
6
+ /** The row, or a refusal naming what row level security did not return. */
7
+ export declare function only<T>(rows: T[], what: string): T;
8
+ /** The row, or a refusal that says what was not visible rather than crashing. */
9
+ export declare function onlyVisible<T>(rows: T[], what: string): T;
10
+ /** `{ id: name }` for a set of rows, to put names next to foreign keys. */
11
+ export declare function namesOf(backend: Backend, table: string, ids: (string | null | undefined)[]): Promise<Record<string, string>>;
12
+ /**
13
+ * The row a caller already created under this reference, if there is one.
14
+ *
15
+ * A creation that carries a `client_ref` is looked up before it is made, so a
16
+ * caller that repeats itself after a dropped connection gets the first row
17
+ * back rather than a second one. The lookup is a convenience and not the
18
+ * guarantee: two callers racing each other both find nothing, and the unique
19
+ * index on `(company_id, client_ref)` is what refuses the slower one.
20
+ */
21
+ export declare function createdBefore(backend: Backend, table: 'contacts' | 'documents' | 'payments', company: string, clientRef: string | undefined, columns: string[]): Promise<Record<string, unknown> | undefined>;
22
+ /** Resolves codes to ids in one query, and says which code was unknown. */
23
+ /**
24
+ * The currency an amount is in when the caller names none: the company's own.
25
+ *
26
+ * Never a literal. A company that keeps its books in Canadian dollars would
27
+ * get a euro invoice out of a euro written here, and a wrong answer is worse
28
+ * than a refusal. `companies.currency_code` is never null, so there is always
29
+ * one to find.
30
+ */
31
+ export declare function companyCurrency(backend: Backend, company: string): Promise<string>;
32
+ export declare function idsByCode(backend: Backend, table: 'accounts' | 'taxes' | 'journals', company: string, codes: string[]): Promise<Map<string, string>>;
33
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/books/shared.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAExD,2EAA2E;AAC3E,wBAAgB,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAQlD;AAGD,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAQzD;AAGD,2EAA2E;AAC3E,wBAAsB,OAAO,CAC3B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GACjC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CASjC;AAGD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,UAAU,GAAG,WAAW,GAAG,UAAU,EAC5C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAW9C;AAED,2EAA2E;AAC3E;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAaxF;AAED,wBAAsB,SAAS,CAC7B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,EACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqB9B"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * What the bookkeeping functions share: reading one row or saying why not,
3
+ * turning codes into ids, and the currency a company keeps its books in.
4
+ */
5
+ import { BooksError } from './backend.js';
6
+ /** The row, or a refusal naming what row level security did not return. */
7
+ export function only(rows, what) {
8
+ const row = rows[0];
9
+ if (row === undefined) {
10
+ throw new BooksError(`not_found: ${what}. Either it does not exist, or your role on that company does not allow this.`);
11
+ }
12
+ return row;
13
+ }
14
+ /** The row, or a refusal that says what was not visible rather than crashing. */
15
+ export function onlyVisible(rows, what) {
16
+ const row = rows[0];
17
+ if (row === undefined) {
18
+ throw new BooksError(`not_found: ${what}. Either it does not exist or you are not a member of the company that holds it.`);
19
+ }
20
+ return row;
21
+ }
22
+ /** `{ id: name }` for a set of rows, to put names next to foreign keys. */
23
+ export async function namesOf(backend, table, ids) {
24
+ const wanted = [...new Set(ids.filter((id) => typeof id === 'string'))];
25
+ if (wanted.length === 0)
26
+ return {};
27
+ const rows = await backend.select({
28
+ table,
29
+ columns: ['id', 'name'],
30
+ where: [{ column: 'id', op: 'in', value: wanted }],
31
+ });
32
+ return Object.fromEntries(rows.map((row) => [row.id, row.name]));
33
+ }
34
+ /**
35
+ * The row a caller already created under this reference, if there is one.
36
+ *
37
+ * A creation that carries a `client_ref` is looked up before it is made, so a
38
+ * caller that repeats itself after a dropped connection gets the first row
39
+ * back rather than a second one. The lookup is a convenience and not the
40
+ * guarantee: two callers racing each other both find nothing, and the unique
41
+ * index on `(company_id, client_ref)` is what refuses the slower one.
42
+ */
43
+ export async function createdBefore(backend, table, company, clientRef, columns) {
44
+ if (clientRef === undefined)
45
+ return undefined;
46
+ const rows = await backend.select({
47
+ table,
48
+ columns,
49
+ where: [
50
+ { column: 'company_id', op: 'eq', value: company },
51
+ { column: 'client_ref', op: 'eq', value: clientRef },
52
+ ],
53
+ });
54
+ return rows[0];
55
+ }
56
+ /** Resolves codes to ids in one query, and says which code was unknown. */
57
+ /**
58
+ * The currency an amount is in when the caller names none: the company's own.
59
+ *
60
+ * Never a literal. A company that keeps its books in Canadian dollars would
61
+ * get a euro invoice out of a euro written here, and a wrong answer is worse
62
+ * than a refusal. `companies.currency_code` is never null, so there is always
63
+ * one to find.
64
+ */
65
+ export async function companyCurrency(backend, company) {
66
+ const rows = await backend.select({
67
+ table: 'companies',
68
+ columns: ['currency_code'],
69
+ where: [{ column: 'id', op: 'eq', value: company }],
70
+ });
71
+ const currency = rows[0]?.currency_code;
72
+ if (currency === undefined) {
73
+ throw new BooksError(`not_found: company ${company}. Either it does not exist or you are not a member of it.`);
74
+ }
75
+ return currency;
76
+ }
77
+ export async function idsByCode(backend, table, company, codes) {
78
+ const singular = { accounts: 'account', taxes: 'tax', journals: 'journal' }[table];
79
+ const wanted = [...new Set(codes)];
80
+ if (wanted.length === 0)
81
+ return new Map();
82
+ const rows = await backend.select({
83
+ table,
84
+ columns: ['id', 'code'],
85
+ where: [
86
+ { column: 'company_id', op: 'eq', value: company },
87
+ { column: 'code', op: 'in', value: wanted },
88
+ ],
89
+ });
90
+ const found = new Map(rows.map((row) => [row.code, row.id]));
91
+ for (const code of wanted) {
92
+ if (!found.has(code)) {
93
+ throw new BooksError(`unknown_${singular}_code: no ${singular} "${code}" in this company. list_accounts, get_company and the ekwo://companies/{id}/taxes resource say what exists.`);
94
+ }
95
+ }
96
+ return found;
97
+ }
98
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/books/shared.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A thin client over the Ekwo schema.
3
+ *
4
+ * It is deliberately small: Supabase already exposes every table as REST with
5
+ * an OpenAPI description, so there is nothing to wrap there. What is worth
6
+ * wrapping is the handful of functions that carry the accounting rules, and
7
+ * the FEC export.
8
+ *
9
+ * `@supabase/supabase-js` is an optional peer dependency; only the two
10
+ * methods used here are required, so any structurally compatible client
11
+ * works and this package keeps zero runtime dependencies.
12
+ */
13
+ import { type FecOptions } from '@ekwo-ai/fec';
14
+ import type { AgedBalanceRow, Entry, Instance, InstanceEdition, InstanceAdmin, IsoDate, TrialBalanceRow, Uuid, VatReturnRow } from './types.js';
15
+ export interface RpcResult<T> {
16
+ data: T | null;
17
+ error: {
18
+ message: string;
19
+ code?: string;
20
+ details?: string;
21
+ } | null;
22
+ }
23
+ /** The slice of `@supabase/supabase-js` this client needs. */
24
+ export interface SupabaseLike {
25
+ rpc<T = unknown>(fn: string, args?: Record<string, unknown>): PromiseLike<RpcResult<T>>;
26
+ }
27
+ export declare class EkwoError extends Error {
28
+ name: string;
29
+ readonly code: string | undefined;
30
+ constructor(message: string, code?: string);
31
+ }
32
+ export interface PeriodRange {
33
+ companyId: Uuid;
34
+ from: IsoDate;
35
+ to: IsoDate;
36
+ }
37
+ export declare class EkwoClient {
38
+ private readonly db;
39
+ constructor(db: SupabaseLike);
40
+ /** Books a document and returns the entry it produced. */
41
+ postDocument(documentId: Uuid): Promise<Entry>;
42
+ /** Numbers and posts an entry that was built by hand. */
43
+ postEntry(entryId: Uuid): Promise<Entry>;
44
+ /**
45
+ * Matches a debit line against a credit line. With no amount, the smaller
46
+ * of the two open amounts is used.
47
+ */
48
+ reconcile(debitLineId: Uuid, creditLineId: Uuid, amount?: number): Promise<unknown>;
49
+ trialBalance(range: PeriodRange): Promise<TrialBalanceRow[]>;
50
+ vatReturn(range: PeriodRange): Promise<VatReturnRow[]>;
51
+ agedBalance(companyId: Uuid, at: IsoDate, group?: 'receivable' | 'payable'): Promise<AgedBalanceRow[]>;
52
+ /** The French FEC for a period, ready to be written to disk. */
53
+ generateFec(range: PeriodRange, options?: FecOptions): Promise<string>;
54
+ /**
55
+ * Records the installation. Called once, by the installer; raises if this
56
+ * database has already been set up.
57
+ */
58
+ initInstance(organizationName: string, country: string, edition?: InstanceEdition): Promise<Instance>;
59
+ /**
60
+ * Makes a user an instance administrator. The first claim is open; after
61
+ * that only an administrator may appoint another.
62
+ */
63
+ claimInstanceAdmin(userId?: Uuid): Promise<InstanceAdmin>;
64
+ /**
65
+ * Opt-in: records an address so Ekwo can reach the operator. Never
66
+ * required, and `unregisterInstance()` undoes it.
67
+ */
68
+ registerInstance(contactEmail: string): Promise<Instance>;
69
+ unregisterInstance(): Promise<Instance>;
70
+ /** Installs a country chart of accounts, journals and taxes into a company. */
71
+ installCountryTemplate(companyId: Uuid, country: string): Promise<void>;
72
+ }
73
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAA6B,KAAK,UAAU,EAAoB,MAAM,cAAc,CAAC;AAC5F,OAAO,KAAK,EACV,cAAc,EACd,KAAK,EACL,QAAQ,EACR,eAAe,EACf,aAAa,EACb,OAAO,EACP,eAAe,EACf,IAAI,EACJ,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACf,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACpE;AAED,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;CACzF;AAED,qBAAa,SAAU,SAAQ,KAAK;IACzB,IAAI,SAAe;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;gBAEtB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAI3C;AAYD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,EAAE,EAAE,OAAO,CAAC;CACb;AAED,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C,0DAA0D;IACpD,YAAY,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC;IAKpD,yDAAyD;IACnD,SAAS,CAAC,OAAO,EAAE,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC;IAK9C;;;OAGG;IACG,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASnF,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAS5D,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAStD,WAAW,CACf,SAAS,EAAE,IAAI,EACf,EAAE,EAAE,OAAO,EACX,KAAK,GAAE,YAAY,GAAG,SAAwB,GAC7C,OAAO,CAAC,cAAc,EAAE,CAAC;IAS5B,gEAAgE;IAC1D,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAS5E;;;OAGG;IACG,YAAY,CAChB,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAA6B,GACrC,OAAO,CAAC,QAAQ,CAAC;IASpB;;;OAGG;IACG,kBAAkB,CAAC,MAAM,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC;IAO/D;;;OAGG;IACG,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAOzD,kBAAkB,IAAI,OAAO,CAAC,QAAQ,CAAC;IAK7C,+EAA+E;IACzE,sBAAsB,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS9E"}