From 711ce6b83a71d592ad604ecac2460c58ea376578 Mon Sep 17 00:00:00 2001 From: Manus Date: Thu, 30 Jul 2026 13:53:07 +0000 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20du=20syst=C3=A8me=20de=20?= =?UTF-8?q?connecteurs=20web=20:=20table=20webImportSources,=20CRUD=20tRPC?= =?UTF-8?q?,=20page=20WebImportSources.tsx,=20endpoint=20/api/web-import/p?= =?UTF-8?q?ush-invoice,=20script=20cron=20SFR=20(scripts/web-import/sfr-co?= =?UTF-8?q?nnector.mjs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 2 + client/src/components/DashboardLayout.tsx | 3 +- client/src/pages/WebImportSources.tsx | 404 ++++ drizzle/0036_broken_rattler.sql | 18 + drizzle/meta/0036_snapshot.json | 2357 +++++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 36 + scripts/web-import/README.md | 70 + scripts/web-import/sfr-connector.mjs | 216 ++ server/_core/index.ts | 50 + server/db.ts | 76 +- server/routers.ts | 69 + todo.md | 8 + 13 files changed, 3314 insertions(+), 2 deletions(-) create mode 100644 client/src/pages/WebImportSources.tsx create mode 100644 drizzle/0036_broken_rattler.sql create mode 100644 drizzle/meta/0036_snapshot.json create mode 100644 scripts/web-import/README.md create mode 100644 scripts/web-import/sfr-connector.mjs diff --git a/client/src/App.tsx b/client/src/App.tsx index 3c0e26a..c2374c1 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -21,6 +21,7 @@ import BapHistory from "./pages/BapHistory"; import ImportReport from "./pages/ImportReport"; import LearningSettings from "./pages/LearningSettings"; import VentilationFreePro from "./pages/VentilationFreePro"; +import WebImportSources from "./pages/WebImportSources"; function Router() { return ( @@ -42,6 +43,7 @@ function Router() { + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 43d376c..f0af045 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -25,7 +25,7 @@ import { import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap, ChevronDown, Receipt, Cog, ClipboardList, CheckSquare, Brain, BarChart2, BarChart3 } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap, ChevronDown, Receipt, Cog, ClipboardList, CheckSquare, Brain, BarChart2, BarChart3, Globe } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; @@ -75,6 +75,7 @@ const menuStructure: MenuItem[] = [ { icon: List, label: "Administration des listes", path: "/lists-admin" }, { icon: Zap, label: "Automatismes", path: "/automation-rules" }, { icon: Brain, label: "Apprentissages IA", path: "/learning-settings" }, + { icon: Globe, label: "Connecteurs web", path: "/web-import-sources" }, { icon: Users, label: "Utilisateurs", path: "/users", adminOnly: true }, ], }, diff --git a/client/src/pages/WebImportSources.tsx b/client/src/pages/WebImportSources.tsx new file mode 100644 index 0000000..9bf7bb4 --- /dev/null +++ b/client/src/pages/WebImportSources.tsx @@ -0,0 +1,404 @@ +import { useState } from "react"; +import { trpc } from "@/lib/trpc"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Globe, Plus, Pencil, Trash2, Key, CheckCircle, XCircle, Clock, RefreshCw } from "lucide-react"; +import { toast } from "sonner"; + +const CONNECTOR_TYPES = [ + { value: "sfr", label: "SFR Pro", url: "https://www.sfr.fr/mon-espace-client/" }, + { value: "orange", label: "Orange Pro", url: "https://espaceclient.orange.fr/" }, + { value: "bouygues", label: "Bouygues Telecom", url: "https://www.bouyguestelecom.fr/mon-compte/" }, + { value: "free", label: "Free Pro", url: "https://pro.free.fr/" }, + { value: "starlink", label: "Starlink", url: "https://www.starlink.com/account/" }, + { value: "custom", label: "Autre (personnalisé)", url: "" }, +]; + +const FREQUENCY_LABELS: Record = { + manual: "Manuel", + daily: "Quotidien", + weekly: "Hebdomadaire", + monthly: "Mensuel", +}; + +type Source = { + id: number; + name: string; + connectorType: string; + portalUrl: string; + loginEmail: string; + loginPassword: string; + frequency: "manual" | "daily" | "weekly" | "monthly"; + autoEnabled: number; + lastSuccessAt: Date | null; + lastStatus: string | null; + lastImportCount: number | null; + apiToken: string; + createdAt: Date; + updatedAt: Date; +}; + +type FormData = { + name: string; + connectorType: string; + portalUrl: string; + loginEmail: string; + loginPassword: string; + frequency: "manual" | "daily" | "weekly" | "monthly"; + autoEnabled: number; +}; + +const emptyForm: FormData = { + name: "", + connectorType: "sfr", + portalUrl: "https://www.sfr.fr/mon-espace-client/", + loginEmail: "", + loginPassword: "", + frequency: "monthly", + autoEnabled: 0, +}; + +export default function WebImportSources() { + const [showForm, setShowForm] = useState(false); + const [editSource, setEditSource] = useState(null); + const [deleteId, setDeleteId] = useState(null); + const [showToken, setShowToken] = useState(null); + const [form, setForm] = useState(emptyForm); + + const { data: sources = [], refetch } = trpc.webImportSources.list.useQuery(); + + const { data: tokenData } = trpc.webImportSources.getToken.useQuery( + { id: showToken! }, + { enabled: showToken !== null } + ); + + const createMutation = trpc.webImportSources.create.useMutation({ + onSuccess: () => { + toast.success("Source créée", { description: "Le connecteur web a été ajouté." }); + setShowForm(false); + setForm(emptyForm); + refetch(); + }, + onError: (e) => toast.error("Erreur", { description: e.message }), + }); + + const updateMutation = trpc.webImportSources.update.useMutation({ + onSuccess: () => { + toast.success("Source mise à jour"); + setEditSource(null); + setForm(emptyForm); + refetch(); + }, + onError: (e) => toast.error("Erreur", { description: e.message }), + }); + + const deleteMutation = trpc.webImportSources.delete.useMutation({ + onSuccess: () => { + toast.success("Source supprimée"); + setDeleteId(null); + refetch(); + }, + onError: (e) => toast.error("Erreur", { description: e.message }), + }); + + function openCreate() { + setForm(emptyForm); + setEditSource(null); + setShowForm(true); + } + + function openEdit(source: Source) { + setForm({ + name: source.name, + connectorType: source.connectorType, + portalUrl: source.portalUrl, + loginEmail: source.loginEmail, + loginPassword: "", // Ne pas pré-remplir le mot de passe + frequency: source.frequency, + autoEnabled: source.autoEnabled, + }); + setEditSource(source); + setShowForm(true); + } + + function handleConnectorTypeChange(value: string) { + const connector = CONNECTOR_TYPES.find(c => c.value === value); + setForm(f => ({ + ...f, + connectorType: value, + name: f.name || connector?.label || "", + portalUrl: connector?.url || f.portalUrl, + })); + } + + function handleSubmit() { + if (!form.name || !form.loginEmail || (!editSource && !form.loginPassword)) { + toast.error("Champs requis", { description: "Nom, identifiant et mot de passe sont obligatoires." }); + return; + } + if (editSource) { + const updateData: any = { id: editSource.id, ...form }; + if (!form.loginPassword) delete updateData.loginPassword; // Ne pas écraser si vide + updateMutation.mutate(updateData); + } else { + createMutation.mutate(form); + } + } + + return ( + +
+ {/* En-tête */} +
+
+
+ +
+
+

Connecteurs web

+

Import automatique de factures depuis des espaces clients

+
+
+ +
+ + {/* Info technique */} +
+ Fonctionnement : Un script cron tourne sur le serveur LWS et se connecte automatiquement aux espaces clients configurés pour télécharger les nouvelles factures. Le token API affiché ci-dessous est utilisé par ce script pour s'authentifier auprès de l'application. +
+ + {/* Liste des sources */} + {sources.length === 0 ? ( +
+ +

Aucun connecteur configuré

+

Ajoutez un connecteur pour importer automatiquement des factures depuis un espace client web.

+
+ ) : ( +
+ {(sources as Source[]).map((source) => ( +
+
+
+
+ +
+
+
+ {source.name} + {source.connectorType} + + {source.autoEnabled ? "Auto activé" : "Manuel"} + + {FREQUENCY_LABELS[source.frequency]} +
+

{source.portalUrl}

+

+ Identifiant : {source.loginEmail} +

+
+ {source.lastSuccessAt ? ( + + + Dernier import : {new Date(source.lastSuccessAt).toLocaleDateString("fr-FR")} + {source.lastImportCount !== null && ` (${source.lastImportCount} facture(s))`} + + ) : ( + + + Jamais importé + + )} + {source.lastStatus && !source.lastSuccessAt && ( + + + {source.lastStatus} + + )} +
+
+
+
+ + + +
+
+ + {/* Token API affiché inline */} + {showToken === source.id && tokenData && ( +
+

Token API (à configurer dans le script cron) :

+ {tokenData.apiToken} +
+ )} +
+ ))} +
+ )} + + {/* Dialog création / édition */} + { if (!open) { setShowForm(false); setEditSource(null); } }}> + + + {editSource ? "Modifier le connecteur" : "Nouveau connecteur web"} + +
+
+ + +
+
+ + setForm(f => ({ ...f, name: e.target.value }))} + placeholder="Ex : SFR Pro - Itinova" + /> +
+
+ + setForm(f => ({ ...f, portalUrl: e.target.value }))} + placeholder="https://..." + /> +
+
+ + setForm(f => ({ ...f, loginEmail: e.target.value }))} + placeholder="votre@email.com" + /> +
+
+ + setForm(f => ({ ...f, loginPassword: e.target.value }))} + placeholder={editSource ? "••••••••" : "Mot de passe"} + /> +
+
+
+ + +
+
+
+ setForm(f => ({ ...f, autoEnabled: v ? 1 : 0 }))} + /> + +
+
+
+
+ + + + +
+
+ + {/* Confirmation suppression */} + { if (!open) setDeleteId(null); }}> + + + Supprimer ce connecteur ? + + Cette action est irréversible. Le connecteur et son token API seront supprimés. + + + + Annuler + deleteId !== null && deleteMutation.mutate({ id: deleteId })} + > + Supprimer + + + + +
+
+ ); +} diff --git a/drizzle/0036_broken_rattler.sql b/drizzle/0036_broken_rattler.sql new file mode 100644 index 0000000..a98f2aa --- /dev/null +++ b/drizzle/0036_broken_rattler.sql @@ -0,0 +1,18 @@ +CREATE TABLE `webImportSources` ( + `id` int AUTO_INCREMENT NOT NULL, + `userId` int NOT NULL, + `name` varchar(100) NOT NULL, + `connectorType` varchar(50) NOT NULL, + `portalUrl` varchar(500) NOT NULL, + `loginEmail` varchar(320) NOT NULL, + `loginPassword` text NOT NULL, + `frequency` enum('manual','daily','weekly','monthly') NOT NULL DEFAULT 'monthly', + `autoEnabled` int NOT NULL DEFAULT 0, + `lastSuccessAt` timestamp, + `lastStatus` text, + `lastImportCount` int DEFAULT 0, + `apiToken` varchar(128) NOT NULL, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `webImportSources_id` PRIMARY KEY(`id`) +); diff --git a/drizzle/meta/0036_snapshot.json b/drizzle/meta/0036_snapshot.json new file mode 100644 index 0000000..6e4ddab --- /dev/null +++ b/drizzle/meta/0036_snapshot.json @@ -0,0 +1,2357 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "728ba895-5eda-452b-8c20-b42c4abade47", + "prevId": "3e95ab88-f9a1-4294-9e86-c02bfa5548fc", + "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 + }, + "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": {}, + "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 e3a45e1..710d19d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -253,6 +253,13 @@ "when": 1785404752594, "tag": "0035_eminent_dreadnoughts", "breakpoints": true + }, + { + "idx": 36, + "version": "5", + "when": 1785419093588, + "tag": "0036_broken_rattler", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 07dfe60..6425776 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -524,3 +524,39 @@ export const deletedInvoices = mysqlTable("deletedInvoices", { }); 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; diff --git a/scripts/web-import/README.md b/scripts/web-import/README.md new file mode 100644 index 0000000..2cbb71a --- /dev/null +++ b/scripts/web-import/README.md @@ -0,0 +1,70 @@ +# Connecteurs Web - Scripts d'import automatique + +## Prérequis sur le serveur LWS + +```bash +cd /opt/web-import +npm install playwright +npx playwright install chromium --with-deps +``` + +## Configuration + +1. Depuis l'application : **Configuration > Connecteurs web** → créer une source SFR → copier le token API +2. Créer un fichier de configuration : + +```bash +cp config.example.json config.json +# Éditer config.json avec vos valeurs +``` + +Contenu de `config.json` : +```json +{ + "sfrLogin": "votre-login@sfr.fr", + "sfrPassword": "votre-mot-de-passe", + "appUrl": "https://demat-facturation.santinova-soft.org", + "apiToken": "votre-token-api-copié-depuis-lappli", + "downloadDir": "/tmp/sfr-invoices", + "processedFile": "/tmp/sfr-processed.json" +} +``` + +Ou utiliser des variables d'environnement : +```bash +export SFR_LOGIN=votre-login@sfr.fr +export SFR_PASSWORD=votre-mot-de-passe +export APP_URL=https://demat-facturation.santinova-soft.org +export API_TOKEN=votre-token-api +``` + +## Exécution manuelle + +```bash +node sfr-connector.mjs +``` + +## Planification (cron) + +Ajouter dans le crontab (`crontab -e`) : + +``` +# Import SFR le 5 de chaque mois à 8h00 +0 8 5 * * /usr/bin/node /opt/web-import/sfr-connector.mjs >> /var/log/sfr-import.log 2>&1 +``` + +## Ajouter un nouveau connecteur + +Dupliquer `sfr-connector.mjs` et adapter : +1. L'URL du portail (`portalUrl`) +2. Les sélecteurs CSS pour le login et les liens de factures +3. Le nom du fichier de suivi (`processedFile`) + +## Fonctionnement + +1. Le script se connecte au site SFR avec les identifiants fournis +2. Il navigue vers la section factures +3. Il télécharge les PDFs non encore traités +4. Il les envoie à l'application via l'endpoint `/api/web-import/push-invoice` +5. L'application extrait les données avec l'IA et crée les factures +6. Le script marque les factures comme traitées pour éviter les doublons diff --git a/scripts/web-import/sfr-connector.mjs b/scripts/web-import/sfr-connector.mjs new file mode 100644 index 0000000..4bd9c3d --- /dev/null +++ b/scripts/web-import/sfr-connector.mjs @@ -0,0 +1,216 @@ +/** + * Connecteur SFR Pro - Script cron pour import automatique de factures + * + * Prérequis sur le serveur LWS : + * npm install playwright @playwright/test + * npx playwright install chromium + * + * Configuration : + * Copier .env.example en .env et remplir les variables + * + * Utilisation : + * node sfr-connector.mjs + * + * Cron (mensuel le 5 du mois à 8h) : + * 0 8 5 * * /usr/bin/node /opt/web-import/sfr-connector.mjs >> /var/log/sfr-import.log 2>&1 + */ + +import { chromium } from 'playwright'; +import fs from 'fs'; +import path from 'path'; +import https from 'https'; +import http from 'http'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// ============ CONFIGURATION ============ +// Ces variables peuvent être définies dans un fichier .env ou directement ici +const CONFIG = { + // URL de l'espace client SFR Pro + portalUrl: process.env.SFR_PORTAL_URL || 'https://www.sfr-business.fr/espace-client/', + // Identifiants SFR + login: process.env.SFR_LOGIN || '', + password: process.env.SFR_PASSWORD || '', + // URL de l'application de dématérialisation + appUrl: process.env.APP_URL || 'https://demat-facturation.santinova-soft.org', + // Token API de la source web (récupéré depuis l'interface Connecteurs web) + apiToken: process.env.API_TOKEN || '', + // Dossier temporaire pour les PDFs téléchargés + downloadDir: process.env.DOWNLOAD_DIR || '/tmp/sfr-invoices', + // Ne pas réimporter les factures déjà traitées (fichier de suivi) + processedFile: process.env.PROCESSED_FILE || '/tmp/sfr-processed.json', +}; + +// ============ HELPERS ============ +function log(msg) { + console.log(`[${new Date().toISOString()}] [SFR] ${msg}`); +} + +function loadProcessed() { + try { + if (fs.existsSync(CONFIG.processedFile)) { + return JSON.parse(fs.readFileSync(CONFIG.processedFile, 'utf8')); + } + } catch {} + return []; +} + +function saveProcessed(list) { + fs.writeFileSync(CONFIG.processedFile, JSON.stringify(list, null, 2)); +} + +async function pushInvoiceToApp(filePath, fileName) { + const fileBuffer = fs.readFileSync(filePath); + const fileBase64 = fileBuffer.toString('base64'); + + const body = JSON.stringify({ + apiToken: CONFIG.apiToken, + fileName, + fileBase64, + mimeType: 'application/pdf', + }); + + return new Promise((resolve, reject) => { + const url = new URL(`${CONFIG.appUrl}/api/web-import/push-invoice`); + const options = { + hostname: url.hostname, + port: url.port || (url.protocol === 'https:' ? 443 : 80), + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + }, + }; + const lib = url.protocol === 'https:' ? https : http; + const req = lib.request(options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + resolve({ status: res.statusCode, body: JSON.parse(data) }); + } catch { + resolve({ status: res.statusCode, body: data }); + } + }); + }); + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +// ============ CONNECTEUR SFR ============ +async function runSfrConnector() { + log('Démarrage du connecteur SFR Pro'); + + if (!CONFIG.login || !CONFIG.password || !CONFIG.apiToken) { + log('ERREUR : SFR_LOGIN, SFR_PASSWORD et API_TOKEN sont requis'); + process.exit(1); + } + + // Créer le dossier de téléchargement + if (!fs.existsSync(CONFIG.downloadDir)) { + fs.mkdirSync(CONFIG.downloadDir, { recursive: true }); + } + + const processed = loadProcessed(); + let newInvoices = 0; + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + acceptDownloads: true, + }); + const page = await context.newPage(); + + try { + // 1. Naviguer vers l'espace client SFR + log(`Navigation vers ${CONFIG.portalUrl}`); + await page.goto(CONFIG.portalUrl, { waitUntil: 'networkidle', timeout: 30000 }); + + // 2. Accepter les cookies si présent + try { + await page.click('[id*="accept"], [class*="accept-cookie"], #didomi-notice-agree-button', { timeout: 3000 }); + log('Cookies acceptés'); + } catch {} + + // 3. Remplir le formulaire de connexion + log('Connexion en cours...'); + await page.fill('input[type="email"], input[name="login"], input[id*="login"], input[id*="email"]', CONFIG.login); + await page.fill('input[type="password"], input[name="password"], input[id*="password"]', CONFIG.password); + await page.click('button[type="submit"], input[type="submit"], button:has-text("Connexion"), button:has-text("Se connecter")'); + + await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); + log('Connecté'); + + // 4. Naviguer vers la section factures + // Adapter selon la structure réelle du site SFR Pro + await page.goto(`${CONFIG.portalUrl}factures`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); + + // Chercher les liens de factures PDF + const invoiceLinks = await page.$$eval( + 'a[href*=".pdf"], a[href*="facture"], a[href*="invoice"], a[download]', + links => links.map(a => ({ + href: a.href, + text: a.textContent?.trim() || '', + download: a.getAttribute('download') || '', + })) + ); + + log(`${invoiceLinks.length} lien(s) de facture trouvé(s)`); + + // 5. Télécharger et envoyer chaque facture + for (const link of invoiceLinks) { + const invoiceId = link.href || link.text; + if (processed.includes(invoiceId)) { + log(`Déjà traité : ${link.text}`); + continue; + } + + try { + // Télécharger le PDF + const [download] = await Promise.all([ + context.waitForEvent('download', { timeout: 15000 }), + page.click(`a[href="${link.href}"]`).catch(() => page.goto(link.href)), + ]); + + const fileName = download?.suggestedFilename() || `sfr-facture-${Date.now()}.pdf`; + const filePath = path.join(CONFIG.downloadDir, fileName); + await download?.saveAs(filePath); + + log(`Téléchargé : ${fileName}`); + + // Envoyer à l'application + const result = await pushInvoiceToApp(filePath, fileName); + log(`Envoyé : ${fileName} → ${JSON.stringify(result.body)}`); + + // Marquer comme traité + processed.push(invoiceId); + saveProcessed(processed); + newInvoices++; + + // Nettoyer le fichier temporaire + fs.unlinkSync(filePath); + } catch (err) { + log(`ERREUR sur ${link.text} : ${err.message}`); + } + } + + } catch (err) { + log(`ERREUR FATALE : ${err.message}`); + await page.screenshot({ path: path.join(CONFIG.downloadDir, 'error-screenshot.png') }).catch(() => {}); + throw err; + } finally { + await browser.close(); + } + + log(`Terminé : ${newInvoices} nouvelle(s) facture(s) importée(s)`); + return newInvoices; +} + +// ============ POINT D'ENTRÉE ============ +runSfrConnector().catch(err => { + console.error(`[FATAL] ${err.message}`); + process.exit(1); +}); diff --git a/server/_core/index.ts b/server/_core/index.ts index f287c58..f40d858 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -224,6 +224,56 @@ async function startServer() { } }); + // ============= WEB IMPORT SOURCES - Endpoint pour script cron externe ============= + app.post("/api/web-import/push-invoice", async (req, res) => { + try { + const { apiToken, fileName, fileBase64, mimeType } = req.body; + if (!apiToken || !fileName || !fileBase64) { + res.status(400).json({ error: "apiToken, fileName et fileBase64 sont requis" }); + return; + } + const { getWebImportSourceByToken, getImportSettingsByUser, createInvoice, findDuplicateInvoice, isInvoiceBlacklisted, updateWebImportSourceStatus, createSourceFile } = await import('../db'); + const source = await getWebImportSourceByToken(apiToken); + if (!source) { + res.status(401).json({ error: "Token invalide" }); + return; + } + const pdfBuffer = Buffer.from(fileBase64, 'base64'); + const fileMime = mimeType || 'application/pdf'; + // Stocker le fichier source en DB + const sourceFile = await createSourceFile({ + userId: source.userId, + fileName, + fileKey: `web-import/${source.userId}/${Date.now()}-${fileName}`, + fileUrl: '', + }); + const importSettings = await getImportSettingsByUser(source.userId); + const aiSettings = { + aiProvider: importSettings?.aiProvider || 'manus', + mistralApiKey: importSettings?.mistralApiKey || undefined, + manusForgeApiUrl: importSettings?.manusForgeApiUrl || undefined, + manusForgeApiKey: importSettings?.manusForgeApiKey || undefined, + }; + const { extractInvoicesWithMistral } = await import('../invoiceExtractor'); + const extractResult = await extractInvoicesWithMistral(pdfBuffer, source.userId, sourceFile.id, 'mistral-large-latest', undefined, aiSettings); + let imported = 0; + let duplicates = 0; + for (const inv of extractResult.invoices || []) { + const blacklisted = await isInvoiceBlacklisted(inv.invoiceNumber || null, source.userId); + if (blacklisted) { duplicates++; continue; } + const dup = await findDuplicateInvoice(inv.invoiceNumber || null, String(inv.totalAmount ?? ''), source.userId); + if (dup) { duplicates++; continue; } + await createInvoice({ ...inv, userId: source.userId, sourceFileId: sourceFile.id } as any); + imported++; + } + await updateWebImportSourceStatus(source.id, 'success', imported, true); + res.json({ success: true, imported, duplicates, total: (extractResult.invoices || []).length }); + } catch (err: any) { + console.error('[WebImport] Erreur push-invoice:', err.message); + res.status(500).json({ error: err.message }); + } + }); + // tRPC API app.use( "/api/trpc", diff --git a/server/db.ts b/server/db.ts index c1d29ac..da76716 100644 --- a/server/db.ts +++ b/server/db.ts @@ -47,7 +47,10 @@ import { InvoiceLearning, deletedInvoices, InsertDeletedInvoice, - DeletedInvoice + DeletedInvoice, + webImportSources, + InsertWebImportSource, + WebImportSource } from "../drizzle/schema"; import { ENV } from './_core/env'; @@ -1147,3 +1150,74 @@ export async function updateFreeproLastRun( .set(update) .where(eq(freeproSettings.userId, userId)); } + +// ============= WEB IMPORT SOURCES ============= + +/** Génère un token API aléatoire de 64 caractères */ +function generateApiToken(): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let token = ''; + for (let i = 0; i < 64; i++) { + token += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return token; +} + +export async function getWebImportSourcesByUser(userId: number): Promise { + const db = await getDb(); + if (!db) return []; + return db.select().from(webImportSources).where(eq(webImportSources.userId, userId)).orderBy(desc(webImportSources.createdAt)); +} + +export async function getWebImportSourceById(id: number): Promise { + const db = await getDb(); + if (!db) return undefined; + const result = await db.select().from(webImportSources).where(eq(webImportSources.id, id)).limit(1); + return result[0]; +} + +export async function getWebImportSourceByToken(token: string): Promise { + const db = await getDb(); + if (!db) return undefined; + const result = await db.select().from(webImportSources).where(eq(webImportSources.apiToken, token)).limit(1); + return result[0]; +} + +export async function createWebImportSource(data: Omit): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const apiToken = generateApiToken(); + const result = await db.insert(webImportSources).values({ ...data, apiToken }); + const insertedId = Number(result[0].insertId); + const inserted = await db.select().from(webImportSources).where(eq(webImportSources.id, insertedId)).limit(1); + return inserted[0]!; +} + +export async function updateWebImportSource(id: number, data: Partial): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.update(webImportSources).set({ ...data, updatedAt: new Date() }).where(eq(webImportSources.id, id)); +} + +export async function deleteWebImportSource(id: number): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.delete(webImportSources).where(eq(webImportSources.id, id)); +} + +export async function updateWebImportSourceStatus( + id: number, + status: string, + importCount: number, + success: boolean +): Promise { + const db = await getDb(); + if (!db) return; + const update: Partial = { + lastStatus: status, + lastImportCount: importCount, + updatedAt: new Date(), + }; + if (success) update.lastSuccessAt = new Date(); + await db.update(webImportSources).set(update).where(eq(webImportSources.id, id)); +} diff --git a/server/routers.ts b/server/routers.ts index 9d71e0c..91d7070 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -77,6 +77,12 @@ import { deleteLearning, deleteAllLearnings, getBapPdfUrlsByInvoiceIds, + getWebImportSourcesByUser, + getWebImportSourceById, + createWebImportSource, + updateWebImportSource, + deleteWebImportSource, + updateWebImportSourceStatus, } from "./db"; import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; @@ -2609,5 +2615,68 @@ export const appRouter = router({ return { success: true, webUrl: result.webUrl, fileName }; }), }), + + // ============= WEB IMPORT SOURCES ============= + webImportSources: router({ + list: protectedProcedure.query(async ({ ctx }) => { + return getWebImportSourcesByUser(ctx.user.id); + }), + + create: protectedProcedure + .input(z.object({ + name: z.string().min(1).max(100), + connectorType: z.string().min(1).max(50), + portalUrl: z.string().url(), + loginEmail: z.string().min(1), + loginPassword: z.string().min(1), + frequency: z.enum(['manual', 'daily', 'weekly', 'monthly']).default('monthly'), + autoEnabled: z.number().min(0).max(1).default(0), + })) + .mutation(async ({ input, ctx }) => { + return createWebImportSource({ ...input, userId: ctx.user.id }); + }), + + update: protectedProcedure + .input(z.object({ + id: z.number(), + name: z.string().min(1).max(100).optional(), + connectorType: z.string().min(1).max(50).optional(), + portalUrl: z.string().url().optional(), + loginEmail: z.string().min(1).optional(), + loginPassword: z.string().optional(), + frequency: z.enum(['manual', 'daily', 'weekly', 'monthly']).optional(), + autoEnabled: z.number().min(0).max(1).optional(), + })) + .mutation(async ({ input, ctx }) => { + const source = await getWebImportSourceById(input.id); + if (!source || source.userId !== ctx.user.id) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Source introuvable' }); + } + const { id, ...data } = input; + await updateWebImportSource(id, data); + return { success: true }; + }), + + delete: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input, ctx }) => { + const source = await getWebImportSourceById(input.id); + if (!source || source.userId !== ctx.user.id) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Source introuvable' }); + } + await deleteWebImportSource(input.id); + return { success: true }; + }), + + getToken: protectedProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input, ctx }) => { + const source = await getWebImportSourceById(input.id); + if (!source || source.userId !== ctx.user.id) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Source introuvable' }); + } + return { apiToken: source.apiToken }; + }), + }), }); export type AppRouter = typeof appRouter; diff --git a/todo.md b/todo.md index bf9084e..376e1d9 100644 --- a/todo.md +++ b/todo.md @@ -684,3 +684,11 @@ - [ ] Modifier getServiceSignaturesByUser → retourner toutes les signatures de service (sans filtre userId) - [ ] Migrer les données en production : dédoublonner les listes fusionnées - [ ] Déployer en production + +## Connecteurs web (scraping login/mdp) +- [ ] Table webImportSources dans le schéma DB +- [ ] Procédures tRPC CRUD pour webImportSources +- [ ] Interface de gestion des sources web dans les paramètres +- [ ] Endpoint API sécurisé pour déclencher l'import et recevoir les PDFs +- [ ] Script cron externe Node.js + Playwright pour SFR +- [ ] Framework connecteur générique extensible