@fin.cx/csvparser
Reads bank and payment-provider CSV statements into movements: signed amounts in integer cents, days as YYYY-MM-DD, the counterparty, the purpose, and the provider's id, balance, fee and exchange where the file states them. A row it cannot read is reported and left out, never guessed. It has no runtime dependencies and writes nothing anywhere.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Installation
pnpm add @fin.cx/csvparser
Known exports
A file is recognized by its header (detectCsvFormat), or named with format:
| Format | Header (excerpt) | Days | Amounts | Currency |
|---|---|---|---|---|
bunq |
Date;Interest Date;Amount;Account;Counterparty;Name;Description (; or ,) |
YYYY-MM-DD |
-1.234,56 |
none in the file: give the account's |
wise |
"TransferWise ID",Date,Amount,Currency,…,"Exchange Rate",… |
DD-MM-YYYY |
-1500.00 |
per row |
paypal |
"Datum","Uhrzeit","Zeitzone",…,"Brutto","Gebühr","Netto","Guthaben","Transaktionscode",… |
DD.MM.YYYY |
12,34 |
per row |
commerzbank |
Buchungstag;Wertstellung;Umsatzart;Buchungstext;Betrag;Währung;… |
DD.MM.YYYY |
-1.119,00 |
per row |
spendesk |
Payment date,Settlement date,…,Debit,Credit,Currency,… |
YYYY-MM-DD |
119.00 |
per row |
fidor |
Datum;Beschreibung;Beschreibung2;Wert |
DD.MM.YYYY |
-800,00 |
none in the file: give the account's |
The bunq and Wise layouts were checked against real exports. The others follow the parsers of 2.x.
- PayPal: the amount is
Netto, what moved the balance.Gebühris reported asfeeCentswith its sign as printed (a charged fee negative, a refunded one positive), andBruttostays inraw. An export of all activity says for each row whether it moved the balance (Auswirkung auf Guthaben, PayPal's Balance Impact:Soll/Debit,Haben/CreditorMemo): aMemorow, such as an authorisation or a payment still pending, moved nothing, so it is no movement and is listed inunbooked(balance_unaffected); a row that leaves the cell empty or states another value is not booked by a guess either: it is listed inunbooked(balance_impact_unknown) and reported as the anomalyunknown_balance_impact. The export is recognized by its German headers (Datum,Uhrzeit,Zeitzone,Währung,Brutto,Gebühr,Netto,Transaktionscode); an English one is not, and is read through a mapping. The balance impact is read fromAuswirkung auf GuthabenorBalance Impact. TheStatuscolumn is not read; it stays inraw(a payment refunded later keeps the balance it moved, whatever its status says now). - Wise: the
Total fees(with the sign printed) and the exchange columns are reported as printed, without interpretation. - The own account: a movement's
ownAccountis the account the file was exported from, as the row states it (iban,accountNumber,bankCode, each as printed), and the statement'sownAccountslists every one the file states, once each, in the order of its first row. bunq states it inAccount(the IBAN), Commerzbank inAuftraggeberkonto,Bankleitzahl AuftraggeberkontoandIBAN Auftraggeberkonto; there the counterparty is named inBuchungstextonly, socounterpartyAccountstays empty. The Commerzbank reading follows the Commerzbank import configuration of Firefly III; it was not checked against a real export. Wise, PayPal, Spendesk and Fidor exports do not state it, and theirownAccountsis empty. Checking a file against the account it is imported into is the caller's decision. - Debit and credit columns (Spendesk, or a mapping): a row states its amount on exactly one side, where an empty side or 0 states nothing. A debit is money out and a credit money in, whatever sign is printed.
Usage
import { parseCsvStatement, detectCsvFormat } from '@fin.cx/csvparser';
const statement = parseCsvStatement(fileBytes, { currency: 'EUR' }); // currency only for files that name none (bunq, Fidor, a mapping without one)
statement.format; // 'bunq'
statement.transactions; // [{ line: 2, bookingDate: '2024-04-02', amountCents: -123456, currency: 'EUR', counterpartyName: 'Muster GmbH', remittance: 'Rechnung 4711', raw: {…} }, …]
statement.anomalies; // [{ kind: 'unreadable_amount', line: 7, column: 'Amount', value: '-1,005', detail: … }]
statement.ownAccounts; // [{ iban: 'NL00BUNQ0000000001' }]: the account the file was exported from
statement.unbooked; // [{ line: 3, reason: 'balance_unaffected', column: 'Auswirkung auf Guthaben', value: 'Memo', raw: {…} }]: rows that are no movements (PayPal)
Encoding. A file is read as UTF-8 when it is valid UTF-8, else as Windows-1252, the other encoding German bank exports use. The runtime's TextDecoder('windows-1252') must follow the WHATWG Encoding Standard. Node.js 25.2.1 does not (nodejs/node#60888).
Any other layout
const statement = parseCsvStatement(fileBytes, {
mapping: {
skipLines: 2, // lines before the header
dateFormat: 'DD.MM.YY', // 'YYYY-MM-DD' | 'DD.MM.YYYY' | 'DD.MM.YY' | 'DD-MM-YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY'
decimalSeparator: ',',
currency: 'EUR', // the currency of every row, when the file has no currency column (or currency: 'Währung' among the columns)
columns: {
bookingDate: 'Buchung',
valueDate: 'Valuta',
debit: 'Soll', // or amount: 'Betrag'
credit: 'Haben',
counterpartyName: 'Empfänger',
remittance: ['Verwendungszweck'],
balanceAfter: 'Saldo',
ownIban: 'IBAN', // the account the file was exported from, where a column states it; also ownAccountNumber, ownBankCode
},
},
});
What is reported
- Anomalies:
unreadable_amount,unreadable_date,missing_value(also a row with neither a debit nor a credit),unknown_currency(also an empty currency cell, which nothing fills in) andambiguous_amount(a row with both a debit and a credit) leave the row out, and so doeslong_row, a value beyond the columns of the header (empty cells beyond it, as a trailing;leaves, are accepted).short_rowreads the row as far as it goes.unknown_balance_impact(a PayPal row whose balance impact is empty or none ofSoll,Haben,Debit,Credit,Memo) leaves the row out as well, and lists it inunbooked. - Rows that are no movements:
unbookedlists the rows left out for what the file states of their effect on the balance, with their line, reason, the column and value, and the row:balance_unaffected(PayPal'sMemo, not an anomaly) andbalance_impact_unknown(empty or unknown, also an anomaly). - Amounts: an amount with more than two decimals, or thousands separators in the wrong places, is unreadable, never rounded.
- Two-digit years (
DD.MM.YY) are read as 2000–2099. - The currency comes from exactly one place: the file's currency column, the mapping's
currency, or thecurrencyoption, which is taken only for a file that names no currency and a mapping that states none. - The whole file:
CsvStatementErroris thrown with one of these codes:unknown_format,currency_required(a file that names no currency, given none),currency_conflict(a currency given where the file or the mapping states it),mapping_invalid(a mapping that names neither an amount nor a debit and a credit, names a column the header lacks, names both a currency column and the currency of every row, or is given together with aformat),empty.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.