@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 +21 -0
- package/README.md +206 -0
- package/dist/checks.d.ts +12 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +39 -0
- package/dist/checks.js.map +1 -0
- package/dist/decimal.d.ts +16 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +25 -0
- package/dist/decimal.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +546 -0
- package/dist/index.js.map +1 -0
- package/dist/records.d.ts +17 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +91 -0
- package/dist/records.js.map +1 -0
- package/dist/types.d.ts +254 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
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).
|
package/dist/checks.d.ts
ADDED
|
@@ -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"}
|
package/dist/decimal.js
ADDED
|
@@ -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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|