#!/usr/bin/env node
/**
* ICS PDF to CSV Converter
*
* Converts ICS creditcard PDF statements to CSV format compatible with SnelStart import.
*
* Usage:
* node scripts/ics-pdf-to-csv.cjs <input-folder> [output-csv]
* node scripts/ics-pdf-to-csv.cjs ~/Desktop/Snelstart-Administratie/prive
* node scripts/ics-pdf-to-csv.cjs ~/Desktop/Snelstart-Administratie/prive output.csv
*/
const fs = require('fs');
const path = require('path');
// Try to load pdf-parse, install if not available
let PDFParse;
try {
const pdfModule = require('pdf-parse');
PDFParse = pdfModule.PDFParse || pdfModule.default || pdfModule;
} catch (e) {
console.error('pdf-parse not installed. Run: npm install pdf-parse');
process.exit(1);
}
// Dutch month names to numbers
const DUTCH_MONTHS = {
'jan': '01', 'feb': '02', 'mrt': '03', 'apr': '04',
'mei': '05', 'jun': '06', 'jul': '07', 'aug': '08',
'sep': '09', 'okt': '10', 'nov': '11', 'dec': '12',
'januari': '01', 'februari': '02', 'maart': '03', 'april': '04',
'juni': '06', 'juli': '07', 'augustus': '08',
'september': '09', 'oktober': '10', 'november': '11', 'december': '12'
};
/**
* Parse a Dutch date like "22 dec." or "03 jan." to components
*/
function parseDutchDate(dateStr, year) {
// Handle formats: "22 dec.", "03 jan.", "22 dec"
const match = dateStr.trim().match(/^(\d{1,2})\s+([a-z]+)\.?$/i);
if (!match) return null;
const day = match[1].padStart(2, '0');
const monthStr = match[2].toLowerCase();
const month = DUTCH_MONTHS[monthStr];
if (!month) return null;
return { day, month, year };
}
/**
* Convert date components to DD-MM-YYYY format
*/
function formatDate(dateObj) {
return `${dateObj.day}-${dateObj.month}-${dateObj.year}`;
}
/**
* Parse amount string like "21,54" or "3.000,00" to number
*/
function parseAmount(amountStr) {
if (!amountStr) return 0;
// Remove thousand separators (.) and convert decimal comma to dot
const cleaned = amountStr.replace(/\./g, '').replace(',', '.');
return parseFloat(cleaned) || 0;
}
/**
* Extract statement year from PDF text
*/
function extractStatementYear(text) {
// Look for "23 januari 2025" pattern in header
const match = text.match(/(\d{1,2})\s+(januari|februari|maart|april|mei|juni|juli|augustus|september|oktober|november|december)\s+(\d{4})/i);
if (match) {
return match[3];
}
return new Date().getFullYear().toString();
}
/**
* Extract ICS customer number from PDF text
*/
function extractCustomerNumber(text) {
const match = text.match(/ICS-klantnummer\s*[\n\r]*\s*(\d+)/);
return match ? match[1] : 'unknown';
}
/**
* Parse transactions from PDF text
*/
function parseTransactions(text, statementYear) {
const transactions = [];
const lines = text.split('\n');
let currentYear = statementYear;
let prevMonth = null;
// Pattern for transaction lines:
// "22 dec. 23 dec. Lulu Press, Inc. 844-2120689 US 21,54 Af"
// "01 dec. 03 dec. MIDJOURNEY INC. SOUTH SAN FRA US 96,00 USD 80,91 Bij"
// "06 jan. 06 jan. IDEAL BETALING, DANK U 3.000,00 Bij"
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip header lines and empty lines
if (!line || line.startsWith('Datum') || line.startsWith('International Card') ||
line.startsWith('Vorig openstaand') || line.startsWith('Uw Card met') ||
line.startsWith('ALBERT BARTH') || line.includes('Nu beschikbaar') ||
line.includes('Uw betalingen aan') || line.includes('Het minimaal te betalen') ||
line.includes('Bestedingslimiet') || line.includes('Minimaal te betalen') ||
line.includes('depositogarantiestelsel') || line.startsWith('Wisselkoers') ||
line.startsWith('Postbus') || line.startsWith('Telefoon') ||
line.startsWith('Kvk') || line.includes('www.icscards') ||
line.startsWith('Bankrek') || line.startsWith('BIC:') ||
line.startsWith('ICS identificatie') || line.match(/^NL\d+ZZZ/)) {
continue;
}
// Try to match transaction pattern
// Format: "DD mmm. DD mmm. Description Amount Af/Bij"
const txMatch = line.match(/^(\d{1,2}\s+[a-z]+\.?)\s+(\d{1,2}\s+[a-z]+\.?)\s+(.+?)\s+([\d.,]+)\s+(Af|Bij)$/i);
if (txMatch) {
const [, transDateStr, bookDateStr, description, amountStr, direction] = txMatch;
// Parse dates
const transDate = parseDutchDate(transDateStr, currentYear);
const bookDate = parseDutchDate(bookDateStr, currentYear);
if (!transDate || !bookDate) continue;
// Handle year rollover (dec -> jan)
if (prevMonth && prevMonth === '12' && transDate.month === '01') {
currentYear = (parseInt(currentYear) + 1).toString();
transDate.year = currentYear;
bookDate.year = currentYear;
}
// Handle year rollback for statements that span year boundary
if (prevMonth && prevMonth === '01' && transDate.month === '12') {
transDate.year = (parseInt(currentYear) - 1).toString();
if (bookDate.month === '12') {
bookDate.year = transDate.year;
}
}
prevMonth = transDate.month;
const amount = parseAmount(amountStr);
const isDebit = direction.toLowerCase() === 'af';
transactions.push({
transactionDate: formatDate(transDate),
bookingDate: formatDate(bookDate),
description: description.trim(),
amount: isDebit ? -amount : amount,
direction: direction
});
}
}
return transactions;
}
/**
* Process a single PDF file
*/
async function processPdf(filePath) {
const dataBuffer = fs.readFileSync(filePath);
const parser = new PDFParse(dataBuffer);
const data = await parser.parse();
const text = data.text;
const year = extractStatementYear(text);
const customerNumber = extractCustomerNumber(text);
const transactions = parseTransactions(text, year);
return {
file: path.basename(filePath),
customerNumber,
year,
transactions
};
}
/**
* Convert transactions to CSV format compatible with ICS CSV export
*/
function toCsv(allTransactions, customerNumber) {
// CSV header matching ICS format
const header = 'Kaartnummer;Naam kaarthouder;Transactiedatum;Boekdatum;Bedrag transactie;Valuta;Bedrag in EUR;Omschrijving;Type transactie';
const rows = allTransactions.map(tx => {
const amount = tx.amount.toFixed(2).replace('.', ',');
const type = tx.amount >= 0 ? 'Transaction' : 'Transaction';
return [
customerNumber,
'ALBERT BARTH',
tx.transactionDate,
tx.bookingDate,
amount,
'EUR',
amount,
`"${tx.description.replace(/"/g, '""')}"`,
type
].join(';');
});
return [header, ...rows].join('\n');
}
/**
* Main function
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: node scripts/ics-pdf-to-csv.cjs <input-folder> [output-csv]');
console.log('');
console.log('Examples:');
console.log(' node scripts/ics-pdf-to-csv.cjs ~/Desktop/Snelstart-Administratie/prive');
console.log(' node scripts/ics-pdf-to-csv.cjs ~/Desktop/Snelstart-Administratie/prive transactions.csv');
process.exit(1);
}
const inputPath = args[0];
let outputPath = args[1];
// Check if input is a directory or file
const stat = fs.statSync(inputPath);
let pdfFiles = [];
if (stat.isDirectory()) {
// Find all PDF files in directory
pdfFiles = fs.readdirSync(inputPath)
.filter(f => f.toLowerCase().endsWith('.pdf'))
.sort()
.map(f => path.join(inputPath, f));
if (!outputPath) {
outputPath = path.join(inputPath, 'transactions.csv');
}
} else {
pdfFiles = [inputPath];
if (!outputPath) {
outputPath = inputPath.replace('.pdf', '.csv');
}
}
if (pdfFiles.length === 0) {
console.error('No PDF files found');
process.exit(1);
}
console.log(`Found ${pdfFiles.length} PDF files`);
let allTransactions = [];
let customerNumber = 'unknown';
for (const pdfFile of pdfFiles) {
console.log(`Processing: ${path.basename(pdfFile)}`);
try {
const result = await processPdf(pdfFile);
customerNumber = result.customerNumber;
allTransactions.push(...result.transactions);
console.log(` -> ${result.transactions.length} transactions`);
} catch (err) {
console.error(` Error: ${err.message}`);
}
}
// Sort by transaction date
allTransactions.sort((a, b) => {
const [aDay, aMonth, aYear] = a.transactionDate.split('-');
const [bDay, bMonth, bYear] = b.transactionDate.split('-');
const aDate = new Date(aYear, aMonth - 1, aDay);
const bDate = new Date(bYear, bMonth - 1, bDay);
return aDate - bDate;
});
// Remove duplicates (same date, amount, description)
const seen = new Set();
const uniqueTransactions = [];
for (const tx of allTransactions) {
const key = `${tx.transactionDate}|${tx.amount}|${tx.description}`;
if (!seen.has(key)) {
seen.add(key);
uniqueTransactions.push(tx);
}
}
const duplicatesRemoved = allTransactions.length - uniqueTransactions.length;
// Generate CSV
const csv = toCsv(uniqueTransactions, customerNumber);
fs.writeFileSync(outputPath, csv);
console.log('');
console.log('Summary:');
console.log(` Total transactions: ${allTransactions.length}`);
console.log(` Duplicates removed: ${duplicatesRemoved}`);
console.log(` Unique transactions: ${uniqueTransactions.length}`);
console.log(` Output: ${outputPath}`);
// Calculate totals
const totalDebit = uniqueTransactions.filter(t => t.amount < 0).reduce((sum, t) => sum + Math.abs(t.amount), 0);
const totalCredit = uniqueTransactions.filter(t => t.amount > 0).reduce((sum, t) => sum + t.amount, 0);
console.log(` Total debit: €${totalDebit.toFixed(2)}`);
console.log(` Total credit: €${totalCredit.toFixed(2)}`);
}
main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});