@ekwo-ai/journal-report 0.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Karuna Co OÜ, trading as Ekwo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # `@ekwo-ai/journal-report`
2
+
3
+ Reads a **journal report**, or a **general ledger detail**, saved as CSV — the
4
+ report a cloud accounting service prints of every journal it posted, one row
5
+ per line with the number of its journal — into plain objects, in TypeScript,
6
+ with no dependencies. Beside it, the chart of accounts and the contacts, which
7
+ the same service exports as CSV directly.
8
+
9
+ ```ts
10
+ import { readJournalReport } from '@ekwo-ai/journal-report';
11
+
12
+ const books = readJournalReport([report, chart, contacts], { dateOrder: 'dmy' });
13
+
14
+ books.entries; // [{ number: '102', journal: 'Receivable Invoice', date: '2025-01-15', lines: […] }, …]
15
+ books.accounts; // [{ code: '090', name: 'Business Bank Account', type: 'Bank' }, …]
16
+ books.violations; // a journal that does not balance
17
+ ```
18
+
19
+ ## The files
20
+
21
+ **The report** (required). Such a service exports its reports as a
22
+ spreadsheet, not as CSV: open it and save it as CSV. A report is laid out for a
23
+ person, so:
24
+
25
+ - the **header** is the first row that names a `Date`, an `Account Code`, a
26
+ `Debit` and a `Credit` — the title rows above it are passed over;
27
+ - a **line** is a row whose date cell holds a date; the rows that head a group,
28
+ total it, or give an opening or closing balance are passed over;
29
+ - lines are gathered into entries by their **journal number** (`Journal ID`,
30
+ `Journal Number`, `Journal #`), which the report has to carry: add the column
31
+ in the report's column picker before exporting it, with the account code. A
32
+ report without it is refused by name — there would be no way to put its lines
33
+ back into entries.
34
+
35
+ Optional columns: `Source` (kept as the journal of the entry), `Account`,
36
+ `Description`, `Narration`, `Reference`, `Contact`.
37
+
38
+ - A **date written in digits** — `03/04/2026` — is read in the order the
39
+ caller names (`dateOrder: 'dmy' | 'mdy' | 'ymd'`) and refused without one:
40
+ the order follows the region of the organisation, and the file does not say
41
+ it. A date in ISO or with its month in letters (`3 Apr 2026`) needs none.
42
+ - An **amount** is read as a spreadsheet displays it. In a comma-separated file
43
+ the decimal mark is a point and a comma only groups thousands, three digits at
44
+ a time (`1,234.50`). In a file separated by a semicolon or a tab the decimal
45
+ mark is the one mark the number carries, and thousands are grouped by a space
46
+ only. A negative is a minus or brackets. Anything else is refused.
47
+
48
+ **The chart of accounts** (optional): `*Code`, `*Name`, `*Type`, the rest
49
+ ignored. Codes are kept as text: `090` stays `090`.
50
+
51
+ **The contacts** (optional): `*ContactName`, `AccountNumber`, `EmailAddress`,
52
+ `TaxNumber`, `CompanyNumber`; the country only when it is written as a
53
+ two-letter code.
54
+
55
+ ## What it returns
56
+
57
+ The shape declared in [`src/types.ts`](./src/types.ts), which is, field for
58
+ field, the shape every reader of an accounting export in this repository
59
+ declares for itself. A report says no currency: it prints the organisation's
60
+ and does not name it. Amounts are decimal strings, never negative. What does
61
+ not add up comes back in `violations`; what is not a report is thrown as a
62
+ `BooksFileError` with a `code`.
63
+
64
+ It knows no chart of accounts: in [Ekwo OS](https://github.com/Ekwo-ai/ekwo-os)
65
+ the correspondence is made by `ekwo import journal-report <files…> --date-order dmy`.
66
+
67
+ ## Sources
68
+
69
+ This reader was written against the reports of **Xero**, read on 22 September
70
+ 2026 from its help centre:
71
+
72
+ - [General Ledger Detail report](https://central.xero.com/0/article/General-Ledger-Detail-report)
73
+ and [Journal report](https://central.xero.com/0/article/Journal-report): the
74
+ columns each offers, the Journal ID and Account Code columns of the column
75
+ picker, the grouping by account or none;
76
+ - [Export or print a report](https://central.xero.com/0/article/Export-or-print-a-report):
77
+ reports export to PDF, a spreadsheet or Google Sheets, not to CSV;
78
+ - [Export or print your chart of accounts](https://central.xero.com/0/article/Export-or-print-your-chart-of-accounts)
79
+ and [Import a chart of accounts](https://central.xero.com/s/article/Import-a-chart-of-accounts):
80
+ the columns of the chart and the account types;
81
+ - [Export contacts out of Xero](https://central.xero.com/0/article/Export-contacts-out-of-Xero)
82
+ and [Import contacts into Xero](https://central.xero.com/0/article/Import-contacts-into-Xero):
83
+ the columns of the contacts file.
84
+
85
+ `ekwo import xero <files…>` is the same as `ekwo import journal-report`.
86
+
87
+ What those pages do not state — the exact text of the title and total rows —
88
+ is why the reader finds the header by its columns and a line by its date,
89
+ rather than by position. The fixtures under `test/fixtures/` are written by
90
+ hand from those pages and hold no real data.
91
+
92
+ MIT.
package/dist/csv.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The comma-separated values a spreadsheet saves, as RFC 4180 describes them:
3
+ * fields between double quotes may hold the separator, a line break and a
4
+ * doubled quote. Nothing else about the file is assumed.
5
+ *
6
+ * The separator is **read from the header**, which this format requires: the
7
+ * first of `,`, `;` and a tab that the header row contains outside quotes. A
8
+ * spreadsheet saves with a comma or a semicolon depending on the language of
9
+ * the machine, and the header is the one row whose content is known, so this
10
+ * is reading the file rather than guessing at it.
11
+ */
12
+ import type { Encoding } from './types.js';
13
+ export type Separator = ',' | ';' | '\t';
14
+ /** Bytes to text, in the encoding the caller said, refusing what is not that. */
15
+ export declare function decode(input: string | Uint8Array, encoding: Encoding, maxBytes: number): string;
16
+ /** The separator of a header line: the first of the three found outside quotes. */
17
+ export declare function separatorOf(headerLine: string): Separator;
18
+ /** Every row of the file, each an array of fields, with the row number it starts on. */
19
+ export declare function parseCsv(text: string, separator: Separator): {
20
+ row: number;
21
+ fields: string[];
22
+ }[];
23
+ /** A header cell as it is compared: trimmed, lower case, without the `*` some exports put on a required column. */
24
+ export declare function headerKey(cell: string): string;
25
+ //# sourceMappingURL=csv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;AAEzC,iFAAiF;AACjF,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA0B/F;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAQzD;AAED,wFAAwF;AACxF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,CAqDhG;AAED,mHAAmH;AACnH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C"}
package/dist/csv.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The comma-separated values a spreadsheet saves, as RFC 4180 describes them:
3
+ * fields between double quotes may hold the separator, a line break and a
4
+ * doubled quote. Nothing else about the file is assumed.
5
+ *
6
+ * The separator is **read from the header**, which this format requires: the
7
+ * first of `,`, `;` and a tab that the header row contains outside quotes. A
8
+ * spreadsheet saves with a comma or a semicolon depending on the language of
9
+ * the machine, and the header is the one row whose content is known, so this
10
+ * is reading the file rather than guessing at it.
11
+ */
12
+ import { BooksFileError } from './errors.js';
13
+ const BYTE_ORDER_MARK = 0xfeff;
14
+ /** Bytes to text, in the encoding the caller said, refusing what is not that. */
15
+ export function decode(input, encoding, maxBytes) {
16
+ let text;
17
+ if (typeof input === 'string') {
18
+ if (input.length > maxBytes)
19
+ throw new BooksFileError('too_large', `the file is longer than ${maxBytes} bytes`);
20
+ text = input;
21
+ }
22
+ else {
23
+ if (input.byteLength > maxBytes) {
24
+ throw new BooksFileError('too_large', `the file is ${input.byteLength} bytes, over the limit of ${maxBytes}`);
25
+ }
26
+ if (encoding === 'iso-8859-1') {
27
+ const parts = [];
28
+ for (let i = 0; i < input.length; i += 8192)
29
+ parts.push(String.fromCharCode(...input.subarray(i, i + 8192)));
30
+ text = parts.join('');
31
+ }
32
+ else {
33
+ try {
34
+ text = new TextDecoder('utf-8', { fatal: true }).decode(input);
35
+ }
36
+ catch {
37
+ throw new BooksFileError('unsupported_encoding', "the bytes are not UTF-8; if the file was saved in Latin-1, say so with { encoding: 'iso-8859-1' } — it is not guessed, because every sequence of bytes is valid Latin-1");
38
+ }
39
+ }
40
+ }
41
+ if (text.charCodeAt(0) === BYTE_ORDER_MARK)
42
+ text = text.slice(1);
43
+ return text;
44
+ }
45
+ /** The separator of a header line: the first of the three found outside quotes. */
46
+ export function separatorOf(headerLine) {
47
+ let quoted = false;
48
+ for (const char of headerLine) {
49
+ if (char === '"')
50
+ quoted = !quoted;
51
+ else if (!quoted && (char === ',' || char === ';' || char === '\t'))
52
+ return char;
53
+ }
54
+ // A header of one column has no separator; any will do, and a comma is it.
55
+ return ',';
56
+ }
57
+ /** Every row of the file, each an array of fields, with the row number it starts on. */
58
+ export function parseCsv(text, separator) {
59
+ const rows = [];
60
+ let fields = [];
61
+ let field = '';
62
+ let quoted = false;
63
+ let line = 1;
64
+ let startedOn = 1;
65
+ let fieldStarted = false;
66
+ const endRow = () => {
67
+ fields.push(field);
68
+ // A blank line is no row; a row of empty fields is kept and read.
69
+ if (!(fields.length === 1 && fields[0] === ''))
70
+ rows.push({ row: startedOn, fields });
71
+ fields = [];
72
+ field = '';
73
+ fieldStarted = false;
74
+ };
75
+ for (let i = 0; i < text.length; i += 1) {
76
+ const char = text[i];
77
+ if (quoted) {
78
+ if (char === '"') {
79
+ if (text[i + 1] === '"') {
80
+ field += '"';
81
+ i += 1;
82
+ }
83
+ else
84
+ quoted = false;
85
+ }
86
+ else {
87
+ if (char === '\n')
88
+ line += 1;
89
+ field += char;
90
+ }
91
+ continue;
92
+ }
93
+ if (char === '"' && !fieldStarted) {
94
+ quoted = true;
95
+ fieldStarted = true;
96
+ }
97
+ else if (char === separator) {
98
+ fields.push(field);
99
+ field = '';
100
+ fieldStarted = false;
101
+ }
102
+ else if (char === '\r' && text[i + 1] === '\n') {
103
+ // The \n that follows ends the row.
104
+ }
105
+ else if (char === '\n' || char === '\r') {
106
+ endRow();
107
+ line += 1;
108
+ startedOn = line;
109
+ }
110
+ else {
111
+ field += char;
112
+ fieldStarted = true;
113
+ }
114
+ }
115
+ if (quoted)
116
+ throw new BooksFileError('malformed_csv', 'a quoted field is never closed', startedOn);
117
+ if (field !== '' || fields.length > 0)
118
+ endRow();
119
+ return rows;
120
+ }
121
+ /** A header cell as it is compared: trimmed, lower case, without the `*` some exports put on a required column. */
122
+ export function headerKey(cell) {
123
+ return cell.trim().replace(/^\*/, '').trim().toLowerCase();
124
+ }
125
+ //# sourceMappingURL=csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.js","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Amounts added without a float.
3
+ *
4
+ * An amount is held as a `bigint` of millionths: enough for any currency and
5
+ * any rate a spreadsheet prints, and exact. What leaves is a string with two
6
+ * decimals, and more when the file wrote more that are not zero — dropping a
7
+ * written zero is not rounding, dropping anything else would be, and is never
8
+ * done here.
9
+ */
10
+ /** Millionths as a decimal string: signed, two decimals, more when they are written. */
11
+ export declare function formatDecimal(value: bigint): string;
12
+ //# sourceMappingURL=decimal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decimal.d.ts","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMnD"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Amounts added without a float.
3
+ *
4
+ * An amount is held as a `bigint` of millionths: enough for any currency and
5
+ * any rate a spreadsheet prints, and exact. What leaves is a string with two
6
+ * decimals, and more when the file wrote more that are not zero — dropping a
7
+ * written zero is not rounding, dropping anything else would be, and is never
8
+ * done here.
9
+ */
10
+ const SCALE = 6;
11
+ /** Millionths as a decimal string: signed, two decimals, more when they are written. */
12
+ export function formatDecimal(value) {
13
+ const negative = value < 0n;
14
+ const digits = (negative ? -value : value).toString().padStart(SCALE + 1, '0');
15
+ const whole = digits.slice(0, -SCALE);
16
+ const fraction = digits.slice(-SCALE).replace(/0+$/, '').padEnd(2, '0');
17
+ return `${negative ? '-' : ''}${whole}.${fraction}`;
18
+ }
19
+ //# sourceMappingURL=decimal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decimal.js","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,KAAK,GAAG,CAAC,CAAC;AAEhB,wFAAwF;AACxF,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,QAAQ,GAAG,KAAK,GAAG,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACxE,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAC;AACtD,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What makes a file unreadable, as opposed to books that read and do not add
3
+ * up. The second comes back in `violations`; the first is thrown, because a
4
+ * file with no header the reader recognises has no row to hang a violation on.
5
+ */
6
+ export type BooksFileErrorCode =
7
+ /** Larger than `maxBytes`. Refused before a single character is read. */
8
+ 'too_large'
9
+ /** Bytes that are not the encoding announced. */
10
+ | 'unsupported_encoding'
11
+ /** Nothing in the file, or a header and nothing under it. */
12
+ | 'empty_file'
13
+ /** A header that lacks a column the format needs, or names one twice. */
14
+ | 'missing_column'
15
+ /** A quote that is never closed. */
16
+ | 'malformed_csv'
17
+ /** A value the format does not allow where it is: a date, an amount. */
18
+ | 'invalid_value';
19
+ export declare class BooksFileError extends Error {
20
+ readonly code: BooksFileErrorCode;
21
+ /** The row the reader stopped on, from 1, when there is one. */
22
+ readonly row: number | null;
23
+ constructor(code: BooksFileErrorCode, message: string, row?: number | null);
24
+ }
25
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,kBAAkB;AAC5B,yEAAyE;AACvE,WAAW;AACb,iDAAiD;GAC/C,sBAAsB;AACxB,6DAA6D;GAC3D,YAAY;AACd,yEAAyE;GACvE,gBAAgB;AAClB,oCAAoC;GAClC,eAAe;AACjB,wEAAwE;GACtE,eAAe,CAAC;AAEpB,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEhB,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,GAAG,IAAW;CAMjF"}
package/dist/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ export class BooksFileError extends Error {
2
+ code;
3
+ /** The row the reader stopped on, from 1, when there is one. */
4
+ row;
5
+ constructor(code, message, row = null) {
6
+ super(row === null ? message : `row ${row}: ${message}`);
7
+ this.name = 'BooksFileError';
8
+ this.code = code;
9
+ this.row = row;
10
+ }
11
+ }
12
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAmBA,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAqB;IAClC,gEAAgE;IACvD,GAAG,CAAgB;IAE5B,YAAY,IAAwB,EAAE,OAAe,EAAE,MAAqB,IAAI;QAC9E,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,OAAO,EAAE,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;CACF"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A journal report, or a general ledger detail, saved as CSV: the report a
3
+ * cloud accounting service prints of every journal it posted, one row per
4
+ * line, with the number of the journal the line belongs to. Such a service
5
+ * exports its reports to a spreadsheet and not to CSV; the spreadsheet is
6
+ * saved as CSV, and that file is what this reads — beside it, the chart of
7
+ * accounts and the contacts, which the same service exports as CSV directly.
8
+ *
9
+ * A report is laid out for a person: a few rows of title before the header,
10
+ * rows that head a group or total it, rows of opening and closing balance. So
11
+ * the header is **the first row that names a date, an account code, a debit
12
+ * and a credit**, and a line is **a row whose date cell holds a date**; every
13
+ * other row is a heading or a total and is passed over. Lines are gathered
14
+ * into entries by their journal number, which the report has to carry.
15
+ *
16
+ * A date the report wrote as digits in an order nobody can read off the digits
17
+ * — `03/04/2026` — is read in the order the caller names, and refused without
18
+ * one: which order a report uses follows the region of the organisation, and
19
+ * the file does not say it. A date with the month in letters, or in ISO, needs
20
+ * no order.
21
+ *
22
+ * It depends on nothing, reads no database and knows no chart of accounts.
23
+ */
24
+ import type { ImportedBooks, ReadOptions } from './types.js';
25
+ export * from './types.js';
26
+ export { BooksFileError, type BooksFileErrorCode } from './errors.js';
27
+ /** The order of day, month and year in a date written only in digits. */
28
+ export type DateOrder = 'dmy' | 'mdy' | 'ymd';
29
+ export interface JournalReportOptions extends ReadOptions {
30
+ /** How a date written only in digits is to be read. Required when the report writes one; never guessed. */
31
+ dateOrder?: DateOrder;
32
+ }
33
+ /** The columns of the report, each under the names it may carry. */
34
+ export declare const REPORT_COLUMNS: {
35
+ readonly date: readonly ["date"];
36
+ readonly journal: readonly ["journal id", "journal number", "journal no.", "journal no", "journal #", "journal"];
37
+ readonly source: readonly ["source", "source type"];
38
+ readonly accountCode: readonly ["account code"];
39
+ readonly account: readonly ["account", "account name"];
40
+ readonly description: readonly ["description"];
41
+ readonly narration: readonly ["narration"];
42
+ readonly reference: readonly ["reference"];
43
+ readonly contact: readonly ["contact", "contact name"];
44
+ readonly debit: readonly ["debit"];
45
+ readonly credit: readonly ["credit"];
46
+ };
47
+ /** The columns of the chart of accounts. */
48
+ export declare const CHART_COLUMNS: {
49
+ readonly code: readonly ["code", "account code"];
50
+ readonly name: readonly ["name", "account name"];
51
+ readonly type: readonly ["type", "account type"];
52
+ };
53
+ /** The columns of the contacts. */
54
+ export declare const CONTACT_COLUMNS: {
55
+ readonly name: readonly ["contactname", "contact name"];
56
+ readonly accountNumber: readonly ["accountnumber", "account number"];
57
+ readonly email: readonly ["emailaddress", "email address", "email"];
58
+ readonly tax: readonly ["taxnumber", "tax number"];
59
+ readonly company: readonly ["companynumber", "company number"];
60
+ readonly country: readonly ["pocountry", "sacountry"];
61
+ };
62
+ /**
63
+ * Reads the files — the report, and optionally the chart of accounts and the
64
+ * contacts — in any order, each recognised by its header.
65
+ */
66
+ export declare function readJournalReport(inputs: readonly (string | Uint8Array)[], options?: JournalReportOptions): ImportedBooks;
67
+ /**
68
+ * A date, or null for a cell that is not one — which is how a heading or a
69
+ * total row is told from a line. A cell that looks like a date and is not a
70
+ * day of the calendar is refused, never passed over.
71
+ */
72
+ export declare function readDate(value: string, order: DateOrder | undefined, row: number): string | null;
73
+ /**
74
+ * An amount as a spreadsheet displays it once saved as CSV. In a file
75
+ * separated by commas the decimal mark is a point and a comma can only group
76
+ * thousands, three digits at a time (`1,234.50`). In a file separated by a
77
+ * semicolon or a tab the decimal mark is whichever of the two the number
78
+ * carries once, and thousands may only be grouped by a space. A negative is a
79
+ * minus or brackets. Anything else is refused.
80
+ */
81
+ export declare function amount(raw: string, separator: string, row: number): bigint;
82
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,OAAO,KAAK,EAEV,aAAa,EAIb,WAAW,EAEZ,MAAM,YAAY,CAAC;AAEpB,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAItE,yEAAyE;AACzE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAE9C,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,2GAA2G;IAC3G,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,oEAAoE;AACpE,eAAO,MAAM,cAAc;;;;;;;;;;;;CAYjB,CAAC;AAEX,4CAA4C;AAC5C,eAAO,MAAM,aAAa;;;;CAIhB,CAAC;AAEX,mCAAmC;AACnC,eAAO,MAAM,eAAe;;;;;;;CAOlB,CAAC;AAyBX;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAwK7H;AAID;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmChG;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CA0B1E"}
package/dist/index.js ADDED
@@ -0,0 +1,327 @@
1
+ /**
2
+ * A journal report, or a general ledger detail, saved as CSV: the report a
3
+ * cloud accounting service prints of every journal it posted, one row per
4
+ * line, with the number of the journal the line belongs to. Such a service
5
+ * exports its reports to a spreadsheet and not to CSV; the spreadsheet is
6
+ * saved as CSV, and that file is what this reads — beside it, the chart of
7
+ * accounts and the contacts, which the same service exports as CSV directly.
8
+ *
9
+ * A report is laid out for a person: a few rows of title before the header,
10
+ * rows that head a group or total it, rows of opening and closing balance. So
11
+ * the header is **the first row that names a date, an account code, a debit
12
+ * and a credit**, and a line is **a row whose date cell holds a date**; every
13
+ * other row is a heading or a total and is passed over. Lines are gathered
14
+ * into entries by their journal number, which the report has to carry.
15
+ *
16
+ * A date the report wrote as digits in an order nobody can read off the digits
17
+ * — `03/04/2026` — is read in the order the caller names, and refused without
18
+ * one: which order a report uses follows the region of the organisation, and
19
+ * the file does not say it. A date with the month in letters, or in ISO, needs
20
+ * no order.
21
+ *
22
+ * It depends on nothing, reads no database and knows no chart of accounts.
23
+ */
24
+ import { decode, headerKey, parseCsv, separatorOf } from './csv.js';
25
+ import { formatDecimal } from './decimal.js';
26
+ import { BooksFileError } from './errors.js';
27
+ export * from './types.js';
28
+ export { BooksFileError } from './errors.js';
29
+ const DEFAULTS = { encoding: 'utf-8', maxBytes: 256 * 1024 * 1024 };
30
+ /** The columns of the report, each under the names it may carry. */
31
+ export const REPORT_COLUMNS = {
32
+ date: ['date'],
33
+ journal: ['journal id', 'journal number', 'journal no.', 'journal no', 'journal #', 'journal'],
34
+ source: ['source', 'source type'],
35
+ accountCode: ['account code'],
36
+ account: ['account', 'account name'],
37
+ description: ['description'],
38
+ narration: ['narration'],
39
+ reference: ['reference'],
40
+ contact: ['contact', 'contact name'],
41
+ debit: ['debit'],
42
+ credit: ['credit'],
43
+ };
44
+ /** The columns of the chart of accounts. */
45
+ export const CHART_COLUMNS = {
46
+ code: ['code', 'account code'],
47
+ name: ['name', 'account name'],
48
+ type: ['type', 'account type'],
49
+ };
50
+ /** The columns of the contacts. */
51
+ export const CONTACT_COLUMNS = {
52
+ name: ['contactname', 'contact name'],
53
+ accountNumber: ['accountnumber', 'account number'],
54
+ email: ['emailaddress', 'email address', 'email'],
55
+ tax: ['taxnumber', 'tax number'],
56
+ company: ['companynumber', 'company number'],
57
+ country: ['pocountry', 'sacountry'],
58
+ };
59
+ function locate(keys, spec) {
60
+ const at = {};
61
+ for (const [column, names] of Object.entries(spec)) {
62
+ const found = keys.findIndex((key) => names.includes(key));
63
+ if (found !== -1)
64
+ at[column] = found;
65
+ }
66
+ return at;
67
+ }
68
+ const has = (keys, names) => keys.some((key) => names.includes(key));
69
+ function isReportHeader(keys) {
70
+ return has(keys, REPORT_COLUMNS.date) && has(keys, REPORT_COLUMNS.accountCode) && has(keys, REPORT_COLUMNS.debit) && has(keys, REPORT_COLUMNS.credit);
71
+ }
72
+ /**
73
+ * Reads the files — the report, and optionally the chart of accounts and the
74
+ * contacts — in any order, each recognised by its header.
75
+ */
76
+ export function readJournalReport(inputs, options = {}) {
77
+ const sheets = inputs.map((input, index) => {
78
+ const text = decode(input, options.encoding ?? DEFAULTS.encoding, options.maxBytes ?? DEFAULTS.maxBytes);
79
+ // The title rows above the header may hold no separator at all, so the
80
+ // separator is read from the first line that holds one.
81
+ const firstWithSeparator = text.split(/\r?\n/).find((line) => /[,;\t]/.test(line)) ?? '';
82
+ const separator = separatorOf(firstWithSeparator);
83
+ return { file: index + 1, separator, rows: parseCsv(text, separator) };
84
+ });
85
+ const violations = [];
86
+ const accounts = new Map();
87
+ const contacts = new Map();
88
+ const reports = [];
89
+ for (const sheet of sheets) {
90
+ const headerAt = sheet.rows.findIndex((r) => isReportHeader(r.fields.map(headerKey)));
91
+ if (headerAt !== -1) {
92
+ reports.push({ sheet, headerAt });
93
+ continue;
94
+ }
95
+ const header = sheet.rows[0];
96
+ if (header === undefined)
97
+ throw new BooksFileError('empty_file', `file ${sheet.file} is empty`);
98
+ const keys = header.fields.map(headerKey);
99
+ if (has(keys, CHART_COLUMNS.code) && has(keys, CHART_COLUMNS.type)) {
100
+ const at = locate(keys, CHART_COLUMNS);
101
+ for (const { fields } of sheet.rows.slice(1)) {
102
+ const code = (fields[at.code] ?? '').trim();
103
+ if (code === '')
104
+ continue;
105
+ const name = at.name === undefined ? '' : (fields[at.name] ?? '').trim();
106
+ const type = at.type === undefined ? '' : (fields[at.type] ?? '').trim();
107
+ accounts.set(code, { code, name: name === '' ? null : name, type: type === '' ? null : type });
108
+ }
109
+ }
110
+ else if (has(keys, CONTACT_COLUMNS.name)) {
111
+ const at = locate(keys, CONTACT_COLUMNS);
112
+ for (const { fields } of sheet.rows.slice(1)) {
113
+ const cell = (column) => {
114
+ const index = at[column];
115
+ return index === undefined ? '' : (fields[index] ?? '').trim();
116
+ };
117
+ const name = cell('name');
118
+ if (name === '')
119
+ continue;
120
+ const country = cell('country').toUpperCase();
121
+ contacts.set(name, {
122
+ code: name,
123
+ name,
124
+ vatNumber: cell('tax') === '' ? null : cell('tax'),
125
+ registrationNumber: cell('company') === '' ? null : cell('company'),
126
+ email: cell('email') === '' ? null : cell('email'),
127
+ country: /^[A-Z]{2}$/.test(country) ? country : null,
128
+ });
129
+ }
130
+ }
131
+ else {
132
+ throw new BooksFileError('missing_column', `file ${sheet.file} is neither a report (a header with Date, Account Code, Debit and Credit), a chart of accounts (Code, Type) nor contacts (ContactName)`);
133
+ }
134
+ }
135
+ if (reports.length === 0) {
136
+ throw new BooksFileError('missing_column', 'no file is a report: one needs a header row with Date, Account Code, Debit and Credit');
137
+ }
138
+ const entries = new Map();
139
+ for (const { sheet, headerAt } of reports) {
140
+ const header = sheet.rows[headerAt];
141
+ const at = locate(header.fields.map(headerKey), REPORT_COLUMNS);
142
+ if (at.journal === undefined) {
143
+ throw new BooksFileError('missing_column', `the report in file ${sheet.file} carries no journal number, and without it its lines cannot be put back into entries. Add the Journal ID column to the report before exporting it`, header.row);
144
+ }
145
+ for (const { row, fields } of sheet.rows.slice(headerAt + 1)) {
146
+ const cell = (column) => {
147
+ const index = at[column];
148
+ return index === undefined ? '' : (fields[index] ?? '').trim();
149
+ };
150
+ const date = readDate(cell('date'), options.dateOrder, row);
151
+ if (date === null)
152
+ continue; // a heading, a total, a balance: not a line
153
+ const journal = cell('journal');
154
+ const account = cell('accountCode');
155
+ if (journal === '' || account === '') {
156
+ violations.push({ rule: 'required', message: 'a dated line names no journal number or no account code and was left out', row });
157
+ continue;
158
+ }
159
+ let debit = amount(cell('debit'), sheet.separator, row);
160
+ let credit = amount(cell('credit'), sheet.separator, row);
161
+ if (debit !== 0n && credit !== 0n)
162
+ violations.push({ rule: 'one_side', message: 'a line carries both a debit and a credit', row });
163
+ if (debit < 0n) {
164
+ credit -= debit;
165
+ debit = 0n;
166
+ }
167
+ if (credit < 0n) {
168
+ debit -= credit;
169
+ credit = 0n;
170
+ }
171
+ if (!accounts.has(account)) {
172
+ const name = cell('account');
173
+ accounts.set(account, { code: account, name: name === '' ? null : name, type: null });
174
+ }
175
+ const contact = cell('contact');
176
+ if (contact !== '' && !contacts.has(contact)) {
177
+ contacts.set(contact, { code: contact, name: contact, vatNumber: null, registrationNumber: null, email: null, country: null });
178
+ }
179
+ const label = cell('description');
180
+ const line = {
181
+ account,
182
+ contact: contact === '' ? null : contact,
183
+ label: label === '' ? null : label,
184
+ debit: formatDecimal(debit),
185
+ credit: formatDecimal(credit),
186
+ currency: null,
187
+ amountCurrency: null,
188
+ dueDate: null,
189
+ matching: null,
190
+ };
191
+ let entry = entries.get(journal);
192
+ if (entry === undefined) {
193
+ const source = cell('source');
194
+ const reference = cell('reference');
195
+ const narration = cell('narration');
196
+ entry = {
197
+ journal: source === '' ? null : source,
198
+ journalName: source === '' ? null : source,
199
+ number: journal,
200
+ date,
201
+ reference: reference === '' ? null : reference,
202
+ description: narration !== '' ? narration : line.label,
203
+ lines: [],
204
+ row,
205
+ debits: 0n,
206
+ credits: 0n,
207
+ };
208
+ entries.set(journal, entry);
209
+ }
210
+ else if (entry.date !== date) {
211
+ violations.push({ rule: 'one_date', message: `journal ${journal} is dated ${entry.date} on its first line and ${date} here`, row });
212
+ }
213
+ entry.lines.push(line);
214
+ entry.debits += debit;
215
+ entry.credits += credit;
216
+ }
217
+ }
218
+ const read = [];
219
+ for (const { debits, credits, ...entry } of entries.values()) {
220
+ if (debits !== credits) {
221
+ violations.push({
222
+ rule: 'balance',
223
+ message: `journal ${entry.number} has debit ${formatDecimal(debits)} and credit ${formatDecimal(credits)}`,
224
+ row: entry.row,
225
+ });
226
+ }
227
+ read.push(entry);
228
+ }
229
+ if (read.length === 0)
230
+ throw new BooksFileError('empty_file', 'the report holds no dated line');
231
+ return {
232
+ format: 'journal-report',
233
+ // A report prints amounts in the currency of the organisation and does not say which.
234
+ currency: null,
235
+ accounts: [...accounts.values()],
236
+ contacts: [...contacts.values()],
237
+ entries: read,
238
+ opening: [],
239
+ violations,
240
+ };
241
+ }
242
+ const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
243
+ /**
244
+ * A date, or null for a cell that is not one — which is how a heading or a
245
+ * total row is told from a line. A cell that looks like a date and is not a
246
+ * day of the calendar is refused, never passed over.
247
+ */
248
+ export function readDate(value, order, row) {
249
+ let y;
250
+ let m;
251
+ let d;
252
+ let match;
253
+ if ((match = /^(\d{4})-(\d{2})-(\d{2})(?:[ T].*)?$/.exec(value)) !== null) {
254
+ [y, m, d] = [Number(match[1]), Number(match[2]), Number(match[3])];
255
+ }
256
+ else if ((match = /^(\d{1,2})[ -]([A-Za-z]{3,9})[ -](\d{4})$/.exec(value)) !== null) {
257
+ const month = MONTHS.indexOf(match[2].slice(0, 3).toLowerCase());
258
+ if (month === -1)
259
+ return null;
260
+ [y, m, d] = [Number(match[3]), month + 1, Number(match[1])];
261
+ }
262
+ else if ((match = /^([A-Za-z]{3,9}) (\d{1,2}),? (\d{4})$/.exec(value)) !== null) {
263
+ const month = MONTHS.indexOf(match[1].slice(0, 3).toLowerCase());
264
+ if (month === -1)
265
+ return null;
266
+ [y, m, d] = [Number(match[3]), month + 1, Number(match[2])];
267
+ }
268
+ else if ((match = /^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/.exec(value)) !== null) {
269
+ if (order === undefined) {
270
+ throw new BooksFileError('invalid_value', `"${value}" is a date written in digits, and nothing in the file says in which order; name it (dateOrder: 'dmy', 'mdy' or 'ymd')`, row);
271
+ }
272
+ const [a, b, c] = [Number(match[1]), Number(match[2]), Number(match[3])];
273
+ [y, m, d] = order === 'dmy' ? [c, b, a] : order === 'mdy' ? [c, a, b] : [a, b, c];
274
+ if (y < 100)
275
+ y += 2000;
276
+ }
277
+ else {
278
+ return null;
279
+ }
280
+ const iso = `${String(y).padStart(4, '0')}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
281
+ const parsed = new Date(`${iso}T00:00:00Z`);
282
+ if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== iso) {
283
+ throw new BooksFileError('invalid_value', `"${value}" is not a date of the calendar`, row);
284
+ }
285
+ return iso;
286
+ }
287
+ /**
288
+ * An amount as a spreadsheet displays it once saved as CSV. In a file
289
+ * separated by commas the decimal mark is a point and a comma can only group
290
+ * thousands, three digits at a time (`1,234.50`). In a file separated by a
291
+ * semicolon or a tab the decimal mark is whichever of the two the number
292
+ * carries once, and thousands may only be grouped by a space. A negative is a
293
+ * minus or brackets. Anything else is refused.
294
+ */
295
+ export function amount(raw, separator, row) {
296
+ let value = raw.replace(/[  ]/g, ' ').trim();
297
+ if (value === '')
298
+ return 0n;
299
+ let negative = false;
300
+ if (value.startsWith('(') && value.endsWith(')')) {
301
+ negative = true;
302
+ value = value.slice(1, -1).trim();
303
+ }
304
+ if (value.startsWith('-')) {
305
+ negative = !negative;
306
+ value = value.slice(1).trim();
307
+ }
308
+ let normalised = null;
309
+ if (separator === ',') {
310
+ if (/^\d{1,3}(,\d{3})+(\.\d+)?$/.test(value) || /^\d+(\.\d+)?$/.test(value))
311
+ normalised = value.replace(/,/g, '');
312
+ }
313
+ else {
314
+ const grouped = value.replace(/ (?=\d{3}(\D|$))/g, '');
315
+ if (/^\d+([.,]\d+)?$/.test(grouped))
316
+ normalised = grouped.replace(',', '.');
317
+ }
318
+ if (normalised === null) {
319
+ throw new BooksFileError('invalid_value', `"${raw}" is not an amount this reader accepts`, row);
320
+ }
321
+ const [whole, fraction = ''] = normalised.split('.');
322
+ if (fraction.length > 6)
323
+ throw new BooksFileError('invalid_value', `"${raw}" has more than six decimals`, row);
324
+ const units = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, '0') || '0');
325
+ return negative ? -units : units;
326
+ }
327
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * What a set of books is once it has been read out of the software that kept
3
+ * them: accounts, parties, entries and an opening balance. The shape is
4
+ * declared here and imported from nowhere — it names no table and no ledger.
5
+ *
6
+ * It is, field for field, the shape every reader of an accounting export in
7
+ * this repository declares for itself (`@ekwo-ai/fec`, and the readers beside
8
+ * this one), so whatever takes one takes the others. It is not a shared
9
+ * abstraction: none imports another, and a reader that has nothing to say for
10
+ * a part of it returns that part empty.
11
+ *
12
+ * Every amount is a **decimal string**, never negative: a line carries a debit
13
+ * or a credit, and the side is the sign. No figure passes through a `number`.
14
+ */
15
+ /** ISO 8601 calendar date, `YYYY-MM-DD`. */
16
+ export type IsoDate = string;
17
+ /** A fixed-point number, `1234.50`, never negative. */
18
+ export type Decimal = string;
19
+ /** One account of the chart the books were kept on, in the words of that software. */
20
+ export interface ImportedAccount {
21
+ code: string;
22
+ name: string | null;
23
+ /** What the source calls its kind, verbatim. Null when it says nothing. */
24
+ type: string | null;
25
+ }
26
+ /** A customer, a supplier or anybody else a line names. */
27
+ export interface ImportedContact {
28
+ /** How the source identifies the party on a line: its code, or its name where it has no code. */
29
+ code: string;
30
+ name: string;
31
+ vatNumber: string | null;
32
+ registrationNumber: string | null;
33
+ email: string | null;
34
+ /** ISO 3166-1 alpha-2, when the source writes one. Never inferred. */
35
+ country: string | null;
36
+ }
37
+ export interface ImportedLine {
38
+ /** The account code in the source's chart. */
39
+ account: string;
40
+ /** The `code` of an {@link ImportedContact}, or null. */
41
+ contact: string | null;
42
+ label: string | null;
43
+ debit: Decimal;
44
+ credit: Decimal;
45
+ /** A foreign currency the line was in, when the source says so. */
46
+ currency: string | null;
47
+ /** The amount in that currency, never negative. */
48
+ amountCurrency: Decimal | null;
49
+ dueDate: IsoDate | null;
50
+ /** The reconciliation mark the source put on the line, as written. */
51
+ matching: string | null;
52
+ }
53
+ export interface ImportedEntry {
54
+ /** The journal code in the source, or null when it has none. */
55
+ journal: string | null;
56
+ journalName: string | null;
57
+ /** The entry's number in the source. */
58
+ number: string | null;
59
+ date: IsoDate;
60
+ reference: string | null;
61
+ description: string | null;
62
+ lines: ImportedLine[];
63
+ /** The row of the file the entry starts on, from 1, for a message. */
64
+ row: number | null;
65
+ }
66
+ export interface Violation {
67
+ rule: string;
68
+ message: string;
69
+ /** The row of the file, from 1, or null for a rule about the whole file. */
70
+ row: number | null;
71
+ }
72
+ export interface ImportedBooks {
73
+ /** Which reader: `journal-report` here. */
74
+ format: string;
75
+ /** The currency the file says its amounts are in. Null when it says none: never guessed. */
76
+ currency: string | null;
77
+ accounts: ImportedAccount[];
78
+ contacts: ImportedContact[];
79
+ entries: ImportedEntry[];
80
+ /** A trial balance: one line per account, to become the opening entry of a year. */
81
+ opening: ImportedLine[];
82
+ violations: Violation[];
83
+ }
84
+ export type Encoding = 'utf-8' | 'iso-8859-1';
85
+ export interface ReadOptions {
86
+ /**
87
+ * How bytes are decoded. Default `utf-8`, fatally: bytes that are not UTF-8
88
+ * are refused by name, never read as something else. A spreadsheet that
89
+ * saves in Latin-1 is said to be so, because every sequence of bytes is
90
+ * valid Latin-1 and a guess would always succeed.
91
+ */
92
+ encoding?: Encoding;
93
+ /** Refused beyond this many bytes, before anything is read. Default 64 MiB. */
94
+ maxBytes?: number;
95
+ }
96
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4CAA4C;AAC5C,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,uDAAuD;AACvD,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,sFAAsF;AACtF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sEAAsE;IACtE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,mDAAmD;IACnD,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wCAAwC;IACxC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,sEAAsE;IACtE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,oFAAoF;IACpF,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What a set of books is once it has been read out of the software that kept
3
+ * them: accounts, parties, entries and an opening balance. The shape is
4
+ * declared here and imported from nowhere — it names no table and no ledger.
5
+ *
6
+ * It is, field for field, the shape every reader of an accounting export in
7
+ * this repository declares for itself (`@ekwo-ai/fec`, and the readers beside
8
+ * this one), so whatever takes one takes the others. It is not a shared
9
+ * abstraction: none imports another, and a reader that has nothing to say for
10
+ * a part of it returns that part empty.
11
+ *
12
+ * Every amount is a **decimal string**, never negative: a line carries a debit
13
+ * or a credit, and the side is the sign. No figure passes through a `number`.
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@ekwo-ai/journal-report",
3
+ "version": "0.6.0",
4
+ "description": "Reads a journal report or a general ledger detail saved as CSV — date, journal number, account code, debit, credit — with the chart of accounts and the contacts exported beside it, into plain objects in TypeScript: amounts as decimal strings, and what does not add up. Zero dependencies.",
5
+ "keywords": [
6
+ "journal-report",
7
+ "general-ledger",
8
+ "csv",
9
+ "accounting",
10
+ "bookkeeping",
11
+ "migration",
12
+ "import",
13
+ "parser"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Ekwo AI",
17
+ "homepage": "https://github.com/Ekwo-ai/ekwo-os/tree/main/packages/formats/journal-report#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Ekwo-ai/ekwo-os.git",
21
+ "directory": "packages/formats/journal-report"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/Ekwo-ai/ekwo-os/issues"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "main": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js"
42
+ }
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.build.json",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "test": "vitest run"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.0.0",
51
+ "typescript": "^5.9.0",
52
+ "vitest": "^3.2.0"
53
+ }
54
+ }