Files
fintrack/scripts/import-csv-to-db.ts

308 lines
9.2 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import { prisma } from '../lib/prisma';
import { transactionService } from '../services/transaction.service';
import { generateId } from '../lib/store-db';
interface CSVTransaction {
date: string;
amount: string;
libelle: string;
beneficiaire: string;
iban: string;
bic: string;
numeroCompte: string;
codeBanque: string;
categorie: string;
commentaire: string;
numeroCheque: string;
tags: string;
compte: string;
}
function parseCSVLine(line: string): string[] {
const result: string[] = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
result.push(current.trim());
current = '';
} else {
current += char;
}
}
result.push(current.trim());
return result;
}
function parseCSV(csvPath: string): CSVTransaction[] {
const content = fs.readFileSync(csvPath, 'utf-8');
const lines = content.split('\n');
// Skip header lines (first 8 lines)
const dataLines = lines.slice(8);
const transactions: CSVTransaction[] = [];
for (const line of dataLines) {
if (!line.trim()) continue;
const fields = parseCSVLine(line);
if (fields.length < 13) continue;
// Skip if date or amount is missing
if (!fields[0] || !fields[1] || !fields[2]) continue;
transactions.push({
date: fields[0],
amount: fields[1],
libelle: fields[2],
beneficiaire: fields[3],
iban: fields[4],
bic: fields[5],
numeroCompte: fields[6],
codeBanque: fields[7],
categorie: fields[8],
commentaire: fields[9],
numeroCheque: fields[10],
tags: fields[11],
compte: fields[12],
});
}
return transactions;
}
function parseDate(dateStr: string): string {
// Format: DD/MM/YYYY -> YYYY-MM-DD
const [day, month, year] = dateStr.split('/');
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
}
function parseAmount(amountStr: string): number {
if (!amountStr || amountStr.trim() === '' || amountStr === '""') {
return 0;
}
// Remove quotes, spaces (including non-breaking spaces), and replace comma with dot
const cleaned = amountStr.replace(/["\s\u00A0]/g, '').replace(',', '.');
const parsed = parseFloat(cleaned);
return isNaN(parsed) ? 0 : parsed;
}
function generateFITID(transaction: CSVTransaction, index: number): string {
const date = parseDate(transaction.date);
const dateStr = date.replace(/-/g, '');
const amountStr = Math.abs(parseAmount(transaction.amount)).toFixed(2).replace('.', '');
const libelleHash = transaction.libelle.substring(0, 20).replace(/[^A-Z0-9]/gi, '');
return `${dateStr}-${amountStr}-${libelleHash}-${index}`;
}
function removeAccountPrefix(accountName: string): string {
// Remove prefixes: LivretA, LDDS, CCP, PEL (case insensitive)
const prefixes = ['LivretA', 'Livret A', 'LDDS', 'CCP', 'PEL'];
let cleaned = accountName;
for (const prefix of prefixes) {
// Remove prefix followed by optional spaces and dashes
const regex = new RegExp(`^${prefix}\\s*-?\\s*`, 'i');
cleaned = cleaned.replace(regex, '');
}
return cleaned.trim();
}
function determineAccountType(accountName: string): "CHECKING" | "SAVINGS" | "CREDIT_CARD" | "OTHER" {
const upper = accountName.toUpperCase();
if (upper.includes('LIVRET') || upper.includes('LDDS') || upper.includes('PEL')) {
return 'SAVINGS';
}
if (upper.includes('CCP') || upper.includes('COMPTE COURANT')) {
return 'CHECKING';
}
return 'OTHER';
}
async function main() {
const csvPath = path.join(__dirname, '../temp/all account.csv');
if (!fs.existsSync(csvPath)) {
console.error(`Fichier CSV introuvable: ${csvPath}`);
process.exit(1);
}
console.log('Lecture du fichier CSV...');
const csvTransactions = parseCSV(csvPath);
console.log(`${csvTransactions.length} transactions trouvées`);
// Group by account
const accountsMap = new Map<string, CSVTransaction[]>();
for (const transaction of csvTransactions) {
if (!transaction.compte) continue;
const amount = parseAmount(transaction.amount);
if (amount === 0) continue; // Skip zero-amount transactions
if (!accountsMap.has(transaction.compte)) {
accountsMap.set(transaction.compte, []);
}
accountsMap.get(transaction.compte)!.push(transaction);
}
console.log(`${accountsMap.size} comptes trouvés\n`);
let totalTransactionsCreated = 0;
let totalAccountsCreated = 0;
let totalAccountsUpdated = 0;
// Process each account
for (const [accountName, transactions] of accountsMap.entries()) {
console.log(`Traitement du compte: ${accountName}`);
console.log(` ${transactions.length} transactions`);
// Remove prefixes and extract account number from account name
const cleanedAccountName = removeAccountPrefix(accountName);
const accountNumber = cleanedAccountName.replace(/[^A-Z0-9]/gi, '').substring(0, 22);
const bankId = transactions[0]?.codeBanque || 'FR';
console.log(` Numéro de compte extrait: ${accountNumber}`);
// Find account by account number (try multiple strategies)
let account = await prisma.account.findFirst({
where: {
accountNumber: accountNumber,
bankId: bankId,
},
});
// If not found with bankId, try without bankId constraint
if (!account) {
account = await prisma.account.findFirst({
where: {
accountNumber: accountNumber,
},
});
}
// If still not found, try to find by account number in existing account numbers
// (some accounts might have been created with prefixes in accountNumber)
if (!account) {
const allAccounts = await prisma.account.findMany({
where: {
accountNumber: {
contains: accountNumber,
},
},
});
// Try to find exact match in accountNumber (after cleaning)
for (const acc of allAccounts) {
const cleanedExisting = removeAccountPrefix(acc.accountNumber);
const existingNumber = cleanedExisting.replace(/[^A-Z0-9]/gi, '');
if (existingNumber === accountNumber) {
account = acc;
break;
}
}
}
if (!account) {
console.log(` → Création du compte...`);
account = await prisma.account.create({
data: {
name: accountName,
bankId: bankId,
accountNumber: accountNumber,
type: determineAccountType(accountName),
folderId: null,
balance: 0,
currency: 'EUR',
lastImport: null,
externalUrl: null,
},
});
totalAccountsCreated++;
} else {
console.log(` → Compte existant trouvé: ${account.name} (${account.accountNumber})`);
totalAccountsUpdated++;
}
// Sort transactions by date
transactions.sort((a, b) => {
const dateA = parseDate(a.date);
const dateB = parseDate(b.date);
return dateA.localeCompare(dateB);
});
// Calculate balance
const balance = transactions.reduce((sum, t) => sum + parseAmount(t.amount), 0);
// Prepare transactions for insertion
const dbTransactions = transactions.map((transaction, index) => {
const amount = parseAmount(transaction.amount);
const date = parseDate(transaction.date);
// Build memo from available fields
let memo = transaction.libelle;
if (transaction.beneficiaire) {
memo += ` - ${transaction.beneficiaire}`;
}
if (transaction.categorie) {
memo += ` [${transaction.categorie}]`;
}
if (transaction.commentaire) {
memo += ` (${transaction.commentaire})`;
}
return {
id: generateId(),
accountId: account.id,
date: date,
amount: amount,
description: transaction.libelle.substring(0, 255),
type: amount >= 0 ? 'CREDIT' as const : 'DEBIT' as const,
categoryId: null, // Will be auto-categorized later if needed
isReconciled: false,
fitId: generateFITID(transaction, index),
memo: memo.length > 255 ? memo.substring(0, 255) : memo,
checkNum: transaction.numeroCheque || undefined,
};
});
// Insert transactions (will skip duplicates based on fitId)
const result = await transactionService.createMany(dbTransactions);
console.log(`${result.count} nouvelles transactions insérées`);
totalTransactionsCreated += result.count;
// Update account balance and lastImport
await prisma.account.update({
where: { id: account.id },
data: {
balance: balance,
lastImport: new Date().toISOString(),
},
});
console.log(` ✓ Solde mis à jour: ${balance.toFixed(2)} EUR\n`);
}
console.log('\n=== Résumé ===');
console.log(`Comptes créés: ${totalAccountsCreated}`);
console.log(`Comptes mis à jour: ${totalAccountsUpdated}`);
console.log(`Transactions insérées: ${totalTransactionsCreated}`);
console.log('\n✓ Import terminé!');
await prisma.$disconnect();
}
main().catch((error) => {
console.error('Erreur:', error);
process.exit(1);
});