594 lines
31 KiB
TypeScript
594 lines
31 KiB
TypeScript
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 d’import, afin de ne jamais modifier les
|
||
* champs d’une 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;
|