@ekwo-ai/cfonb120 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 +184 -0
- package/dist/checks.d.ts +18 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +54 -0
- package/dist/checks.js.map +1 -0
- package/dist/decimal.d.ts +20 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +40 -0
- package/dist/decimal.js.map +1 -0
- package/dist/errors.d.ts +30 -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 +343 -0
- package/dist/index.js.map +1 -0
- package/dist/records.d.ts +18 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +92 -0
- package/dist/records.js.map +1 -0
- package/dist/types.d.ts +239 -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,184 @@
|
|
|
1
|
+
# `@ekwo-ai/cfonb120`
|
|
2
|
+
|
|
3
|
+
Reads a **CFONB 120** file — the *relevé de compte sur support informatique*
|
|
4
|
+
French banks deliver, records of 120 characters in fixed positions — into plain
|
|
5
|
+
objects, in 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 { readCfonb120 } from '@ekwo-ai/cfonb120';
|
|
12
|
+
|
|
13
|
+
const { statements, violations } = readCfonb120(bytesOrString, { ibanCountry: 'FR' });
|
|
14
|
+
|
|
15
|
+
for (const statement of statements) {
|
|
16
|
+
statement.account.identifier; // { kind: 'iban', value } — or { kind: 'other', value } when no country was named
|
|
17
|
+
statement.openingBalance; // { type: 'OPBD', amount: '1000.00', currency: 'EUR', date: '2026-02-28' }
|
|
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-06'
|
|
23
|
+
line.counterparty; // { name, account, agentBic, ultimateName }
|
|
24
|
+
line.remittance; // { unstructured: [...], structured: [{ reference, type, issuer }] }
|
|
25
|
+
line.additionalInformation; // the label the bank gave 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 CFONB 120 that was read, without
|
|
34
|
+
an adapter. What only this format says comes after those fields, under its own
|
|
35
|
+
names (`interbankOperationCode`, `entryNumber`, `referenceZone`, `unavailable`,
|
|
36
|
+
`complements`…).
|
|
37
|
+
|
|
38
|
+
A format is not a country. Nothing here assumes the euro, two decimals or a
|
|
39
|
+
language — and the format itself **carries no country**, which is why this
|
|
40
|
+
reader does not supply one (see *The account* below).
|
|
41
|
+
|
|
42
|
+
## What it does
|
|
43
|
+
|
|
44
|
+
- Reads records **01** (old balance), **04** (movement), **05** (complement)
|
|
45
|
+
and **07** (new balance), and returns **every statement** of the file.
|
|
46
|
+
- Reads the **sign written over the last digit** of an amount — `{` and `A` to
|
|
47
|
+
`I` for 0 to 9 of a credit, `}` and `J` to `R` for 0 to 9 of a debit — and
|
|
48
|
+
the **number of decimals from the record** (position 20). Amounts come back
|
|
49
|
+
as decimal strings with exactly that many decimals: `150000` for a yen
|
|
50
|
+
account, `1.001` for a dinar one. Sums are made on integers; nothing is
|
|
51
|
+
rounded and nothing knows that a euro has cents.
|
|
52
|
+
- Checks that **old balance + movements = new balance**. When it is not, the
|
|
53
|
+
statement comes back as the bank wrote it, `balanced` is `false`, and
|
|
54
|
+
`balance_mismatch` says the difference. **Nothing is corrected.**
|
|
55
|
+
- Reads the **complements** the 2010 addendum standardised for SEPA: payer and
|
|
56
|
+
beneficiary (`NPY`, `NBE`), their identifiers (`IPY`, `IBE`), the parties
|
|
57
|
+
behind them (`NPO`, `NBU`), their accounts (`CPY`, `CBE`), the remittance
|
|
58
|
+
(`LCC` + `LC2`, put back into the one text they were cut from; `LCS`,
|
|
59
|
+
structured, kept apart), the end-to-end reference and purpose (`RCN`), the
|
|
60
|
+
payment information and instruction identifiers (`REF`), the mandate and its
|
|
61
|
+
sequence (`RUM`), the amount of origin and the rate (`MMO`, returned beside
|
|
62
|
+
what moved, **never converted**), and free lines (`LIB`). Every complement,
|
|
63
|
+
a bank's own qualifiers included, is also returned as written
|
|
64
|
+
(`complements`).
|
|
65
|
+
- Names the **counterparty** as the payer of money coming in and the
|
|
66
|
+
beneficiary of money going out — and the other way round when the movement
|
|
67
|
+
carries a reject code, where the parties keep the roles they had in the
|
|
68
|
+
original.
|
|
69
|
+
- Reports a movement **booked outside its statement**
|
|
70
|
+
(`booking_date_outside_statement`): the brochure defines a statement as the
|
|
71
|
+
movements booked strictly after the date of the old balance and up to the
|
|
72
|
+
date of the new one.
|
|
73
|
+
|
|
74
|
+
## The account, and the country nobody wrote
|
|
75
|
+
|
|
76
|
+
A CFONB 120 identifies an account by a **bank code, a branch code and an
|
|
77
|
+
account number**. It carries no IBAN and **no country**: the same lay-out is
|
|
78
|
+
used wherever that way of numbering accounts is. So:
|
|
79
|
+
|
|
80
|
+
- by default the identifier is `{ kind: 'other', value }`, the three joined, as
|
|
81
|
+
written — and `statement.account` gives them apart;
|
|
82
|
+
- with `{ ibanCountry: 'FR' }` — or any other two letters — it is the IBAN
|
|
83
|
+
those three make **in the country the caller named**: the key of the relevé
|
|
84
|
+
d'identité bancaire is computed (`ribKey()`), then the check digits of
|
|
85
|
+
ISO 13616 (`ibanOf()`). Parts that cannot make one are `invalid_iban`, and
|
|
86
|
+
the account comes back as written.
|
|
87
|
+
|
|
88
|
+
The reader never picks the country. Whoever imports the file knows where the
|
|
89
|
+
account is held; the file does not say.
|
|
90
|
+
|
|
91
|
+
## A statement is hostile input
|
|
92
|
+
|
|
93
|
+
- **Every record is 120 characters or the file is refused**
|
|
94
|
+
(`invalid_record_length`, with the record's number). Nothing is padded:
|
|
95
|
+
a file whose trailing blanks were trimmed on the way is refused, because a
|
|
96
|
+
reader that pads cannot tell a trimmed record from a truncated one. A file
|
|
97
|
+
with no line break at all — common for this format — is read as consecutive
|
|
98
|
+
records, if its length is a whole number of them.
|
|
99
|
+
- **An unknown record code is refused by name** (`unknown_record`), and so is a
|
|
100
|
+
known one out of place (`unexpected_record`). A file that stops before a new
|
|
101
|
+
balance is `incomplete_statement`.
|
|
102
|
+
- **A record of another account in the middle of a statement is refused**
|
|
103
|
+
(`inconsistent_record`): the brochure says bank, branch, currency, decimals
|
|
104
|
+
and account are identical in every record of one statement. That is also
|
|
105
|
+
what refuses a movement in another currency, or on another scale.
|
|
106
|
+
- **UTF-8, decoded fatally**, which reads the ASCII the brochure allows.
|
|
107
|
+
`{ encoding: 'iso-8859-1' }` is said by the caller and never guessed.
|
|
108
|
+
**EBCDIC**, which the brochure also describes, is `unsupported_encoding`.
|
|
109
|
+
- **Size** is checked before a record is read (`maxBytes`, 32 MiB by default).
|
|
110
|
+
|
|
111
|
+
## What is thrown, and what comes back as a violation
|
|
112
|
+
|
|
113
|
+
**Thrown** (`StatementFileError`, with a `code` and the `record` it stopped on):
|
|
114
|
+
`too_large`, `unsupported_encoding`, `empty_file`, `invalid_record_length`,
|
|
115
|
+
`unknown_record`, `unexpected_record`, `inconsistent_record`,
|
|
116
|
+
`incomplete_statement`.
|
|
117
|
+
|
|
118
|
+
**Returned** in `violations`, with the statement and the line it is on:
|
|
119
|
+
|
|
120
|
+
| code | when |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `balance_mismatch` | old balance + movements ≠ new balance; the message says the difference |
|
|
123
|
+
| `invalid_amount`, `invalid_date`, `booking_date_missing` | the line comes back with `null` there |
|
|
124
|
+
| `booking_date_outside_statement` | see above |
|
|
125
|
+
| `invalid_iban` | `ibanCountry` was given and the account's parts do not make an IBAN |
|
|
126
|
+
|
|
127
|
+
## What it does not do
|
|
128
|
+
|
|
129
|
+
- **It gives a movement no bank reference, because the format has none.**
|
|
130
|
+
`bankReference` is always `null`. The "numéro d'écriture" is a cheque or
|
|
131
|
+
remittance number, or zeros; the `REF` complement carries the payer's
|
|
132
|
+
references, not the bank's; the reference zone is the ordering party's. A
|
|
133
|
+
core that recognises a line by the reference of the bank will recognise no
|
|
134
|
+
line of a CFONB 120 in a camt.053 of the same month — it will import both.
|
|
135
|
+
That is a property of the format, said here rather than papered over.
|
|
136
|
+
- **It gives a statement no number**, for the same reason: its `id` is made of
|
|
137
|
+
the dates of its two balances.
|
|
138
|
+
- **It converts nothing, matches nothing, and does not decide what a duplicate
|
|
139
|
+
is.**
|
|
140
|
+
- **It does not explain the operation codes** (interbank or internal), nor the
|
|
141
|
+
reject codes: they are returned as written.
|
|
142
|
+
- **It does not parse the label** (zone 2-M), whose recommended structure
|
|
143
|
+
(chapter 5 of the brochure) differs by payment instrument and is a
|
|
144
|
+
recommendation.
|
|
145
|
+
- **It does not read EBCDIC, the 240-character format, or Windows-1252.**
|
|
146
|
+
|
|
147
|
+
## Sources
|
|
148
|
+
|
|
149
|
+
- CFONB, *Relevé de compte sur support informatique* (brochure, July 2004),
|
|
150
|
+
<https://www.cfonb.org/fichiers/20130612113947_7_4_Releve_de_Compte_sur_support_informatique_2004_07.pdf>.
|
|
151
|
+
- CFONB, *Évolutions du relevé de compte 120 caractères pour les opérations de
|
|
152
|
+
virements et de prélèvements SEPA*, version 2.0 (March 2010),
|
|
153
|
+
<https://www.cfonb.org/files/fichiers/20130612114053_7_8_Evol_Releve_Cpt_120_caract._ope._SCT_et_SDD_sepa_V2_0_2010_03.pdf>.
|
|
154
|
+
Both downloaded on 18 September 2026.
|
|
155
|
+
- ISO 13616 (IBAN), ISO 7064 (MOD 97-10), ISO 4217 (currency codes).
|
|
156
|
+
|
|
157
|
+
## What is verified, and what is not
|
|
158
|
+
|
|
159
|
+
**Verified**: every position this reader takes was written from the tables of
|
|
160
|
+
the two documents above, and the test builder (`test/build.ts`) was written
|
|
161
|
+
from the same tables, separately from the reader. The fixtures under
|
|
162
|
+
`test/fixtures/` are what the builder writes, compared on every run. The twenty
|
|
163
|
+
characters that carry a digit and a sign are each read.
|
|
164
|
+
|
|
165
|
+
**Not verified** — and this is the larger half:
|
|
166
|
+
|
|
167
|
+
- **No file from a bank has been read.** Every statement here is invented.
|
|
168
|
+
Reader and builder share an author and a reading of the brochure: where that
|
|
169
|
+
reading is wrong, both are, and the tests pass. *The first real file from
|
|
170
|
+
each bank is a test nobody has run.*
|
|
171
|
+
- **How strictly banks keep to 120.** Files whose trailing blanks were trimmed
|
|
172
|
+
exist; this reader refuses them by name rather than guess what was there.
|
|
173
|
+
- **The counterparty of a rejected operation** — that the parties keep their
|
|
174
|
+
original roles when a reject code is present — is this package's reading,
|
|
175
|
+
taken from how the camt.053 reader treats a reversal. Banks may differ.
|
|
176
|
+
- **The key of the relevé d'identité bancaire** is computed from its published
|
|
177
|
+
rule and held against ISO 13616 in the tests; it is not held against a real
|
|
178
|
+
account, because none is in this repository.
|
|
179
|
+
- **Which qualifiers banks really send.** The 2010 addendum lists them; banks
|
|
180
|
+
add their own, which are returned as written and otherwise ignored.
|
|
181
|
+
|
|
182
|
+
## Licence
|
|
183
|
+
|
|
184
|
+
MIT. See [`LICENSE`](./LICENSE).
|
package/dist/checks.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
/**
|
|
4
|
+
* The two-digit key that completes a bank code, a branch code and an account
|
|
5
|
+
* number into the twenty-three characters printed on a relevé d'identité
|
|
6
|
+
* bancaire. A letter of the account number counts as a digit: A and J as 1,
|
|
7
|
+
* B, K and S as 2, … I, R and Z as 9. Null when the three parts are not what
|
|
8
|
+
* the format says they are.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ribKey(bankCode: string, branchCode: string, accountNumber: string): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* The IBAN of an account a CFONB 120 identifies, **in the country the caller
|
|
13
|
+
* names**: the format carries a bank, a branch and an account, and no country —
|
|
14
|
+
* the same lay-out serves more than one. Null when the parts are not what the
|
|
15
|
+
* format says, or the country is not two letters.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ibanOf(country: string, bankCode: string, branchCode: string, accountNumber: string): string | null;
|
|
18
|
+
//# 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;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcjG;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB,MAAM,GAAG,IAAI,CAMf"}
|
package/dist/checks.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* The two-digit key that completes a bank code, a branch code and an account
|
|
19
|
+
* number into the twenty-three characters printed on a relevé d'identité
|
|
20
|
+
* bancaire. A letter of the account number counts as a digit: A and J as 1,
|
|
21
|
+
* B, K and S as 2, … I, R and Z as 9. Null when the three parts are not what
|
|
22
|
+
* the format says they are.
|
|
23
|
+
*/
|
|
24
|
+
export function ribKey(bankCode, branchCode, accountNumber) {
|
|
25
|
+
if (!/^[0-9]{5}$/.test(bankCode) || !/^[0-9]{5}$/.test(branchCode) || !/^[0-9A-Z]{11}$/.test(accountNumber)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const numeric = [...accountNumber]
|
|
29
|
+
.map((character) => {
|
|
30
|
+
const code = character.charCodeAt(0);
|
|
31
|
+
if (code < 65)
|
|
32
|
+
return character;
|
|
33
|
+
const rank = code - 65; // A = 0
|
|
34
|
+
return String(rank < 9 ? rank + 1 : rank < 18 ? rank - 8 : rank - 16);
|
|
35
|
+
})
|
|
36
|
+
.join('');
|
|
37
|
+
const sum = 89n * BigInt(bankCode) + 15n * BigInt(branchCode) + 3n * BigInt(numeric);
|
|
38
|
+
return String(97n - (sum % 97n)).padStart(2, '0');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The IBAN of an account a CFONB 120 identifies, **in the country the caller
|
|
42
|
+
* names**: the format carries a bank, a branch and an account, and no country —
|
|
43
|
+
* the same lay-out serves more than one. Null when the parts are not what the
|
|
44
|
+
* format says, or the country is not two letters.
|
|
45
|
+
*/
|
|
46
|
+
export function ibanOf(country, bankCode, branchCode, accountNumber) {
|
|
47
|
+
const key = ribKey(bankCode, branchCode, accountNumber);
|
|
48
|
+
if (key === null || !/^[A-Z]{2}$/.test(country))
|
|
49
|
+
return null;
|
|
50
|
+
const bban = bankCode + branchCode + accountNumber + key;
|
|
51
|
+
const check = String(98 - mod97(`${bban}${country}00`)).padStart(2, '0');
|
|
52
|
+
return `${country}${check}${bban}`;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amounts as the file wrote them, added without a float.
|
|
3
|
+
*
|
|
4
|
+
* A CFONB 120 amount is fourteen positions: thirteen digits, and a last
|
|
5
|
+
* character that is a digit **and** the sign, written over one another the way
|
|
6
|
+
* punched cards did. `{` and `A` to `I` are 0 to 9 of a credit; `}` and `J` to
|
|
7
|
+
* `R` are 0 to 9 of a debit. How many of the fourteen digits are decimals is
|
|
8
|
+
* said by another position of the same record, and read from there: nothing
|
|
9
|
+
* here knows that a euro has cents.
|
|
10
|
+
*/
|
|
11
|
+
/** The fourteen positions as a signed integer of minor units, or null when they are not an amount. */
|
|
12
|
+
export declare function parseSigned(field: string): bigint | null;
|
|
13
|
+
/** Fourteen digits, unsigned: the amount of origin of a `MMO` complement. */
|
|
14
|
+
export declare function parseUnsigned(field: string): bigint | null;
|
|
15
|
+
/**
|
|
16
|
+
* Minor units as a decimal string with exactly the decimals the record
|
|
17
|
+
* announced: signed, no leading zeros, no exponent, nothing rounded.
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatAmount(value: bigint, decimals: number): string;
|
|
20
|
+
//# sourceMappingURL=decimal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decimal.d.ts","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQxD;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE1D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMpE"}
|
package/dist/decimal.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amounts as the file wrote them, added without a float.
|
|
3
|
+
*
|
|
4
|
+
* A CFONB 120 amount is fourteen positions: thirteen digits, and a last
|
|
5
|
+
* character that is a digit **and** the sign, written over one another the way
|
|
6
|
+
* punched cards did. `{` and `A` to `I` are 0 to 9 of a credit; `}` and `J` to
|
|
7
|
+
* `R` are 0 to 9 of a debit. How many of the fourteen digits are decimals is
|
|
8
|
+
* said by another position of the same record, and read from there: nothing
|
|
9
|
+
* here knows that a euro has cents.
|
|
10
|
+
*/
|
|
11
|
+
const POSITIVE = '{ABCDEFGHI';
|
|
12
|
+
const NEGATIVE = '}JKLMNOPQR';
|
|
13
|
+
/** The fourteen positions as a signed integer of minor units, or null when they are not an amount. */
|
|
14
|
+
export function parseSigned(field) {
|
|
15
|
+
if (!/^[0-9]{13}.$/.test(field))
|
|
16
|
+
return null;
|
|
17
|
+
const last = field.charAt(13);
|
|
18
|
+
const positive = POSITIVE.indexOf(last);
|
|
19
|
+
const negative = NEGATIVE.indexOf(last);
|
|
20
|
+
if (positive < 0 && negative < 0)
|
|
21
|
+
return null;
|
|
22
|
+
const value = BigInt(field.slice(0, 13) + String(positive >= 0 ? positive : negative));
|
|
23
|
+
return negative >= 0 ? -value : value;
|
|
24
|
+
}
|
|
25
|
+
/** Fourteen digits, unsigned: the amount of origin of a `MMO` complement. */
|
|
26
|
+
export function parseUnsigned(field) {
|
|
27
|
+
return /^[0-9]+$/.test(field) ? BigInt(field) : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Minor units as a decimal string with exactly the decimals the record
|
|
31
|
+
* announced: signed, no leading zeros, no exponent, nothing rounded.
|
|
32
|
+
*/
|
|
33
|
+
export function formatAmount(value, decimals) {
|
|
34
|
+
const negative = value < 0n;
|
|
35
|
+
const digits = (negative ? -value : value).toString().padStart(decimals + 1, '0');
|
|
36
|
+
const whole = digits.slice(0, digits.length - decimals);
|
|
37
|
+
const fraction = decimals === 0 ? '' : `.${digits.slice(-decimals)}`;
|
|
38
|
+
return `${negative ? '-' : ''}${whole}${fraction}`;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=decimal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decimal.js","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,QAAQ,GAAG,YAAY,CAAC;AAC9B,MAAM,QAAQ,GAAG,YAAY,CAAC;AAE9B,sGAAsG;AACtG,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,QAAQ,GAAG,CAAC,IAAI,QAAQ,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvF,OAAO,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AACxC,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,QAAgB;IAC1D,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,QAAQ,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IAClF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;IACrE,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,GAAG,QAAQ,EAAE,CAAC;AACrD,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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 — EBCDIC among them —, a replacement or a control character. */
|
|
11
|
+
| 'unsupported_encoding'
|
|
12
|
+
/** Nothing in the file. */
|
|
13
|
+
| 'empty_file'
|
|
14
|
+
/** A record that is not 120 characters long. */
|
|
15
|
+
| 'invalid_record_length'
|
|
16
|
+
/** A record whose code is none of 01, 04, 05, 07. */
|
|
17
|
+
| 'unknown_record'
|
|
18
|
+
/** A known record where the format does not allow it: a movement before an old balance, a complement without its movement. */
|
|
19
|
+
| 'unexpected_record'
|
|
20
|
+
/** A record that is of another bank, branch, currency, number of decimals or account than the old balance of its statement. */
|
|
21
|
+
| 'inconsistent_record'
|
|
22
|
+
/** The file stops before the new balance of a statement it began, or an old balance names no account. */
|
|
23
|
+
| 'incomplete_statement';
|
|
24
|
+
export declare class StatementFileError extends Error {
|
|
25
|
+
readonly code: StatementFileErrorCode;
|
|
26
|
+
/** The record the reader stopped on, from 1, when there is one. */
|
|
27
|
+
readonly record: number | null;
|
|
28
|
+
constructor(code: StatementFileErrorCode, message: string, record?: number | null);
|
|
29
|
+
}
|
|
30
|
+
//# 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,6GAA6G;GAC3G,sBAAsB;AACxB,2BAA2B;GACzB,YAAY;AACd,gDAAgD;GAC9C,uBAAuB;AACzB,qDAAqD;GACnD,gBAAgB;AAClB,8HAA8H;GAC5H,mBAAmB;AACrB,+HAA+H;GAC7H,qBAAqB;AACvB,yGAAyG;GACvG,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":"AAwBA,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 { ibanOf, isValidIban, ribKey } from './checks.js';
|
|
3
|
+
export { StatementFileError, type StatementFileErrorCode } from './errors.js';
|
|
4
|
+
export type * from './types.js';
|
|
5
|
+
/** Every record of a CFONB 120 file is this long, the line break not counted. */
|
|
6
|
+
export declare const RECORD_LENGTH = 120;
|
|
7
|
+
/**
|
|
8
|
+
* Read a CFONB 120 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 CFONB 120 at all: a
|
|
12
|
+
* record of the wrong length, an unknown record, a record out of place or of
|
|
13
|
+
* another account, an encoding that is not the one announced. Everything else
|
|
14
|
+
* comes back, with the trouble named in `violations` and nothing silently
|
|
15
|
+
* repaired: a new balance that does not follow from the old one is returned as
|
|
16
|
+
* the bank wrote it, and reported.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readCfonb120(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":"AAIA,OAAO,KAAK,EAGV,WAAW,EAIX,aAAa,EAId,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC9E,mBAAmB,YAAY,CAAC;AAEhC,iFAAiF;AACjF,eAAO,MAAM,aAAa,MAAM,CAAC;AAmYjC;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAUjG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { ibanOf, isValidIban } from './checks.js';
|
|
2
|
+
import { formatAmount, parseSigned, parseUnsigned } from './decimal.js';
|
|
3
|
+
import { StatementFileError } from './errors.js';
|
|
4
|
+
import { decode, splitRecords } from './records.js';
|
|
5
|
+
export { ibanOf, isValidIban, ribKey } from './checks.js';
|
|
6
|
+
export { StatementFileError } from './errors.js';
|
|
7
|
+
/** Every record of a CFONB 120 file is this long, the line break not counted. */
|
|
8
|
+
export const RECORD_LENGTH = 120;
|
|
9
|
+
const DEFAULTS = { maxBytes: 32 * 1024 * 1024, pivotYear: 80 };
|
|
10
|
+
// --- positions ---------------------------------------------------------------
|
|
11
|
+
/** Positions `from` to `to` of a record, counted from 1 and inclusive, as the brochure writes them. */
|
|
12
|
+
function at(record, from, to = from) {
|
|
13
|
+
return record.slice(from - 1, to);
|
|
14
|
+
}
|
|
15
|
+
/** A zone as text: blanks around it gone, null when nothing is left. */
|
|
16
|
+
function zone(record, from, to) {
|
|
17
|
+
const value = at(record, from, to).trim();
|
|
18
|
+
return value === '' ? null : value;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Bank, branch, currency, decimals and account: zones B and D to H, which the
|
|
22
|
+
* brochure says are identical in every record of one statement. Zone G is
|
|
23
|
+
* reserved, and left out: a bank that writes something there is not another
|
|
24
|
+
* account.
|
|
25
|
+
*/
|
|
26
|
+
function identity(record) {
|
|
27
|
+
return `${at(record, 3, 7)}|${at(record, 12, 20)}|${at(record, 22, 32)}`;
|
|
28
|
+
}
|
|
29
|
+
function gather(records) {
|
|
30
|
+
const out = [];
|
|
31
|
+
let current = null;
|
|
32
|
+
let previous = '';
|
|
33
|
+
records.forEach((record, index) => {
|
|
34
|
+
const code = at(record, 1, 2);
|
|
35
|
+
const refuse = (message) => {
|
|
36
|
+
throw new StatementFileError('unexpected_record', message, index + 1);
|
|
37
|
+
};
|
|
38
|
+
if (!['01', '04', '05', '07'].includes(code)) {
|
|
39
|
+
throw new StatementFileError('unknown_record', `record code "${code}": a statement of account has 01, 04, 05 and 07`, index + 1);
|
|
40
|
+
}
|
|
41
|
+
if (code === '01') {
|
|
42
|
+
if (current)
|
|
43
|
+
refuse('an old balance, and the statement before it has no new balance');
|
|
44
|
+
current = { record: index + 1, oldBalance: record, movements: [] };
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
if (!current)
|
|
48
|
+
return refuse(`a record ${code} before any old balance`);
|
|
49
|
+
if (identity(record) !== identity(current.oldBalance)) {
|
|
50
|
+
throw new StatementFileError('inconsistent_record', `bank, branch, currency, decimals and account are "${identity(record)}" here and "${identity(current.oldBalance)}" on the old balance of the statement: a record of another account, or of another currency, in the middle of this one`, index + 1);
|
|
51
|
+
}
|
|
52
|
+
if (code === '04')
|
|
53
|
+
current.movements.push({ record: index + 1, r04: record, complements: [] });
|
|
54
|
+
else if (code === '05') {
|
|
55
|
+
const movement = current.movements[current.movements.length - 1];
|
|
56
|
+
if (!movement || (previous !== '04' && previous !== '05')) {
|
|
57
|
+
return refuse('a complement that follows no movement');
|
|
58
|
+
}
|
|
59
|
+
movement.complements.push(record);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
out.push({ ...current, newBalance: record });
|
|
63
|
+
current = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
previous = code;
|
|
67
|
+
});
|
|
68
|
+
if (current) {
|
|
69
|
+
throw new StatementFileError('incomplete_statement', `the statement that begins at record ${current.record} has no new balance: the file stops before its end`);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
// --- small readers -----------------------------------------------------------
|
|
74
|
+
/** `JJMMAA` to `YYYY-MM-DD`: null when blank or zeros, undefined when it is not a date. */
|
|
75
|
+
function day(field, pivotYear) {
|
|
76
|
+
if (field === '000000' || field.trim() === '')
|
|
77
|
+
return null;
|
|
78
|
+
if (!/^[0-9]{6}$/.test(field))
|
|
79
|
+
return undefined;
|
|
80
|
+
const date = Number(field.slice(0, 2));
|
|
81
|
+
const month = Number(field.slice(2, 4));
|
|
82
|
+
const short = Number(field.slice(4, 6));
|
|
83
|
+
const year = (short >= pivotYear ? 1900 : 2000) + short;
|
|
84
|
+
if (month < 1 || month > 12 || date < 1)
|
|
85
|
+
return undefined;
|
|
86
|
+
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
87
|
+
const length = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
88
|
+
if (date > length)
|
|
89
|
+
return undefined;
|
|
90
|
+
return `${year}-${field.slice(2, 4)}-${field.slice(0, 2)}`;
|
|
91
|
+
}
|
|
92
|
+
/** A counterparty's account: an IBAN when it passes ISO 13616, else what was written. */
|
|
93
|
+
function otherAccount(text) {
|
|
94
|
+
if (text === null)
|
|
95
|
+
return null;
|
|
96
|
+
const value = text.replace(/\s+/g, '').toUpperCase();
|
|
97
|
+
if (value === '')
|
|
98
|
+
return null;
|
|
99
|
+
if (isValidIban(value))
|
|
100
|
+
return { kind: 'iban', value };
|
|
101
|
+
return { kind: 'other', value, scheme: null, issuer: null };
|
|
102
|
+
}
|
|
103
|
+
// --- a statement -------------------------------------------------------------
|
|
104
|
+
function readStatement(raw, position, options, violations) {
|
|
105
|
+
const report = (code, message, line) => {
|
|
106
|
+
violations.push(line === undefined ? { code, message, statement: position } : { code, message, statement: position, line });
|
|
107
|
+
};
|
|
108
|
+
const dayOf = (field, what, line) => {
|
|
109
|
+
const value = day(field, options.pivotYear);
|
|
110
|
+
if (value === undefined) {
|
|
111
|
+
report('invalid_date', `${what} "${field}" is not a date (JJMMAA)`, line);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
};
|
|
116
|
+
const first = raw.oldBalance;
|
|
117
|
+
const bankCode = at(first, 3, 7);
|
|
118
|
+
const branchCode = at(first, 12, 16);
|
|
119
|
+
const accountNumber = at(first, 22, 32).trim().toUpperCase();
|
|
120
|
+
const currency = /^[A-Z]{3}$/.test(at(first, 17, 19)) ? at(first, 17, 19) : null;
|
|
121
|
+
if (accountNumber === '' || !/^[0-9]$/.test(at(first, 20))) {
|
|
122
|
+
throw new StatementFileError('incomplete_statement', accountNumber === ''
|
|
123
|
+
? 'the old balance names no account: there is nothing to attach its lines to'
|
|
124
|
+
: `the number of decimals is "${at(first, 20)}", which is not a digit: no amount of this statement can be read`, raw.record);
|
|
125
|
+
}
|
|
126
|
+
const decimals = Number(at(first, 20));
|
|
127
|
+
let identifier = {
|
|
128
|
+
kind: 'other',
|
|
129
|
+
value: `${bankCode}${branchCode}${accountNumber}`.replace(/\s+/g, ''),
|
|
130
|
+
scheme: null,
|
|
131
|
+
issuer: null,
|
|
132
|
+
};
|
|
133
|
+
if (options.ibanCountry !== null) {
|
|
134
|
+
const iban = ibanOf(options.ibanCountry, bankCode, branchCode, accountNumber);
|
|
135
|
+
if (iban === null) {
|
|
136
|
+
report('invalid_iban', `bank code "${bankCode}", branch code "${branchCode}" and account number "${accountNumber}" do not make an IBAN in "${options.ibanCountry}": five digits, five digits, eleven letters or digits, and a country of two letters; the account is returned as written`);
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
identifier = { kind: 'iban', value: iban };
|
|
140
|
+
}
|
|
141
|
+
const balance = (type, record, what) => {
|
|
142
|
+
const scaled = parseSigned(at(record, 91, 104));
|
|
143
|
+
if (scaled === null) {
|
|
144
|
+
report('invalid_amount', `the ${what} balance "${at(record, 91, 104)}" is not thirteen digits and a last character that carries the sign ({ A–I, } J–R)`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
balance: {
|
|
149
|
+
type,
|
|
150
|
+
amount: formatAmount(scaled, decimals),
|
|
151
|
+
currency,
|
|
152
|
+
date: dayOf(at(record, 35, 40), `the date of the ${what} balance`),
|
|
153
|
+
},
|
|
154
|
+
scaled,
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
const opening = balance('OPBD', raw.oldBalance, 'old');
|
|
158
|
+
const closing = balance('CLBD', raw.newBalance, 'new');
|
|
159
|
+
const lines = [];
|
|
160
|
+
let checkable = opening !== null && closing !== null;
|
|
161
|
+
let movementTotal = 0n;
|
|
162
|
+
for (const movement of raw.movements) {
|
|
163
|
+
const index = lines.length + 1;
|
|
164
|
+
const record = movement.r04;
|
|
165
|
+
const scaled = parseSigned(at(record, 91, 104));
|
|
166
|
+
if (scaled === null) {
|
|
167
|
+
checkable = false;
|
|
168
|
+
report('invalid_amount', `the amount of movement ${index}, "${at(record, 91, 104)}", is not thirteen digits and a last character that carries the sign ({ A–I, } J–R)`, index);
|
|
169
|
+
}
|
|
170
|
+
else
|
|
171
|
+
movementTotal += scaled;
|
|
172
|
+
const bookingDate = dayOf(at(record, 35, 40), `the booking date of movement ${index}`, index);
|
|
173
|
+
if (bookingDate === null)
|
|
174
|
+
report('booking_date_missing', `movement ${index} has no booking date`, index);
|
|
175
|
+
else if ((opening?.balance.date && bookingDate <= opening.balance.date) ||
|
|
176
|
+
(closing?.balance.date && bookingDate > closing.balance.date)) {
|
|
177
|
+
report('booking_date_outside_statement', `movement ${index} is booked on ${bookingDate}, and the statement runs after ${opening?.balance.date ?? '…'} and up to ${closing?.balance.date ?? '…'}`, index);
|
|
178
|
+
}
|
|
179
|
+
// The complements, by qualifier. Zone 2b-M is positions 49 to 118; the
|
|
180
|
+
// qualifiers that carry two zones cut it at 84.
|
|
181
|
+
const complements = movement.complements.map((complement) => ({
|
|
182
|
+
qualifier: at(complement, 46, 48),
|
|
183
|
+
text: at(complement, 49, 118).trimEnd(),
|
|
184
|
+
record: complement,
|
|
185
|
+
}));
|
|
186
|
+
const find = (qualifier) => complements.find((complement) => complement.qualifier === qualifier)?.record ?? null;
|
|
187
|
+
const whole = (qualifier) => {
|
|
188
|
+
const complement = find(qualifier);
|
|
189
|
+
return complement === null ? null : zone(complement, 49, 118);
|
|
190
|
+
};
|
|
191
|
+
const halves = (qualifier) => {
|
|
192
|
+
const complement = find(qualifier);
|
|
193
|
+
return complement === null ? [null, null] : [zone(complement, 49, 83), zone(complement, 84, 118)];
|
|
194
|
+
};
|
|
195
|
+
const returnReason = (() => {
|
|
196
|
+
const value = zone(record, 41, 42);
|
|
197
|
+
return value === null || /^0+$/.test(value) ? null : value;
|
|
198
|
+
})();
|
|
199
|
+
const reversal = returnReason !== null;
|
|
200
|
+
// Who is on the other side: the payer of money coming in, the beneficiary
|
|
201
|
+
// of money going out — and the reverse on a rejected operation, where the
|
|
202
|
+
// parties keep the roles they had in the original.
|
|
203
|
+
const credit = scaled !== null && scaled >= 0n;
|
|
204
|
+
const payer = credit !== reversal;
|
|
205
|
+
const counterparty = {
|
|
206
|
+
name: whole(payer ? 'NPY' : 'NBE'),
|
|
207
|
+
account: otherAccount(halves(payer ? 'CPY' : 'CBE')[0]),
|
|
208
|
+
agentBic: null,
|
|
209
|
+
ultimateName: whole(payer ? 'NPO' : 'NBU'),
|
|
210
|
+
};
|
|
211
|
+
const [identifierValue, identifierType] = halves(payer ? 'IPY' : 'IBE');
|
|
212
|
+
const remittance = { unstructured: [], structured: [] };
|
|
213
|
+
for (const complement of complements) {
|
|
214
|
+
if (complement.qualifier === 'LIB' && complement.text.trim() !== '') {
|
|
215
|
+
remittance.unstructured.push(complement.text.trim());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const lcc = find('LCC');
|
|
219
|
+
const lc2 = find('LC2');
|
|
220
|
+
const client = `${lcc ? at(lcc, 49, 118) : ''}${lc2 ? at(lc2, 49, 118) : ''}`.trim();
|
|
221
|
+
if (client !== '')
|
|
222
|
+
remittance.unstructured.push(client);
|
|
223
|
+
const structured = halves('LCS')[0];
|
|
224
|
+
if (structured !== null)
|
|
225
|
+
remittance.structured.push({ reference: structured, type: null, issuer: null });
|
|
226
|
+
const [endToEndId, purpose] = halves('RCN');
|
|
227
|
+
const [paymentInformationId, instructionId] = halves('REF');
|
|
228
|
+
const mandate = find('RUM');
|
|
229
|
+
let instructedAmount = null;
|
|
230
|
+
let exchangeRate = null;
|
|
231
|
+
const origin = find('MMO');
|
|
232
|
+
if (origin !== null) {
|
|
233
|
+
const originCurrency = at(origin, 49, 51);
|
|
234
|
+
const originAmount = parseUnsigned(at(origin, 53, 66));
|
|
235
|
+
if (/^[A-Z]{3}$/.test(originCurrency) && /^[0-9]$/.test(at(origin, 52)) && originAmount !== null) {
|
|
236
|
+
instructedAmount = { amount: formatAmount(originAmount, Number(at(origin, 52))), currency: originCurrency };
|
|
237
|
+
}
|
|
238
|
+
const rate = parseUnsigned(at(origin, 69, 79));
|
|
239
|
+
if (/^[0-9]{2}$/.test(at(origin, 67, 68)) && rate !== null && rate !== 0n) {
|
|
240
|
+
exchangeRate = formatAmount(rate, Number(at(origin, 67, 68)));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const entryNumber = zone(record, 82, 88);
|
|
244
|
+
const interbank = zone(record, 33, 34);
|
|
245
|
+
lines.push({
|
|
246
|
+
index,
|
|
247
|
+
entry: index,
|
|
248
|
+
detail: null,
|
|
249
|
+
detailCount: null,
|
|
250
|
+
status: 'BOOK',
|
|
251
|
+
booked: true,
|
|
252
|
+
reversal,
|
|
253
|
+
bookingDate,
|
|
254
|
+
valueDate: dayOf(at(record, 43, 48), `the value date of movement ${index}`, index),
|
|
255
|
+
amount: scaled === null ? null : formatAmount(scaled, decimals),
|
|
256
|
+
currency,
|
|
257
|
+
entryAmount: null,
|
|
258
|
+
bankReference: null,
|
|
259
|
+
entryReference: null,
|
|
260
|
+
endToEndId,
|
|
261
|
+
transactionId: null,
|
|
262
|
+
instructionId,
|
|
263
|
+
paymentInformationId,
|
|
264
|
+
mandateId: mandate === null ? null : zone(mandate, 49, 83),
|
|
265
|
+
counterparty: Object.values(counterparty).every((value) => value === null) ? null : counterparty,
|
|
266
|
+
remittance,
|
|
267
|
+
bankTransactionCode: interbank === null
|
|
268
|
+
? null
|
|
269
|
+
: { domain: null, family: null, subFamily: null, proprietary: interbank, proprietaryIssuer: 'CFONB' },
|
|
270
|
+
returnReason,
|
|
271
|
+
instructedAmount,
|
|
272
|
+
exchangeRate,
|
|
273
|
+
additionalInformation: zone(record, 49, 79),
|
|
274
|
+
internalOperationCode: zone(record, 8, 11),
|
|
275
|
+
interbankOperationCode: interbank,
|
|
276
|
+
entryNumber: entryNumber === null || /^0+$/.test(entryNumber) ? null : entryNumber,
|
|
277
|
+
commissionExempt: at(record, 89) === '1',
|
|
278
|
+
unavailable: at(record, 90) === '1',
|
|
279
|
+
referenceZone: zone(record, 105, 120),
|
|
280
|
+
purpose,
|
|
281
|
+
sequenceType: mandate === null ? null : zone(mandate, 84, 87),
|
|
282
|
+
counterpartyIdentifier: identifierValue === null ? null : { value: identifierValue, type: identifierType },
|
|
283
|
+
complements: complements.map(({ qualifier, text }) => ({ qualifier, text })),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// Old balance plus what moved is the new balance, or the file says so.
|
|
287
|
+
let balanced = null;
|
|
288
|
+
if (checkable && opening && closing) {
|
|
289
|
+
const computed = opening.scaled + movementTotal;
|
|
290
|
+
balanced = computed === closing.scaled;
|
|
291
|
+
if (!balanced) {
|
|
292
|
+
report('balance_mismatch', `old balance ${opening.balance.amount} plus movements ${formatAmount(movementTotal, decimals)} is ${formatAmount(computed, decimals)}, and the statement closes at ${closing.balance.amount}: a difference of ${formatAmount(closing.scaled - computed, decimals)}${currency ? ` ${currency}` : ''}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const balances = [opening?.balance, closing?.balance].filter((item) => item !== undefined && item !== null);
|
|
296
|
+
return {
|
|
297
|
+
id: `${opening?.balance.date ?? at(raw.oldBalance, 35, 40)}/${closing?.balance.date ?? at(raw.newBalance, 35, 40)}`,
|
|
298
|
+
electronicSequenceNumber: null,
|
|
299
|
+
legalSequenceNumber: null,
|
|
300
|
+
createdAt: null,
|
|
301
|
+
period: null,
|
|
302
|
+
page: null,
|
|
303
|
+
account: {
|
|
304
|
+
identifier,
|
|
305
|
+
currency,
|
|
306
|
+
name: null,
|
|
307
|
+
ownerName: null,
|
|
308
|
+
servicerBic: null,
|
|
309
|
+
bankCode,
|
|
310
|
+
branchCode,
|
|
311
|
+
accountNumber,
|
|
312
|
+
decimals,
|
|
313
|
+
},
|
|
314
|
+
openingBalance: opening?.balance ?? null,
|
|
315
|
+
closingBalance: closing?.balance ?? null,
|
|
316
|
+
balances,
|
|
317
|
+
lines,
|
|
318
|
+
balanced,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Read a CFONB 120 file — a string, or the bytes as they came — into its
|
|
323
|
+
* statements, their lines, and what does not add up.
|
|
324
|
+
*
|
|
325
|
+
* Throws a `StatementFileError` for what is not a readable CFONB 120 at all: a
|
|
326
|
+
* record of the wrong length, an unknown record, a record out of place or of
|
|
327
|
+
* another account, an encoding that is not the one announced. Everything else
|
|
328
|
+
* comes back, with the trouble named in `violations` and nothing silently
|
|
329
|
+
* repaired: a new balance that does not follow from the old one is returned as
|
|
330
|
+
* the bank wrote it, and reported.
|
|
331
|
+
*/
|
|
332
|
+
export function readCfonb120(input, options = {}) {
|
|
333
|
+
const text = decode(input, options.encoding ?? 'utf-8', options.maxBytes ?? DEFAULTS.maxBytes);
|
|
334
|
+
const records = splitRecords(text, RECORD_LENGTH);
|
|
335
|
+
const violations = [];
|
|
336
|
+
const settings = {
|
|
337
|
+
pivotYear: options.pivotYear ?? DEFAULTS.pivotYear,
|
|
338
|
+
ibanCountry: options.ibanCountry === undefined ? null : options.ibanCountry.toUpperCase(),
|
|
339
|
+
};
|
|
340
|
+
const statements = gather(records).map((raw, index) => readStatement(raw, index + 1, settings, violations));
|
|
341
|
+
return { format: 'cfonb120', version: null, namespace: 'cfonb120', statements, violations };
|
|
342
|
+
}
|
|
343
|
+
//# 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,18 @@
|
|
|
1
|
+
export type Encoding = 'utf-8' | 'iso-8859-1';
|
|
2
|
+
/**
|
|
3
|
+
* Bytes to text, fatally: a byte that is not the encoding announced is an
|
|
4
|
+
* error and not a replacement character in somebody's name. ISO-8859-1 maps
|
|
5
|
+
* every byte, so it is never guessed — the caller says so, or the bytes are
|
|
6
|
+
* UTF-8 (of which ASCII, which is what the standard writes, is a subset). EBCDIC, which
|
|
7
|
+
* the brochure also describes, is neither, and is refused by this name.
|
|
8
|
+
*/
|
|
9
|
+
export declare function decode(input: string | Uint8Array, encoding: Encoding, maxBytes: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* The records of the file: its lines, or — when the file has no line break at
|
|
12
|
+
* all, as some transfers deliver it — consecutive slices of the record length.
|
|
13
|
+
* Every record is exactly `length` characters, or the file is refused: nothing
|
|
14
|
+
* is padded and nothing is trimmed, because the last position of a record
|
|
15
|
+
* means something.
|
|
16
|
+
*/
|
|
17
|
+
export declare function splitRecords(text: string, length: number): string[];
|
|
18
|
+
//# sourceMappingURL=records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;AAY9C;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8C/F;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CA0BnE"}
|
package/dist/records.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { StatementFileError } from './errors.js';
|
|
2
|
+
const BYTE_ORDER_MARK = 0xfeff;
|
|
3
|
+
const END_OF_FILE_MARK = 0x1a;
|
|
4
|
+
const REPLACEMENT = 0xfffd;
|
|
5
|
+
/** A control character other than the two that end a line, or the replacement character. */
|
|
6
|
+
function unreadable(code) {
|
|
7
|
+
if (code === 0x0a || code === 0x0d)
|
|
8
|
+
return false;
|
|
9
|
+
return code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === REPLACEMENT;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Bytes to text, fatally: a byte that is not the encoding announced is an
|
|
13
|
+
* error and not a replacement character in somebody's name. ISO-8859-1 maps
|
|
14
|
+
* every byte, so it is never guessed — the caller says so, or the bytes are
|
|
15
|
+
* UTF-8 (of which ASCII, which is what the standard writes, is a subset). EBCDIC, which
|
|
16
|
+
* the brochure also describes, is neither, and is refused by this name.
|
|
17
|
+
*/
|
|
18
|
+
export function decode(input, encoding, maxBytes) {
|
|
19
|
+
let text;
|
|
20
|
+
if (typeof input === 'string') {
|
|
21
|
+
if (input.length > maxBytes) {
|
|
22
|
+
throw new StatementFileError('too_large', `the file is longer than ${maxBytes} bytes`);
|
|
23
|
+
}
|
|
24
|
+
text = input;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
if (input.byteLength > maxBytes) {
|
|
28
|
+
throw new StatementFileError('too_large', `the file is ${input.byteLength} bytes, over the limit of ${maxBytes}`);
|
|
29
|
+
}
|
|
30
|
+
if (encoding === 'iso-8859-1') {
|
|
31
|
+
const parts = [];
|
|
32
|
+
for (let i = 0; i < input.length; i += 8192) {
|
|
33
|
+
parts.push(String.fromCharCode(...input.subarray(i, i + 8192)));
|
|
34
|
+
}
|
|
35
|
+
text = parts.join('');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
try {
|
|
39
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(input);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new StatementFileError('unsupported_encoding', "the bytes are not UTF-8; if the bank writes ISO-8859-1, say so with { encoding: 'iso-8859-1' } — it is not guessed, because every sequence of bytes is valid ISO-8859-1");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (text.charCodeAt(0) === BYTE_ORDER_MARK)
|
|
47
|
+
text = text.slice(1);
|
|
48
|
+
// The DOS end-of-file mark some transfers leave behind.
|
|
49
|
+
let end = text.length;
|
|
50
|
+
while (end > 0 && text.charCodeAt(end - 1) === END_OF_FILE_MARK)
|
|
51
|
+
end -= 1;
|
|
52
|
+
text = text.slice(0, end);
|
|
53
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
54
|
+
const code = text.charCodeAt(i);
|
|
55
|
+
if (unreadable(code)) {
|
|
56
|
+
throw new StatementFileError('unsupported_encoding', `the file holds the character U+${code.toString(16).toUpperCase().padStart(4, '0')} at offset ${i}: a control or replacement character, which is a file decoded with the wrong encoding, or not a text file`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The records of the file: its lines, or — when the file has no line break at
|
|
63
|
+
* all, as some transfers deliver it — consecutive slices of the record length.
|
|
64
|
+
* Every record is exactly `length` characters, or the file is refused: nothing
|
|
65
|
+
* is padded and nothing is trimmed, because the last position of a record
|
|
66
|
+
* means something.
|
|
67
|
+
*/
|
|
68
|
+
export function splitRecords(text, length) {
|
|
69
|
+
let records;
|
|
70
|
+
if (!/[\r\n]/.test(text) && text.length > length) {
|
|
71
|
+
if (text.length % length !== 0) {
|
|
72
|
+
throw new StatementFileError('invalid_record_length', `the file has no line break and its ${text.length} characters are not a whole number of ${length}-character records`);
|
|
73
|
+
}
|
|
74
|
+
records = [];
|
|
75
|
+
for (let i = 0; i < text.length; i += length)
|
|
76
|
+
records.push(text.slice(i, i + length));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
records = text.split(/\r\n|\n|\r/);
|
|
80
|
+
while (records.length > 0 && records[records.length - 1] === '')
|
|
81
|
+
records.pop();
|
|
82
|
+
}
|
|
83
|
+
if (records.length === 0)
|
|
84
|
+
throw new StatementFileError('empty_file', 'the file holds no record');
|
|
85
|
+
records.forEach((record, index) => {
|
|
86
|
+
if (record.length !== length) {
|
|
87
|
+
throw new StatementFileError('invalid_record_length', `${record.length} characters where the format has ${length}`, index + 1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return records;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.js","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a statement is once it has been read: an account, two balances and
|
|
3
|
+
* lines. The shape is declared here and imported from nowhere — it names no
|
|
4
|
+
* table and no ledger. It is, field for field, the shape `@ekwo-ai/camt053`
|
|
5
|
+
* declares for itself, so whatever takes one takes the other; what only
|
|
6
|
+
* CFONB 120 says comes after, under names of its own.
|
|
7
|
+
*
|
|
8
|
+
* Every amount is a **decimal string**, signed from the account holder's side:
|
|
9
|
+
* money in is positive, money out is negative, an overdrawn balance is
|
|
10
|
+
* negative. No figure of a statement passes through a `number`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* How an account is identified. A CFONB 120 carries a bank code, a branch code
|
|
14
|
+
* and an account number, and neither an IBAN nor a country: the identifier is
|
|
15
|
+
* `other`, the three joined, unless the caller names the country
|
|
16
|
+
* (`ibanCountry`) — then it is the IBAN those three make there.
|
|
17
|
+
*/
|
|
18
|
+
export type AccountIdentifier = {
|
|
19
|
+
kind: 'iban';
|
|
20
|
+
/** Without spaces, upper case. */
|
|
21
|
+
value: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'other';
|
|
24
|
+
/** Bank code, branch code and account number, twenty-one characters; or a counterparty's account as written. */
|
|
25
|
+
value: string;
|
|
26
|
+
scheme: string | null;
|
|
27
|
+
issuer: string | null;
|
|
28
|
+
};
|
|
29
|
+
export interface StatementAccount {
|
|
30
|
+
identifier: AccountIdentifier;
|
|
31
|
+
/** Zone E of the records: the ISO 4217 code. */
|
|
32
|
+
currency: string | null;
|
|
33
|
+
/** Always null: the format names no account and no holder. */
|
|
34
|
+
name: string | null;
|
|
35
|
+
ownerName: string | null;
|
|
36
|
+
servicerBic: string | null;
|
|
37
|
+
bankCode: string;
|
|
38
|
+
branchCode: string;
|
|
39
|
+
accountNumber: string;
|
|
40
|
+
/** Zone F: how many of an amount's digits are decimals, as the records of this statement say. */
|
|
41
|
+
decimals: number;
|
|
42
|
+
}
|
|
43
|
+
export interface StatementBalance {
|
|
44
|
+
/** `OPBD` for the old balance (record 01), `CLBD` for the new one (record 07). */
|
|
45
|
+
type: string;
|
|
46
|
+
/** Signed: a debit balance is negative. */
|
|
47
|
+
amount: string;
|
|
48
|
+
currency: string | null;
|
|
49
|
+
/** `YYYY-MM-DD`. */
|
|
50
|
+
date: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface StructuredReference {
|
|
53
|
+
/** The `LCS` complement: the structured remittance of the payment, as written. */
|
|
54
|
+
reference: string;
|
|
55
|
+
/** Always null: the complement carries the reference and not its type. */
|
|
56
|
+
type: string | null;
|
|
57
|
+
issuer: string | null;
|
|
58
|
+
}
|
|
59
|
+
export interface Remittance {
|
|
60
|
+
/** The `LIB` complements, one each, then `LCC` and `LC2` put back into the one text they were cut from. */
|
|
61
|
+
unstructured: string[];
|
|
62
|
+
structured: StructuredReference[];
|
|
63
|
+
}
|
|
64
|
+
export interface Counterparty {
|
|
65
|
+
/** `NPY` for money in, `NBE` for money out — and the other way round on a rejected operation. */
|
|
66
|
+
name: string | null;
|
|
67
|
+
/** `CPY` or `CBE`, by the same rule. */
|
|
68
|
+
account: AccountIdentifier | null;
|
|
69
|
+
/** Always null. */
|
|
70
|
+
agentBic: string | null;
|
|
71
|
+
/** `NPO` or `NBU`, by the same rule. */
|
|
72
|
+
ultimateName: string | null;
|
|
73
|
+
}
|
|
74
|
+
export interface BankTransactionCode {
|
|
75
|
+
/** Always null: CFONB has its own code list, not the ISO 20022 one. */
|
|
76
|
+
domain: string | null;
|
|
77
|
+
family: string | null;
|
|
78
|
+
subFamily: string | null;
|
|
79
|
+
/** Zone 2-I, the two characters of the interbank operation code. */
|
|
80
|
+
proprietary: string | null;
|
|
81
|
+
/** `CFONB`. */
|
|
82
|
+
proprietaryIssuer: string | null;
|
|
83
|
+
}
|
|
84
|
+
export interface StatementLine {
|
|
85
|
+
/** Position among the statement's lines, from 1. */
|
|
86
|
+
index: number;
|
|
87
|
+
/** The same: a CFONB 120 movement is never split. */
|
|
88
|
+
entry: number;
|
|
89
|
+
/** Always null. */
|
|
90
|
+
detail: number | null;
|
|
91
|
+
detailCount: number | null;
|
|
92
|
+
/** `BOOK`: by its definition the statement "shows accounting entries only". */
|
|
93
|
+
status: string;
|
|
94
|
+
booked: boolean;
|
|
95
|
+
/** A reject code is present (zone 2-K): the operation comes back. */
|
|
96
|
+
reversal: boolean;
|
|
97
|
+
/** Zone 2-J. */
|
|
98
|
+
bookingDate: string | null;
|
|
99
|
+
/** Zone 2-L. */
|
|
100
|
+
valueDate: string | null;
|
|
101
|
+
/** Signed: positive is money in. Null when the file's figure is unreadable. */
|
|
102
|
+
amount: string | null;
|
|
103
|
+
currency: string | null;
|
|
104
|
+
entryAmount: string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Always null. The format has **no reference the bank gives a movement**:
|
|
107
|
+
* `entryNumber` is a cheque or remittance number or zeros, and the `REF`
|
|
108
|
+
* complement carries the payer's references, not the bank's.
|
|
109
|
+
*/
|
|
110
|
+
bankReference: string | null;
|
|
111
|
+
entryReference: string | null;
|
|
112
|
+
/** `RCN`, first zone. */
|
|
113
|
+
endToEndId: string | null;
|
|
114
|
+
transactionId: string | null;
|
|
115
|
+
/** `REF`, second zone. */
|
|
116
|
+
instructionId: string | null;
|
|
117
|
+
/** `REF`, first zone. */
|
|
118
|
+
paymentInformationId: string | null;
|
|
119
|
+
/** `RUM`, first zone. */
|
|
120
|
+
mandateId: string | null;
|
|
121
|
+
counterparty: Counterparty | null;
|
|
122
|
+
remittance: Remittance;
|
|
123
|
+
bankTransactionCode: BankTransactionCode | null;
|
|
124
|
+
/** Zone 2-K, the reject code, as written; null when blank or zeros. */
|
|
125
|
+
returnReason: string | null;
|
|
126
|
+
/** `MMO`: the amount and currency of origin, unsigned and unconverted. */
|
|
127
|
+
instructedAmount: {
|
|
128
|
+
amount: string;
|
|
129
|
+
currency: string;
|
|
130
|
+
} | null;
|
|
131
|
+
/** `MMO`: the rate, with the decimals the complement announces. */
|
|
132
|
+
exchangeRate: string | null;
|
|
133
|
+
/** Zone 2-M, the label the bank gives the movement. */
|
|
134
|
+
additionalInformation: string | null;
|
|
135
|
+
/** Zone 2-C, the bank's own operation code. */
|
|
136
|
+
internalOperationCode: string | null;
|
|
137
|
+
/** Zone 2-I. */
|
|
138
|
+
interbankOperationCode: string | null;
|
|
139
|
+
/** Zone 2-O: a cheque, remittance or voucher number; null when zeros. */
|
|
140
|
+
entryNumber: string | null;
|
|
141
|
+
/** Zone 2-P is `1`: exempt from the account movement commission. */
|
|
142
|
+
commissionExempt: boolean;
|
|
143
|
+
/** Zone 2-Q is `1`: booked, and not available yet. */
|
|
144
|
+
unavailable: boolean;
|
|
145
|
+
/** Zone 2-S. */
|
|
146
|
+
referenceZone: string | null;
|
|
147
|
+
/** `RCN`, second zone. */
|
|
148
|
+
purpose: string | null;
|
|
149
|
+
/** `RUM`, second zone: OOFF, FRST, RCUR, FNAL. */
|
|
150
|
+
sequenceType: string | null;
|
|
151
|
+
/** `IPY` or `IBE`: the identifier of the counterparty and what kind of identifier it is. */
|
|
152
|
+
counterpartyIdentifier: {
|
|
153
|
+
value: string;
|
|
154
|
+
type: string | null;
|
|
155
|
+
} | null;
|
|
156
|
+
/** Every complement (record 05) of the movement, in order: its qualifier and its seventy positions, as written. */
|
|
157
|
+
complements: {
|
|
158
|
+
qualifier: string;
|
|
159
|
+
text: string;
|
|
160
|
+
}[];
|
|
161
|
+
}
|
|
162
|
+
export interface Statement {
|
|
163
|
+
/**
|
|
164
|
+
* CFONB 120 gives a statement no identifier and no number. This one is made
|
|
165
|
+
* of what it does give: the dates of its two balances,
|
|
166
|
+
* `2026-02-28/2026-03-31`. The same file always makes the same one.
|
|
167
|
+
*/
|
|
168
|
+
id: string;
|
|
169
|
+
/** Always null: the format numbers no statement. */
|
|
170
|
+
electronicSequenceNumber: string | null;
|
|
171
|
+
legalSequenceNumber: string | null;
|
|
172
|
+
createdAt: string | null;
|
|
173
|
+
/** Always null: the dates of the two balances are what the format gives. */
|
|
174
|
+
period: {
|
|
175
|
+
from: string | null;
|
|
176
|
+
to: string | null;
|
|
177
|
+
} | null;
|
|
178
|
+
page: {
|
|
179
|
+
number: string;
|
|
180
|
+
last: boolean;
|
|
181
|
+
} | null;
|
|
182
|
+
account: StatementAccount;
|
|
183
|
+
openingBalance: StatementBalance | null;
|
|
184
|
+
closingBalance: StatementBalance | null;
|
|
185
|
+
balances: StatementBalance[];
|
|
186
|
+
lines: StatementLine[];
|
|
187
|
+
/**
|
|
188
|
+
* Old balance plus the movements equals new balance: true, false, or null
|
|
189
|
+
* when it could not be checked. False is reported as `balance_mismatch` and
|
|
190
|
+
* **never corrected**.
|
|
191
|
+
*/
|
|
192
|
+
balanced: boolean | null;
|
|
193
|
+
}
|
|
194
|
+
export interface Violation {
|
|
195
|
+
/** A stable token, for a caller that branches on it. */
|
|
196
|
+
code: ViolationCode;
|
|
197
|
+
/** What is wrong, in a sentence. */
|
|
198
|
+
message: string;
|
|
199
|
+
/** Which statement of the file, from 1. */
|
|
200
|
+
statement: number;
|
|
201
|
+
/** Which line of that statement (`StatementLine.index`), where there is one. */
|
|
202
|
+
line?: number;
|
|
203
|
+
}
|
|
204
|
+
export type ViolationCode = 'balance_mismatch' | 'invalid_amount' | 'invalid_date' | 'booking_date_missing' | 'booking_date_outside_statement' | 'invalid_iban';
|
|
205
|
+
export interface StatementFile {
|
|
206
|
+
format: 'cfonb120';
|
|
207
|
+
/** Null: the brochure has an edition (July 2004, completed in March 2010) and the file does not say which it follows. */
|
|
208
|
+
version: string | null;
|
|
209
|
+
/**
|
|
210
|
+
* What was read, under one name: `cfonb120`. The word is XML's and is kept
|
|
211
|
+
* because it is the key under which `@ekwo-ai/camt053` says the same thing.
|
|
212
|
+
*/
|
|
213
|
+
namespace: string;
|
|
214
|
+
statements: Statement[];
|
|
215
|
+
violations: Violation[];
|
|
216
|
+
}
|
|
217
|
+
export interface ReadOptions {
|
|
218
|
+
/** Refuse a larger file unread. Default 32 MiB. */
|
|
219
|
+
maxBytes?: number;
|
|
220
|
+
/**
|
|
221
|
+
* How bytes are decoded. Default `utf-8`, fatally — which reads ASCII, the
|
|
222
|
+
* character set of the brochure. `iso-8859-1` is said, never guessed.
|
|
223
|
+
* Ignored when the input is already a string.
|
|
224
|
+
*/
|
|
225
|
+
encoding?: 'utf-8' | 'iso-8859-1';
|
|
226
|
+
/**
|
|
227
|
+
* The format writes a year on two digits. `YY` at or above this is 19YY,
|
|
228
|
+
* under it 20YY. Default 80.
|
|
229
|
+
*/
|
|
230
|
+
pivotYear?: number;
|
|
231
|
+
/**
|
|
232
|
+
* Two letters. When given, every account of the file is returned as the
|
|
233
|
+
* IBAN its bank code, branch code and account number make **in that
|
|
234
|
+
* country**. Left out, nothing is assumed and the identifier is `other`:
|
|
235
|
+
* the format carries no country, and the same lay-out serves several.
|
|
236
|
+
*/
|
|
237
|
+
ibanCountry?: string;
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a statement is once it has been read: an account, two balances and
|
|
3
|
+
* lines. The shape is declared here and imported from nowhere — it names no
|
|
4
|
+
* table and no ledger. It is, field for field, the shape `@ekwo-ai/camt053`
|
|
5
|
+
* declares for itself, so whatever takes one takes the other; what only
|
|
6
|
+
* CFONB 120 says comes after, under names of its own.
|
|
7
|
+
*
|
|
8
|
+
* Every amount is a **decimal string**, signed from the account holder's side:
|
|
9
|
+
* money in is positive, money out is negative, an overdrawn balance is
|
|
10
|
+
* negative. No figure of a statement passes through a `number`.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekwo-ai/cfonb120",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Reads a CFONB 120 statement of account (relevé de compte sur support informatique, 120-character records) into plain objects in TypeScript: account, balances and lines, amounts as decimal strings, and what does not add up. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cfonb",
|
|
7
|
+
"cfonb120",
|
|
8
|
+
"afb120",
|
|
9
|
+
"bank-statement",
|
|
10
|
+
"releve-de-compte",
|
|
11
|
+
"reconciliation",
|
|
12
|
+
"fixed-width",
|
|
13
|
+
"parser"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Ekwo AI",
|
|
17
|
+
"homepage": "https://github.com/Ekwo-ai/ekwo-os/tree/main/packages/formats/cfonb120#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Ekwo-ai/ekwo-os.git",
|
|
21
|
+
"directory": "packages/formats/cfonb120"
|
|
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
|
+
}
|