Files
LogiFlow/server/invoiceVerification.ts
Claude 4220131432 fix(types): bring real code to zero TypeScript errors, fix three latent bugs
Continues the cleanup from 164 errors down to 88, all of which now sit in
MemStorage — the in-memory dev mock — documented as known debt. Production
server code and the entire client are at zero errors.

Three genuine defects surfaced by the types and fixed:
- deleteAnnouncement returned void while routes check the result and answer
  404 "not found" on falsy: deleting an announcement succeeded in DB but the
  API reported failure on the fallback paths; it now returns a real boolean
- AnnouncementMemoryStorage declared getAnnouncement twice; the first
  implementation was dead at runtime (second definition wins) and is removed
- one route called storage.getDeliveryById(), a method that does not exist,
  crashing with a TypeError whenever hit; it now calls getDelivery()

Everything else is type-level only (annotations, null-to-undefined for
inline styles, honest signatures for markClientCalled's comment parameter
and the invoice verification result's invoiceAmountTTC field), verified
behavior-neutral: client build, production server bundle and the SMTP
end-to-end test all pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe
2026-08-14 16:42:24 +00:00

974 lines
34 KiB
TypeScript

import { storage } from "./storage.js";
/**
* Service de vérification des factures avec NocoDB
* Gère la vérification automatique des références de factures
*/
export class InvoiceVerificationService {
/**
* Générer une clé de cache unique
*/
private generateCacheKey(invoiceReference: string, groupId: number): string {
return `${groupId}_${invoiceReference.trim().toLowerCase()}`;
}
/**
* Vérifier dans le cache d'abord
*/
async checkCache(invoiceReference: string, groupId: number): Promise<any | null> {
try {
const cacheKey = this.generateCacheKey(invoiceReference, groupId);
console.log('🔍 [CACHE] Recherche cache pour:', { invoiceReference, groupId, cacheKey });
const cached = await storage.getInvoiceVerificationCache(cacheKey);
console.log('🔍 [CACHE] Résultat cache:', {
found: !!cached,
isReconciled: cached?.isReconciled,
expired: cached ? new Date() >= new Date(cached.expiresAt) : 'N/A',
expiresAt: cached?.expiresAt,
currentTime: new Date().toISOString()
});
// ✅ CACHE PERMANENT : Facture validée avec coche verte = JAMAIS re-vérifier
if (cached && cached.isReconciled && cached.exists) {
console.log('🛡️ [CACHE] PERMANENT - Facture validée avec coche verte, AUCUNE vérification:', {
invoiceReference,
groupId,
exists: cached.exists,
invoiceAmount: cached.invoiceAmount,
dueDate: cached.dueDate
});
return {
exists: cached.exists,
matchType: cached.matchType,
errorMessage: cached.errorMessage,
invoiceReference: cached.invoiceReference,
supplierName: cached.supplierName,
invoiceAmount: cached.invoiceAmount ? parseFloat(cached.invoiceAmount) : undefined,
invoiceAmountTTC: cached.invoiceAmountTTC ? parseFloat(cached.invoiceAmountTTC) : undefined,
dueDate: cached.dueDate ? new Date(cached.dueDate).toISOString() : null,
fromCache: true,
permanent: true
};
}
// Cache temporaire non expiré
if (cached && new Date() < new Date(cached.expiresAt)) {
console.log('✅ [CACHE] Cache temporaire hit pour:', { invoiceReference, groupId, exists: cached.exists });
return {
exists: cached.exists,
matchType: cached.matchType,
errorMessage: cached.errorMessage,
invoiceReference: cached.invoiceReference,
supplierName: cached.supplierName,
invoiceAmount: cached.invoiceAmount ? parseFloat(cached.invoiceAmount) : undefined,
invoiceAmountTTC: cached.invoiceAmountTTC ? parseFloat(cached.invoiceAmountTTC) : undefined,
dueDate: cached.dueDate ? new Date(cached.dueDate).toISOString() : null,
fromCache: true,
permanent: false
};
}
if (cached && new Date() >= new Date(cached.expiresAt)) {
console.log('⏰ [CACHE] Cache temporaire expiré pour:', { invoiceReference, groupId });
} else {
console.log('❌ [CACHE] Cache miss pour:', { invoiceReference, groupId });
}
return null;
} catch (error) {
console.error('❌ [CACHE] Erreur lecture cache:', error);
return null;
}
}
/**
* Sauvegarder dans le cache avec durée adaptative
*/
async saveToCache(invoiceReference: string, groupId: number, result: any, supplierName?: string, isReconciled: boolean = false): Promise<void> {
try {
const cacheKey = this.generateCacheKey(invoiceReference, groupId);
// Durée de cache adaptative selon les cas
const expiresAt = new Date();
let cacheDescription = '';
if (isReconciled) {
// Cache PERMANENT pour factures validées - expire dans 50 ans
expiresAt.setFullYear(expiresAt.getFullYear() + 50);
cacheDescription = 'PERMANENT (validé)';
} else if (result.exists) {
// Facture trouvée mais non validée - cache 6h pour permettre corrections
expiresAt.setHours(expiresAt.getHours() + 6);
cacheDescription = 'temporaire 6h (trouvé)';
} else {
// Facture non trouvée - cache 12h pour éviter spam
expiresAt.setHours(expiresAt.getHours() + 12);
cacheDescription = 'temporaire 12h (pas trouvé)';
}
console.log('💾 [CACHE] Tentative sauvegarde:', {
invoiceReference,
groupId,
cacheKey,
exists: result.exists,
isReconciled,
cacheType: cacheDescription,
expiresAt: expiresAt.toISOString()
});
const cacheData = {
cacheKey,
groupId,
invoiceReference,
supplierName: supplierName || null,
invoiceAmount: result.invoiceAmount || null,
invoiceAmountTTC: result.invoiceAmountTTC || null,
dueDate: result.dueDate ? new Date(result.dueDate) : null,
exists: result.exists,
matchType: result.matchType,
errorMessage: result.errorMessage || null,
cacheHit: false,
apiCallTime: null,
isReconciled, // Nouveau champ pour cache permanent
expiresAt
};
const savedCache = await storage.saveInvoiceVerificationCache(cacheData);
console.log('✅ [CACHE] Résultat sauvé en cache:', {
id: savedCache.id,
invoiceReference,
groupId,
exists: result.exists,
isReconciled,
cacheType: cacheDescription,
cacheKey
});
} catch (error) {
// Gérer spécifiquement les erreurs de contrainte unique (duplicate key)
if (error && typeof error === 'object' && 'code' in error && error.code === '23505') {
console.log('🔄 [CACHE] Cache déjà existant pour cette clé, ignoré:', { invoiceReference, groupId });
return;
}
console.error('❌ [CACHE] Erreur sauvegarde cache:', error);
console.error('❌ [CACHE] Détails erreur:', {
name: error instanceof Error ? error.name : 'Unknown',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : 'No stack'
});
}
}
/**
* Met à jour le cache pour marquer une facture comme réconciliée (cache permanent)
*/
async updateCacheAsReconciled(invoiceReference: string, groupId: number): Promise<void> {
try {
const cacheKey = this.generateCacheKey(invoiceReference, groupId);
console.log('🔄 [CACHE] Mise à jour cache comme réconcilié:', { invoiceReference, groupId, cacheKey });
// Récupérer le cache existant
const cached = await storage.getInvoiceVerificationCache(cacheKey);
if (!cached) {
console.log('❌ [CACHE] Aucun cache trouvé à mettre à jour');
return;
}
// Si déjà réconcilié, ne rien faire
if (cached.isReconciled) {
console.log('✅ [CACHE] Cache déjà marqué comme réconcilié');
return;
}
// Mettre à jour avec cache permanent (50 ans)
const expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 50);
const updatedCacheData = {
...cached,
isReconciled: true,
expiresAt
};
await storage.saveInvoiceVerificationCache(updatedCacheData);
console.log('✅ [CACHE] Cache mis à jour comme PERMANENT:', {
invoiceReference,
groupId,
cacheKey,
newExpiresAt: expiresAt.toISOString()
});
} catch (error) {
console.error('❌ [CACHE] Erreur mise à jour cache réconcilié:', error);
}
}
/**
* Met à jour tous les caches des livraisons déjà validées pour les marquer comme permanents
*/
async updateExistingReconciledCaches(): Promise<void> {
try {
console.log('🔄 [CACHE] Mise à jour en lot des caches pour livraisons validées...');
// Récupérer toutes les livraisons validées (reconciled = true)
const deliveries = await storage.getDeliveries();
const reconciledDeliveries = deliveries.filter(d => d.reconciled);
console.log(`📊 [CACHE] ${reconciledDeliveries.length} livraisons validées trouvées`);
let updatedCount = 0;
for (const delivery of reconciledDeliveries) {
try {
// Mettre à jour le cache pour la référence de facture
if (delivery.invoiceReference && delivery.invoiceReference.trim()) {
await this.updateCacheAsReconciled(delivery.invoiceReference, delivery.groupId);
updatedCount++;
}
// Mettre à jour le cache pour le numéro BL
if (delivery.blNumber && delivery.blNumber.trim()) {
await this.updateCacheAsReconciled(delivery.blNumber, delivery.groupId);
updatedCount++;
}
} catch (error) {
console.error(`❌ [CACHE] Erreur mise à jour cache pour livraison ${delivery.id}:`, error);
}
}
console.log(`✅ [CACHE] Mise à jour terminée: ${updatedCount} caches mis à jour comme permanents`);
} catch (error) {
console.error('❌ [CACHE] Erreur lors de la mise à jour en lot:', error);
}
}
/**
* Vérifie une référence de facture pour un groupe donné
*/
async verifyInvoice(invoiceReference: string, groupId: number, forceRefresh: boolean = false, isReconciled: boolean = false): Promise<{
exists: boolean;
matchType: 'invoice_reference' | 'bl_number' | 'none';
errorMessage?: string;
invoiceReference?: string;
invoiceAmount?: number;
invoiceAmountTTC?: number;
supplierName?: string;
dueDate?: string | null;
fromCache?: boolean;
}> {
try {
console.log('🔍 [INVOICE] Début vérification facture:', { invoiceReference, groupId, forceRefresh });
if (!invoiceReference || !invoiceReference.trim()) {
return {
exists: false,
matchType: 'none',
errorMessage: 'Référence de facture vide'
};
}
// Vérifier le cache d'abord (sauf si refresh forcé)
if (!forceRefresh) {
console.log('🔍 [INVOICE] Vérification cache...');
const cachedResult = await this.checkCache(invoiceReference, groupId);
if (cachedResult) {
console.log('✅ [INVOICE] Résultat depuis cache:', cachedResult);
return cachedResult;
}
console.log('🔍 [INVOICE] Pas de cache, requête API...');
} else {
console.log('🔄 [INVOICE] Refresh forcé, ignorant le cache');
}
// Récupérer la configuration du groupe
const group = await storage.getGroup(groupId);
if (!group) {
return {
exists: false,
matchType: 'none',
errorMessage: 'Groupe non trouvé'
};
}
console.log('🔧 Configuration groupe:', {
groupName: group.name,
hasNocodbConfig: !!group.nocodbConfigId,
hasTableName: !!group.nocodbTableName,
hasWebhook: !!group.webhookUrl
});
// Si pas de configuration NocoDB, retourner un résultat par défaut
if (!group.nocodbConfigId && !group.nocodbTableName && !group.webhookUrl) {
console.log('⚠️ Pas de configuration NocoDB pour ce groupe');
return {
exists: false,
matchType: 'none',
errorMessage: 'Configuration NocoDB manquante pour ce magasin'
};
}
// Pour le développement, simuler une vérification
if (process.env.NODE_ENV === 'development') {
console.log('🔧 Mode développement - simulation vérification');
// Simuler différents cas selon la référence
if (invoiceReference.toLowerCase().includes('test')) {
const result = {
exists: true,
matchType: 'invoice_reference' as const,
invoiceReference: invoiceReference,
invoiceAmount: 123.45,
supplierName: 'Fournisseur Test'
};
// Sauvegarder en cache
await this.saveToCache(invoiceReference, groupId, result, result.supplierName, isReconciled);
return result;
}
if (invoiceReference.toLowerCase().includes('bl')) {
const result = {
exists: true,
matchType: 'bl_number' as const,
invoiceReference: `FACT_${invoiceReference}`,
invoiceAmount: 67.89,
supplierName: 'Fournisseur BL'
};
// Sauvegarder en cache
await this.saveToCache(invoiceReference, groupId, result, result.supplierName, isReconciled);
return result;
}
const result = {
exists: false,
matchType: 'none' as const,
errorMessage: 'Facture non trouvée (mode développement)'
};
// Sauvegarder même les résultats "non trouvé" pour éviter les rappels répétés
await this.saveToCache(invoiceReference, groupId, result, undefined, isReconciled);
return result;
}
// En production, faire l'appel réel à NocoDB
console.log('🔍 Vérification NocoDB en production...');
try {
// Récupérer la configuration NocoDB active
const nocodbConfig = await storage.getActiveNocodbConfig();
if (!nocodbConfig) {
console.log('⚠️ Pas de configuration NocoDB active');
return {
exists: false,
matchType: 'none',
errorMessage: 'Configuration NocoDB non trouvée'
};
}
console.log('🔧 Configuration NocoDB trouvée:', {
configName: nocodbConfig.name,
baseUrl: nocodbConfig.baseUrl,
projectId: nocodbConfig.projectId,
hasToken: !!nocodbConfig.apiToken
});
// Utiliser l'ID de table configuré dans le groupe
const tableId = group.nocodbTableId || 'mrr733dfb8wtt9b'; // Fallback par défaut
console.log('🔧 Utilisation table ID:', {
groupTable: group.nocodbTableName,
configuredId: group.nocodbTableId,
resolvedId: tableId
});
// Vérifier d'abord par référence de facture
let matchResult = await this.searchInNocoDB(
invoiceReference,
group.invoiceColumnName || 'RefFacture',
nocodbConfig,
tableId
);
// Gérer les erreurs d'API spécifiques
if (matchResult.error) {
const result = {
exists: false,
matchType: 'none' as const,
errorMessage: matchResult.error
};
// Sauvegarder en cache même les erreurs pour éviter les rappels répétés
await this.saveToCache(invoiceReference, groupId, result, undefined, isReconciled);
return result;
}
if (matchResult.found) {
const result = {
exists: true,
matchType: 'invoice_reference' as const,
invoiceReference: matchResult.data.invoice_reference || invoiceReference,
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
invoiceAmountTTC: parseFloat(matchResult.data[group.nocodbInvoiceAmountTTCColumnName || 'Montant TTC'] || '0'),
supplierName: matchResult.data[group.nocodbSupplierColumnName || 'supplier'] || 'Inconnu',
dueDate: matchResult.data[group.nocodbDueDateColumnName || 'Echeance'] || null
};
// Sauvegarder en cache si succès
await this.saveToCache(invoiceReference, groupId, result, result.supplierName, isReconciled);
return result;
}
// Si pas trouvé par référence de facture, chercher par numéro de BL
if (group.nocodbBlColumnName) {
matchResult = await this.searchInNocoDB(
invoiceReference,
group.nocodbBlColumnName || 'Numero_BL',
nocodbConfig,
tableId
);
// Gérer les erreurs d'API pour la recherche BL
if (matchResult.error) {
const result = {
exists: false,
matchType: 'none' as const,
errorMessage: matchResult.error
};
// Sauvegarder en cache même les erreurs
await this.saveToCache(invoiceReference, groupId, result, undefined, isReconciled);
return result;
}
if (matchResult.found) {
const result = {
exists: true,
matchType: 'bl_number' as const,
invoiceReference: matchResult.data.invoice_reference || `BL_${invoiceReference}`,
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
invoiceAmountTTC: parseFloat(matchResult.data[group.nocodbInvoiceAmountTTCColumnName || 'Montant TTC'] || '0'),
supplierName: matchResult.data[group.nocodbSupplierColumnName || 'supplier'] || 'Inconnu',
dueDate: matchResult.data[group.nocodbDueDateColumnName || 'Echeance'] || null
};
// Sauvegarder en cache si succès
await this.saveToCache(invoiceReference, groupId, result, result.supplierName, isReconciled);
return result;
}
}
// Aucune correspondance trouvée
const notFoundResult = {
exists: false,
matchType: 'none' as const,
errorMessage: 'Aucune correspondance trouvée dans NocoDB'
};
// Sauvegarder même les résultats "non trouvé" pour éviter les rappels répétés
await this.saveToCache(invoiceReference, groupId, notFoundResult, undefined, isReconciled);
return notFoundResult;
} catch (error) {
const errorMessage = `Erreur NocoDB: ${error instanceof Error ? error.message : 'Erreur inconnue'}`;
// Log condensé en production
if (process.env.NODE_ENV === 'production') {
console.error('❌ Erreur vérification NocoDB:', error instanceof Error ? error.message : 'Erreur inconnue');
} else {
console.error('❌ Erreur lors de la vérification NocoDB:', error);
}
const errorResult = {
exists: false,
matchType: 'none' as const,
errorMessage
};
// Sauvegarder les erreurs en cache pour éviter les rappels répétés
await this.saveToCache(invoiceReference, groupId, errorResult, undefined, isReconciled);
return errorResult;
}
} catch (error) {
console.error('❌ Erreur vérification facture:', error);
return {
exists: false,
matchType: 'none',
errorMessage: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Recherche dans NocoDB avec protection contre les erreurs
*/
private async searchInNocoDB(
searchValue: string,
columnName: string,
config: any,
tableId: string
): Promise<{ found: boolean; data?: any; error?: string }> {
try {
// Protection contre les appels avec des paramètres invalides
if (!searchValue || !columnName || !config || !tableId) {
return { found: false, error: 'Paramètres invalides' };
}
// Utiliser directement l'ID de table fourni
const searchUrl = `${config.baseUrl}/api/v1/db/data/v1/${config.projectId}/${tableId}`;
// Log condensé en production pour éviter le spam
if (process.env.NODE_ENV === 'production') {
console.log('🔍 Recherche NocoDB:', { tableId, column: columnName });
} else {
console.log('🔍 Recherche NocoDB:', {
url: searchUrl,
tableId: tableId,
column: columnName,
value: searchValue
});
}
// Créer un AbortController pour le timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 secondes timeout
try {
const response = await fetch(`${searchUrl}?where=(${columnName},eq,${encodeURIComponent(searchValue)})`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'xc-token': config.apiToken
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorMsg = `HTTP ${response.status}: ${response.statusText}`;
console.error('❌ Erreur réponse NocoDB:', errorMsg);
return { found: false, error: errorMsg };
}
const result = await response.json();
// Log condensé en production
if (process.env.NODE_ENV !== 'production') {
console.log('✅ Réponse NocoDB:', result);
}
if (result.list && result.list.length > 0) {
return {
found: true,
data: result.list[0]
};
}
return { found: false };
} catch (fetchError: any) {
clearTimeout(timeoutId);
// Gestion spécifique des erreurs réseau
if (fetchError.name === 'AbortError') {
const timeoutMsg = 'Timeout - API NocoDB non accessible';
console.error('⏱️ Timeout NocoDB:', timeoutMsg);
return { found: false, error: timeoutMsg };
}
if (fetchError.code === 'ERR_CERT_VERIFIER_CHANGED' || fetchError.message?.includes('certificate')) {
const certMsg = 'Erreur certificat SSL - Vérifiez la configuration NocoDB';
console.error('🔒 Erreur certificat NocoDB:', certMsg);
return { found: false, error: certMsg };
}
if (fetchError.code === 'ENOTFOUND' || fetchError.code === 'ECONNREFUSED') {
const connMsg = 'NocoDB non accessible - Vérifiez la connectivité réseau';
console.error('🌐 Erreur connexion NocoDB:', connMsg);
return { found: false, error: connMsg };
}
throw fetchError; // Relancer les autres erreurs
}
} catch (error: any) {
// Log d'erreur condensé en production
const errorMsg = error?.message || 'Erreur inconnue';
if (process.env.NODE_ENV === 'production') {
console.error('❌ Erreur NocoDB:', errorMsg);
} else {
console.error('❌ Erreur appel NocoDB:', error);
}
return { found: false, error: errorMsg };
}
}
/**
* Vérifie une facture par numéro de BL et nom de fournisseur
*/
async verifyInvoiceByBL(blNumber: string, supplierName: string, groupId: number, forceRefresh: boolean = false, isReconciled: boolean = false): Promise<{
exists: boolean;
matchType: 'invoice_reference' | 'bl_number' | 'none';
errorMessage?: string;
invoiceReference?: string;
invoiceAmount?: number;
invoiceAmountTTC?: number;
supplierName?: string;
dueDate?: string | null;
fromCache?: boolean;
}> {
try {
console.log('🔍 Début vérification facture par BL:', { blNumber, supplierName, groupId, forceRefresh });
if (!blNumber || !blNumber.trim()) {
return {
exists: false,
matchType: 'none',
errorMessage: 'Numéro BL vide'
};
}
// Générer une clé de cache pour le BL
const cacheKey = `bl_${groupId}_${blNumber.trim().toLowerCase()}_${supplierName.toLowerCase()}`;
// Vérifier le cache d'abord (sauf si refresh forcé)
if (!forceRefresh) {
try {
const cached = await storage.getInvoiceVerificationCache(cacheKey);
if (cached && new Date() < new Date(cached.expiresAt)) {
console.log('💾 Cache hit pour BL:', { blNumber, groupId });
return {
exists: cached.exists,
matchType: cached.matchType as 'invoice_reference' | 'bl_number' | 'none',
errorMessage: cached.errorMessage || undefined,
invoiceReference: cached.invoiceReference || undefined,
supplierName: cached.supplierName || undefined,
fromCache: true
};
}
} catch (error) {
console.error('❌ Erreur lecture cache BL:', error);
}
}
// Récupérer la configuration du groupe
const group = await storage.getGroup(groupId);
if (!group) {
return {
exists: false,
matchType: 'none',
errorMessage: 'Groupe non trouvé'
};
}
console.log('🔧 Configuration groupe pour BL:', {
groupName: group.name,
hasNocodbConfig: !!group.nocodbConfigId,
hasTableName: !!group.nocodbTableName,
hasWebhook: !!group.webhookUrl,
blColumnName: group.nocodbBlColumnName
});
// Si pas de configuration NocoDB, retourner un résultat par défaut
if (!group.nocodbConfigId && !group.nocodbTableName && !group.webhookUrl) {
console.log('⚠️ Pas de configuration NocoDB pour ce groupe');
return {
exists: false,
matchType: 'none',
errorMessage: 'Configuration NocoDB manquante pour ce magasin'
};
}
// Pour le développement, simuler une vérification
if (process.env.NODE_ENV === 'development') {
console.log('🔧 Mode développement - simulation vérification BL');
if (blNumber && blNumber.trim()) {
const result = {
exists: true,
matchType: 'bl_number' as const,
invoiceReference: `FACT_${blNumber}`,
invoiceAmount: 156.78,
supplierName: supplierName
};
// Sauvegarder en cache
try {
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
await storage.saveInvoiceVerificationCache({
cacheKey,
groupId,
invoiceReference: result.invoiceReference,
supplierName: supplierName,
exists: result.exists,
matchType: result.matchType,
errorMessage: null,
cacheHit: false,
apiCallTime: null,
expiresAt
});
} catch (error) {
console.error('❌ Erreur sauvegarde cache BL:', error);
}
return result;
}
return {
exists: false,
matchType: 'none',
errorMessage: 'BL non trouvé (mode développement)'
};
}
// En production, faire l'appel réel à NocoDB
console.log('🔍 Vérification BL NocoDB en production...');
try {
// Récupérer la configuration NocoDB active
const nocodbConfig = await storage.getActiveNocodbConfig();
if (!nocodbConfig) {
console.log('⚠️ Pas de configuration NocoDB active');
return {
exists: false,
matchType: 'none',
errorMessage: 'Configuration NocoDB non trouvée'
};
}
// Utiliser l'ID de table configuré dans le groupe
const tableId = group.nocodbTableId || 'mrr733dfb8wtt9b';
console.log('🔧 Utilisation table ID pour BL:', {
groupTable: group.nocodbTableName,
configuredId: group.nocodbTableId,
resolvedId: tableId
});
// Rechercher par numéro de BL
const blColumnName = group.nocodbBlColumnName || 'Numero_BL';
let matchResult = await this.searchInNocoDB(
blNumber,
blColumnName,
nocodbConfig,
tableId
);
// Gérer les erreurs d'API spécifiques
if (matchResult.error) {
const result = {
exists: false,
matchType: 'none' as const,
errorMessage: matchResult.error
};
// Sauvegarder en cache même les erreurs
try {
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
await storage.saveInvoiceVerificationCache({
cacheKey,
groupId,
invoiceReference: blNumber,
supplierName: supplierName,
exists: false,
matchType: 'none',
errorMessage: result.errorMessage,
cacheHit: false,
apiCallTime: null,
isReconciled,
expiresAt
});
} catch (error) {
console.error('❌ Erreur sauvegarde cache erreur BL:', error);
}
return result;
}
if (matchResult.found) {
// Vérifier que le fournisseur correspond
const foundSupplier = matchResult.data[group.nocodbSupplierColumnName || 'supplier'] || '';
const supplierMatch = foundSupplier.toLowerCase().includes(supplierName.toLowerCase()) ||
supplierName.toLowerCase().includes(foundSupplier.toLowerCase());
if (!supplierMatch) {
console.log('⚠️ Fournisseur ne correspond pas:', {
expected: supplierName,
found: foundSupplier
});
return {
exists: false,
matchType: 'none',
errorMessage: `BL trouvé mais fournisseur différent: ${foundSupplier} vs ${supplierName}`
};
}
const result = {
exists: true,
matchType: 'bl_number' as const,
invoiceReference: matchResult.data[group.invoiceColumnName || 'RefFacture'] || `BL_${blNumber}`,
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
invoiceAmountTTC: parseFloat(matchResult.data[group.nocodbInvoiceAmountTTCColumnName || 'Montant TTC'] || '0'),
supplierName: foundSupplier || supplierName,
dueDate: matchResult.data[group.nocodbDueDateColumnName || 'Echeance'] || null
};
// Sauvegarder en cache si succès
try {
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
await storage.saveInvoiceVerificationCache({
cacheKey,
groupId,
invoiceReference: result.invoiceReference,
supplierName: result.supplierName,
exists: true,
matchType: 'bl_number',
errorMessage: null,
cacheHit: false,
apiCallTime: null,
isReconciled,
expiresAt
});
} catch (error) {
console.error('❌ Erreur sauvegarde cache succès BL:', error);
}
return result;
}
// Aucune correspondance trouvée
const notFoundResult = {
exists: false,
matchType: 'none' as const,
errorMessage: 'Aucun BL correspondant trouvé dans NocoDB'
};
// Sauvegarder même les résultats "non trouvé"
try {
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
await storage.saveInvoiceVerificationCache({
cacheKey,
groupId,
invoiceReference: blNumber,
supplierName: supplierName,
exists: false,
matchType: 'none',
errorMessage: notFoundResult.errorMessage,
cacheHit: false,
apiCallTime: null,
isReconciled,
expiresAt
});
} catch (error) {
console.error('❌ Erreur sauvegarde cache not found BL:', error);
}
return notFoundResult;
} catch (error) {
const errorMessage = `Erreur NocoDB BL: ${error instanceof Error ? error.message : 'Erreur inconnue'}`;
console.error('❌ Erreur lors de la vérification BL NocoDB:', error);
const errorResult = {
exists: false,
matchType: 'none' as const,
errorMessage
};
// Sauvegarder les erreurs en cache
try {
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
await storage.saveInvoiceVerificationCache({
cacheKey,
groupId,
invoiceReference: blNumber,
supplierName: supplierName,
exists: false,
matchType: 'none',
errorMessage: errorResult.errorMessage,
cacheHit: false,
apiCallTime: null,
isReconciled,
expiresAt
});
} catch (error) {
console.error('❌ Erreur sauvegarde cache erreur BL:', error);
}
return errorResult;
}
} catch (error) {
console.error('❌ Erreur vérification facture par BL:', error);
return {
exists: false,
matchType: 'none',
errorMessage: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Recherche par numéro de BL (méthode simplifiée pour compatibilité)
*/
async searchByBLNumber(blNumber: string, groupId: number): Promise<any> {
console.log('🔍 Recherche par BL (legacy):', { blNumber, groupId });
const result = await this.verifyInvoiceByBL(blNumber, '', groupId, false);
if (result.exists) {
return {
found: true,
data: {
invoiceReference: result.invoiceReference,
amount: result.invoiceAmount,
supplier: result.supplierName
}
};
} else {
return {
found: false,
error: result.errorMessage || 'BL non trouvé'
};
}
}
/**
* Validation d'une référence de facture
*/
validateInvoiceReference(reference: string): boolean {
if (!reference || typeof reference !== 'string') {
return false;
}
const trimmed = reference.trim();
return trimmed.length > 0 && trimmed.length <= 100;
}
/**
* Nettoyage d'une référence de facture
*/
cleanInvoiceReference(reference: string): string {
if (!reference || typeof reference !== 'string') {
return '';
}
return reference.trim().toUpperCase();
}
}
// Instance unique du service
export const invoiceVerificationService = new InvoiceVerificationService();