import { eq, and, desc, sql, inArray } from "drizzle-orm"; import { drizzle } from "drizzle-orm/mysql2"; import { InsertUser, users, sourceFiles, InsertSourceFile, SourceFile, invoices, InsertInvoice, Invoice, userSettings, InsertUserSettings, UserSettings, importLogs, InsertImportLog, ImportLog, llmLogs, InsertLlmLog, LlmLog, importSettings, InsertImportSettings, ImportSettings, departmentList, InsertDepartment, Department, accountingAllocationList, InsertAccountingAllocation, AccountingAllocation, automationRules, InsertAutomationRule, AutomationRule, llmFieldsConfig, InsertLlmFieldConfig, LlmFieldConfig, signatures, InsertSignature, Signature, serviceSignatures, InsertServiceSignature, ServiceSignature, bapHistory, InsertBapHistory, BapHistory, invoiceLearnings, InsertInvoiceLearning, InvoiceLearning, deletedInvoices, InsertDeletedInvoice, DeletedInvoice } from "../drizzle/schema"; import { ENV } from './_core/env'; let _db: ReturnType | null = null; export async function getDb() { if (!_db && process.env.DATABASE_URL) { try { _db = drizzle(process.env.DATABASE_URL); } catch (error) { console.warn("[Database] Failed to connect:", error); _db = null; } } return _db; } // ============= USER OPERATIONS ============= export async function upsertUser(user: InsertUser): Promise { const db = await getDb(); if (!db) { console.warn("[Database] Cannot upsert user: database not available"); return; } try { const values: InsertUser = { email: user.email, loginMethod: user.loginMethod, }; const updateSet: Record = {}; if (user.openId !== undefined) { values.openId = user.openId; updateSet.openId = user.openId; } if (user.azureAdId !== undefined) { values.azureAdId = user.azureAdId; updateSet.azureAdId = user.azureAdId; } if (user.name !== undefined) { values.name = user.name; updateSet.name = user.name; } if (user.passwordHash !== undefined) { values.passwordHash = user.passwordHash; updateSet.passwordHash = user.passwordHash; } if (user.isActive !== undefined) { values.isActive = user.isActive; updateSet.isActive = user.isActive; } if (user.lastSignedIn !== undefined) { values.lastSignedIn = user.lastSignedIn; updateSet.lastSignedIn = user.lastSignedIn; } if (user.role !== undefined) { values.role = user.role; updateSet.role = user.role; } else if (user.openId === ENV.ownerOpenId) { values.role = 'admin'; updateSet.role = 'admin'; } if (!values.lastSignedIn) { values.lastSignedIn = new Date(); } if (Object.keys(updateSet).length === 0) { updateSet.lastSignedIn = new Date(); } await db.insert(users).values(values).onDuplicateKeyUpdate({ set: updateSet, }); } catch (error) { console.error("[Database] Failed to upsert user:", error); throw error; } } export async function getUserByOpenId(openId: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function getUserByEmail(email: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(users).where(eq(users.email, email)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function getUserByAzureAdId(azureAdId: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(users).where(eq(users.azureAdId, azureAdId)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function createLocalUser(email: string, passwordHash: string, name: string, role: "user" | "admin" = "user") { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.insert(users).values({ email, passwordHash, name, loginMethod: "local", role, isActive: 1, }); return getUserByEmail(email); } export async function getAllUsers() { const db = await getDb(); if (!db) return []; return db.select().from(users).orderBy(desc(users.createdAt)); } export async function updateUserPassword(userId: number, newPasswordHash: string) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(users).set({ passwordHash: newPasswordHash }).where(eq(users.id, userId)); } export async function toggleUserActive(userId: number, isActive: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(users).set({ isActive }).where(eq(users.id, userId)); } export async function deleteUser(userId: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(users).where(eq(users.id, userId)); } // ============= SOURCE FILE OPERATIONS ============= export async function createSourceFile(data: InsertSourceFile): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(sourceFiles).values(data); const insertedId = Number(result[0].insertId); const inserted = await db.select().from(sourceFiles).where(eq(sourceFiles.id, insertedId)).limit(1); return inserted[0]!; } export async function getSourceFileById(id: number): Promise { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(sourceFiles).where(eq(sourceFiles.id, id)).limit(1); return result[0]; } export async function updateSourceFile(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(sourceFiles).set(data).where(eq(sourceFiles.id, id)); } export async function getSourceFilesByUserId(userId: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(sourceFiles).where(eq(sourceFiles.userId, userId)).orderBy(desc(sourceFiles.createdAt)); } // ============= INVOICE OPERATIONS ============= export async function createInvoice(data: InsertInvoice): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(invoices).values(data); const insertedId = Number(result[0].insertId); const inserted = await db.select().from(invoices).where(eq(invoices.id, insertedId)).limit(1); return inserted[0]!; } export async function getInvoiceById(id: number): Promise { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(invoices).where(eq(invoices.id, id)).limit(1); return result[0]; } export async function getInvoicesByUserId(userId: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(invoices).where(eq(invoices.userId, userId)).orderBy(desc(invoices.createdAt)); } export async function getInvoicesByUser(userId: number): Promise { return getInvoicesByUserId(userId); } export async function getAllInvoices(): Promise { const db = await getDb(); if (!db) return []; return db.select().from(invoices).orderBy(desc(invoices.createdAt)); } export async function updateInvoice(id: number, data: Partial) { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(invoices).set(data).where(eq(invoices.id, id)); } export async function deleteInvoice(id: number) { const db = await getDb(); if (!db) throw new Error("Database not available"); // Enregistrer dans la blacklist avant suppression const invoice = await db.select().from(invoices).where(eq(invoices.id, id)).limit(1); if (invoice[0] && invoice[0].invoiceNumber) { await db.insert(deletedInvoices).values({ userId: invoice[0].userId, invoiceNumber: invoice[0].invoiceNumber, totalAmount: invoice[0].totalAmount ?? undefined, supplierName: invoice[0].supplierName ?? undefined, }).onDuplicateKeyUpdate({ set: { deletedAt: new Date() } }); } await db.delete(invoices).where(eq(invoices.id, id)); } /** Vérifie si une facture est dans la blacklist (supprimée manuellement) */ export async function isInvoiceBlacklisted( invoiceNumber: string | null, userId: number ): Promise { if (!invoiceNumber) return false; const db = await getDb(); if (!db) return false; const result = await db.select().from(deletedInvoices) .where(and(eq(deletedInvoices.userId, userId), eq(deletedInvoices.invoiceNumber, invoiceNumber))) .limit(1); return result.length > 0; } export async function searchInvoices(userId: number | null, query: string): Promise { const db = await getDb(); if (!db) return []; const searchPattern = `%${query}%`; if (userId === null) { // Admin : recherche globale sans filtre userId return db.select().from(invoices) .where(sql`(${invoices.supplierName} LIKE ${searchPattern} OR ${invoices.invoiceNumber} LIKE ${searchPattern})`) .orderBy(desc(invoices.createdAt)); } return db.select().from(invoices) .where( and( eq(invoices.userId, userId), sql`(${invoices.supplierName} LIKE ${searchPattern} OR ${invoices.invoiceNumber} LIKE ${searchPattern})` ) ) .orderBy(desc(invoices.createdAt)); } export async function getInvoiceStats(userId: number) { const db = await getDb(); if (!db) return { total: 0, completed: 0, processing: 0, error: 0, totalAmount: 0, averageScore: 0, lastActivity: null, weeklyData: [], topSuppliers: [], suppliersList: [], topRecipients: [], recipientsList: [] }; const allInvoices = await getInvoicesByUserId(userId); // Calculate basic stats const completed = allInvoices.filter(i => i.status === "completed"); const totalAmount = completed.reduce((sum, inv) => sum + (Number(inv.totalAmount) || 0), 0); const averageScore = completed.length > 0 ? completed.reduce((sum, inv) => sum + (inv.qualityScore || 0), 0) / completed.length : 0; // Get last activity const sortedByDate = [...allInvoices].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); const lastActivity = sortedByDate[0]?.createdAt || null; // Calculate weekly data (last 30 days) const now = new Date(); const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); const weeklyMap = new Map(); allInvoices.forEach(inv => { const date = new Date(inv.createdAt); if (date >= thirtyDaysAgo) { const weekStart = new Date(date); weekStart.setDate(date.getDate() - date.getDay()); // Start of week const weekKey = `${weekStart.getDate().toString().padStart(2, '0')}/${(weekStart.getMonth() + 1).toString().padStart(2, '0')}`; weeklyMap.set(weekKey, (weeklyMap.get(weekKey) || 0) + 1); } }); const weeklyData = Array.from(weeklyMap.entries()) .map(([week, count]) => ({ week, count })) .sort((a, b) => { const [dayA, monthA] = a.week.split('/').map(Number); const [dayB, monthB] = b.week.split('/').map(Number); return (monthA! * 100 + dayA!) - (monthB! * 100 + dayB!); }); // Calculate top suppliers const supplierMap = new Map(); completed.forEach(inv => { const supplier = inv.supplierName || 'Inconnu'; const current = supplierMap.get(supplier) || { count: 0, amount: 0 }; supplierMap.set(supplier, { count: current.count + 1, amount: current.amount + (Number(inv.totalAmount) || 0) }); }); const topSuppliers = Array.from(supplierMap.entries()) .map(([name, data]) => ({ name, count: data.count, amount: data.amount, percentage: completed.length > 0 ? (data.count / completed.length * 100) : 0 })) .sort((a, b) => b.count - a.count) .slice(0, 10); const suppliersList = Array.from(supplierMap.entries()) .map(([name, data]) => ({ name, count: data.count, amount: data.amount })) .sort((a, b) => b.count - a.count); // Calculate top recipients const recipientMap = new Map(); completed.forEach(inv => { const recipient = (inv as any).recipientName; if (!recipient) return; const current = recipientMap.get(recipient) || { count: 0, amount: 0 }; recipientMap.set(recipient, { count: current.count + 1, amount: current.amount + (Number(inv.totalAmount) || 0) }); }); const topRecipients = Array.from(recipientMap.entries()) .map(([name, data]) => ({ name, count: data.count, amount: data.amount, percentage: completed.length > 0 ? (data.count / completed.length * 100) : 0 })) .sort((a, b) => b.count - a.count) .slice(0, 10); const recipientsList = Array.from(recipientMap.entries()) .map(([name, data]) => ({ name, count: data.count, amount: data.amount })) .sort((a, b) => b.count - a.count); return { total: allInvoices.length, completed: completed.length, processing: allInvoices.filter(i => i.status === "processing").length, error: allInvoices.filter(i => i.status === "error").length, totalAmount, averageScore: Math.round(averageScore), lastActivity, weeklyData, topSuppliers, suppliersList, topRecipients, recipientsList }; } export async function findDuplicateInvoice( invoiceNumber: string | null, totalAmount: string | null, userId?: number ): Promise { if (!invoiceNumber || !totalAmount) return undefined; const db = await getDb(); if (!db) return undefined; const conditions = [ eq(invoices.invoiceNumber, invoiceNumber), eq(invoices.totalAmount, totalAmount), ]; if (userId !== undefined) { conditions.push(eq(invoices.userId, userId)); } const result = await db.select().from(invoices) .where(and(...conditions)) .limit(1); return result[0]; } // ============= USER SETTINGS OPERATIONS ============= export async function getUserSettings(userId: number): Promise { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(userSettings).where(eq(userSettings.userId, userId)).limit(1); return result[0]; } export async function upsertUserSettings(data: InsertUserSettings) { const db = await getDb(); if (!db) throw new Error("Database not available"); const existing = await getUserSettings(data.userId); if (existing) { await db.update(userSettings).set(data).where(eq(userSettings.userId, data.userId)); } else { await db.insert(userSettings).values(data); } } // ============= IMPORT LOG OPERATIONS ============= export async function createImportLog(data: InsertImportLog): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(importLogs).values(data); const insertedId = Number(result[0].insertId); const inserted = await db.select().from(importLogs).where(eq(importLogs.id, insertedId)).limit(1); return inserted[0]!; } export async function getImportLogsByUser(userId: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(importLogs).where(eq(importLogs.userId, userId)).orderBy(desc(importLogs.importedAt)); } export async function getAllImportLogs(): Promise { const db = await getDb(); if (!db) return []; return db.select().from(importLogs).orderBy(desc(importLogs.importedAt)); } export async function deleteAllImportLogs(userId: number | null): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); if (userId === null) { // Admin : supprime tous les logs await db.delete(importLogs); } else { await db.delete(importLogs).where(eq(importLogs.userId, userId)); } } // ============= LLM LOG OPERATIONS ============= export async function createLlmLog(data: InsertLlmLog): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(llmLogs).values(data); const insertedId = Number(result[0].insertId); const inserted = await db.select().from(llmLogs).where(eq(llmLogs.id, insertedId)).limit(1); return inserted[0]!; } export async function getLlmLogsBySourceFile(sourceFileId: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(llmLogs).where(eq(llmLogs.sourceFileId, sourceFileId)).orderBy(desc(llmLogs.createdAt)); } export async function getLlmLogsByInvoice(invoiceId: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(llmLogs).where(eq(llmLogs.invoiceId, invoiceId)).orderBy(desc(llmLogs.createdAt)); } // ============= IMPORT SETTINGS OPERATIONS ============= export async function getImportSettingsByUser(userId: number): Promise { const db = await getDb(); if (!db) return null; const result = await db.select().from(importSettings).where(eq(importSettings.userId, userId)).limit(1); return result[0] || null; } export async function upsertImportSettings(data: InsertImportSettings): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); // Check if settings exist for this user const existing = await getImportSettingsByUser(data.userId!); if (existing) { // Update existing settings await db.update(importSettings) .set({ ...data, updatedAt: new Date(), }) .where(eq(importSettings.userId, data.userId!)); const updated = await getImportSettingsByUser(data.userId!); return updated!; } else { // Insert new settings const result = await db.insert(importSettings).values(data); const insertedId = Number(result[0].insertId); const inserted = await db.select().from(importSettings).where(eq(importSettings.id, insertedId)).limit(1); return inserted[0]!; } } // ============= DEPARTMENT LIST OPERATIONS ============= export async function getDepartmentsByUser(_userId?: number): Promise { const db = await getDb(); if (!db) return []; return await db.select().from(departmentList).orderBy(departmentList.name); } export async function createDepartment(data: InsertDepartment): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const [department] = await db.insert(departmentList).values(data).$returningId(); return await db.select().from(departmentList).where(eq(departmentList.id, department.id)).then(rows => rows[0]); } export async function deleteDepartment(id: number): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(departmentList).where(eq(departmentList.id, id)); } // ============= ACCOUNTING ALLOCATION LIST OPERATIONS ============= export async function getAccountingAllocationsByUser(_userId?: number): Promise { const db = await getDb(); if (!db) return []; return await db.select().from(accountingAllocationList).orderBy(accountingAllocationList.name); } export async function createAccountingAllocation(data: InsertAccountingAllocation): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const [allocation] = await db.insert(accountingAllocationList).values(data).$returningId(); return await db.select().from(accountingAllocationList).where(eq(accountingAllocationList.id, allocation.id)).then(rows => rows[0]); } export async function deleteAccountingAllocation(id: number): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(accountingAllocationList).where(eq(accountingAllocationList.id, id)); } // ============= AUTOMATION RULES OPERATIONS ============= export async function getAutomationRulesByUser(userId: number): Promise { const db = await getDb(); if (!db) return []; return await db.select().from(automationRules).where(eq(automationRules.userId, userId)).orderBy(automationRules.priority); } export async function getAutomationRuleById(id: number): Promise { const db = await getDb(); if (!db) return null; const result = await db.select().from(automationRules).where(eq(automationRules.id, id)).limit(1); return result[0] || null; } export async function createAutomationRule(data: InsertAutomationRule): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(automationRules).values(data); const insertedId = result[0].insertId; const newRule = await getAutomationRuleById(insertedId); return newRule!; } export async function updateAutomationRule(id: number, data: Partial): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.update(automationRules).set(data).where(eq(automationRules.id, id)); const updated = await getAutomationRuleById(id); return updated!; } export async function deleteAutomationRule(id: number): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.delete(automationRules).where(eq(automationRules.id, id)); } // ============= INITIALIZE DEFAULT VALUES ============= export async function initializeDefaultLists(userId: number): Promise { const db = await getDb(); if (!db) return; // Initialize default departments const defaultDepartments = ["DSI", "TRAVAUX", "AUTRE"]; const existingDepts = await db.select().from(departmentList).where(eq(departmentList.userId, userId)); const existingDeptNames = new Set(existingDepts.map(d => d.name)); for (const name of defaultDepartments) { if (!existingDeptNames.has(name)) { try { await db.insert(departmentList).values({ userId, name }); } catch (error) { // Ignore errors } } } // Initialize default accounting allocations const defaultAllocations = ["TOUS", "PA", "HEP", "SANITAIRE", "AUTRE"]; const existingAllocs = await db.select().from(accountingAllocationList).where(eq(accountingAllocationList.userId, userId)); const existingAllocNames = new Set(existingAllocs.map(a => a.name)); for (const name of defaultAllocations) { if (!existingAllocNames.has(name)) { try { await db.insert(accountingAllocationList).values({ userId, name }); } catch (error) { // Ignore errors } } } } // ============= LLM FIELDS CONFIG OPERATIONS ============= export async function getLlmFieldsConfigByUser(userId: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(llmFieldsConfig).where(eq(llmFieldsConfig.userId, userId)).orderBy(llmFieldsConfig.displayOrder); } export async function upsertLlmFieldConfig(data: InsertLlmFieldConfig): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const existing = await db.select().from(llmFieldsConfig) .where(and( eq(llmFieldsConfig.userId, data.userId), eq(llmFieldsConfig.fieldName, data.fieldName) )) .limit(1); if (existing.length > 0) { await db.update(llmFieldsConfig) .set({ ...data, updatedAt: new Date() }) .where(eq(llmFieldsConfig.id, existing[0].id)); return (await db.select().from(llmFieldsConfig).where(eq(llmFieldsConfig.id, existing[0].id)))[0]; } else { const result = await db.insert(llmFieldsConfig).values(data); const insertedId = (result as any).insertId; return (await db.select().from(llmFieldsConfig).where(eq(llmFieldsConfig.id, Number(insertedId))))[0]; } } export async function initializeDefaultLlmFields(userId: number): Promise { const db = await getDb(); if (!db) return; const defaultFields = [ { fieldName: "supplierName", displayName: "Nom du fournisseur", isRequired: 1, displayOrder: 1 }, { fieldName: "invoiceNumber", displayName: "Numéro de facture", isRequired: 1, displayOrder: 2 }, { fieldName: "invoiceDate", displayName: "Date de facture", isRequired: 1, displayOrder: 3 }, { fieldName: "totalAmount", displayName: "Montant total TTC", isRequired: 1, displayOrder: 4 }, { fieldName: "deliveryNoteNumber", displayName: "Numéro de bon de livraison", isRequired: 0, displayOrder: 5 }, { fieldName: "orderNumber", displayName: "Numéro de commande", isRequired: 0, displayOrder: 6 }, { fieldName: "recipientName", displayName: "Destinataire", isRequired: 0, displayOrder: 7 }, ]; const existingFields = await db.select().from(llmFieldsConfig).where(eq(llmFieldsConfig.userId, userId)); const existingFieldNames = new Set(existingFields.map(f => f.fieldName)); for (const field of defaultFields) { if (!existingFieldNames.has(field.fieldName)) { try { await db.insert(llmFieldsConfig).values({ userId, ...field }); } catch (error) { // Ignore errors } } } } // ============= SIGNATURES HELPERS ============= export async function getSignaturesByUser(_userId?: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(signatures); } export async function getSignatureById(id: number): Promise { const db = await getDb(); if (!db) return undefined; const results = await db.select().from(signatures).where(eq(signatures.id, id)); return results[0]; } export async function createSignature(data: InsertSignature): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(signatures).values(data); const insertId = (result[0] as any).insertId; const created = await getSignatureById(insertId); if (!created) throw new Error("Failed to retrieve created signature"); return created; } export async function deleteSignature(id: number): Promise { const db = await getDb(); if (!db) return; await db.delete(signatures).where(eq(signatures.id, id)); } // ============= SERVICE SIGNATURES HELPERS ============= export async function getServiceSignaturesByUser(_userId?: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(serviceSignatures); } export async function getServiceSignatureByService(userId: number, serviceName: string): Promise { const db = await getDb(); if (!db) return undefined; const results = await db.select().from(serviceSignatures) .where(eq(serviceSignatures.userId, userId)) .limit(1); // Filter in JS to avoid SQL case sensitivity issues return results.find(r => r.serviceName.toLowerCase() === serviceName.toLowerCase()); } export async function upsertServiceSignature(data: InsertServiceSignature): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); await db.insert(serviceSignatures).values(data) .onDuplicateKeyUpdate({ set: { signatureId: data.signatureId, updatedAt: new Date() } }); } export async function deleteServiceSignature(userId: number, serviceName: string): Promise { const db = await getDb(); if (!db) return; const all = await db.select().from(serviceSignatures).where(eq(serviceSignatures.userId, userId)); const match = all.find(r => r.serviceName.toLowerCase() === serviceName.toLowerCase()); if (match) { await db.delete(serviceSignatures).where(eq(serviceSignatures.id, match.id)); } } // ============= BAP HISTORY HELPERS ============= export async function createBapHistoryEntry(data: InsertBapHistory): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(bapHistory).values(data); const insertId = (result[0] as any).insertId; const created = await getBapHistoryById(insertId); if (!created) throw new Error("Failed to retrieve created BAP history entry"); return created; } export async function getBapHistoryById(id: number): Promise { const db = await getDb(); if (!db) return undefined; const results = await db.select().from(bapHistory).where(eq(bapHistory.id, id)); return results[0]; } export async function getBapHistoryByUser(userId: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(bapHistory) .where(eq(bapHistory.userId, userId)) .orderBy(desc(bapHistory.validatedAt)); } export async function getAllBapHistory(): Promise { const db = await getDb(); if (!db) return []; return db.select().from(bapHistory).orderBy(desc(bapHistory.validatedAt)); } export async function deleteBapHistoryEntry(id: number): Promise { const db = await getDb(); if (!db) return; await db.delete(bapHistory).where(eq(bapHistory.id, id)); } // ── Invoice Learnings (corrections manuelles apprises) ──────────────────────── /** Normalise le nom du fournisseur pour la clé de correspondance */ export function normalizeSupplierId(supplierName: string): string { return supplierName.trim().toLowerCase().replace(/\s+/g, ' '); } /** Récupère tous les apprentissages d'un utilisateur */ export async function getLearningsByUser(userId: number): Promise { const db = await getDb(); if (!db) return []; return db.select().from(invoiceLearnings).where(eq(invoiceLearnings.userId, userId)); } /** Récupère les apprentissages pour un fournisseur donné */ export async function getLearningsBySupplier(userId: number, supplierName: string): Promise { const db = await getDb(); if (!db) return []; const key = normalizeSupplierId(supplierName); return db.select().from(invoiceLearnings).where( and(eq(invoiceLearnings.userId, userId), eq(invoiceLearnings.supplierKey, key)) ); } /** Enregistre ou met à jour un apprentissage (upsert par userId + supplierKey + fieldName) */ export async function upsertLearning(data: { userId: number; supplierName: string; fieldName: string; originalValue?: string; correctedValue: string; }): Promise { const db = await getDb(); if (!db) return; const supplierKey = normalizeSupplierId(data.supplierName); // Chercher si une entrée existe déjà const existing = await db.select().from(invoiceLearnings).where( and( eq(invoiceLearnings.userId, data.userId), eq(invoiceLearnings.supplierKey, supplierKey), eq(invoiceLearnings.fieldName, data.fieldName) ) ); if (existing.length > 0) { await db.update(invoiceLearnings) .set({ correctedValue: data.correctedValue, originalValue: data.originalValue ?? existing[0].originalValue, applyCount: (existing[0].applyCount || 1) + 1, }) .where(eq(invoiceLearnings.id, existing[0].id)); } else { await db.insert(invoiceLearnings).values({ userId: data.userId, supplierKey, fieldName: data.fieldName, originalValue: data.originalValue, correctedValue: data.correctedValue, applyCount: 1, }); } } /** Supprime un apprentissage par ID */ export async function deleteLearning(id: number): Promise { const db = await getDb(); if (!db) return; await db.delete(invoiceLearnings).where(eq(invoiceLearnings.id, id)); } /** Supprime tous les apprentissages d'un utilisateur */ export async function deleteAllLearnings(userId: number): Promise { const db = await getDb(); if (!db) return; await db.delete(invoiceLearnings).where(eq(invoiceLearnings.userId, userId)); } export async function getBapPdfUrlByInvoiceId(invoiceId: number): Promise { const db = await getDb(); if (!db) return undefined; const results = await db.select({ pdfUrl: bapHistory.pdfUrl }) .from(bapHistory) .where(eq(bapHistory.invoiceId, invoiceId)) .orderBy(desc(bapHistory.validatedAt)) .limit(1); return results[0]?.pdfUrl ?? undefined; } export async function getBapPdfUrlsByInvoiceIds(invoiceIds: number[]): Promise> { const db = await getDb(); if (!db || invoiceIds.length === 0) return {}; const results = await db.select({ invoiceId: bapHistory.invoiceId, pdfUrl: bapHistory.pdfUrl, validatedAt: bapHistory.validatedAt }) .from(bapHistory) .where(inArray(bapHistory.invoiceId, invoiceIds)) .orderBy(desc(bapHistory.validatedAt)); // Keep only the most recent pdfUrl per invoiceId const map: Record = {}; for (const row of results) { if (row.invoiceId && row.pdfUrl && !map[row.invoiceId]) { map[row.invoiceId] = row.pdfUrl; } } return map; } // Mettre à jour le pdfUrl d'une entrée bapHistory (après re-génération) export async function updateBapHistoryPdfUrl(id: number, pdfUrl: string): Promise { const db = await getDb(); if (!db) return; await db.update(bapHistory).set({ pdfUrl }).where(eq(bapHistory.id, id)); } // ============= FREEPRO VENTILATION OPERATIONS ============= import { freeproImports, InsertFreeproImport, FreeproImport, freeproVentilationLines, InsertFreeproVentilationLine, FreeproVentilationLine, } from "../drizzle/schema"; /** Crée un import FreePro et ses lignes de ventilation */ export async function createFreeproImport( data: InsertFreeproImport, lines: Omit[] ): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); const result = await db.insert(freeproImports).values(data); const importId = Number((result as any)[0]?.insertId ?? (result as any).insertId); if (lines.length > 0) { const mappedLines = lines.map((l) => ({ ...l, importId })); // Insérer par lots de 50 pour éviter les limites de paramètres MySQL const BATCH_SIZE = 50; for (let i = 0; i < mappedLines.length; i += BATCH_SIZE) { const batch = mappedLines.slice(i, i + BATCH_SIZE); await db.insert(freeproVentilationLines).values(batch); } } return importId; } /** Récupère tous les imports FreePro d'un utilisateur (sans les lignes) */ export async function getFreeproImportsByUser(userId: number): Promise { const db = await getDb(); if (!db) return []; return db .select() .from(freeproImports) .where(eq(freeproImports.userId, userId)) .orderBy(desc(freeproImports.annee), desc(freeproImports.mois)); } /** Récupère un import FreePro avec ses lignes */ export async function getFreeproImportWithLines( importId: number ): Promise<{ import: FreeproImport; lines: FreeproVentilationLine[] } | null> { const db = await getDb(); if (!db) return null; const imports = await db .select() .from(freeproImports) .where(eq(freeproImports.id, importId)) .limit(1); if (!imports[0]) return null; const lines = await db .select() .from(freeproVentilationLines) .where(eq(freeproVentilationLines.importId, importId)) .orderBy(freeproVentilationLines.structure, freeproVentilationLines.type); return { import: imports[0], lines }; } /** Met à jour le statut d'export SharePoint d'un import FreePro */ export async function updateFreeproSharepointStatus( importId: number, status: 'success' | 'error', path?: string | null, error?: string | null ): Promise { const db = await getDb(); if (!db) return; await db .update(freeproImports) .set({ sharepointUploadStatus: status, sharepointUploadPath: path ?? null, sharepointUploadError: error ?? null, sharepointExportedAt: new Date(), }) .where(eq(freeproImports.id, importId)); } /** Supprime un import FreePro et ses lignes */ export async function deleteFreeproImport(importId: number): Promise { const db = await getDb(); if (!db) return; await db.delete(freeproVentilationLines).where(eq(freeproVentilationLines.importId, importId)); await db.delete(freeproImports).where(eq(freeproImports.id, importId)); } // ============= FREEPRO SETTINGS OPERATIONS ============= import { freeproSettings, InsertFreeproSettings, FreeproSettings, } from "../drizzle/schema"; /** Récupère les paramètres FreePro d'un utilisateur */ export async function getFreeproSettings(userId: number): Promise { const db = await getDb(); if (!db) return null; const rows = await db .select() .from(freeproSettings) .where(eq(freeproSettings.userId, userId)) .limit(1); return rows[0] ?? null; } /** Crée ou met à jour les paramètres FreePro d'un utilisateur */ export async function upsertFreeproSettings( userId: number, data: Partial> ): Promise { const db = await getDb(); if (!db) return; const existing = await getFreeproSettings(userId); if (existing) { await db .update(freeproSettings) .set({ ...data, updatedAt: new Date() }) .where(eq(freeproSettings.userId, userId)); } else { await db.insert(freeproSettings).values({ userId, portalUrl: data.portalUrl ?? "https://pro.free.fr", loginEmail: data.loginEmail ?? null, loginPassword: data.loginPassword ?? null, frequency: data.frequency ?? "manual", maxAnteriority: data.maxAnteriority ?? null, autoEnabled: data.autoEnabled ?? 0, lastSuccessAt: data.lastSuccessAt ?? null, lastStatus: data.lastStatus ?? null, lastImportCount: data.lastImportCount ?? 0, }); } } /** Met à jour uniquement le statut de la dernière récupération FreePro */ export async function updateFreeproLastRun( userId: number, status: string, importCount: number, success: boolean ): Promise { const db = await getDb(); if (!db) return; const update: Partial = { lastStatus: status, lastImportCount: importCount, updatedAt: new Date(), }; if (success) { update.lastSuccessAt = new Date(); } await db .update(freeproSettings) .set(update) .where(eq(freeproSettings.userId, userId)); }