@ingram-tech/coda 0.1.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/README.md +188 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +372 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @ingram-tech/coda
|
|
2
|
+
|
|
3
|
+
TypeScript parser for [CODA](https://www.febelfin.be/en/expertise/electronic-banking/standards) (Coded statement of account) bank files. CODA is the Belgian standard (maintained by Febelfin) for electronic bank-to-customer account statements, used by all Belgian banks and widely supported across Europe.
|
|
4
|
+
|
|
5
|
+
Parses CODA v2.x files into fully typed objects. Returns `null` on invalid input instead of throwing.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ingram-tech/coda
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { parseCoda } from "@ingram-tech/coda";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
|
|
19
|
+
const content = readFileSync("statement.cod", "utf-8");
|
|
20
|
+
const file = parseCoda(content);
|
|
21
|
+
|
|
22
|
+
if (file) {
|
|
23
|
+
for (const stmt of file.statements) {
|
|
24
|
+
console.log(`Account: ${stmt.account.number} (${stmt.account.currency})`);
|
|
25
|
+
console.log(`Holder: ${stmt.accountHolderName}`);
|
|
26
|
+
console.log(`Old balance: ${stmt.oldBalance.amount}`);
|
|
27
|
+
console.log(`New balance: ${stmt.newBalance?.amount}`);
|
|
28
|
+
|
|
29
|
+
for (const m of stmt.movements) {
|
|
30
|
+
console.log(` ${m.amount > 0 ? "+" : ""}${m.amount} ${m.counterpartyName ?? ""}`);
|
|
31
|
+
console.log(` ${m.communication}`);
|
|
32
|
+
|
|
33
|
+
if (m.structuredCommunicationType === 101) {
|
|
34
|
+
// Belgian structured payment reference (+++xxx/xxxx/xxxxx+++)
|
|
35
|
+
const ref = m.communication;
|
|
36
|
+
console.log(` Ref: +++${ref.slice(0, 3)}/${ref.slice(3, 7)}/${ref.slice(7)}+++`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const msg of stmt.freeCommunications) {
|
|
41
|
+
console.log(` Message: ${msg}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Design
|
|
48
|
+
|
|
49
|
+
- **Single function, null return** -- `parseCoda(content)` returns a `CodaFile` or `null`. No exceptions for malformed input.
|
|
50
|
+
- **Signed amounts** -- Balances and movements are signed numbers: positive for credit, negative for debit. No separate sign field to check.
|
|
51
|
+
- **Faithful extraction** -- Fields are extracted at their standard-defined positions. Communications are concatenated across record parts (2.1 + 2.2 + 2.3 for movements, 3.1 + 3.2 + 3.3 for information records) and right-trimmed.
|
|
52
|
+
- **No deep structured communication parsing** -- The `communicationType` and `structuredCommunicationType` fields tell you the format; the `communication` field gives you the raw content. Type 101/102 Belgian structured references come through as 12-digit strings ready for mod-97 validation. More exotic types (127 SEPA direct debit, 105 FX details, etc.) are left as raw strings for the caller to parse.
|
|
53
|
+
- **Flat movement model** -- Each record 2.1 becomes a `CodaMovement`, with fields from 2.2 and 2.3 merged in. Information records (3.x) are attached as an `information[]` array on the preceding movement.
|
|
54
|
+
|
|
55
|
+
## Parsed fields
|
|
56
|
+
|
|
57
|
+
The parser extracts all fields defined in the CODA v2.8 standard:
|
|
58
|
+
|
|
59
|
+
- **Header (record 0)**: creation date, bank ID, duplicate flag, file reference, addressee name, BIC, company ID, separate application code, MT940 references, version
|
|
60
|
+
- **Old balance (record 1)**: account number (Belgian BBAN, foreign BBAN, Belgian IBAN, or foreign IBAN), currency, country code, paper/CODA statement sequence numbers, account holder name, account description, opening balance (signed amount + date)
|
|
61
|
+
- **Movements (records 2.1 / 2.2 / 2.3)**:
|
|
62
|
+
- 2.1: sequence/detail numbers, bank reference, signed amount, value date, entry date, 8-digit transaction code (type/family/transaction/category), communication (structured or free), globalisation code
|
|
63
|
+
- 2.2: customer reference, counterparty BIC, R-transaction type and reason, SEPA CategoryPurpose and Purpose codes
|
|
64
|
+
- 2.3: counterparty account number and currency, counterparty name, communication continuation
|
|
65
|
+
- **Information (records 3.1 / 3.2 / 3.3)**: sequence/detail numbers, bank reference, transaction code, communication (structured or free), linked to parent movement
|
|
66
|
+
- **New balance (record 8)**: closing balance (signed amount + date)
|
|
67
|
+
- **Free communications (record 4)**: free-text messages, grouped by sequence number
|
|
68
|
+
- **Trailer (record 9)**: debit and credit movement totals
|
|
69
|
+
|
|
70
|
+
## API reference
|
|
71
|
+
|
|
72
|
+
### `parseCoda(content: string): CodaFile | null`
|
|
73
|
+
|
|
74
|
+
Parse a CODA file. Returns `null` if the input is empty, doesn't start with a record 0, or cannot be parsed. Handles both `\n` and `\r\n` line endings. Multiple statements in a single file (delimited by record 0 boundaries) are supported.
|
|
75
|
+
|
|
76
|
+
### Types
|
|
77
|
+
|
|
78
|
+
#### `CodaFile`
|
|
79
|
+
|
|
80
|
+
| Field | Type | Description |
|
|
81
|
+
|-------|------|-------------|
|
|
82
|
+
| `statements` | `CodaStatement[]` | One or more bank statements |
|
|
83
|
+
|
|
84
|
+
#### `CodaStatement`
|
|
85
|
+
|
|
86
|
+
| Field | Type | Description |
|
|
87
|
+
|-------|------|-------------|
|
|
88
|
+
| `creationDate` | `Date` | File creation date |
|
|
89
|
+
| `bankId` | `number` | Bank identification number |
|
|
90
|
+
| `isDuplicate` | `boolean` | Whether this is a duplicate file |
|
|
91
|
+
| `fileReference` | `string?` | File reference assigned by the bank |
|
|
92
|
+
| `addressee` | `string` | Name of the addressee |
|
|
93
|
+
| `bic` | `string?` | BIC of the bank holding the account |
|
|
94
|
+
| `companyId` | `string?` | Belgian company identification number |
|
|
95
|
+
| `separateApplication` | `string` | Separate application code (5 positions) |
|
|
96
|
+
| `transactionReference` | `string?` | MT940 transaction reference (tag 20) |
|
|
97
|
+
| `relatedReference` | `string?` | MT940 related reference (tag 21) |
|
|
98
|
+
| `version` | `number` | CODA standard version |
|
|
99
|
+
| `account` | `CodaAccount` | Account details |
|
|
100
|
+
| `paperStatementSequence` | `number` | Paper statement sequence number |
|
|
101
|
+
| `codaStatementSequence` | `number` | CODA statement sequence number |
|
|
102
|
+
| `accountHolderName` | `string?` | Name of the account holder |
|
|
103
|
+
| `accountDescription` | `string?` | Account description |
|
|
104
|
+
| `oldBalance` | `CodaBalance` | Opening balance |
|
|
105
|
+
| `newBalance` | `CodaBalance?` | Closing balance (absent in empty files) |
|
|
106
|
+
| `movements` | `CodaMovement[]` | Transaction movements |
|
|
107
|
+
| `freeCommunications` | `string[]` | Free communication texts |
|
|
108
|
+
| `totalDebit` | `number` | Sum of debit movement amounts from trailer |
|
|
109
|
+
| `totalCredit` | `number` | Sum of credit movement amounts from trailer |
|
|
110
|
+
|
|
111
|
+
#### `CodaAccount`
|
|
112
|
+
|
|
113
|
+
| Field | Type | Description |
|
|
114
|
+
|-------|------|-------------|
|
|
115
|
+
| `structure` | `"belgian-bban" \| "foreign-bban" \| "belgian-iban" \| "foreign-iban"` | Account number format |
|
|
116
|
+
| `number` | `string` | Account number (BBAN or IBAN) |
|
|
117
|
+
| `currency` | `string?` | ISO currency code |
|
|
118
|
+
| `countryCode` | `string?` | ISO country code (Belgian BBAN only) |
|
|
119
|
+
|
|
120
|
+
#### `CodaBalance`
|
|
121
|
+
|
|
122
|
+
| Field | Type | Description |
|
|
123
|
+
|-------|------|-------------|
|
|
124
|
+
| `amount` | `number` | Signed amount (positive = credit, negative = debit) |
|
|
125
|
+
| `date` | `Date` | Balance date |
|
|
126
|
+
|
|
127
|
+
#### `CodaMovement`
|
|
128
|
+
|
|
129
|
+
| Field | Type | Description |
|
|
130
|
+
|-------|------|-------------|
|
|
131
|
+
| `sequenceNumber` | `number` | Continuous sequence number |
|
|
132
|
+
| `detailNumber` | `number` | Detail number within sequence |
|
|
133
|
+
| `bankReference` | `string` | Bank reference (informative) |
|
|
134
|
+
| `amount` | `number` | Signed amount |
|
|
135
|
+
| `valueDate` | `Date?` | Value date |
|
|
136
|
+
| `entryDate` | `Date` | Entry/booking date |
|
|
137
|
+
| `transactionCode` | `CodaTransactionCode` | 8-digit transaction code |
|
|
138
|
+
| `communication` | `string` | Full communication text |
|
|
139
|
+
| `communicationType` | `"structured" \| "unstructured"` | Communication format |
|
|
140
|
+
| `structuredCommunicationType` | `number?` | 3-digit type (e.g. 101, 127) |
|
|
141
|
+
| `paperStatementSequence` | `number` | Paper statement sequence |
|
|
142
|
+
| `globalisationCode` | `number?` | Globalisation hierarchy level (1-9) |
|
|
143
|
+
| `customerReference` | `string?` | Customer reference |
|
|
144
|
+
| `counterpartyBic` | `string?` | Counterparty's bank BIC |
|
|
145
|
+
| `rTransactionType` | `number?` | R-transaction type (1-5) |
|
|
146
|
+
| `rTransactionReason` | `string?` | ISO reason return code |
|
|
147
|
+
| `categoryPurpose` | `string?` | SEPA CategoryPurpose |
|
|
148
|
+
| `purpose` | `string?` | SEPA Purpose |
|
|
149
|
+
| `counterpartyAccountNumber` | `string?` | Counterparty account number |
|
|
150
|
+
| `counterpartyAccountCurrency` | `string?` | Counterparty account currency |
|
|
151
|
+
| `counterpartyName` | `string?` | Counterparty name |
|
|
152
|
+
| `information` | `CodaInformation[]` | Linked information records |
|
|
153
|
+
|
|
154
|
+
#### `CodaTransactionCode`
|
|
155
|
+
|
|
156
|
+
| Field | Type | Description |
|
|
157
|
+
|-------|------|-------------|
|
|
158
|
+
| `type` | `number` | Type (0=simple, 1=customer total, 2=bank total, 5/6/7/8/9=details, 3=with detail) |
|
|
159
|
+
| `family` | `number` | Family (01-39 domestic/SEPA, 41-79 foreign, 80-89 other) |
|
|
160
|
+
| `transaction` | `number` | Transaction within family |
|
|
161
|
+
| `category` | `number` | Category (000=net amount, others for cost breakdowns) |
|
|
162
|
+
|
|
163
|
+
#### `CodaInformation`
|
|
164
|
+
|
|
165
|
+
| Field | Type | Description |
|
|
166
|
+
|-------|------|-------------|
|
|
167
|
+
| `sequenceNumber` | `number` | Matches parent movement |
|
|
168
|
+
| `detailNumber` | `number` | Detail number |
|
|
169
|
+
| `bankReference` | `string` | Bank reference |
|
|
170
|
+
| `transactionCode` | `CodaTransactionCode` | Transaction code |
|
|
171
|
+
| `communication` | `string` | Full communication text |
|
|
172
|
+
| `communicationType` | `"structured" \| "unstructured"` | Communication format |
|
|
173
|
+
| `structuredCommunicationType` | `number?` | 3-digit type code |
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm test # run tests in watch mode
|
|
179
|
+
npm run test:run # run tests once
|
|
180
|
+
npm run lint # eslint
|
|
181
|
+
npm run format # prettier
|
|
182
|
+
npm run build # build to dist/
|
|
183
|
+
npm run ci # type-check + lint + test + build
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Top-level result of parsing a CODA file. */
|
|
2
|
+
interface CodaFile {
|
|
3
|
+
/** One or more statements contained in the file. */
|
|
4
|
+
statements: CodaStatement[];
|
|
5
|
+
}
|
|
6
|
+
/** A single bank statement within a CODA file. */
|
|
7
|
+
interface CodaStatement {
|
|
8
|
+
/** File creation date. */
|
|
9
|
+
creationDate: Date;
|
|
10
|
+
/** Bank identification number (3 digits). */
|
|
11
|
+
bankId: number;
|
|
12
|
+
/** Whether this is a duplicate file. */
|
|
13
|
+
isDuplicate: boolean;
|
|
14
|
+
/** File reference assigned by the bank. */
|
|
15
|
+
fileReference?: string;
|
|
16
|
+
/** Name of the addressee. */
|
|
17
|
+
addressee: string;
|
|
18
|
+
/** BIC of the bank holding the account. */
|
|
19
|
+
bic?: string;
|
|
20
|
+
/** Identification number of the Belgium-based account holder. */
|
|
21
|
+
companyId?: string;
|
|
22
|
+
/** Separate application code (5 positions). */
|
|
23
|
+
separateApplication: string;
|
|
24
|
+
/** Transaction reference (MT940 tag 20). */
|
|
25
|
+
transactionReference?: string;
|
|
26
|
+
/** Related reference (MT940 tag 21). */
|
|
27
|
+
relatedReference?: string;
|
|
28
|
+
/** CODA standard version number. */
|
|
29
|
+
version: number;
|
|
30
|
+
/** Account details. */
|
|
31
|
+
account: CodaAccount;
|
|
32
|
+
/** Paper statement sequence number. */
|
|
33
|
+
paperStatementSequence: number;
|
|
34
|
+
/** CODA statement sequence number. */
|
|
35
|
+
codaStatementSequence: number;
|
|
36
|
+
/** Name of the account holder (from old balance record). */
|
|
37
|
+
accountHolderName?: string;
|
|
38
|
+
/** Account description text. */
|
|
39
|
+
accountDescription?: string;
|
|
40
|
+
/** Opening balance. */
|
|
41
|
+
oldBalance: CodaBalance;
|
|
42
|
+
/** Closing balance. Absent in empty files without record 8. */
|
|
43
|
+
newBalance?: CodaBalance;
|
|
44
|
+
/** Transaction movements (records 2.x with linked 3.x). */
|
|
45
|
+
movements: CodaMovement[];
|
|
46
|
+
/** Free communication texts. */
|
|
47
|
+
freeCommunications: string[];
|
|
48
|
+
/** Sum of debit movement amounts. */
|
|
49
|
+
totalDebit: number;
|
|
50
|
+
/** Sum of credit movement amounts. */
|
|
51
|
+
totalCredit: number;
|
|
52
|
+
}
|
|
53
|
+
/** Account number and currency information. */
|
|
54
|
+
interface CodaAccount {
|
|
55
|
+
/** Account number structure type. */
|
|
56
|
+
structure: "belgian-bban" | "foreign-bban" | "belgian-iban" | "foreign-iban";
|
|
57
|
+
/** Account number (BBAN or IBAN). */
|
|
58
|
+
number: string;
|
|
59
|
+
/** ISO currency code. */
|
|
60
|
+
currency?: string;
|
|
61
|
+
/** ISO country code (available for Belgian BBAN). */
|
|
62
|
+
countryCode?: string;
|
|
63
|
+
}
|
|
64
|
+
/** A balance (old or new) with amount and date. */
|
|
65
|
+
interface CodaBalance {
|
|
66
|
+
/** Signed amount: positive for credit, negative for debit. */
|
|
67
|
+
amount: number;
|
|
68
|
+
/** Balance date. */
|
|
69
|
+
date: Date;
|
|
70
|
+
}
|
|
71
|
+
/** A single transaction movement assembled from records 2.1, 2.2, 2.3. */
|
|
72
|
+
interface CodaMovement {
|
|
73
|
+
/** Continuous sequence number (groups related records). */
|
|
74
|
+
sequenceNumber: number;
|
|
75
|
+
/** Detail number within the sequence. */
|
|
76
|
+
detailNumber: number;
|
|
77
|
+
/** Bank reference number (informative). */
|
|
78
|
+
bankReference: string;
|
|
79
|
+
/** Signed amount: positive for credit, negative for debit. */
|
|
80
|
+
amount: number;
|
|
81
|
+
/** Value date. Undefined if not known (000000). */
|
|
82
|
+
valueDate?: Date;
|
|
83
|
+
/** Entry/booking date. */
|
|
84
|
+
entryDate: Date;
|
|
85
|
+
/** 8-digit transaction code broken into type/family/transaction/category. */
|
|
86
|
+
transactionCode: CodaTransactionCode;
|
|
87
|
+
/** Full communication text concatenated from all record parts. */
|
|
88
|
+
communication: string;
|
|
89
|
+
/** Whether the communication uses structured or free format. */
|
|
90
|
+
communicationType: "structured" | "unstructured";
|
|
91
|
+
/** 3-digit structured communication type code (e.g. 101, 102, 127). */
|
|
92
|
+
structuredCommunicationType?: number;
|
|
93
|
+
/** Paper statement sequence number. */
|
|
94
|
+
paperStatementSequence: number;
|
|
95
|
+
/** Globalisation code (1-9). Undefined if not set. */
|
|
96
|
+
globalisationCode?: number;
|
|
97
|
+
/** Customer reference (up to 35 chars). */
|
|
98
|
+
customerReference?: string;
|
|
99
|
+
/** BIC of the counterparty's bank. */
|
|
100
|
+
counterpartyBic?: string;
|
|
101
|
+
/**
|
|
102
|
+
* R-transaction type.
|
|
103
|
+
* 1=reject, 2=return, 3=refund, 4=reversal, 5=cancellation.
|
|
104
|
+
*/
|
|
105
|
+
rTransactionType?: number;
|
|
106
|
+
/** ISO reason return code for R-transactions. */
|
|
107
|
+
rTransactionReason?: string;
|
|
108
|
+
/** SEPA CategoryPurpose code. */
|
|
109
|
+
categoryPurpose?: string;
|
|
110
|
+
/** SEPA Purpose code. */
|
|
111
|
+
purpose?: string;
|
|
112
|
+
/** Counterparty account number. */
|
|
113
|
+
counterpartyAccountNumber?: string;
|
|
114
|
+
/** Counterparty account currency. */
|
|
115
|
+
counterpartyAccountCurrency?: string;
|
|
116
|
+
/** Counterparty name. */
|
|
117
|
+
counterpartyName?: string;
|
|
118
|
+
/** Information records (3.x) linked to this movement. */
|
|
119
|
+
information: CodaInformation[];
|
|
120
|
+
}
|
|
121
|
+
/** 8-digit CODA transaction code. */
|
|
122
|
+
interface CodaTransactionCode {
|
|
123
|
+
/** Type digit (0-9): simple, totalised by customer/bank, detail, etc. */
|
|
124
|
+
type: number;
|
|
125
|
+
/** Family code (01-89): broad category (transfers, cards, etc.). */
|
|
126
|
+
family: number;
|
|
127
|
+
/** Transaction code within the family. */
|
|
128
|
+
transaction: number;
|
|
129
|
+
/** Category code (000-999): additional detail. */
|
|
130
|
+
category: number;
|
|
131
|
+
}
|
|
132
|
+
/** An information record group assembled from records 3.1, 3.2, 3.3. */
|
|
133
|
+
interface CodaInformation {
|
|
134
|
+
/** Continuous sequence number (matches the parent movement). */
|
|
135
|
+
sequenceNumber: number;
|
|
136
|
+
/** Detail number. */
|
|
137
|
+
detailNumber: number;
|
|
138
|
+
/** Bank reference. */
|
|
139
|
+
bankReference: string;
|
|
140
|
+
/** Transaction code. */
|
|
141
|
+
transactionCode: CodaTransactionCode;
|
|
142
|
+
/** Full communication text concatenated from all parts. */
|
|
143
|
+
communication: string;
|
|
144
|
+
/** Whether the communication uses structured or free format. */
|
|
145
|
+
communicationType: "structured" | "unstructured";
|
|
146
|
+
/** 3-digit structured communication type code. */
|
|
147
|
+
structuredCommunicationType?: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse a CODA file content string into a structured {@link CodaFile}.
|
|
152
|
+
* Returns `null` if the input cannot be parsed.
|
|
153
|
+
*/
|
|
154
|
+
declare function parseCoda(content: string): CodaFile | null;
|
|
155
|
+
|
|
156
|
+
export { type CodaAccount, type CodaBalance, type CodaFile, type CodaInformation, type CodaMovement, type CodaStatement, type CodaTransactionCode, parseCoda };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// src/parser.ts
|
|
2
|
+
function parseCoda(content) {
|
|
3
|
+
try {
|
|
4
|
+
const lines = content.split(/\r?\n/).filter((l) => l.length > 0);
|
|
5
|
+
if (lines.length === 0) return null;
|
|
6
|
+
if (lines[0]?.[0] !== "0") return null;
|
|
7
|
+
const groups = groupIntoStatements(lines);
|
|
8
|
+
if (groups.length === 0) return null;
|
|
9
|
+
const statements = [];
|
|
10
|
+
for (const group of groups) {
|
|
11
|
+
const stmt = parseStatementGroup(group);
|
|
12
|
+
if (stmt) statements.push(stmt);
|
|
13
|
+
}
|
|
14
|
+
if (statements.length === 0) return null;
|
|
15
|
+
return { statements };
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function groupIntoStatements(lines) {
|
|
21
|
+
const groups = [];
|
|
22
|
+
let current = [];
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (line[0] === "0") {
|
|
25
|
+
if (current.length > 0) groups.push(current);
|
|
26
|
+
current = [line];
|
|
27
|
+
} else {
|
|
28
|
+
current.push(line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (current.length > 0) groups.push(current);
|
|
32
|
+
return groups;
|
|
33
|
+
}
|
|
34
|
+
function sub(line, start, end) {
|
|
35
|
+
return line.substring(start - 1, end);
|
|
36
|
+
}
|
|
37
|
+
function trimSub(line, start, end) {
|
|
38
|
+
return sub(line, start, end).trim();
|
|
39
|
+
}
|
|
40
|
+
function numSub(line, start, end) {
|
|
41
|
+
const s = sub(line, start, end).trim();
|
|
42
|
+
if (s === "") return 0;
|
|
43
|
+
const n = Number(s);
|
|
44
|
+
return Number.isFinite(n) ? n : 0;
|
|
45
|
+
}
|
|
46
|
+
function parseAmount(signChar, amountStr) {
|
|
47
|
+
const raw = Number(amountStr.trim());
|
|
48
|
+
if (!Number.isFinite(raw)) return 0;
|
|
49
|
+
const amount = raw / 1e3;
|
|
50
|
+
return signChar === "1" ? -amount : amount;
|
|
51
|
+
}
|
|
52
|
+
function parseCodaDate(s) {
|
|
53
|
+
const t = s.trim();
|
|
54
|
+
if (!t || t === "000000") return void 0;
|
|
55
|
+
const day = Number(t.substring(0, 2));
|
|
56
|
+
const month = Number(t.substring(2, 4));
|
|
57
|
+
const year = Number(t.substring(4, 6));
|
|
58
|
+
if (!Number.isFinite(day) || !Number.isFinite(month) || !Number.isFinite(year))
|
|
59
|
+
return void 0;
|
|
60
|
+
const fullYear = year >= 80 ? 1900 + year : 2e3 + year;
|
|
61
|
+
return new Date(fullYear, month - 1, day);
|
|
62
|
+
}
|
|
63
|
+
function parseTransactionCode(raw) {
|
|
64
|
+
return {
|
|
65
|
+
type: Number(raw.substring(0, 1)) || 0,
|
|
66
|
+
family: Number(raw.substring(1, 3)) || 0,
|
|
67
|
+
transaction: Number(raw.substring(3, 5)) || 0,
|
|
68
|
+
category: Number(raw.substring(5, 8)) || 0
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseAccountField(structureCode, field) {
|
|
72
|
+
switch (structureCode) {
|
|
73
|
+
case "0":
|
|
74
|
+
return {
|
|
75
|
+
structure: "belgian-bban",
|
|
76
|
+
number: field.substring(0, 12).trim(),
|
|
77
|
+
currency: field.substring(13, 16).trim() || void 0,
|
|
78
|
+
countryCode: field.substring(17, 19).trim() || void 0
|
|
79
|
+
};
|
|
80
|
+
case "1":
|
|
81
|
+
return {
|
|
82
|
+
structure: "foreign-bban",
|
|
83
|
+
number: field.substring(0, 34).trim(),
|
|
84
|
+
currency: field.substring(34, 37).trim() || void 0
|
|
85
|
+
};
|
|
86
|
+
case "2":
|
|
87
|
+
return {
|
|
88
|
+
structure: "belgian-iban",
|
|
89
|
+
number: field.substring(0, 31).trim(),
|
|
90
|
+
currency: field.substring(34, 37).trim() || void 0
|
|
91
|
+
};
|
|
92
|
+
case "3":
|
|
93
|
+
return {
|
|
94
|
+
structure: "foreign-iban",
|
|
95
|
+
number: field.substring(0, 34).trim(),
|
|
96
|
+
currency: field.substring(34, 37).trim() || void 0
|
|
97
|
+
};
|
|
98
|
+
default:
|
|
99
|
+
return { structure: "belgian-bban", number: field.trim() };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function parseCounterpartyAccount(field) {
|
|
103
|
+
const trimmed = field.trimEnd();
|
|
104
|
+
if (!trimmed) return {};
|
|
105
|
+
const match = /^(.*\S)\s+([A-Z]{3})$/.exec(trimmed);
|
|
106
|
+
if (match?.[1] && match[2]) {
|
|
107
|
+
return { number: match[1], currency: match[2] };
|
|
108
|
+
}
|
|
109
|
+
return { number: trimmed };
|
|
110
|
+
}
|
|
111
|
+
function parseCommunicationField(typeChar, field) {
|
|
112
|
+
if (typeChar === "1") {
|
|
113
|
+
const code = Number(field.substring(0, 3));
|
|
114
|
+
return {
|
|
115
|
+
rawContent: field.substring(3),
|
|
116
|
+
communicationType: "structured",
|
|
117
|
+
structuredCommunicationType: Number.isFinite(code) ? code : void 0
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { rawContent: field, communicationType: "unstructured" };
|
|
121
|
+
}
|
|
122
|
+
function parseHeader(line) {
|
|
123
|
+
if (line[0] !== "0") return null;
|
|
124
|
+
return {
|
|
125
|
+
creationDate: parseCodaDate(sub(line, 6, 11)) ?? /* @__PURE__ */ new Date(0),
|
|
126
|
+
bankId: numSub(line, 12, 14),
|
|
127
|
+
isDuplicate: sub(line, 17, 17) === "D",
|
|
128
|
+
fileReference: trimSub(line, 25, 34) || void 0,
|
|
129
|
+
addressee: trimSub(line, 35, 60),
|
|
130
|
+
bic: trimSub(line, 61, 71) || void 0,
|
|
131
|
+
companyId: trimSub(line, 72, 82) || void 0,
|
|
132
|
+
separateApplication: trimSub(line, 84, 88),
|
|
133
|
+
transactionReference: trimSub(line, 89, 104) || void 0,
|
|
134
|
+
relatedReference: trimSub(line, 105, 120) || void 0,
|
|
135
|
+
version: numSub(line, 128, 128)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function parseOldBalance(line) {
|
|
139
|
+
if (line[0] !== "1") return null;
|
|
140
|
+
return {
|
|
141
|
+
account: parseAccountField(sub(line, 2, 2), sub(line, 6, 42)),
|
|
142
|
+
paperStatementSequence: numSub(line, 3, 5),
|
|
143
|
+
codaStatementSequence: numSub(line, 126, 128),
|
|
144
|
+
accountHolderName: trimSub(line, 65, 90) || void 0,
|
|
145
|
+
accountDescription: trimSub(line, 91, 125) || void 0,
|
|
146
|
+
oldBalance: {
|
|
147
|
+
amount: parseAmount(sub(line, 43, 43), sub(line, 44, 58)),
|
|
148
|
+
date: parseCodaDate(sub(line, 59, 64)) ?? /* @__PURE__ */ new Date(0)
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function parseRecord21(line) {
|
|
153
|
+
if (line[0] !== "2" || line[1] !== "1") return null;
|
|
154
|
+
const glob = numSub(line, 125, 125);
|
|
155
|
+
return {
|
|
156
|
+
sequenceNumber: numSub(line, 3, 6),
|
|
157
|
+
detailNumber: numSub(line, 7, 10),
|
|
158
|
+
bankReference: trimSub(line, 11, 31),
|
|
159
|
+
amount: parseAmount(sub(line, 32, 32), sub(line, 33, 47)),
|
|
160
|
+
valueDate: parseCodaDate(sub(line, 48, 53)),
|
|
161
|
+
entryDate: parseCodaDate(sub(line, 116, 121)) ?? /* @__PURE__ */ new Date(0),
|
|
162
|
+
transactionCode: parseTransactionCode(sub(line, 54, 61)),
|
|
163
|
+
comm: parseCommunicationField(sub(line, 62, 62), sub(line, 63, 115)),
|
|
164
|
+
paperStatementSequence: numSub(line, 122, 124),
|
|
165
|
+
globalisationCode: glob > 0 ? glob : void 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function parseRecord22(line) {
|
|
169
|
+
if (line[0] !== "2" || line[1] !== "2") return null;
|
|
170
|
+
const rTypeStr = trimSub(line, 113, 113);
|
|
171
|
+
const rType = rTypeStr ? Number(rTypeStr) : void 0;
|
|
172
|
+
return {
|
|
173
|
+
sequenceNumber: numSub(line, 3, 6),
|
|
174
|
+
detailNumber: numSub(line, 7, 10),
|
|
175
|
+
commContinuation: sub(line, 11, 63),
|
|
176
|
+
customerReference: trimSub(line, 64, 98) || void 0,
|
|
177
|
+
counterpartyBic: trimSub(line, 99, 109) || void 0,
|
|
178
|
+
rTransactionType: rType !== void 0 && Number.isFinite(rType) ? rType : void 0,
|
|
179
|
+
rTransactionReason: trimSub(line, 114, 117) || void 0,
|
|
180
|
+
categoryPurpose: trimSub(line, 118, 121) || void 0,
|
|
181
|
+
purpose: trimSub(line, 122, 125) || void 0
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function parseRecord23(line) {
|
|
185
|
+
if (line[0] !== "2" || line[1] !== "3") return null;
|
|
186
|
+
return {
|
|
187
|
+
sequenceNumber: numSub(line, 3, 6),
|
|
188
|
+
detailNumber: numSub(line, 7, 10),
|
|
189
|
+
counterpartyAccountField: sub(line, 11, 47),
|
|
190
|
+
counterpartyName: trimSub(line, 48, 82),
|
|
191
|
+
commContinuation: sub(line, 83, 125)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function parseRecord31(line) {
|
|
195
|
+
if (line[0] !== "3" || line[1] !== "1") return null;
|
|
196
|
+
return {
|
|
197
|
+
sequenceNumber: numSub(line, 3, 6),
|
|
198
|
+
detailNumber: numSub(line, 7, 10),
|
|
199
|
+
bankReference: trimSub(line, 11, 31),
|
|
200
|
+
transactionCode: parseTransactionCode(sub(line, 32, 39)),
|
|
201
|
+
comm: parseCommunicationField(sub(line, 40, 40), sub(line, 41, 113))
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function parseRecord32Comm(line) {
|
|
205
|
+
if (line[0] !== "3" || line[1] !== "2") return null;
|
|
206
|
+
return sub(line, 11, 115);
|
|
207
|
+
}
|
|
208
|
+
function parseRecord33Comm(line) {
|
|
209
|
+
if (line[0] !== "3" || line[1] !== "3") return null;
|
|
210
|
+
return sub(line, 11, 100);
|
|
211
|
+
}
|
|
212
|
+
function parseNewBalance(line) {
|
|
213
|
+
if (line[0] !== "8") return null;
|
|
214
|
+
return {
|
|
215
|
+
amount: parseAmount(sub(line, 42, 42), sub(line, 43, 57)),
|
|
216
|
+
date: parseCodaDate(sub(line, 58, 63)) ?? /* @__PURE__ */ new Date(0)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function parseTrailer(line) {
|
|
220
|
+
if (line[0] !== "9") return null;
|
|
221
|
+
return {
|
|
222
|
+
totalDebit: Number(sub(line, 23, 37).trim()) / 1e3 || 0,
|
|
223
|
+
totalCredit: Number(sub(line, 38, 52).trim()) / 1e3 || 0
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function assembleMovement(r21, r22, r23, infoGroups) {
|
|
227
|
+
const commParts = [r21.comm.rawContent];
|
|
228
|
+
if (r22) commParts.push(r22.commContinuation);
|
|
229
|
+
if (r23) commParts.push(r23.commContinuation);
|
|
230
|
+
const fullComm = commParts.join("").trimEnd();
|
|
231
|
+
let counterpartyAccountNumber;
|
|
232
|
+
let counterpartyAccountCurrency;
|
|
233
|
+
if (r23) {
|
|
234
|
+
const cp = parseCounterpartyAccount(r23.counterpartyAccountField);
|
|
235
|
+
counterpartyAccountNumber = cp.number;
|
|
236
|
+
counterpartyAccountCurrency = cp.currency;
|
|
237
|
+
}
|
|
238
|
+
const information = infoGroups.map((ig) => {
|
|
239
|
+
const parts = [ig.r31.comm.rawContent, ...ig.continuations];
|
|
240
|
+
const comm = parts.join("").trimEnd();
|
|
241
|
+
return {
|
|
242
|
+
sequenceNumber: ig.r31.sequenceNumber,
|
|
243
|
+
detailNumber: ig.r31.detailNumber,
|
|
244
|
+
bankReference: ig.r31.bankReference,
|
|
245
|
+
transactionCode: ig.r31.transactionCode,
|
|
246
|
+
communication: comm,
|
|
247
|
+
communicationType: ig.r31.comm.communicationType,
|
|
248
|
+
structuredCommunicationType: ig.r31.comm.structuredCommunicationType
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
sequenceNumber: r21.sequenceNumber,
|
|
253
|
+
detailNumber: r21.detailNumber,
|
|
254
|
+
bankReference: r21.bankReference,
|
|
255
|
+
amount: r21.amount,
|
|
256
|
+
valueDate: r21.valueDate,
|
|
257
|
+
entryDate: r21.entryDate,
|
|
258
|
+
transactionCode: r21.transactionCode,
|
|
259
|
+
communication: fullComm,
|
|
260
|
+
communicationType: r21.comm.communicationType,
|
|
261
|
+
structuredCommunicationType: r21.comm.structuredCommunicationType,
|
|
262
|
+
paperStatementSequence: r21.paperStatementSequence,
|
|
263
|
+
globalisationCode: r21.globalisationCode,
|
|
264
|
+
customerReference: r22?.customerReference,
|
|
265
|
+
counterpartyBic: r22?.counterpartyBic,
|
|
266
|
+
rTransactionType: r22?.rTransactionType,
|
|
267
|
+
rTransactionReason: r22?.rTransactionReason,
|
|
268
|
+
categoryPurpose: r22?.categoryPurpose,
|
|
269
|
+
purpose: r22?.purpose,
|
|
270
|
+
counterpartyAccountNumber,
|
|
271
|
+
counterpartyAccountCurrency,
|
|
272
|
+
counterpartyName: r23?.counterpartyName,
|
|
273
|
+
information
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function parseStatementGroup(lines) {
|
|
277
|
+
if (lines.length === 0) return null;
|
|
278
|
+
const headerLine = lines.find((l) => l[0] === "0");
|
|
279
|
+
if (!headerLine) return null;
|
|
280
|
+
const header = parseHeader(headerLine);
|
|
281
|
+
if (!header) return null;
|
|
282
|
+
const balLine = lines.find((l) => l[0] === "1");
|
|
283
|
+
if (!balLine) return null;
|
|
284
|
+
const oldBal = parseOldBalance(balLine);
|
|
285
|
+
if (!oldBal) return null;
|
|
286
|
+
const movements = [];
|
|
287
|
+
const movInfoLines = lines.filter((l) => l[0] === "2" || l[0] === "3");
|
|
288
|
+
let cur21 = null;
|
|
289
|
+
let cur22 = null;
|
|
290
|
+
let cur23 = null;
|
|
291
|
+
let curInfoGroups = [];
|
|
292
|
+
function flushMovement() {
|
|
293
|
+
if (cur21) {
|
|
294
|
+
movements.push(assembleMovement(cur21, cur22, cur23, curInfoGroups));
|
|
295
|
+
}
|
|
296
|
+
cur21 = null;
|
|
297
|
+
cur22 = null;
|
|
298
|
+
cur23 = null;
|
|
299
|
+
curInfoGroups = [];
|
|
300
|
+
}
|
|
301
|
+
let i = 0;
|
|
302
|
+
while (i < movInfoLines.length) {
|
|
303
|
+
const line = movInfoLines[i] ?? "";
|
|
304
|
+
const recType = line[0];
|
|
305
|
+
const article = line[1];
|
|
306
|
+
if (recType === "2" && article === "1") {
|
|
307
|
+
flushMovement();
|
|
308
|
+
cur21 = parseRecord21(line);
|
|
309
|
+
i++;
|
|
310
|
+
} else if (recType === "2" && article === "2") {
|
|
311
|
+
cur22 = parseRecord22(line);
|
|
312
|
+
i++;
|
|
313
|
+
} else if (recType === "2" && article === "3") {
|
|
314
|
+
cur23 = parseRecord23(line);
|
|
315
|
+
i++;
|
|
316
|
+
} else if (recType === "3" && article === "1") {
|
|
317
|
+
const r31 = parseRecord31(line);
|
|
318
|
+
if (r31) {
|
|
319
|
+
const continuations = [];
|
|
320
|
+
i++;
|
|
321
|
+
while (i < movInfoLines.length) {
|
|
322
|
+
const next = movInfoLines[i] ?? "";
|
|
323
|
+
if (next[0] === "3" && next[1] === "2") {
|
|
324
|
+
const c = parseRecord32Comm(next);
|
|
325
|
+
if (c) continuations.push(c);
|
|
326
|
+
i++;
|
|
327
|
+
} else if (next[0] === "3" && next[1] === "3") {
|
|
328
|
+
const c = parseRecord33Comm(next);
|
|
329
|
+
if (c) continuations.push(c);
|
|
330
|
+
i++;
|
|
331
|
+
} else {
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
curInfoGroups.push({ r31, continuations });
|
|
336
|
+
} else {
|
|
337
|
+
i++;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
i++;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
flushMovement();
|
|
344
|
+
const newBalLine = lines.find((l) => l[0] === "8");
|
|
345
|
+
const newBalance = newBalLine ? parseNewBalance(newBalLine) : void 0;
|
|
346
|
+
const freeCommMap = /* @__PURE__ */ new Map();
|
|
347
|
+
for (const line of lines) {
|
|
348
|
+
if (line[0] !== "4") continue;
|
|
349
|
+
const seqNum = numSub(line, 3, 6);
|
|
350
|
+
const text = trimSub(line, 33, 112);
|
|
351
|
+
if (!text) continue;
|
|
352
|
+
const parts = freeCommMap.get(seqNum) ?? [];
|
|
353
|
+
parts.push(text);
|
|
354
|
+
freeCommMap.set(seqNum, parts);
|
|
355
|
+
}
|
|
356
|
+
const freeCommunications = [...freeCommMap.values()].map((p) => p.join(""));
|
|
357
|
+
const trailerLine = lines.find((l) => l[0] === "9");
|
|
358
|
+
const trailer = trailerLine ? parseTrailer(trailerLine) : null;
|
|
359
|
+
return {
|
|
360
|
+
...header,
|
|
361
|
+
...oldBal,
|
|
362
|
+
movements,
|
|
363
|
+
newBalance: newBalance ?? void 0,
|
|
364
|
+
freeCommunications,
|
|
365
|
+
totalDebit: trailer?.totalDebit ?? 0,
|
|
366
|
+
totalCredit: trailer?.totalCredit ?? 0
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
export {
|
|
370
|
+
parseCoda
|
|
371
|
+
};
|
|
372
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts"],"sourcesContent":["import type {\n\tCodaFile,\n\tCodaStatement,\n\tCodaAccount,\n\tCodaBalance,\n\tCodaMovement,\n\tCodaTransactionCode,\n\tCodaInformation,\n} from \"./types.js\";\n\n/**\n * Parse a CODA file content string into a structured {@link CodaFile}.\n * Returns `null` if the input cannot be parsed.\n */\nexport function parseCoda(content: string): CodaFile | null {\n\ttry {\n\t\tconst lines = content.split(/\\r?\\n/).filter((l) => l.length > 0);\n\t\tif (lines.length === 0) return null;\n\t\tif (lines[0]?.[0] !== \"0\") return null;\n\n\t\tconst groups = groupIntoStatements(lines);\n\t\tif (groups.length === 0) return null;\n\n\t\tconst statements: CodaStatement[] = [];\n\t\tfor (const group of groups) {\n\t\t\tconst stmt = parseStatementGroup(group);\n\t\t\tif (stmt) statements.push(stmt);\n\t\t}\n\n\t\tif (statements.length === 0) return null;\n\t\treturn { statements };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n// ── Line grouping ─────────────────────────────────────────────────────\n\nfunction groupIntoStatements(lines: string[]): string[][] {\n\tconst groups: string[][] = [];\n\tlet current: string[] = [];\n\n\tfor (const line of lines) {\n\t\tif (line[0] === \"0\") {\n\t\t\tif (current.length > 0) groups.push(current);\n\t\t\tcurrent = [line];\n\t\t} else {\n\t\t\tcurrent.push(line);\n\t\t}\n\t}\n\tif (current.length > 0) groups.push(current);\n\treturn groups;\n}\n\n// ── Substring helpers (1-indexed, inclusive) ───────────────────────────\n\nfunction sub(line: string, start: number, end: number): string {\n\treturn line.substring(start - 1, end);\n}\n\nfunction trimSub(line: string, start: number, end: number): string {\n\treturn sub(line, start, end).trim();\n}\n\nfunction numSub(line: string, start: number, end: number): number {\n\tconst s = sub(line, start, end).trim();\n\tif (s === \"\") return 0;\n\tconst n = Number(s);\n\treturn Number.isFinite(n) ? n : 0;\n}\n\n// ── Value parsers ─────────────────────────────────────────────────────\n\nfunction parseAmount(signChar: string, amountStr: string): number {\n\tconst raw = Number(amountStr.trim());\n\tif (!Number.isFinite(raw)) return 0;\n\tconst amount = raw / 1000;\n\treturn signChar === \"1\" ? -amount : amount;\n}\n\nfunction parseCodaDate(s: string): Date | undefined {\n\tconst t = s.trim();\n\tif (!t || t === \"000000\") return undefined;\n\tconst day = Number(t.substring(0, 2));\n\tconst month = Number(t.substring(2, 4));\n\tconst year = Number(t.substring(4, 6));\n\tif (!Number.isFinite(day) || !Number.isFinite(month) || !Number.isFinite(year))\n\t\treturn undefined;\n\tconst fullYear = year >= 80 ? 1900 + year : 2000 + year;\n\treturn new Date(fullYear, month - 1, day);\n}\n\nfunction parseTransactionCode(raw: string): CodaTransactionCode {\n\treturn {\n\t\ttype: Number(raw.substring(0, 1)) || 0,\n\t\tfamily: Number(raw.substring(1, 3)) || 0,\n\t\ttransaction: Number(raw.substring(3, 5)) || 0,\n\t\tcategory: Number(raw.substring(5, 8)) || 0,\n\t};\n}\n\n// ── Account parsing ───────────────────────────────────────────────────\n\nfunction parseAccountField(structureCode: string, field: string): CodaAccount {\n\tswitch (structureCode) {\n\t\tcase \"0\":\n\t\t\t// Belgian BBAN: 12N account + 1AN blank + 3AN currency + 1N qual + 2AN country + 18AN extension\n\t\t\treturn {\n\t\t\t\tstructure: \"belgian-bban\",\n\t\t\t\tnumber: field.substring(0, 12).trim(),\n\t\t\t\tcurrency: field.substring(13, 16).trim() || undefined,\n\t\t\t\tcountryCode: field.substring(17, 19).trim() || undefined,\n\t\t\t};\n\t\tcase \"1\":\n\t\t\t// Foreign BBAN: 34AN number + 3AN currency\n\t\t\treturn {\n\t\t\t\tstructure: \"foreign-bban\",\n\t\t\t\tnumber: field.substring(0, 34).trim(),\n\t\t\t\tcurrency: field.substring(34, 37).trim() || undefined,\n\t\t\t};\n\t\tcase \"2\":\n\t\t\t// Belgian IBAN: 31AN IBAN + 3AN extension + 3AN currency\n\t\t\treturn {\n\t\t\t\tstructure: \"belgian-iban\",\n\t\t\t\tnumber: field.substring(0, 31).trim(),\n\t\t\t\tcurrency: field.substring(34, 37).trim() || undefined,\n\t\t\t};\n\t\tcase \"3\":\n\t\t\t// Foreign IBAN: 34AN IBAN + 3AN currency\n\t\t\treturn {\n\t\t\t\tstructure: \"foreign-iban\",\n\t\t\t\tnumber: field.substring(0, 34).trim(),\n\t\t\t\tcurrency: field.substring(34, 37).trim() || undefined,\n\t\t\t};\n\t\tdefault:\n\t\t\treturn { structure: \"belgian-bban\", number: field.trim() };\n\t}\n}\n\nfunction parseCounterpartyAccount(field: string): {\n\tnumber?: string;\n\tcurrency?: string;\n} {\n\tconst trimmed = field.trimEnd();\n\tif (!trimmed) return {};\n\n\t// Currency is 3 uppercase letters at the end, preceded by whitespace\n\tconst match = /^(.*\\S)\\s+([A-Z]{3})$/.exec(trimmed);\n\tif (match?.[1] && match[2]) {\n\t\treturn { number: match[1], currency: match[2] };\n\t}\n\treturn { number: trimmed };\n}\n\n// ── Communication parsing ─────────────────────────────────────────────\n\ninterface ParsedComm {\n\trawContent: string;\n\tcommunicationType: \"structured\" | \"unstructured\";\n\tstructuredCommunicationType?: number;\n}\n\nfunction parseCommunicationField(typeChar: string, field: string): ParsedComm {\n\tif (typeChar === \"1\") {\n\t\tconst code = Number(field.substring(0, 3));\n\t\treturn {\n\t\t\trawContent: field.substring(3),\n\t\t\tcommunicationType: \"structured\",\n\t\t\tstructuredCommunicationType: Number.isFinite(code) ? code : undefined,\n\t\t};\n\t}\n\treturn { rawContent: field, communicationType: \"unstructured\" };\n}\n\n// ── Record-level parsers ──────────────────────────────────────────────\n\ninterface HeaderResult {\n\tcreationDate: Date;\n\tbankId: number;\n\tisDuplicate: boolean;\n\tfileReference?: string;\n\taddressee: string;\n\tbic?: string;\n\tcompanyId?: string;\n\tseparateApplication: string;\n\ttransactionReference?: string;\n\trelatedReference?: string;\n\tversion: number;\n}\n\nfunction parseHeader(line: string): HeaderResult | null {\n\tif (line[0] !== \"0\") return null;\n\treturn {\n\t\tcreationDate: parseCodaDate(sub(line, 6, 11)) ?? new Date(0),\n\t\tbankId: numSub(line, 12, 14),\n\t\tisDuplicate: sub(line, 17, 17) === \"D\",\n\t\tfileReference: trimSub(line, 25, 34) || undefined,\n\t\taddressee: trimSub(line, 35, 60),\n\t\tbic: trimSub(line, 61, 71) || undefined,\n\t\tcompanyId: trimSub(line, 72, 82) || undefined,\n\t\tseparateApplication: trimSub(line, 84, 88),\n\t\ttransactionReference: trimSub(line, 89, 104) || undefined,\n\t\trelatedReference: trimSub(line, 105, 120) || undefined,\n\t\tversion: numSub(line, 128, 128),\n\t};\n}\n\ninterface OldBalanceResult {\n\taccount: CodaAccount;\n\tpaperStatementSequence: number;\n\tcodaStatementSequence: number;\n\taccountHolderName?: string;\n\taccountDescription?: string;\n\toldBalance: CodaBalance;\n}\n\nfunction parseOldBalance(line: string): OldBalanceResult | null {\n\tif (line[0] !== \"1\") return null;\n\treturn {\n\t\taccount: parseAccountField(sub(line, 2, 2), sub(line, 6, 42)),\n\t\tpaperStatementSequence: numSub(line, 3, 5),\n\t\tcodaStatementSequence: numSub(line, 126, 128),\n\t\taccountHolderName: trimSub(line, 65, 90) || undefined,\n\t\taccountDescription: trimSub(line, 91, 125) || undefined,\n\t\toldBalance: {\n\t\t\tamount: parseAmount(sub(line, 43, 43), sub(line, 44, 58)),\n\t\t\tdate: parseCodaDate(sub(line, 59, 64)) ?? new Date(0),\n\t\t},\n\t};\n}\n\ninterface Raw21 {\n\tsequenceNumber: number;\n\tdetailNumber: number;\n\tbankReference: string;\n\tamount: number;\n\tvalueDate?: Date;\n\tentryDate: Date;\n\ttransactionCode: CodaTransactionCode;\n\tcomm: ParsedComm;\n\tpaperStatementSequence: number;\n\tglobalisationCode?: number;\n}\n\nfunction parseRecord21(line: string): Raw21 | null {\n\tif (line[0] !== \"2\" || line[1] !== \"1\") return null;\n\tconst glob = numSub(line, 125, 125);\n\treturn {\n\t\tsequenceNumber: numSub(line, 3, 6),\n\t\tdetailNumber: numSub(line, 7, 10),\n\t\tbankReference: trimSub(line, 11, 31),\n\t\tamount: parseAmount(sub(line, 32, 32), sub(line, 33, 47)),\n\t\tvalueDate: parseCodaDate(sub(line, 48, 53)),\n\t\tentryDate: parseCodaDate(sub(line, 116, 121)) ?? new Date(0),\n\t\ttransactionCode: parseTransactionCode(sub(line, 54, 61)),\n\t\tcomm: parseCommunicationField(sub(line, 62, 62), sub(line, 63, 115)),\n\t\tpaperStatementSequence: numSub(line, 122, 124),\n\t\tglobalisationCode: glob > 0 ? glob : undefined,\n\t};\n}\n\ninterface Raw22 {\n\tsequenceNumber: number;\n\tdetailNumber: number;\n\tcommContinuation: string;\n\tcustomerReference?: string;\n\tcounterpartyBic?: string;\n\trTransactionType?: number;\n\trTransactionReason?: string;\n\tcategoryPurpose?: string;\n\tpurpose?: string;\n}\n\nfunction parseRecord22(line: string): Raw22 | null {\n\tif (line[0] !== \"2\" || line[1] !== \"2\") return null;\n\tconst rTypeStr = trimSub(line, 113, 113);\n\tconst rType = rTypeStr ? Number(rTypeStr) : undefined;\n\treturn {\n\t\tsequenceNumber: numSub(line, 3, 6),\n\t\tdetailNumber: numSub(line, 7, 10),\n\t\tcommContinuation: sub(line, 11, 63),\n\t\tcustomerReference: trimSub(line, 64, 98) || undefined,\n\t\tcounterpartyBic: trimSub(line, 99, 109) || undefined,\n\t\trTransactionType:\n\t\t\trType !== undefined && Number.isFinite(rType) ? rType : undefined,\n\t\trTransactionReason: trimSub(line, 114, 117) || undefined,\n\t\tcategoryPurpose: trimSub(line, 118, 121) || undefined,\n\t\tpurpose: trimSub(line, 122, 125) || undefined,\n\t};\n}\n\ninterface Raw23 {\n\tsequenceNumber: number;\n\tdetailNumber: number;\n\tcounterpartyAccountField: string;\n\tcounterpartyName: string;\n\tcommContinuation: string;\n}\n\nfunction parseRecord23(line: string): Raw23 | null {\n\tif (line[0] !== \"2\" || line[1] !== \"3\") return null;\n\treturn {\n\t\tsequenceNumber: numSub(line, 3, 6),\n\t\tdetailNumber: numSub(line, 7, 10),\n\t\tcounterpartyAccountField: sub(line, 11, 47),\n\t\tcounterpartyName: trimSub(line, 48, 82),\n\t\tcommContinuation: sub(line, 83, 125),\n\t};\n}\n\ninterface Raw31 {\n\tsequenceNumber: number;\n\tdetailNumber: number;\n\tbankReference: string;\n\ttransactionCode: CodaTransactionCode;\n\tcomm: ParsedComm;\n}\n\nfunction parseRecord31(line: string): Raw31 | null {\n\tif (line[0] !== \"3\" || line[1] !== \"1\") return null;\n\treturn {\n\t\tsequenceNumber: numSub(line, 3, 6),\n\t\tdetailNumber: numSub(line, 7, 10),\n\t\tbankReference: trimSub(line, 11, 31),\n\t\ttransactionCode: parseTransactionCode(sub(line, 32, 39)),\n\t\tcomm: parseCommunicationField(sub(line, 40, 40), sub(line, 41, 113)),\n\t};\n}\n\nfunction parseRecord32Comm(line: string): string | null {\n\tif (line[0] !== \"3\" || line[1] !== \"2\") return null;\n\treturn sub(line, 11, 115);\n}\n\nfunction parseRecord33Comm(line: string): string | null {\n\tif (line[0] !== \"3\" || line[1] !== \"3\") return null;\n\treturn sub(line, 11, 100);\n}\n\nfunction parseNewBalance(line: string): CodaBalance | null {\n\tif (line[0] !== \"8\") return null;\n\treturn {\n\t\tamount: parseAmount(sub(line, 42, 42), sub(line, 43, 57)),\n\t\tdate: parseCodaDate(sub(line, 58, 63)) ?? new Date(0),\n\t};\n}\n\ninterface TrailerResult {\n\ttotalDebit: number;\n\ttotalCredit: number;\n}\n\nfunction parseTrailer(line: string): TrailerResult | null {\n\tif (line[0] !== \"9\") return null;\n\treturn {\n\t\ttotalDebit: Number(sub(line, 23, 37).trim()) / 1000 || 0,\n\t\ttotalCredit: Number(sub(line, 38, 52).trim()) / 1000 || 0,\n\t};\n}\n\n// ── Movement assembly ─────────────────────────────────────────────────\n\nfunction assembleMovement(\n\tr21: Raw21,\n\tr22: Raw22 | null,\n\tr23: Raw23 | null,\n\tinfoGroups: { r31: Raw31; continuations: string[] }[],\n): CodaMovement {\n\t// Concatenate communication from all record parts, trim trailing spaces only\n\tconst commParts = [r21.comm.rawContent];\n\tif (r22) commParts.push(r22.commContinuation);\n\tif (r23) commParts.push(r23.commContinuation);\n\tconst fullComm = commParts.join(\"\").trimEnd();\n\n\t// Counterparty account\n\tlet counterpartyAccountNumber: string | undefined;\n\tlet counterpartyAccountCurrency: string | undefined;\n\tif (r23) {\n\t\tconst cp = parseCounterpartyAccount(r23.counterpartyAccountField);\n\t\tcounterpartyAccountNumber = cp.number;\n\t\tcounterpartyAccountCurrency = cp.currency;\n\t}\n\n\t// Information records\n\tconst information: CodaInformation[] = infoGroups.map((ig) => {\n\t\tconst parts = [ig.r31.comm.rawContent, ...ig.continuations];\n\t\tconst comm = parts.join(\"\").trimEnd();\n\t\treturn {\n\t\t\tsequenceNumber: ig.r31.sequenceNumber,\n\t\t\tdetailNumber: ig.r31.detailNumber,\n\t\t\tbankReference: ig.r31.bankReference,\n\t\t\ttransactionCode: ig.r31.transactionCode,\n\t\t\tcommunication: comm,\n\t\t\tcommunicationType: ig.r31.comm.communicationType,\n\t\t\tstructuredCommunicationType: ig.r31.comm.structuredCommunicationType,\n\t\t};\n\t});\n\n\treturn {\n\t\tsequenceNumber: r21.sequenceNumber,\n\t\tdetailNumber: r21.detailNumber,\n\t\tbankReference: r21.bankReference,\n\t\tamount: r21.amount,\n\t\tvalueDate: r21.valueDate,\n\t\tentryDate: r21.entryDate,\n\t\ttransactionCode: r21.transactionCode,\n\t\tcommunication: fullComm,\n\t\tcommunicationType: r21.comm.communicationType,\n\t\tstructuredCommunicationType: r21.comm.structuredCommunicationType,\n\t\tpaperStatementSequence: r21.paperStatementSequence,\n\t\tglobalisationCode: r21.globalisationCode,\n\t\tcustomerReference: r22?.customerReference,\n\t\tcounterpartyBic: r22?.counterpartyBic,\n\t\trTransactionType: r22?.rTransactionType,\n\t\trTransactionReason: r22?.rTransactionReason,\n\t\tcategoryPurpose: r22?.categoryPurpose,\n\t\tpurpose: r22?.purpose,\n\t\tcounterpartyAccountNumber,\n\t\tcounterpartyAccountCurrency,\n\t\tcounterpartyName: r23?.counterpartyName,\n\t\tinformation,\n\t};\n}\n\n// ── Statement assembly ────────────────────────────────────────────────\n\nfunction parseStatementGroup(lines: string[]): CodaStatement | null {\n\tif (lines.length === 0) return null;\n\n\t// Header (record 0)\n\tconst headerLine = lines.find((l) => l[0] === \"0\");\n\tif (!headerLine) return null;\n\tconst header = parseHeader(headerLine);\n\tif (!header) return null;\n\n\t// Old balance (record 1)\n\tconst balLine = lines.find((l) => l[0] === \"1\");\n\tif (!balLine) return null;\n\tconst oldBal = parseOldBalance(balLine);\n\tif (!oldBal) return null;\n\n\t// Movements: iterate through records 2.x and 3.x in order\n\tconst movements: CodaMovement[] = [];\n\tconst movInfoLines = lines.filter((l) => l[0] === \"2\" || l[0] === \"3\");\n\n\tlet cur21: Raw21 | null = null;\n\tlet cur22: Raw22 | null = null;\n\tlet cur23: Raw23 | null = null;\n\tlet curInfoGroups: { r31: Raw31; continuations: string[] }[] = [];\n\n\tfunction flushMovement(): void {\n\t\tif (cur21) {\n\t\t\tmovements.push(assembleMovement(cur21, cur22, cur23, curInfoGroups));\n\t\t}\n\t\tcur21 = null;\n\t\tcur22 = null;\n\t\tcur23 = null;\n\t\tcurInfoGroups = [];\n\t}\n\n\tlet i = 0;\n\twhile (i < movInfoLines.length) {\n\t\tconst line = movInfoLines[i] ?? \"\";\n\t\tconst recType = line[0];\n\t\tconst article = line[1];\n\n\t\tif (recType === \"2\" && article === \"1\") {\n\t\t\tflushMovement();\n\t\t\tcur21 = parseRecord21(line);\n\t\t\ti++;\n\t\t} else if (recType === \"2\" && article === \"2\") {\n\t\t\tcur22 = parseRecord22(line);\n\t\t\ti++;\n\t\t} else if (recType === \"2\" && article === \"3\") {\n\t\t\tcur23 = parseRecord23(line);\n\t\t\ti++;\n\t\t} else if (recType === \"3\" && article === \"1\") {\n\t\t\tconst r31 = parseRecord31(line);\n\t\t\tif (r31) {\n\t\t\t\tconst continuations: string[] = [];\n\t\t\t\ti++;\n\t\t\t\twhile (i < movInfoLines.length) {\n\t\t\t\t\tconst next = movInfoLines[i] ?? \"\";\n\t\t\t\t\tif (next[0] === \"3\" && next[1] === \"2\") {\n\t\t\t\t\t\tconst c = parseRecord32Comm(next);\n\t\t\t\t\t\tif (c) continuations.push(c);\n\t\t\t\t\t\ti++;\n\t\t\t\t\t} else if (next[0] === \"3\" && next[1] === \"3\") {\n\t\t\t\t\t\tconst c = parseRecord33Comm(next);\n\t\t\t\t\t\tif (c) continuations.push(c);\n\t\t\t\t\t\ti++;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcurInfoGroups.push({ r31, continuations });\n\t\t\t} else {\n\t\t\t\ti++;\n\t\t\t}\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n\tflushMovement();\n\n\t// New balance (record 8)\n\tconst newBalLine = lines.find((l) => l[0] === \"8\");\n\tconst newBalance = newBalLine ? parseNewBalance(newBalLine) : undefined;\n\n\t// Free communications (record 4)\n\tconst freeCommMap = new Map<number, string[]>();\n\tfor (const line of lines) {\n\t\tif (line[0] !== \"4\") continue;\n\t\tconst seqNum = numSub(line, 3, 6);\n\t\tconst text = trimSub(line, 33, 112);\n\t\tif (!text) continue;\n\t\tconst parts = freeCommMap.get(seqNum) ?? [];\n\t\tparts.push(text);\n\t\tfreeCommMap.set(seqNum, parts);\n\t}\n\tconst freeCommunications = [...freeCommMap.values()].map((p) => p.join(\"\"));\n\n\t// Trailer (record 9)\n\tconst trailerLine = lines.find((l) => l[0] === \"9\");\n\tconst trailer = trailerLine ? parseTrailer(trailerLine) : null;\n\n\treturn {\n\t\t...header,\n\t\t...oldBal,\n\t\tmovements,\n\t\tnewBalance: newBalance ?? undefined,\n\t\tfreeCommunications,\n\t\ttotalDebit: trailer?.totalDebit ?? 0,\n\t\ttotalCredit: trailer?.totalCredit ?? 0,\n\t};\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingram-tech/coda",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript parser for Belgian CODA (Coded statement of account) bank files",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"prepack": "npm run build",
|
|
21
|
+
"lint": "eslint",
|
|
22
|
+
"format": "prettier --write .",
|
|
23
|
+
"format:check": "prettier --check .",
|
|
24
|
+
"type-check": "tsc --noEmit",
|
|
25
|
+
"test": "vitest",
|
|
26
|
+
"test:run": "vitest --run",
|
|
27
|
+
"test:coverage": "vitest --coverage",
|
|
28
|
+
"ci": "npm run type-check && npm run lint && npm run test:run && npm run build"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"eslint": "^9.0.0",
|
|
33
|
+
"prettier": "^3.8.1",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.9.3",
|
|
36
|
+
"typescript-eslint": "^8.0.0",
|
|
37
|
+
"vitest": "^4.0.18"
|
|
38
|
+
},
|
|
39
|
+
"prettier": {
|
|
40
|
+
"printWidth": 88,
|
|
41
|
+
"tabWidth": 4,
|
|
42
|
+
"useTabs": true,
|
|
43
|
+
"overrides": [
|
|
44
|
+
{
|
|
45
|
+
"files": "*.{yml,yaml}",
|
|
46
|
+
"options": {
|
|
47
|
+
"tabWidth": 2,
|
|
48
|
+
"useTabs": false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|