Files
demat-facturation/drizzle/schema.ts

594 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { decimal, index, int, mysqlEnum, mysqlTable, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core";
/**
* Core user table backing auth flow.
* Supports multiple authentication methods: Manus OAuth, local, and Azure AD
*/
export const users = mysqlTable("users", {
id: int("id").autoincrement().primaryKey(),
/** Manus OAuth identifier (openId) - Optional for backward compatibility */
openId: varchar("openId", { length: 64 }).unique(),
/** Azure AD Object ID - Unique identifier from Azure AD */
azureAdId: varchar("azureAdId", { length: 128 }).unique(),
name: text("name"),
email: varchar("email", { length: 320 }).notNull().unique(),
/** Hashed password for local authentication (bcrypt) */
passwordHash: varchar("passwordHash", { length: 255 }),
/** Authentication method: 'manus', 'local', 'azure-ad' */
loginMethod: mysqlEnum("loginMethod", ["manus", "local", "azure-ad"]).notNull(),
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
/** Account status for manual user management */
isActive: int("isActive").default(1).notNull(), // 0 = inactive, 1 = active
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
});
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
/**
* Source files table storing uploaded PDF files that may contain multiple invoices
*/
export const sourceFiles = mysqlTable("sourceFiles", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
fileName: varchar("fileName", { length: 255 }).notNull(),
fileKey: text("fileKey").notNull(), // Local storage key with YYYY-MM prefix
fileUrl: text("fileUrl").notNull(), // Public URL
/** Empreinte du PDF source, globale à l'application pour bloquer tout réimport identique. */
contentHash: varchar("contentHash", { length: 64 }),
totalInvoicesDetected: int("totalInvoicesDetected").default(0).notNull(),
processingStatus: mysqlEnum("processingStatus", ["processing", "completed", "error"]).default("processing").notNull(),
processingProgress: varchar("processingProgress", { length: 255 }), // Progress message (e.g., "Extraction 3/9 factures...")
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
contentHashIdx: uniqueIndex("source_file_content_hash_unique").on(table.contentHash),
}));
export type SourceFile = typeof sourceFiles.$inferSelect;
export type InsertSourceFile = typeof sourceFiles.$inferInsert;
/**
* Invoices table storing individual invoices extracted from source files
*/
export const invoices = mysqlTable("invoices", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
sourceFileId: int("sourceFileId").notNull(), // Reference to the source PDF file
invoiceIndexInFile: int("invoiceIndexInFile").default(1).notNull(), // Position in the source file (1, 2, 3...)
// File storage information (for individual invoice if split, or reference to source)
fileName: varchar("fileName", { length: 255 }).notNull(),
fileKey: text("fileKey").notNull(), // Local storage key
fileUrl: text("fileUrl").notNull(), // Public URL
// Extracted metadata
supplierName: varchar("supplierName", { length: 255 }),
invoiceNumber: varchar("invoiceNumber", { length: 100 }),
invoiceDate: timestamp("invoiceDate"),
deliveryNoteNumber: varchar("deliveryNoteNumber", { length: 100 }),
orderNumber: varchar("orderNumber", { length: 100 }),
totalAmount: decimal("totalAmount", { precision: 10, scale: 2 }),
recipientName: varchar("recipientName", { length: 255 }), // Destinataire de la facture
pageRange: varchar("pageRange", { length: 20 }), // ex: "1-2" ou "5"
qualityScore: int("qualityScore"), // Score de qualité de l'extraction (0-100)
// Metadata JSON file
metadataFileKey: text("metadataFileKey"), // Storage key for JSON metadata
metadataFileUrl: text("metadataFileUrl"), // Public URL for JSON
// Processing status
status: mysqlEnum("status", ["processing", "completed", "error"]).default("processing").notNull(),
errorMessage: text("errorMessage"),
// Export status
exportStatus: mysqlEnum("exportStatus", ["not_exported", "exported", "export_error"]).default("not_exported").notNull(),
// Manual correction tracking
manuallyEdited: int("manuallyEdited").default(0).notNull(), // 0 = false, 1 = true
// Business fields (optional)
serviceConcerne: varchar("serviceConcerne", { length: 100 }), // Service concerné (DSI, TRAVAUX, etc.)
typeAchat: mysqlEnum("typeAchat", ["CAPEX", "OPEX"]), // Type d'achat
ventilationComptable: varchar("ventilationComptable", { length: 100 }), // Ventilation comptable (TOUS, PA, HEP, etc.)
// Automation tracking - JSON array of field names that were auto-filled by automation rules
// Example: ["typeAchat", "serviceConcerne", "ventilationComptable"]
autoFilledFields: text("autoFilledFields"), // JSON string array
// Extracted text from PDF
extractedText: text("extractedText"), // Full text extracted from the invoice PDF
// Subscription flag
isSubscription: int("isSubscription").default(0).notNull(), // 0 = NON, 1 = OUI
// BAP Validation
bapValidated: int("bapValidated").default(0).notNull(), // 0 = non validé, 1 = validé BAP
bapValidatedAt: timestamp("bapValidatedAt"), // Date de validation BAP
// SFTP Export tracking
exportedAt: timestamp("exportedAt"),
exportMode: mysqlEnum("exportMode", ["manual", "automatic"]),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
}, (table) => {
return {
// Unique constraint: no duplicate invoices with same supplier, invoice number, and date
supplierInvoiceDateIdx: uniqueIndex("supplier_invoice_date_unique").on(table.supplierName, table.invoiceNumber, table.invoiceDate),
};
});
export type Invoice = typeof invoices.$inferSelect;
export type InsertInvoice = typeof invoices.$inferInsert;
/**
* User settings table for application preferences
*/
export const userSettings = mysqlTable("userSettings", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull().unique(), // One settings record per user
llmModel: varchar("llmModel", { length: 50 }).default("mistral-large-latest").notNull(), // Mistral model for invoice extraction
orderNumberFormat: text("orderNumberFormat"), // Format/pattern du numéro de commande pour aider l'extraction
// Mots-clés personnalisés pour améliorer la détection (séparés par des virgules)
invoiceNumberKeywords: text("invoiceNumberKeywords"), // Ex: "Référence, Ref facture, Invoice ref"
deliveryNoteKeywords: text("deliveryNoteKeywords"), // Ex: "Livraison, Delivery, Expédition"
orderNumberKeywords: text("orderNumberKeywords"), // Ex: "Cde client, Référence commande, PO Number"
supplierKeywords: text("supplierKeywords"), // Ex: "Vendeur, Société, Émetteur"
totalAmountKeywords: text("totalAmountKeywords"), // Ex: "Net à payer, Total à régler, Amount due"
subscriptionKeywords: text("subscriptionKeywords"), // Ex: "Abonnement, Subscription, Mensuel, Annuel"
recipientKeywords: text("recipientKeywords"), // Ex: "Destinataire, À l'attention de, Client"
sftpRecipientFilter: text("sftpRecipientFilter"), // Filtre destinataire pour l'export SFTP (vide = tous)
// SFTP Configuration
sftpHost: varchar("sftpHost", { length: 255 }),
sftpPort: int("sftpPort").default(22),
sftpUsername: varchar("sftpUsername", { length: 255 }),
sftpPassword: text("sftpPassword"), // Encrypted password
sftpRemotePath: varchar("sftpRemotePath", { length: 500 }).default("/"), // Remote directory path
sftpAutoExport: int("sftpAutoExport").default(0).notNull(), // 0 = manual, 1 = automatic
// LLM Logs retention
llmLogsRetentionMonths: int("llmLogsRetentionMonths").default(3).notNull(), // Durée de conservation des logs LLM en mois (défaut: 3 mois)
// Seuil de confiance pour les apprentissages IA
learningConfidenceThreshold: int("learningConfidenceThreshold").default(2).notNull(), // Nombre minimum d'applications pour marquer un apprentissage comme Confirmé
// AI Engine configuration
aiProvider: mysqlEnum("aiProvider", ["mistral", "manus", "gemini"]).default("mistral").notNull(), // AI provider: 'mistral', 'manus' or 'gemini'
mistralApiKey: text("mistralApiKey"), // Mistral API key (overrides env MISTRAL_API_KEY)
manusForgeApiKey: text("manusForgeApiKey"), // Manus Forge API key (overrides env BUILT_IN_FORGE_API_KEY)
manusForgeApiUrl: text("manusForgeApiUrl"), // Manus Forge API URL (overrides env BUILT_IN_FORGE_API_URL)
geminiApiKey: text("geminiApiKey"), // Google Gemini API key (overrides env GEMINI_API_KEY)
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type UserSettings = typeof userSettings.$inferSelect;
export type InsertUserSettings = typeof userSettings.$inferInsert;
/**
* Import settings table for configuring different import methods
*/
export const importSettings = mysqlTable("importSettings", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull().unique(), // One settings record per user
// Manual import settings
manualImportEnabled: int("manualImportEnabled").default(1).notNull(), // 0 = disabled, 1 = enabled
// Automatic import from folder settings
autoImportEnabled: int("autoImportEnabled").default(0).notNull(), // 0 = disabled, 1 = enabled
autoImportSourcePath: text("autoImportSourcePath"), // Path to folder to watch
autoImportFrequency: int("autoImportFrequency").default(60).notNull(), // Frequency in minutes (default: 60 = 1 hour)
// Email import settings
emailImportEnabled: int("emailImportEnabled").default(0).notNull(), // 0 = disabled, 1 = enabled
emailImportAddress: varchar("emailImportAddress", { length: 320 }), // Email address to monitor
emailImportPassword: text("emailImportPassword"), // Encrypted password
emailImportHost: varchar("emailImportHost", { length: 255 }), // IMAP server host
emailImportPort: int("emailImportPort").default(993), // IMAP port (default: 993 for SSL)
emailImportFrequency: int("emailImportFrequency").default(30).notNull(), // Frequency in minutes (default: 30)
emailImportSinceDate: int("emailImportSinceDate"), // Timestamp Unix (s) — ne pas lire les emails antérieurs à cette date
emailImportAuthMode: mysqlEnum("emailImportAuthMode", ["basic", "oauth2"]).default("basic").notNull(), // Auth mode: basic (login/password) or oauth2 (Azure AD token)
// Export folder settings
exportFolder: text("exportFolder"), // Path to folder for exporting invoices
exportFolderType: mysqlEnum("exportFolderType", ["local", "teams", "sharepoint"]).default("local").notNull(), // Type de destination d'export
bapExportMode: mysqlEnum("bapExportMode", ["browser", "folder", "both"]).default("browser").notNull(), // BAP export mode: open in browser or save to folder
// Azure AD / Microsoft Graph credentials for SharePoint export
azureTenantId: varchar("azureTenantId", { length: 100 }), // Azure AD Tenant ID
azureClientId: varchar("azureClientId", { length: 100 }), // Azure AD Application (client) ID
azureClientSecret: text("azureClientSecret"), // Azure AD Client Secret (encrypted)
azureSecretExpiresAt: timestamp("azureSecretExpiresAt"), // Azure AD Client Secret expiration date
// AI Engine settings
aiProvider: mysqlEnum("aiProvider", ["mistral", "manus", "gemini"]).default("mistral").notNull(), // AI provider for invoice extraction
mistralApiKey: text("mistralApiKey"), // Mistral API key (overrides env MISTRAL_API_KEY)
manusForgeApiKey: text("manusForgeApiKey"), // Manus Forge API key (overrides env BUILT_IN_FORGE_API_KEY)
manusForgeApiUrl: text("manusForgeApiUrl"), // Manus Forge API URL (overrides env BUILT_IN_FORGE_API_URL)
geminiApiKey: text("geminiApiKey"), // Google Gemini API key (overrides env GEMINI_API_KEY)
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type ImportSettings = typeof importSettings.$inferSelect;
export type InsertImportSettings = typeof importSettings.$inferInsert;
/**
* Import logs table for tracking all import operations
*/
export const importLogs = mysqlTable("importLogs", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
sourceFileId: int("sourceFileId").notNull(), // Reference to source file
fileName: varchar("fileName", { length: 255 }).notNull(),
totalInvoicesDetected: int("totalInvoicesDetected").default(0).notNull(),
invoicesImported: int("invoicesImported").default(0).notNull(),
duplicatesIgnored: int("duplicatesIgnored").default(0).notNull(),
errors: int("errors").default(0).notNull(),
duplicateDetails: text("duplicateDetails"), // JSON array of duplicate invoice info
errorDetails: text("errorDetails"), // JSON array of error messages
warningMessage: text("warningMessage"), // Warning message (e.g. quota exhausted)
/** Source du mode d'import : 'file' = upload manuel, 'folder' = dossier automatique, 'email' = import par email */
importSource: mysqlEnum("importSource", ["file", "folder", "email"]).default("file").notNull(),
/** Déclencheur : manuel, planifié automatiquement, ou non tracé pour les historiques antérieurs. */
importTrigger: mysqlEnum("importTrigger", ["manual", "automatic", "unknown"]).default("unknown").notNull(),
importedAt: timestamp("importedAt").defaultNow().notNull(),
});
export type ImportLog = typeof importLogs.$inferSelect;
export type InsertImportLog = typeof importLogs.$inferInsert;
/**
* LLM Logs table for storing raw LLM responses for debugging and improvement
*/
export const llmLogs = mysqlTable("llmLogs", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
sourceFileId: int("sourceFileId"), // Optional: link to source file if applicable
invoiceId: int("invoiceId"), // Optional: link to invoice if applicable
operation: varchar("operation", { length: 50 }).notNull(), // "detection" or "extraction"
model: varchar("model", { length: 50 }).notNull(), // LLM model used
promptSent: text("promptSent").notNull(), // Full prompt sent to LLM
rawResponse: text("rawResponse").notNull(), // Raw response from LLM (before cleaning)
cleanedResponse: text("cleanedResponse"), // Response after markdown cleaning
success: int("success").default(1).notNull(), // 1 = success, 0 = error
errorMessage: text("errorMessage"), // Error message if failed
processingTimeMs: int("processingTimeMs"), // Processing time in milliseconds
pageRange: varchar("pageRange", { length: 50 }), // Page range for this log (e.g., "1-2")
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type LlmLog = typeof llmLogs.$inferSelect;
export type InsertLlmLog = typeof llmLogs.$inferInsert;
/**
* Department list table for managing "Service concerné" values
*/
export const departmentList = mysqlTable("departmentList", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(), // Each user has their own list
name: varchar("name", { length: 100 }).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
}, (table) => {
return {
// Unique constraint: no duplicate department names for the same user
userDepartmentIdx: uniqueIndex("user_department_unique").on(table.userId, table.name),
};
});
export type Department = typeof departmentList.$inferSelect;
export type InsertDepartment = typeof departmentList.$inferInsert;
/**
* Accounting allocation list table for managing "Ventilation comptable" values
*/
export const accountingAllocationList = mysqlTable("accountingAllocationList", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(), // Each user has their own list
name: varchar("name", { length: 100 }).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
}, (table) => {
return {
// Unique constraint: no duplicate allocation names for the same user
userAllocationIdx: uniqueIndex("user_allocation_unique").on(table.userId, table.name),
};
});
export type AccountingAllocation = typeof accountingAllocationList.$inferSelect;
export type InsertAccountingAllocation = typeof accountingAllocationList.$inferInsert;
/**
* Automation rules table for automatic field filling based on conditions
*/
export const automationRules = mysqlTable("automationRules", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(), // Each user has their own rules
name: varchar("name", { length: 255 }).notNull(), // Rule name for identification
isActive: int("isActive").default(1).notNull(), // 0 = disabled, 1 = enabled
priority: int("priority").default(0).notNull(), // Execution order (lower = higher priority)
// Conditions (IF) - JSON array of condition objects
// Example: [{"field": "supplierName", "operator": "contains", "value": "Microsoft"}, {"field": "totalAmount", "operator": ">", "value": "1000"}]
conditions: text("conditions").notNull(), // JSON string
conditionsLogic: mysqlEnum("conditionsLogic", ["AND", "OR"]).default("AND").notNull(), // How to combine conditions
// Actions (THEN) - JSON object with field assignments
// Example: {"typeAchat": "CAPEX", "serviceConcerne": "DSI", "ventilationComptable": "TOUS"}
actions: text("actions").notNull(), // JSON string
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type AutomationRule = typeof automationRules.$inferSelect;
export type InsertAutomationRule = typeof automationRules.$inferInsert;
/**
* Règles de destination appliquées lors de la validation BAP.
* Elles sont séparées des règles dimport, afin de ne jamais modifier les
* champs dune facture au moment de son export.
*/
export const exportAutomationRules = mysqlTable("exportAutomationRules", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
name: varchar("name", { length: 255 }).notNull(),
isActive: int("isActive").default(1).notNull(),
priority: int("priority").default(0).notNull(),
conditionField: mysqlEnum("conditionField", ["recipientName", "ventilationComptable"]).notNull(),
conditionValue: varchar("conditionValue", { length: 255 }).notNull(),
destinationType: mysqlEnum("destinationType", ["local", "teams", "sharepoint"]).notNull(),
destinationPath: text("destinationPath").notNull(),
openInBrowser: int("openInBrowser").default(0).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
userPriorityIdx: index("export_rule_user_priority_idx").on(table.userId, table.priority),
}));
export type ExportAutomationRule = typeof exportAutomationRules.$inferSelect;
export type InsertExportAutomationRule = typeof exportAutomationRules.$inferInsert;
/**
* LLM Fields Configuration table
* Stores configuration for each field used in invoice extraction
* Allows users to define which fields are required for quality score calculation
*/
export const llmFieldsConfig = mysqlTable("llmFieldsConfig", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(), // Each user has their own configuration
fieldName: varchar("fieldName", { length: 100 }).notNull(), // Field identifier (supplierName, invoiceNumber, etc.)
displayName: varchar("displayName", { length: 255 }).notNull(), // Human-readable field name
isRequired: int("isRequired").default(1).notNull(), // 1 = required for 100% score, 0 = optional
displayOrder: int("displayOrder").default(0).notNull(), // Order in UI
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type LlmFieldConfig = typeof llmFieldsConfig.$inferSelect;
export type InsertLlmFieldConfig = typeof llmFieldsConfig.$inferInsert;
/**
* Signatures table storing user signature images with first/last name
*/
export const signatures = mysqlTable("signatures", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
firstName: varchar("firstName", { length: 100 }).notNull(),
lastName: varchar("lastName", { length: 100 }).notNull(),
imageKey: text("imageKey").notNull(), // Local storage key
imageUrl: text("imageUrl").notNull(), // Public URL to the signature image
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Signature = typeof signatures.$inferSelect;
export type InsertSignature = typeof signatures.$inferInsert;
/**
* Service-Signature association table
* Associates a department/service with a specific signature for PDF exports
*/
export const serviceSignatures = mysqlTable("serviceSignatures", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
serviceName: varchar("serviceName", { length: 100 }).notNull(), // Department name (e.g., "DSI", "TRAVAUX")
signatureId: int("signatureId").notNull(), // FK to signatures.id
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
}, (table) => {
return {
// One association per service per user
userServiceIdx: uniqueIndex("user_service_unique").on(table.userId, table.serviceName),
};
});
export type ServiceSignature = typeof serviceSignatures.$inferSelect;
export type InsertServiceSignature = typeof serviceSignatures.$inferInsert;
/**
* BAP History table - records every BAP validation with PDF export details
*/
export const bapHistory = mysqlTable("bapHistory", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
invoiceId: int("invoiceId").notNull(),
supplierName: varchar("supplierName", { length: 255 }),
invoiceNumber: varchar("invoiceNumber", { length: 100 }),
invoiceDate: timestamp("invoiceDate"),
totalAmount: varchar("totalAmount", { length: 50 }),
typeAchat: varchar("typeAchat", { length: 50 }), // CAPEX / OPEX
serviceConcerne: varchar("serviceConcerne", { length: 100 }),
ventilationComptable: varchar("ventilationComptable", { length: 100 }),
recipientName: varchar("recipientName", { length: 255 }),
exportMode: mysqlEnum("exportMode", ["browser", "folder"]).default("browser").notNull(),
exportPath: text("exportPath"), // null for browser mode
pdfUrl: text("pdfUrl"), // S3 URL for browser mode
signatureName: varchar("signatureName", { length: 255 }), // Signer name if applied
sharepointUploadStatus: mysqlEnum("sharepointUploadStatus", ["success", "error", "skipped"]), // SharePoint upload result
sharepointUploadPath: text("sharepointUploadPath"), // Path/URL of uploaded file in SharePoint
sharepointUploadError: text("sharepointUploadError"), // Error message if upload failed
validatedAt: timestamp("validatedAt").defaultNow().notNull(),
});
export type BapHistory = typeof bapHistory.$inferSelect;
export type InsertBapHistory = typeof bapHistory.$inferInsert;
/**
* Invoice learnings table — corrections manuelles apprises par le système
* Quand l'utilisateur modifie un champ détecté par le LLM, le système mémorise
* la correction pour l'appliquer automatiquement aux prochains imports similaires.
*/
export const invoiceLearnings = mysqlTable("invoiceLearnings", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
/** Clé de correspondance : fournisseur normalisé (minuscules, sans espaces superflus) */
supplierKey: varchar("supplierKey", { length: 255 }).notNull(),
/** Champ corrigé : 'isSubscription' | 'typeAchat' | 'serviceConcerne' | 'ventilationComptable' */
fieldName: varchar("fieldName", { length: 100 }).notNull(),
/** Valeur originale détectée par le LLM (pour affichage dans la page de gestion) */
originalValue: varchar("originalValue", { length: 255 }),
/** Valeur corrigée manuellement par l'utilisateur */
correctedValue: varchar("correctedValue", { length: 255 }).notNull(),
/** Nombre de fois que cette correction a été appliquée */
applyCount: int("applyCount").default(1).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type InvoiceLearning = typeof invoiceLearnings.$inferSelect;
export type InsertInvoiceLearning = typeof invoiceLearnings.$inferInsert;
/**
* FreePro ventilation imports — one record per imported monthly file
*/
export const freeproImports = mysqlTable("freeproImports", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
/** Label du mois, ex: "06/2025" */
moisLabel: varchar("moisLabel", { length: 10 }).notNull(),
/** Année (ex: 2025) */
annee: int("annee").notNull(),
/** Mois numérique (1-12) */
mois: int("mois").notNull(),
/** Référence de la pièce comptable, ex: F202506006010 */
refPiece: varchar("refPiece", { length: 50 }),
/** Nom du fichier Excel importé */
fileName: varchar("fileName", { length: 255 }).notNull(),
/** Nombre de lignes traitées */
nbLignes: int("nbLignes").default(0).notNull(),
/** Total général TTC calculé */
totalTtc: varchar("totalTtc", { length: 30 }),
/** Statut de l'export SharePoint : null = jamais exporté, 'success' = exporté, 'error' = erreur */
sharepointUploadStatus: mysqlEnum("sharepointUploadStatus", ["success", "error"]),
/** URL/chemin du fichier dans SharePoint après export réussi */
sharepointUploadPath: text("sharepointUploadPath"),
/** Message d'erreur si l'export SharePoint a échoué */
sharepointUploadError: text("sharepointUploadError"),
/** Date de l'export SharePoint */
sharepointExportedAt: timestamp("sharepointExportedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type FreeproImport = typeof freeproImports.$inferSelect;
export type InsertFreeproImport = typeof freeproImports.$inferInsert;
/**
* FreePro ventilation lines — aggregated result (structure + type → montant TTC)
*/
export const freeproVentilationLines = mysqlTable("freeproVentilationLines", {
id: int("id").autoincrement().primaryKey(),
importId: int("importId").notNull(), // FK to freeproImports.id
/** Code structure, ex: "1083ADV" ou null pour (vide) */
structure: varchar("structure", { length: 100 }),
/** Type: "Lien fibre" | "Lien 5G" | "Tél. mobile" */
type: varchar("type", { length: 50 }).notNull(),
/** Montant TTC agrégé en centimes (pour éviter les flottants) */
montantCentimes: int("montantCentimes").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type FreeproVentilationLine = typeof freeproVentilationLines.$inferSelect;
export type InsertFreeproVentilationLine = typeof freeproVentilationLines.$inferInsert;
/**
* FreePro settings — paramètres de connexion automatique au portail FreePro
* et de récupération périodique des factures CSV
*/
export const freeproSettings = mysqlTable("freeproSettings", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull().unique(), // Un paramétrage par utilisateur
/** URL du portail FreePro (ex: https://pro.free.fr) */
portalUrl: varchar("portalUrl", { length: 255 }).default("https://pro.free.fr").notNull(),
/** Email de connexion au portail FreePro */
loginEmail: varchar("loginEmail", { length: 320 }),
/** Mot de passe de connexion au portail FreePro */
loginPassword: text("loginPassword"),
/** Fréquence de récupération automatique */
frequency: mysqlEnum("frequency", ["manual", "daily", "weekly", "monthly"]).default("manual").notNull(),
/** Date d'antériorité max (timestamp Unix ms) — ne pas récupérer les factures antérieures à cette date */
maxAnteriority: int("maxAnteriority"), // Timestamp Unix en secondes
/** Activation de la récupération automatique */
autoEnabled: int("autoEnabled").default(0).notNull(), // 0 = désactivé, 1 = activé
/** Date de la dernière récupération réussie */
lastSuccessAt: timestamp("lastSuccessAt"),
/** Message de statut de la dernière récupération */
lastStatus: text("lastStatus"),
/** Nombre de factures récupérées lors du dernier run */
lastImportCount: int("lastImportCount").default(0),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type FreeproSettings = typeof freeproSettings.$inferSelect;
export type InsertFreeproSettings = typeof freeproSettings.$inferInsert;
/**
* Deleted invoices blacklist — prevents re-import of manually deleted invoices.
* When a user deletes an invoice, its invoiceNumber + totalAmount are stored here.
* The email/file import service checks this table before inserting a new invoice.
*/
export const deletedInvoices = mysqlTable("deletedInvoices", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
invoiceNumber: varchar("invoiceNumber", { length: 100 }).notNull(),
totalAmount: varchar("totalAmount", { length: 50 }),
supplierName: varchar("supplierName", { length: 255 }),
deletedAt: timestamp("deletedAt").defaultNow().notNull(),
});
export type DeletedInvoice = typeof deletedInvoices.$inferSelect;
export type InsertDeletedInvoice = typeof deletedInvoices.$inferInsert;
/**
* Web import sources — connecteurs web pour scraper des factures depuis des sites
* (ex: espace client SFR, Starlink, Orange...) avec login/mot de passe.
* Le scraping est exécuté par un script cron externe (Node.js + Playwright) sur LWS.
*/
export const webImportSources = mysqlTable("webImportSources", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
/** Nom affiché (ex: "SFR Pro", "Starlink") */
name: varchar("name", { length: 100 }).notNull(),
/** Type de connecteur — détermine le script Playwright à utiliser */
connectorType: varchar("connectorType", { length: 50 }).notNull(), // ex: "sfr", "starlink", "orange"
/** URL de l'espace client */
portalUrl: varchar("portalUrl", { length: 500 }).notNull(),
/** Identifiant de connexion (email ou login) */
loginEmail: varchar("loginEmail", { length: 320 }).notNull(),
/** Mot de passe chiffré (AES-256) */
loginPassword: text("loginPassword").notNull(),
/** Fréquence de vérification automatique */
frequency: mysqlEnum("frequency", ["manual", "daily", "weekly", "monthly"]).default("monthly").notNull(),
/** Activation de l'import automatique */
autoEnabled: int("autoEnabled").default(0).notNull(), // 0 = désactivé, 1 = activé
/** Date du dernier import réussi */
lastSuccessAt: timestamp("lastSuccessAt"),
/** Statut du dernier import */
lastStatus: text("lastStatus"),
/** Nombre de factures importées lors du dernier run */
lastImportCount: int("lastImportCount").default(0),
/** Token d'API pour que le script externe puisse s'authentifier */
apiToken: varchar("apiToken", { length: 128 }).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type WebImportSource = typeof webImportSources.$inferSelect;
export type InsertWebImportSource = typeof webImportSources.$inferInsert;