jkunz 9a9937601b
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / metadata (push) Skipped
v3.1.0
2026-09-29 08:11:58 +00:00
…
2026-09-29 08:11:58 +00:00
…
2026-09-29 08:11:58 +00:00
…
2026-09-29 08:11:58 +00:00
…

@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ühr is reported as feeCents with its sign as printed (a charged fee negative, a refunded one positive), and Brutto stays in raw. An export of all activity says for each row whether it moved the balance (Auswirkung auf Guthaben, PayPal's Balance Impact: Soll/Debit, Haben/Credit or Memo): a Memo row, such as an authorisation or a payment still pending, moved nothing, so it is no movement and is listed in unbooked (balance_unaffected); a row that leaves the cell empty or states another value is not booked by a guess either: it is listed in unbooked (balance_impact_unknown) and reported as the anomaly unknown_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 from Auswirkung auf Guthaben or Balance Impact. The Status column is not read; it stays in raw (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 ownAccount is the account the file was exported from, as the row states it (iban, accountNumber, bankCode, each as printed), and the statement's ownAccounts lists every one the file states, once each, in the order of its first row. bunq states it in Account (the IBAN), Commerzbank in Auftraggeberkonto, Bankleitzahl Auftraggeberkonto and IBAN Auftraggeberkonto; there the counterparty is named in Buchungstext only, so counterpartyAccount stays 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 their ownAccounts is 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) and ambiguous_amount (a row with both a debit and a credit) leave the row out, and so does long_row, a value beyond the columns of the header (empty cells beyond it, as a trailing ; leaves, are accepted). short_row reads the row as far as it goes. unknown_balance_impact (a PayPal row whose balance impact is empty or none of Soll, Haben, Debit, Credit, Memo) leaves the row out as well, and lists it in unbooked.
  • Rows that are no movements: unbooked lists 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's Memo, not an anomaly) and balance_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 the currency option, which is taken only for a file that names no currency and a mapping that states none.
  • The whole file: CsvStatementError is 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 a format), empty.

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.

S
Description
No description provided
Readme
651 KiB
Languages
TypeScript 100%