Files
demat-facturation/drizzle/schema.ts

253 lines
12 KiB
TypeScript

import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, uniqueIndex, decimal } 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: 64 }).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
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(),
});
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 }),
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.)
// 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
// 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"
// 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)
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)
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
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;