diff --git a/drizzle/0037_goofy_quentin_quire.sql b/drizzle/0037_goofy_quentin_quire.sql new file mode 100644 index 0000000..e3fbbde --- /dev/null +++ b/drizzle/0037_goofy_quentin_quire.sql @@ -0,0 +1,2 @@ +ALTER TABLE `sourceFiles` ADD `contentHash` varchar(64);--> statement-breakpoint +ALTER TABLE `sourceFiles` ADD CONSTRAINT `source_file_content_hash_unique` UNIQUE(`contentHash`); \ No newline at end of file diff --git a/drizzle/meta/0037_snapshot.json b/drizzle/meta/0037_snapshot.json new file mode 100644 index 0000000..774409c --- /dev/null +++ b/drizzle/meta/0037_snapshot.json @@ -0,0 +1,2372 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "d0ab0659-6a58-45a5-9da2-a3bc5538bb3c", + "prevId": "728ba895-5eda-452b-8c20-b42c4abade47", + "tables": { + "accountingAllocationList": { + "name": "accountingAllocationList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_allocation_unique": { + "name": "user_allocation_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "accountingAllocationList_id": { + "name": "accountingAllocationList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automationRules": { + "name": "automationRules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "conditions": { + "name": "conditions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conditionsLogic": { + "name": "conditionsLogic", + "type": "enum('AND','OR')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AND'" + }, + "actions": { + "name": "actions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "automationRules_id": { + "name": "automationRules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "bapHistory": { + "name": "bapHistory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientName": { + "name": "recipientName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('browser','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "exportPath": { + "name": "exportPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pdfUrl": { + "name": "pdfUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureName": { + "name": "signatureName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadStatus": { + "name": "sharepointUploadStatus", + "type": "enum('success','error','skipped')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadPath": { + "name": "sharepointUploadPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadError": { + "name": "sharepointUploadError", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validatedAt": { + "name": "validatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bapHistory_id": { + "name": "bapHistory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "deletedInvoices": { + "name": "deletedInvoices", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "deletedInvoices_id": { + "name": "deletedInvoices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departmentList": { + "name": "departmentList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_department_unique": { + "name": "user_department_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "departmentList_id": { + "name": "departmentList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "freeproImports": { + "name": "freeproImports", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "moisLabel": { + "name": "moisLabel", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mois": { + "name": "mois", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refPiece": { + "name": "refPiece", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nbLignes": { + "name": "nbLignes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "totalTtc": { + "name": "totalTtc", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadStatus": { + "name": "sharepointUploadStatus", + "type": "enum('success','error')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadPath": { + "name": "sharepointUploadPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadError": { + "name": "sharepointUploadError", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointExportedAt": { + "name": "sharepointExportedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "freeproImports_id": { + "name": "freeproImports_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "freeproSettings": { + "name": "freeproSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "portalUrl": { + "name": "portalUrl", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https://pro.free.fr'" + }, + "loginEmail": { + "name": "loginEmail", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginPassword": { + "name": "loginPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frequency": { + "name": "frequency", + "type": "enum('manual','daily','weekly','monthly')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "maxAnteriority": { + "name": "maxAnteriority", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoEnabled": { + "name": "autoEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lastSuccessAt": { + "name": "lastSuccessAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastStatus": { + "name": "lastStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastImportCount": { + "name": "lastImportCount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "freeproSettings_id": { + "name": "freeproSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "freeproSettings_userId_unique": { + "name": "freeproSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "freeproVentilationLines": { + "name": "freeproVentilationLines", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "importId": { + "name": "importId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "structure": { + "name": "structure", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "montantCentimes": { + "name": "montantCentimes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "freeproVentilationLines_id": { + "name": "freeproVentilationLines_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importLogs": { + "name": "importLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "invoicesImported": { + "name": "invoicesImported", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicatesIgnored": { + "name": "duplicatesIgnored", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicateDetails": { + "name": "duplicateDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "errorDetails": { + "name": "errorDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "warningMessage": { + "name": "warningMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importSource": { + "name": "importSource", + "type": "enum('file','folder','email')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'file'" + }, + "importedAt": { + "name": "importedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importLogs_id": { + "name": "importLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importSettings": { + "name": "importSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manualImportEnabled": { + "name": "manualImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "autoImportEnabled": { + "name": "autoImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "autoImportSourcePath": { + "name": "autoImportSourcePath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoImportFrequency": { + "name": "autoImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "emailImportEnabled": { + "name": "emailImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "emailImportAddress": { + "name": "emailImportAddress", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPassword": { + "name": "emailImportPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportHost": { + "name": "emailImportHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPort": { + "name": "emailImportPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 993 + }, + "emailImportFrequency": { + "name": "emailImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "emailImportSinceDate": { + "name": "emailImportSinceDate", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportAuthMode": { + "name": "emailImportAuthMode", + "type": "enum('basic','oauth2')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'basic'" + }, + "exportFolder": { + "name": "exportFolder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportFolderType": { + "name": "exportFolderType", + "type": "enum('local','teams','sharepoint')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "bapExportMode": { + "name": "bapExportMode", + "type": "enum('browser','folder','both')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "azureTenantId": { + "name": "azureTenantId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureClientId": { + "name": "azureClientId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureClientSecret": { + "name": "azureClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureSecretExpiresAt": { + "name": "azureSecretExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aiProvider": { + "name": "aiProvider", + "type": "enum('mistral','manus','gemini')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral'" + }, + "mistralApiKey": { + "name": "mistralApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiKey": { + "name": "manusForgeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiUrl": { + "name": "manusForgeApiUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "geminiApiKey": { + "name": "geminiApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importSettings_id": { + "name": "importSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "importSettings_userId_unique": { + "name": "importSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "invoiceLearnings": { + "name": "invoiceLearnings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierKey": { + "name": "supplierKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldName": { + "name": "fieldName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "originalValue": { + "name": "originalValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "correctedValue": { + "name": "correctedValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applyCount": { + "name": "applyCount", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoiceLearnings_id": { + "name": "invoiceLearnings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceIndexInFile": { + "name": "invoiceIndexInFile", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteNumber": { + "name": "deliveryNoteNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumber": { + "name": "orderNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "decimal(10,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientName": { + "name": "recipientName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qualityScore": { + "name": "qualityScore", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileKey": { + "name": "metadataFileKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileUrl": { + "name": "metadataFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportStatus": { + "name": "exportStatus", + "type": "enum('not_exported','exported','export_error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'not_exported'" + }, + "manuallyEdited": { + "name": "manuallyEdited", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "enum('CAPEX','OPEX')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoFilledFields": { + "name": "autoFilledFields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extractedText": { + "name": "extractedText", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "isSubscription": { + "name": "isSubscription", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bapValidated": { + "name": "bapValidated", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bapValidatedAt": { + "name": "bapValidatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportedAt": { + "name": "exportedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('manual','automatic')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "supplier_invoice_date_unique": { + "name": "supplier_invoice_date_unique", + "columns": [ + "supplierName", + "invoiceNumber", + "invoiceDate" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoices_id": { + "name": "invoices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmFieldsConfig": { + "name": "llmFieldsConfig", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldName": { + "name": "fieldName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "displayName": { + "name": "displayName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isRequired": { + "name": "isRequired", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "displayOrder": { + "name": "displayOrder", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmFieldsConfig_id": { + "name": "llmFieldsConfig_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmLogs": { + "name": "llmLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "operation": { + "name": "operation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "promptSent": { + "name": "promptSent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rawResponse": { + "name": "rawResponse", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanedResponse": { + "name": "cleanedResponse", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "processingTimeMs": { + "name": "processingTimeMs", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmLogs_id": { + "name": "llmLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serviceSignatures": { + "name": "serviceSignatures", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "serviceName": { + "name": "serviceName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signatureId": { + "name": "signatureId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "user_service_unique": { + "name": "user_service_unique", + "columns": [ + "userId", + "serviceName" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serviceSignatures_id": { + "name": "serviceSignatures_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signatures": { + "name": "signatures", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "firstName": { + "name": "firstName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastName": { + "name": "lastName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imageKey": { + "name": "imageKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "signatures_id": { + "name": "signatures_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sourceFiles": { + "name": "sourceFiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contentHash": { + "name": "contentHash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processingStatus": { + "name": "processingStatus", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "processingProgress": { + "name": "processingProgress", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "source_file_content_hash_unique": { + "name": "source_file_content_hash_unique", + "columns": [ + "contentHash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sourceFiles_id": { + "name": "sourceFiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "llmModel": { + "name": "llmModel", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral-large-latest'" + }, + "orderNumberFormat": { + "name": "orderNumberFormat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumberKeywords": { + "name": "invoiceNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteKeywords": { + "name": "deliveryNoteKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumberKeywords": { + "name": "orderNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supplierKeywords": { + "name": "supplierKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmountKeywords": { + "name": "totalAmountKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscriptionKeywords": { + "name": "subscriptionKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientKeywords": { + "name": "recipientKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRecipientFilter": { + "name": "sftpRecipientFilter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpHost": { + "name": "sftpHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPort": { + "name": "sftpPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "sftpUsername": { + "name": "sftpUsername", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPassword": { + "name": "sftpPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRemotePath": { + "name": "sftpRemotePath", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/'" + }, + "sftpAutoExport": { + "name": "sftpAutoExport", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "llmLogsRetentionMonths": { + "name": "llmLogsRetentionMonths", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "learningConfidenceThreshold": { + "name": "learningConfidenceThreshold", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "aiProvider": { + "name": "aiProvider", + "type": "enum('mistral','manus','gemini')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral'" + }, + "mistralApiKey": { + "name": "mistralApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiKey": { + "name": "manusForgeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiUrl": { + "name": "manusForgeApiUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "geminiApiKey": { + "name": "geminiApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "userSettings_id": { + "name": "userSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "userSettings_userId_unique": { + "name": "userSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureAdId": { + "name": "azureAdId", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "enum('manus','local','azure-ad')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + }, + "users_azureAdId_unique": { + "name": "users_azureAdId_unique", + "columns": [ + "azureAdId" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + }, + "webImportSources": { + "name": "webImportSources", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connectorType": { + "name": "connectorType", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "portalUrl": { + "name": "portalUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "loginEmail": { + "name": "loginEmail", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "loginPassword": { + "name": "loginPassword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "frequency": { + "name": "frequency", + "type": "enum('manual','daily','weekly','monthly')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'monthly'" + }, + "autoEnabled": { + "name": "autoEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lastSuccessAt": { + "name": "lastSuccessAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastStatus": { + "name": "lastStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastImportCount": { + "name": "lastImportCount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "apiToken": { + "name": "apiToken", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "webImportSources_id": { + "name": "webImportSources_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 710d19d..102f761 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -260,6 +260,13 @@ "when": 1785419093588, "tag": "0036_broken_rattler", "breakpoints": true + }, + { + "idx": 37, + "version": "5", + "when": 1787394418464, + "tag": "0037_goofy_quentin_quire", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 6425776..8a0bfad 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -36,12 +36,16 @@ export const sourceFiles = mysqlTable("sourceFiles", { 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; diff --git a/server/_core/index.ts b/server/_core/index.ts index 4c2ccc6..1d90cd7 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -17,7 +17,8 @@ import { startEmailImportService } from "../emailImportService"; import { startFolderImportService } from "../folderImportService"; import { handleAzureCallback, isAzureAdConfigured, generateToken, verifyToken } from "../auth"; import { createDatabaseBackup } from "../databaseBackup"; -import { generateStorageKey, localStoragePut } from "../localStorage"; +import { generateStorageKey, localStorageDelete, localStoragePut } from "../localStorage"; +import { calculateFileSha256 } from "../fileFingerprint"; const MAX_WEB_IMPORT_BYTES = 20 * 1024 * 1024; @@ -305,7 +306,7 @@ async function startServer() { return; } - const { getWebImportSourceByToken, createInvoice, findDuplicateInvoice, isInvoiceBlacklisted, updateWebImportSourceStatus, createSourceFile } = await import('../db'); + const { getWebImportSourceByToken, createInvoice, findDuplicateInvoice, isInvoiceBlacklisted, updateWebImportSourceStatus, createSourceFile, getSourceFileByContentHash } = await import('../db'); const source = await getWebImportSourceByToken(apiToken); if (!source) { res.status(401).json({ error: "Token invalide" }); @@ -317,15 +318,35 @@ async function startServer() { return; } + const contentHash = calculateFileSha256(pdfBuffer); + const existingSource = await getSourceFileByContentHash(contentHash); + if (existingSource) { + await updateWebImportSourceStatus(source.id, "success", 0, true); + res.json({ success: true, imported: 0, duplicates: 1, total: 1 }); + return; + } + // Stocker d'abord le PDF de façon persistante, comme les autres sources d'import. const storageKey = generateStorageKey(source.userId, safeFileName); const { url: fileUrl } = await localStoragePut(storageKey, pdfBuffer, "application/pdf"); - const sourceFile = await createSourceFile({ - userId: source.userId, - fileName: safeFileName, - fileKey: storageKey, - fileUrl, - }); + let sourceFile; + try { + sourceFile = await createSourceFile({ + userId: source.userId, + fileName: safeFileName, + fileKey: storageKey, + fileUrl, + contentHash, + }); + } catch (error: any) { + if (error?.code === "ER_DUP_ENTRY" || error?.errno === 1062) { + await localStorageDelete(storageKey).catch(() => undefined); + await updateWebImportSourceStatus(source.id, "success", 0, true); + res.json({ success: true, imported: 0, duplicates: 1, total: 1 }); + return; + } + throw error; + } const userSettings = await getUserSettings(source.userId); const aiSettings = { aiProvider: userSettings?.aiProvider || "manus", diff --git a/server/db.ts b/server/db.ts index c407a22..1292f59 100644 --- a/server/db.ts +++ b/server/db.ts @@ -204,6 +204,22 @@ export async function createSourceFile(data: InsertSourceFile): Promise { + const db = await getDb(); + if (!db) return undefined; + const result = await db + .select() + .from(sourceFiles) + .where(eq(sourceFiles.contentHash, contentHash)) + .limit(1); + return result[0]; +} + export async function getSourceFileById(id: number): Promise { const db = await getDb(); if (!db) return undefined; diff --git a/server/emailImportService.ts b/server/emailImportService.ts index d837854..1210eb7 100644 --- a/server/emailImportService.ts +++ b/server/emailImportService.ts @@ -5,13 +5,15 @@ import { createSourceFile, updateSourceFile, getUserSettings, + getSourceFileByContentHash, findDuplicateInvoice, isInvoiceBlacklisted, createInvoice, createImportLog, } from "./db"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; -import { localStoragePut, generateStorageKey } from "./localStorage"; +import { localStorageDelete, localStoragePut, generateStorageKey } from "./localStorage"; +import { calculateFileSha256 } from "./fileFingerprint"; import { sendImportNotification } from "./notificationService"; import { getOffice365ImapToken, buildXOAuth2String } from "./office365OAuth"; import { applyAutomationRules } from "./automationEngine"; @@ -32,6 +34,23 @@ interface EmailImportConfig { // Store active intervals for each user const activeIntervals = new Map(); +// Une extraction IA peut dépasser la fréquence configurée : ce verrou évite +// qu'un second cycle IMAP traite les mêmes messages avant la fin du premier. +const activeChecks = new Map>(); + +function runEmailCheckExclusive(config: EmailImportConfig): Promise { + const runningCheck = activeChecks.get(config.userId); + if (runningCheck) { + console.log(`[EmailImport] Vérification déjà en cours pour user ${config.userId}, cycle ignoré`); + return runningCheck; + } + + const check = checkEmailsForPDFs(config).finally(() => { + if (activeChecks.get(config.userId) === check) activeChecks.delete(config.userId); + }); + activeChecks.set(config.userId, check); + return check; +} /** * Process a single email attachment (PDF) @@ -49,6 +68,21 @@ async function processEmailAttachment( // Convert attachment content to Buffer const fileBuffer = attachment.content; console.log(`[EmailImport] File size: ${fileBuffer.length} bytes`); + + const contentHash = calculateFileSha256(fileBuffer); + const existingSource = await getSourceFileByContentHash(contentHash); + if (existingSource) { + console.log( + `[EmailImport] PDF déjà importé, extraction ignorée: ${fileName} -> source ${existingSource.id}`, + ); + return { + success: true, + totalInvoices: Math.max(existingSource.totalInvoicesDetected, 1), + imported: 0, + duplicates: Math.max(existingSource.totalInvoicesDetected, 1), + errors: 0, + }; + } // Store source file const sourceFileKey = generateStorageKey(userId, fileName); @@ -65,13 +99,25 @@ async function processEmailAttachment( } // Create source file record - const sourceFile = await createSourceFile({ - userId, - fileName, - fileKey: sourceFileKey, - fileUrl: sourceFileUrl, - processingStatus: "processing", - }); + let sourceFile; + try { + sourceFile = await createSourceFile({ + userId, + fileName, + fileKey: sourceFileKey, + fileUrl: sourceFileUrl, + contentHash, + processingStatus: "processing", + }); + } catch (error: any) { + // La contrainte unique protège également contre deux imports concurrents. + if (error?.code === "ER_DUP_ENTRY" || error?.errno === 1062) { + await localStorageDelete(sourceFileKey).catch(() => undefined); + console.log(`[EmailImport] PDF réservé par un autre traitement: ${fileName}`); + return { success: true, totalInvoices: 1, imported: 0, duplicates: 1, errors: 0 }; + } + throw error; + } console.log(`[EmailImport] Source file record created with ID: ${sourceFile.id}`); @@ -383,64 +429,69 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise { markSeen: false, // Don't mark as seen yet }); - const processedEmails: number[] = []; + const processedEmailUids: number[] = []; + const messageTasks: Promise[] = []; fetch.on("message", (msg, seqno) => { + const uidPromise = new Promise((resolveUid) => { + msg.once("attributes", (attributes) => resolveUid(attributes.uid)); + }); + msg.on("body", (stream) => { - simpleParser(stream as any, async (err, parsed: ParsedMail) => { - if (err) { - console.error("[EmailImport] Error parsing email:", err); - return; - } + const task = (async () => { + try { + const parsed: ParsedMail = await simpleParser(stream as any); + const pdfAttachments = parsed.attachments.filter( + (attachment) => + attachment.contentType === "application/pdf" || + attachment.filename?.toLowerCase().endsWith(".pdf"), + ); - // Check if email has PDF attachments - const pdfAttachments = parsed.attachments.filter( - (att) => - att.contentType === "application/pdf" || - att.filename?.toLowerCase().endsWith(".pdf") - ); + if (pdfAttachments.length === 0) return; - if (pdfAttachments.length === 0) { - return; - } + console.log( + `[EmailImport] Email ${seqno} has ${pdfAttachments.length} PDF attachment(s)`, + ); - console.log( - `[EmailImport] Email ${seqno} has ${pdfAttachments.length} PDF attachment(s)` - ); + let allAttachmentsSucceeded = true; + for (const attachment of pdfAttachments) { + try { + const result = await processEmailAttachment( + config.userId, + attachment, + parsed.subject || "No subject", + ); + allAttachmentsSucceeded = allAttachmentsSucceeded && result.success; - // Process each PDF attachment - for (const attachment of pdfAttachments) { - try { - const result = await processEmailAttachment( - config.userId, - attachment, - parsed.subject || "No subject" - ); - - // Mark this email as successfully processed - if (!processedEmails.includes(seqno)) { - processedEmails.push(seqno); + if (result.success) { + await sendImportNotification(config.userId, { + source: "email", + fileName: attachment.filename || "email-attachment.pdf", + totalInvoices: result.totalInvoices, + imported: result.imported, + duplicates: result.duplicates, + errors: result.errors, + }); + } + } catch (error) { + allAttachmentsSucceeded = false; + console.error( + `[EmailImport] Failed to process attachment from email ${seqno}:`, + error, + ); } - - // Send notification after successful processing - if (result.success) { - await sendImportNotification(config.userId, { - source: "email", - fileName: attachment.filename || "email-attachment.pdf", - totalInvoices: result.totalInvoices, - imported: result.imported, - duplicates: result.duplicates, - errors: result.errors, - }); - } - } catch (error) { - console.error( - `[EmailImport] Failed to process attachment from email ${seqno}:`, - error - ); } + + if (allAttachmentsSucceeded) { + const uid = await uidPromise; + if (!processedEmailUids.includes(uid)) processedEmailUids.push(uid); + } + } catch (error) { + console.error(`[EmailImport] Error parsing email ${seqno}:`, error); } - }); + })(); + + messageTasks.push(task); }); }); @@ -450,16 +501,19 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise { reject(err); }); - fetch.once("end", () => { + fetch.once("end", async () => { console.log(`[EmailImport] Finished fetching emails for user ${config.userId}`); - - // Mark successfully processed emails as seen - if (processedEmails.length > 0) { - imap.addFlags(processedEmails, ["\\Seen"], (err) => { + + // Le flux IMAP peut se terminer avant les traitements IA asynchrones. + // On attend explicitement chaque message avant de le marquer comme lu. + await Promise.allSettled(messageTasks); + + if (processedEmailUids.length > 0) { + imap.addFlags(processedEmailUids, ["\\Seen"], (err) => { if (err) { console.error("[EmailImport] Error marking emails as seen:", err); } else { - console.log(`[EmailImport] Marked ${processedEmails.length} emails as seen`); + console.log(`[EmailImport] Marked ${processedEmailUids.length} emails as seen`); } imap.end(); resolve(); @@ -598,13 +652,13 @@ export async function startEmailImportService(userId: number): Promise ); // Run immediately on start - checkEmailsForPDFs(config).catch((error) => { + runEmailCheckExclusive(config).catch((error) => { console.error(`[EmailImport] Error checking emails for user ${userId}:`, error); }); // Set up interval for periodic checks const interval = setInterval(() => { - checkEmailsForPDFs(config).catch((error) => { + runEmailCheckExclusive(config).catch((error) => { console.error(`[EmailImport] Error checking emails for user ${userId}:`, error); }); }, frequencyMs); @@ -670,7 +724,7 @@ export async function triggerEmailCheck(userId: number): Promise<{ success: bool }; console.log(`[EmailImport] Manual check triggered for user ${userId}`); - await checkEmailsForPDFs(config); + await runEmailCheckExclusive(config); return { success: true, message: "Vérification terminée avec succès" }; } catch (error: any) { diff --git a/server/fileFingerprint.test.ts b/server/fileFingerprint.test.ts new file mode 100644 index 0000000..f8d1cb8 --- /dev/null +++ b/server/fileFingerprint.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { calculateFileSha256 } from "./fileFingerprint"; + +describe("calculateFileSha256", () => { + it("retourne la même empreinte pour un contenu identique", () => { + const content = Buffer.from("facture-pdf"); + expect(calculateFileSha256(content)).toBe(calculateFileSha256(Buffer.from(content))); + }); + + it("distingue deux contenus différents", () => { + expect(calculateFileSha256(Buffer.from("facture-a"))).not.toBe( + calculateFileSha256(Buffer.from("facture-b")), + ); + }); + + it("produit une empreinte SHA-256 hexadécimale", () => { + expect(calculateFileSha256(Buffer.from("facture"))).toMatch(/^[a-f0-9]{64}$/); + }); +}); diff --git a/server/fileFingerprint.ts b/server/fileFingerprint.ts new file mode 100644 index 0000000..7172828 --- /dev/null +++ b/server/fileFingerprint.ts @@ -0,0 +1,12 @@ +import { createHash } from "node:crypto"; + +/** + * Calcule une empreinte déterministe sur les octets du document original. + * + * L'empreinte est calculée avant tout stockage ou traitement IA : deux imports + * du même PDF sont donc reconnus même si le nom du fichier ou l'utilisateur + * diffèrent. + */ +export function calculateFileSha256(buffer: Buffer): string { + return createHash("sha256").update(buffer).digest("hex"); +} diff --git a/server/folderImportService.ts b/server/folderImportService.ts index 8dc30aa..47b6ae7 100644 --- a/server/folderImportService.ts +++ b/server/folderImportService.ts @@ -4,6 +4,7 @@ import path from "path"; import { getImportSettingsByUser, createSourceFile, + getSourceFileByContentHash, updateSourceFile, getUserSettings, findDuplicateInvoice, @@ -11,7 +12,8 @@ import { createImportLog, } from "./db"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; -import { localStoragePut, generateStorageKey } from "./localStorage"; +import { localStorageDelete, localStoragePut, generateStorageKey } from "./localStorage"; +import { calculateFileSha256 } from "./fileFingerprint"; import { sendImportNotification } from "./notificationService"; interface FolderImportConfig { @@ -41,6 +43,13 @@ async function processFolderFile( // Read the file const fileBuffer = await fs.readFile(filePath); console.log(`[FolderImport] File size: ${fileBuffer.length} bytes`); + + const contentHash = calculateFileSha256(fileBuffer); + const existingSource = await getSourceFileByContentHash(contentHash); + if (existingSource) { + console.log(`[FolderImport] PDF déjà importé, fichier ignoré: ${fileName}`); + return { success: true, imported: 0, duplicates: 1, errors: 0 }; + } // Store source file const sourceFileKey = generateStorageKey(userId, fileName); @@ -57,13 +66,23 @@ async function processFolderFile( } // Create source file record - const sourceFile = await createSourceFile({ - userId, - fileName, - fileKey: sourceFileKey, - fileUrl: sourceFileUrl, - processingStatus: "processing", - }); + let sourceFile; + try { + sourceFile = await createSourceFile({ + userId, + fileName, + fileKey: sourceFileKey, + fileUrl: sourceFileUrl, + contentHash, + processingStatus: "processing", + }); + } catch (error: any) { + if (error?.code === "ER_DUP_ENTRY" || error?.errno === 1062) { + await localStorageDelete(sourceFileKey).catch(() => undefined); + return { success: true, imported: 0, duplicates: 1, errors: 0 }; + } + throw error; + } console.log(`[FolderImport] Source file record created with ID: ${sourceFile.id}`); diff --git a/server/routers.ts b/server/routers.ts index a8d34a8..86d44ff 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -24,6 +24,7 @@ import { searchInvoices, getInvoiceStats, createSourceFile, + getSourceFileByContentHash, getSourceFileById, updateSourceFile, getUserSettings, @@ -87,6 +88,7 @@ import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured } from " import fsSync from "fs"; import pathSync from "path"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; +import { calculateFileSha256 } from "./fileFingerprint"; import { localStoragePut, generateStorageKey } from "./localStorage"; import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport"; import { drawBapCartouche } from "./bapCartouche"; @@ -181,6 +183,17 @@ export const appRouter = router({ // Decode base64 file data const fileBuffer = Buffer.from(input.fileData, "base64"); console.log(`[Upload] Received file: ${input.fileName}, size: ${fileBuffer.length} bytes`); + + // Le contrôle sur les octets du PDF intervient avant le stockage et l'appel IA. + // Il reste fiable même si le nom du fichier ou le compte utilisateur diffère. + const contentHash = calculateFileSha256(fileBuffer); + const existingSource = await getSourceFileByContentHash(contentHash); + if (existingSource) { + throw new TRPCError({ + code: "CONFLICT", + message: `Ce PDF a déjà été importé (${existingSource.fileName}).`, + }); + } // Store source file const sourceFileKey = generateStorageKey(userId, input.fileName); @@ -202,6 +215,7 @@ export const appRouter = router({ fileName: input.fileName, fileKey: sourceFileKey, fileUrl: sourceFileUrl, + contentHash, processingStatus: "processing", }); diff --git a/todo.md b/todo.md index 37593e2..3d12780 100644 --- a/todo.md +++ b/todo.md @@ -702,3 +702,18 @@ - [x] Documenter les modules métier, les invariants et les décisions techniques critiques - [x] Ajouter des tests de non-régression ciblés et vérifier build, types et tests - [x] Normaliser les valeurs OAuth de loginMethod avant écriture en base + +## Incident production — erreur HTTP 404 +- [x] Reproduire la 404 et contrôler le domaine, Traefik et les conteneurs +- [x] Identifier et corriger la cause racine sans modifier les données +- [x] Vérifier le retour HTTP 200 et la santé des conteneurs + +## Audit production — 674 factures affichées +- [x] Compter les factures par utilisateur, source et statut +- [x] Identifier les groupes de doublons selon plusieurs clés métier +- [x] Vérifier les références de stockage et les effets de la fusion précédente +- [x] Préparer une correction réversible sans suppression immédiate +- [x] Sauvegarder la base et le volume puis suspendre les imports email +- [x] Bloquer les réimports par empreinte PDF et fiabiliser le traitement IMAP +- [ ] Déployer le correctif anti-réimport et migrer la base de production +- [ ] Appliquer la correction confirmée et vérifier le comptage final