Add invoice verification system using NocoDB for automated data checks

Integrates NocoDB to verify invoice references and supplier matches, and allows searching by BL number to auto-populate delivery details, enhancing reconciliation accuracy and efficiency.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: c52a701b-4841-4d00-b8cf-5a41c12abdf3
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/c52a701b-4841-4d00-b8cf-5a41c12abdf3/3uCfEKb
This commit is contained in:
michaelschal committed 2025-08-13 09:11:48 +00:00
1 parent d418fda3c9
commit bc85434858
5 files changed
+528 -25

No files matched your search

+74
View File
@@ -37,6 +37,7 @@ import {
} from "@shared/schema";
import { hasPermission } from "@shared/permissions";
import { z } from "zod";
import { nocodbVerificationService } from "./services/nocodbVerification";
export async function registerRoutes(app: Express): Promise<Server> {
// Health check endpoint for Docker
@@ -889,6 +890,79 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Routes de vérification NocoDB pour les factures
app.post('/api/deliveries/verify-invoice', isAuthenticated, async (req: any, res) => {
try {
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
const { groupId, invoiceReference, supplierName } = req.body;
if (!groupId || !invoiceReference || !supplierName) {
return res.status(400).json({
message: "groupId, invoiceReference et supplierName sont requis"
});
}
// Vérifier les permissions d'accès au groupe
if (user.role !== 'admin') {
const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || [];
if (!userGroupIds.includes(parseInt(groupId))) {
return res.status(403).json({ message: "Access denied to this group" });
}
}
const result = await nocodbVerificationService.verifyInvoiceReference(
parseInt(groupId),
invoiceReference,
supplierName
);
res.json(result);
} catch (error) {
console.error("Error verifying invoice:", error);
res.status(500).json({ message: "Failed to verify invoice" });
}
});
app.post('/api/deliveries/search-by-bl', isAuthenticated, async (req: any, res) => {
try {
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
const { groupId, blNumber, supplierName } = req.body;
if (!groupId || !blNumber || !supplierName) {
return res.status(400).json({
message: "groupId, blNumber et supplierName sont requis"
});
}
// Vérifier les permissions d'accès au groupe
if (user.role !== 'admin') {
const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || [];
if (!userGroupIds.includes(parseInt(groupId))) {
return res.status(403).json({ message: "Access denied to this group" });
}
}
const result = await nocodbVerificationService.searchByBLNumber(
parseInt(groupId),
blNumber,
supplierName
);
res.json(result);
} catch (error) {
console.error("Error searching by BL:", error);
res.status(500).json({ message: "Failed to search by BL number" });
}
});
// Statistics routes
app.get('/api/stats/monthly', isAuthenticated, async (req: any, res) => {
try {
+212
View File
@@ -0,0 +1,212 @@
import { storage } from '../storage';
export interface InvoiceVerificationResult {
found: boolean;
invoiceReference?: string;
invoiceAmount?: string;
supplierMatch: boolean;
error?: string;
}
export interface BLSearchResult {
found: boolean;
invoiceReference?: string;
invoiceAmount?: string;
error?: string;
}
export class NocoDBVerificationService {
/**
* Vérifie l'existence d'une référence de facture dans NocoDB pour un magasin donné
*/
async verifyInvoiceReference(
groupId: number,
invoiceReference: string,
supplierName: string
): Promise<InvoiceVerificationResult> {
try {
console.log(`🔍 Vérification facture ${invoiceReference} pour fournisseur ${supplierName} (magasin ${groupId})`);
// Récupérer la configuration du magasin
const group = await storage.getGroup(groupId);
if (!group?.nocodbConfigId || !group.nocodbTableName) {
console.log(`❌ Pas de configuration NocoDB pour le magasin ${groupId}`);
return {
found: false,
supplierMatch: false,
error: 'Configuration NocoDB manquante pour ce magasin'
};
}
// Récupérer la configuration NocoDB globale
const nocodbConfig = await storage.getNocodbConfig(group.nocodbConfigId);
if (!nocodbConfig) {
return {
found: false,
supplierMatch: false,
error: 'Configuration NocoDB introuvable'
};
}
// Construire l'URL de l'API NocoDB
const apiUrl = `${nocodbConfig.baseUrl}/api/v1/db/data/v1/${nocodbConfig.projectId}/${group.nocodbTableName}`;
// Construire les paramètres de recherche
const searchParams = new URLSearchParams();
if (group.invoiceColumnName) {
searchParams.append('where', `(${group.invoiceColumnName},eq,${invoiceReference})`);
}
const finalUrl = `${apiUrl}?${searchParams.toString()}`;
console.log(`🌐 Requête NocoDB: ${finalUrl}`);
// Faire la requête à NocoDB
const response = await fetch(finalUrl, {
headers: {
'xc-token': nocodbConfig.apiToken,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.error(`❌ Erreur NocoDB (${response.status}):`, await response.text());
return {
found: false,
supplierMatch: false,
error: `Erreur NocoDB: ${response.status}`
};
}
const data = await response.json();
console.log(`📊 Réponse NocoDB:`, { found: data.list?.length > 0, count: data.list?.length });
if (data.list && data.list.length > 0) {
const record = data.list[0];
// Vérifier si le fournisseur correspond
let supplierMatch = true;
if (group.nocodbSupplierColumnName) {
const nocodbSupplier = record[group.nocodbSupplierColumnName];
supplierMatch = nocodbSupplier === supplierName;
console.log(`🏷️ Vérification fournisseur:`, {
expected: supplierName,
found: nocodbSupplier,
match: supplierMatch
});
}
return {
found: true,
invoiceReference,
invoiceAmount: group.nocodbAmountColumnName ? record[group.nocodbAmountColumnName]?.toString() : undefined,
supplierMatch
};
}
return {
found: false,
supplierMatch: false
};
} catch (error) {
console.error('❌ Erreur lors de la vérification facture:', error);
return {
found: false,
supplierMatch: false,
error: `Erreur: ${error instanceof Error ? error.message : 'Erreur inconnue'}`
};
}
}
/**
* Recherche une facture par numéro de BL et fournisseur
*/
async searchByBLNumber(
groupId: number,
blNumber: string,
supplierName: string
): Promise<BLSearchResult> {
try {
console.log(`🔍 Recherche par BL ${blNumber} pour fournisseur ${supplierName} (magasin ${groupId})`);
// Récupérer la configuration du magasin
const group = await storage.getGroup(groupId);
if (!group?.nocodbConfigId || !group.nocodbTableName || !group.nocodbBlColumnName) {
console.log(`❌ Configuration BL manquante pour le magasin ${groupId}`);
return {
found: false,
error: 'Configuration NocoDB pour recherche BL manquante'
};
}
// Récupérer la configuration NocoDB globale
const nocodbConfig = await storage.getNocodbConfig(group.nocodbConfigId);
if (!nocodbConfig) {
return {
found: false,
error: 'Configuration NocoDB introuvable'
};
}
// Construire l'URL de l'API NocoDB pour recherche par BL
const apiUrl = `${nocodbConfig.baseUrl}/api/v1/db/data/v1/${nocodbConfig.projectId}/${group.nocodbTableName}`;
// Construire les paramètres de recherche par BL et éventuellement fournisseur
const searchParams = new URLSearchParams();
let whereClause = `(${group.nocodbBlColumnName},eq,${blNumber})`;
// Ajouter la condition fournisseur si configurée
if (group.nocodbSupplierColumnName) {
whereClause += `~and(${group.nocodbSupplierColumnName},eq,${supplierName})`;
}
searchParams.append('where', whereClause);
const finalUrl = `${apiUrl}?${searchParams.toString()}`;
console.log(`🌐 Requête NocoDB recherche BL: ${finalUrl}`);
// Faire la requête à NocoDB
const response = await fetch(finalUrl, {
headers: {
'xc-token': nocodbConfig.apiToken,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.error(`❌ Erreur NocoDB recherche BL (${response.status}):`, await response.text());
return {
found: false,
error: `Erreur NocoDB: ${response.status}`
};
}
const data = await response.json();
console.log(`📊 Réponse NocoDB recherche BL:`, { found: data.list?.length > 0, count: data.list?.length });
if (data.list && data.list.length > 0) {
const record = data.list[0];
return {
found: true,
invoiceReference: group.invoiceColumnName ? record[group.invoiceColumnName] : undefined,
invoiceAmount: group.nocodbAmountColumnName ? record[group.nocodbAmountColumnName]?.toString() : undefined
};
}
return {
found: false
};
} catch (error) {
console.error('❌ Erreur lors de la recherche par BL:', error);
return {
found: false,
error: `Erreur: ${error instanceof Error ? error.message : 'Erreur inconnue'}`
};
}
}
}
export const nocodbVerificationService = new NocoDBVerificationService();
+53 -6
View File
@@ -2053,11 +2053,43 @@ class MemStorage implements IStorage {
this.users.set('admin_dev', adminUser);
this.users.set('admin', adminUser); // Also set by username for easy lookup
// Add test groups
// Add test groups with NocoDB configuration
const testGroups = [
{ id: 1, name: 'Magasin A', color: '#FF5733', webhookUrl: 'https://webhook.site/test-facture-magasin-a', createdAt: new Date(), updatedAt: new Date() },
{ id: 2, name: 'Magasin B', color: '#33FF57', webhookUrl: 'https://webhook.site/test-facture-magasin-b', createdAt: new Date(), updatedAt: new Date() },
{ id: 3, name: 'Magasin C', color: '#3357FF', createdAt: new Date(), updatedAt: new Date() }
{
id: 1,
name: 'Magasin A',
color: '#FF5733',
webhookUrl: 'https://webhook.site/test-facture-magasin-a',
nocodbConfigId: 1,
nocodbTableName: 'factures_magasin_a',
invoiceColumnName: 'reference_facture',
nocodbBlColumnName: 'numero_bl',
nocodbAmountColumnName: 'montant',
nocodbSupplierColumnName: 'fournisseur',
createdAt: new Date(),
updatedAt: new Date()
},
{
id: 2,
name: 'Magasin B',
color: '#33FF57',
webhookUrl: 'https://webhook.site/test-facture-magasin-b',
nocodbConfigId: 1,
nocodbTableName: 'factures_magasin_b',
invoiceColumnName: 'ref_facture',
nocodbBlColumnName: 'bl_number',
nocodbAmountColumnName: 'amount',
nocodbSupplierColumnName: 'supplier_name',
createdAt: new Date(),
updatedAt: new Date()
},
{
id: 3,
name: 'Magasin C',
color: '#3357FF',
createdAt: new Date(),
updatedAt: new Date()
}
];
testGroups.forEach(group => this.groups.set(group.id, group as Group));
@@ -2221,7 +2253,9 @@ class MemStorage implements IStorage {
async getGroups(): Promise<Group[]> {
return Array.from(this.groups.values());
}
async getGroup(id: number): Promise<Group | undefined> { return undefined; }
async getGroup(id: number): Promise<Group | undefined> {
return this.groups.get(id);
}
async createGroup(group: InsertGroup): Promise<Group> { return {} as Group; }
async updateGroup(id: number, group: Partial<InsertGroup>): Promise<Group> { return {} as Group; }
async deleteGroup(id: number): Promise<void> {}
@@ -2449,7 +2483,20 @@ class MemStorage implements IStorage {
async removeUserFromGroup(): Promise<void> {}
async getMonthlyStats(): Promise<any> { return {}; }
async getNocodbConfigs(): Promise<any[]> { return []; }
async getNocodbConfig(): Promise<any> { return null; }
async getNocodbConfig(id: number): Promise<any> {
// Retourner une configuration de test pour le développement
if (id === 1) {
return {
id: 1,
name: 'Config Test NocoDB',
baseUrl: 'https://nocodb-test.example.com',
projectId: 'test-project',
apiToken: 'test-token-123',
isActive: true
};
}
return null;
}
async createNocodbConfig(): Promise<any> { return {}; }
async updateNocodbConfig(): Promise<any> { return {}; }
async deleteNocodbConfig(): Promise<void> {}