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(); 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); });