import { eq, and, desc, sql } 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 } 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 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"); await db.delete(invoices).where(eq(invoices.id, id)); } export async function searchInvoices(userId: number, query: string): Promise { const db = await getDb(); if (!db) return []; const searchPattern = `%${query}%`; 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: [] }; 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); 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 }; } export async function findDuplicateInvoice( supplierName: string | null, invoiceNumber: string | null, invoiceDate: Date | null ): Promise { if (!supplierName || !invoiceNumber || !invoiceDate) return undefined; const db = await getDb(); if (!db) return undefined; const result = await db.select().from(invoices) .where( and( eq(invoices.supplierName, supplierName), eq(invoices.invoiceNumber, invoiceNumber), eq(invoices.invoiceDate, invoiceDate) ) ) .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 deleteAllImportLogs(userId: number): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); 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).where(eq(departmentList.userId, userId)).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).where(eq(accountingAllocationList.userId, userId)).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 } } } }