@ekwo-ai/coda 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ekwo AI
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,206 @@
1
+ # `@ekwo-ai/coda`
2
+
3
+ Reads a **CODA** file — the *coded statement of account* Belgian banks deliver,
4
+ records of 128 characters in fixed positions — into plain objects, in
5
+ TypeScript, with no dependencies.
6
+
7
+ Give it the file and it gives you the statements in it, their lines, and the
8
+ list of what does not add up.
9
+
10
+ ```ts
11
+ import { readCoda } from '@ekwo-ai/coda';
12
+
13
+ const { statements, violations } = readCoda(bytesOrString);
14
+
15
+ for (const statement of statements) {
16
+ statement.account.identifier; // { kind: 'iban', value } or { kind: 'other', value, scheme, issuer }
17
+ statement.openingBalance; // { type: 'OPBD', amount: '1000.00', currency: 'EUR', date: '2026-03-04' }
18
+ statement.closingBalance;
19
+ statement.balanced; // true, false, or null when it could not be checked
20
+ for (const line of statement.lines) {
21
+ line.amount; // '-450.50' — signed, a decimal string, never a float
22
+ line.bookingDate; // '2026-03-05'
23
+ line.counterparty; // { name, account, agentBic, ultimateName }
24
+ line.remittance; // { unstructured: [...], structured: [{ reference: '202600010704', type: 'SCOR', issuer: 'BBA' }] }
25
+ line.bankReference; // the bank's own reference for the movement
26
+ }
27
+ }
28
+ ```
29
+
30
+ It knows no database and no ledger. The shape it returns is declared in
31
+ [`src/types.ts`](./src/types.ts), imported from nowhere, and is **field for
32
+ field the shape [`@ekwo-ai/camt053`](../camt053/) declares for itself** — so
33
+ whatever takes a camt.053 that was read takes a CODA that was read, without an
34
+ adapter. What only CODA says comes after those fields, under its own names
35
+ (`sequenceNumber`, `detailNumber`, `transactionCode`, `globalisationCode`,
36
+ `structuredCommunication`, `information`, `freeCommunications`…).
37
+
38
+ A format is not a country. CODA happens to be Belgian the way Factur-X happens
39
+ to be French and German; nothing here assumes the euro, a Belgian account or a
40
+ language, and the tests read a pound and a dinar account.
41
+
42
+ ## What it does
43
+
44
+ - Reads **version 2** of the standard — records 0, 1, 2.1/2.2/2.3, 3.1/3.2/3.3,
45
+ 8, 4 and 9 — and refuses version 1 by name: under the same name it is another
46
+ lay-out.
47
+ - Returns **every statement** of the file. A physical file holds one CODA per
48
+ account and per day, one after the other, and each violation says which.
49
+ - Identifies the account by **what record 1 says it is**: an IBAN (structures 2
50
+ and 3) or a domestic number (0 and 1), with its currency. An IBAN is held
51
+ against ISO 13616 and returned as written when it fails (`invalid_iban`).
52
+ - Signs every amount from the account holder's side and returns it as a
53
+ **decimal string**. CODA writes twelve digits and **three decimals** whatever
54
+ the currency; the reader adds on integers of thousandths and returns two
55
+ decimals, and the third when the file wrote one that is not zero. Nothing is
56
+ rounded.
57
+ - Checks that **old balance + movements = new balance**, to the last decimal.
58
+ When it is not, the statement comes back as the bank wrote it, `balanced` is
59
+ `false`, and `balance_mismatch` says the difference. **Nothing is corrected.**
60
+ - Holds the **trailer record** against the file: the number of records 1, 2, 3
61
+ and 8, and the debit and credit totals, which the standard defines as the sum
62
+ of the movement records *with detail number 0000*
63
+ (`record_count_mismatch`, `debit_total_mismatch`, `credit_total_mismatch`). A
64
+ file that lost a record on the way fails all three at once.
65
+ - **Never counts a total and its details twice.** A movement is the record with
66
+ detail number `0000`; the records that follow under the same sequence number
67
+ are its details (types 5, 6, 7, 8, and 9 beneath a 7). The movement is
68
+ **split into its details when, and only when, they add up to it exactly** —
69
+ each line then keeps its own counterparty, communication and references, plus
70
+ `entry`, `detail`, `detailCount` and `entryAmount` to put it back together.
71
+ Otherwise it stays one line and `batch_not_split` says why. Either way the
72
+ lines add up to what moved on the account. It is the rule of the camt.053
73
+ reader, for the same reason: reconciliation wants the transaction, the
74
+ balance wants the movement.
75
+ - Returns the **Belgian structured communication** (types 101 and 102) as its
76
+ twelve digits with `type: 'SCOR'`, `issuer: 'BBA'` — which is how a camt.053
77
+ carries the same payment — **checks its modulo 97** (where a remainder of
78
+ zero is written 97) and, when it fails, **reports it and returns it as
79
+ written** (`invalid_structured_reference`). `formatBelgianReference()` gives
80
+ the `+++123/4567/89002+++` of the invoice. An ISO 11649 reference (type 100)
81
+ is treated the same way, with `issuer: 'ISO'`.
82
+ - Puts a **free communication back together** from the three records it is cut
83
+ across, and keeps it apart from a structured one.
84
+ - Reads the SEPA direct debit out of structured communication **127**: the
85
+ mandate, the communication, the reason.
86
+ - Keeps **every other structured communication as written**
87
+ (`structuredCommunication: { type, text }`) rather than half decoding it.
88
+ - Attaches the **information records** (3) to their movement: the free ones
89
+ make `additionalInformation`; type 001 names the counterparty when record 2.3
90
+ does not; 008 and 009 name the party behind it.
91
+
92
+ ## A statement is hostile input
93
+
94
+ It arrives by e-mail, from a download, from a third party's API.
95
+
96
+ - **Every record is 128 characters or the file is refused**
97
+ (`invalid_record_length`, with the record's number). Nothing is padded and
98
+ nothing is trimmed: a format of positions that is off by one is off for every
99
+ field after it. A file with no line break at all is read as consecutive
100
+ records, if its length is a whole number of them.
101
+ - **An unknown record is refused by name** (`unknown_record`), and so is a
102
+ known one out of place (`unexpected_record`): a 2.3 whose 2.1 is another
103
+ movement, a movement before the old balance, a second header before a
104
+ trailer. A file that stops before its trailer is `incomplete_statement`.
105
+ - **UTF-8, decoded fatally**, which reads the ASCII the standard writes.
106
+ `{ encoding: 'iso-8859-1' }` is said by the caller and **never guessed** —
107
+ every sequence of bytes is valid ISO-8859-1, so a guess could not fail. A
108
+ control character or a replacement character is `unsupported_encoding`: the
109
+ file was decoded wrongly by somebody already.
110
+ - **Size** is checked before a record is read (`maxBytes`, 32 MiB by default).
111
+
112
+ ## What is thrown, and what comes back as a violation
113
+
114
+ **Thrown** (`StatementFileError`, with a `code` and the `record` it stopped on):
115
+ `too_large`, `unsupported_encoding`, `empty_file`, `invalid_record_length`,
116
+ `unknown_record`, `unexpected_record`, `unsupported_version`,
117
+ `inconsistent_record` (the new balance is of another account than the old one),
118
+ `incomplete_statement`.
119
+
120
+ **Returned** in `violations`, with the statement and the line it is on:
121
+
122
+ | code | when |
123
+ |---|---|
124
+ | `balance_mismatch` | old balance + movements ≠ new balance; the message says the difference |
125
+ | `record_count_mismatch`, `debit_total_mismatch`, `credit_total_mismatch` | the trailer disagrees with the file |
126
+ | `closing_balance_missing` | no record 8 — which is how the standard writes a day without movement; nothing is invented |
127
+ | `separate_application` | the header says the file is an extract; its balances are zeroed by the standard and prove nothing |
128
+ | `currency_missing` | the account zone carries no currency; none is assumed |
129
+ | `invalid_amount`, `invalid_direction`, `invalid_date`, `booking_date_missing` | the line comes back with `null` there |
130
+ | `invalid_iban` | the account is announced as an IBAN and fails its check digits |
131
+ | `invalid_structured_reference` | a structured communication fails its own modulo 97 — returned as written |
132
+ | `batch_not_split` | see above |
133
+
134
+ ## What it does not do
135
+
136
+ - **It converts nothing, matches nothing, and does not decide what a duplicate
137
+ is.**
138
+ - **It does not explain the transaction code.** The eight digits are returned,
139
+ apart (`transactionCode`) and whole (`bankTransactionCode.proprietary`); the
140
+ sixty pages of annex II that say what each means are not in this package.
141
+ - **It decodes four structured communications** (100, 101, 102, 127) out of
142
+ some twenty-five. The others are returned as written.
143
+ - **It does not check the link and next codes** (positions 126 and 128): the
144
+ records that follow are what is read, not what was announced.
145
+ - **It does not read version 1**, and it does not read a year before the pivot
146
+ as this century: `YY` is 20YY under `pivotYear` (80 by default) and 19YY from
147
+ it.
148
+ - **It does not read Windows-1252.** A euro sign at byte 0x80 is a control
149
+ character of ISO-8859-1 and is refused; decode the file and pass the string.
150
+
151
+ ## The bank reference, and the same month in two formats
152
+
153
+ `bankReference` is the "reference number of the bank" of record 2.1. The
154
+ standard says of it (§ 7.4): *"purely informative … The bank may change this
155
+ reference without prior notice. Consequently, it is not recommendable to apply
156
+ any kind of programming in this field."* It is returned because it is what the
157
+ file says, blank or all zeros as `null`.
158
+
159
+ A reader does not decide what a duplicate is, but it should say what it gives
160
+ the one who does: **nothing in the standard promises that this reference is the
161
+ `AcctSvcrRef` the same bank writes in a camt.053 of the same day.** Where it
162
+ is, a core that keys a line on its bank reference will recognise it across the
163
+ two formats; where it is not, it will not.
164
+
165
+ ## Sources
166
+
167
+ - Febelfin, *Coded statement of account (CODA)*, **version 2.6** (February
168
+ 2018),
169
+ <https://febelfin.be/media/pages/publicaties/2021/gecodeerde-berichtgeving-coda/c580ddf2b4-1694763197/standard-coda-2.6-en.pdf>,
170
+ and **version 2.8** (November 2025),
171
+ <https://febelfin.be/media/pages/publicaties/2023/febelfin-standaarden-voor-online-bankieren/d7168c5c37-1764229602/standard-coda-en_-2025.pdf>
172
+ — both downloaded on 18 September 2026. The lay-out of the records (annex I)
173
+ is the same in both; 2.7 and 2.8 add card schemes and a tax category.
174
+ - ISO 13616 (IBAN), ISO 11649 (creditor reference), ISO 7064 (MOD 97-10).
175
+
176
+ ## What is verified, and what is not
177
+
178
+ **Verified**: every position this reader takes was written from annex I of the
179
+ standard, and the test builder (`test/build.ts`) was written from the same
180
+ annex, separately from the reader — so a fixture is a second reading of the
181
+ lay-out, and the two have to agree. The fixtures under `test/fixtures/` are
182
+ what the builder writes, compared on every run.
183
+
184
+ **Not verified** — and this is the larger half:
185
+
186
+ - **No file from a bank has been read.** Every statement here is invented,
187
+ because a real one is somebody's account. Reader and builder share an author
188
+ and a reading of the standard: where that reading is wrong, both are, and the
189
+ tests pass. *The first real file from each bank is a test nobody has run.*
190
+ - **How banks number details.** The standard has the detail number go up for
191
+ each record 2.1 *and* each record 3.1 of a movement; this reader relies on
192
+ the sequence number and on `0000` only, and on the type digit of the
193
+ transaction code for what is a detail of what.
194
+ - **The globalisation code** (position 125) is returned and not used. The
195
+ hierarchy is read from the transaction type, which the trailer's own
196
+ definition of its totals supports; a bank that globalises otherwise would
197
+ show as `batch_not_split`, not as a wrong balance.
198
+ - **Whether details always add up.** Where they do not — a detail given for
199
+ information, a tax shown and not charged — the movement stays one line and
200
+ says so. That is safe, and it is not the same as understood.
201
+ - **The counterparty's account** "comes in the structure of the initial
202
+ payment": it is an IBAN when it passes ISO 13616, else what was written.
203
+
204
+ ## Licence
205
+
206
+ MIT. See [`LICENSE`](./LICENSE).
@@ -0,0 +1,12 @@
1
+ /** ISO 13616: move four characters to the end, letters to numbers, mod 97 is 1. */
2
+ export declare function isValidIban(iban: string): boolean;
3
+ /** ISO 11649: `RF`, two check digits, up to twenty-one characters; same arithmetic as an IBAN. */
4
+ export declare function isValidCreditorReference(reference: string): boolean;
5
+ /**
6
+ * The Belgian structured communication: twelve digits, the last two being the
7
+ * first ten modulo 97 — and 97 when that is zero, never 00.
8
+ */
9
+ export declare function isValidBelgianReference(digits: string): boolean;
10
+ /** `123456789002` as it is printed on an invoice: `+++123/4567/89002+++`. Twelve digits or null. */
11
+ export declare function formatBelgianReference(digits: string): string | null;
12
+ //# sourceMappingURL=checks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AAWA,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGjD;AAED,kGAAkG;AAClG,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAGnE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAI/D;AAED,oGAAoG;AACpG,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGpE"}
package/dist/checks.js ADDED
@@ -0,0 +1,39 @@
1
+ /** ISO 7064 MOD 97-10 over a string of digits and letters (A = 10 … Z = 35). */
2
+ function mod97(text) {
3
+ let remainder = 0;
4
+ for (const character of text) {
5
+ const code = character.charCodeAt(0);
6
+ const value = code >= 65 ? code - 55 : code - 48;
7
+ remainder = (value > 9 ? remainder * 100 + value : remainder * 10 + value) % 97;
8
+ }
9
+ return remainder;
10
+ }
11
+ /** ISO 13616: move four characters to the end, letters to numbers, mod 97 is 1. */
12
+ export function isValidIban(iban) {
13
+ if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/.test(iban))
14
+ return false;
15
+ return mod97(iban.slice(4) + iban.slice(0, 4)) === 1;
16
+ }
17
+ /** ISO 11649: `RF`, two check digits, up to twenty-one characters; same arithmetic as an IBAN. */
18
+ export function isValidCreditorReference(reference) {
19
+ if (!/^RF[0-9]{2}[A-Z0-9]{1,21}$/.test(reference))
20
+ return false;
21
+ return mod97(reference.slice(4) + reference.slice(0, 4)) === 1;
22
+ }
23
+ /**
24
+ * The Belgian structured communication: twelve digits, the last two being the
25
+ * first ten modulo 97 — and 97 when that is zero, never 00.
26
+ */
27
+ export function isValidBelgianReference(digits) {
28
+ if (!/^[0-9]{12}$/.test(digits))
29
+ return false;
30
+ const check = Number(BigInt(digits.slice(0, 10)) % 97n) || 97;
31
+ return check === Number(digits.slice(10));
32
+ }
33
+ /** `123456789002` as it is printed on an invoice: `+++123/4567/89002+++`. Twelve digits or null. */
34
+ export function formatBelgianReference(digits) {
35
+ if (!/^[0-9]{12}$/.test(digits))
36
+ return null;
37
+ return `+++${digits.slice(0, 3)}/${digits.slice(3, 7)}/${digits.slice(7)}+++`;
38
+ }
39
+ //# sourceMappingURL=checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks.js","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,SAAS,KAAK,CAAC,IAAY;IACzB,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,KAAK,MAAM,SAAS,IAAI,IAAI,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;QACjD,SAAS,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC;IAClF,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,IAAI,CAAC,kCAAkC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACjE,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACvD,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,wBAAwB,CAAC,SAAiB;IACxD,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAChE,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9D,OAAO,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,OAAO,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AAChF,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Amounts as the file wrote them, added without a float.
3
+ *
4
+ * A CODA amount is fifteen digits: twelve and **three decimals**, whatever the
5
+ * currency, unsigned — the sign is another position of the record. Sums are
6
+ * made on a `bigint` of thousandths, and what leaves is a string.
7
+ */
8
+ /** Fifteen digits as thousandths, or null when they are not fifteen digits. */
9
+ export declare function parseAmount(field: string): bigint | null;
10
+ /**
11
+ * Thousandths as a decimal string: signed, no leading zeros, two decimals —
12
+ * and the third when the file wrote one that is not zero. Dropping a written
13
+ * zero is not rounding; dropping anything else would be, and is never done.
14
+ */
15
+ export declare function formatAmount(value: bigint): string;
16
+ //# sourceMappingURL=decimal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decimal.d.ts","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Amounts as the file wrote them, added without a float.
3
+ *
4
+ * A CODA amount is fifteen digits: twelve and **three decimals**, whatever the
5
+ * currency, unsigned — the sign is another position of the record. Sums are
6
+ * made on a `bigint` of thousandths, and what leaves is a string.
7
+ */
8
+ const DIGITS = /^[0-9]{15}$/;
9
+ /** Fifteen digits as thousandths, or null when they are not fifteen digits. */
10
+ export function parseAmount(field) {
11
+ return DIGITS.test(field) ? BigInt(field) : null;
12
+ }
13
+ /**
14
+ * Thousandths as a decimal string: signed, no leading zeros, two decimals —
15
+ * and the third when the file wrote one that is not zero. Dropping a written
16
+ * zero is not rounding; dropping anything else would be, and is never done.
17
+ */
18
+ export function formatAmount(value) {
19
+ const negative = value < 0n;
20
+ const digits = (negative ? -value : value).toString().padStart(4, '0');
21
+ const whole = digits.slice(0, -3);
22
+ const fraction = digits.slice(-3).replace(/0$/, '');
23
+ return `${negative ? '-' : ''}${whole}.${fraction}`;
24
+ }
25
+ //# sourceMappingURL=decimal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decimal.js","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,GAAG,aAAa,CAAC;AAE7B,+EAA+E;AAC/E,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,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,CAAC,EAAE,GAAG,CAAC,CAAC;IACvE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACpD,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAC;AACtD,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * What makes a file unreadable, as opposed to a statement that reads and does
3
+ * not add up. The second comes back in `violations`; the first is thrown,
4
+ * because a format of fixed positions that is off by one character is off for
5
+ * every field after it, and there is no statement to hang a violation on.
6
+ */
7
+ export type StatementFileErrorCode =
8
+ /** Larger than `maxBytes`. Refused before a single character is read. */
9
+ 'too_large'
10
+ /** Bytes that are not the encoding announced, a replacement or a control character. */
11
+ | 'unsupported_encoding'
12
+ /** Nothing in the file. */
13
+ | 'empty_file'
14
+ /** A record that is not 128 characters long. */
15
+ | 'invalid_record_length'
16
+ /** A record whose identification is none of 0, 1, 2, 3, 4, 8, 9 — or an article that is none of 1, 2, 3. */
17
+ | 'unknown_record'
18
+ /** A known record where the format does not allow it: a 2.2 without its 2.1, a line before the old balance. */
19
+ | 'unexpected_record'
20
+ /** A header whose version code is not 2: version 1 is another lay-out under the same name. */
21
+ | 'unsupported_version'
22
+ /** Two records of one statement that disagree on the account it is the statement of. */
23
+ | 'inconsistent_record'
24
+ /** The file stops before the trailer record of a statement it began. */
25
+ | 'incomplete_statement';
26
+ export declare class StatementFileError extends Error {
27
+ readonly code: StatementFileErrorCode;
28
+ /** The record the reader stopped on, from 1, when there is one. */
29
+ readonly record: number | null;
30
+ constructor(code: StatementFileErrorCode, message: string, record?: number | null);
31
+ }
32
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB;AAChC,yEAAyE;AACvE,WAAW;AACb,uFAAuF;GACrF,sBAAsB;AACxB,2BAA2B;GACzB,YAAY;AACd,gDAAgD;GAC9C,uBAAuB;AACzB,4GAA4G;GAC1G,gBAAgB;AAClB,+GAA+G;GAC7G,mBAAmB;AACrB,8FAA8F;GAC5F,qBAAqB;AACvB,wFAAwF;GACtF,qBAAqB;AACvB,wEAAwE;GACtE,sBAAsB,CAAC;AAE3B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEnB,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,GAAG,IAAW;CAMxF"}
package/dist/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ export class StatementFileError extends Error {
2
+ code;
3
+ /** The record the reader stopped on, from 1, when there is one. */
4
+ record;
5
+ constructor(code, message, record = null) {
6
+ super(record === null ? message : `record ${record}: ${message}`);
7
+ this.name = 'StatementFileError';
8
+ this.code = code;
9
+ this.record = record;
10
+ }
11
+ }
12
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AA0BA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,CAAyB;IACtC,mEAAmE;IAC1D,MAAM,CAAgB;IAE/B,YAAY,IAA4B,EAAE,OAAe,EAAE,SAAwB,IAAI;QACrF,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,MAAM,KAAK,OAAO,EAAE,CAAC,CAAC;QAClE,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,19 @@
1
+ import type { ReadOptions, StatementFile } from './types.js';
2
+ export { formatBelgianReference, isValidBelgianReference, isValidCreditorReference, isValidIban } from './checks.js';
3
+ export { StatementFileError, type StatementFileErrorCode } from './errors.js';
4
+ export type * from './types.js';
5
+ /** Every record of a CODA file is this long, the line break not counted. */
6
+ export declare const RECORD_LENGTH = 128;
7
+ /**
8
+ * Read a CODA file — a string, or the bytes as they came — into its
9
+ * statements, their lines, and what does not add up.
10
+ *
11
+ * Throws a `StatementFileError` for what is not a readable CODA at all: a
12
+ * record of the wrong length, an unknown record, a record out of place, an
13
+ * encoding that is not the one announced. Everything else comes back, with the
14
+ * trouble named in `violations` and nothing silently repaired: a new balance
15
+ * that does not follow from the old one is returned as the bank wrote it, and
16
+ * reported.
17
+ */
18
+ export declare function readCoda(input: string | Uint8Array, options?: ReadOptions): StatementFile;
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAIV,WAAW,EAIX,aAAa,EAId,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACrH,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC9E,mBAAmB,YAAY,CAAC;AAEhC,4EAA4E;AAC5E,eAAO,MAAM,aAAa,MAAM,CAAC;AA8oBjC;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAQ7F"}