Skip to content

ai-alerts

ISSUE #3 : SYSTÈME D'ALERTES IA

🎯 OBJECTIF

Créer le moteur de détection d'anomalies avec IA pour identifier automatiquement les fraudes fiscales.

📖 CONTEXTE DU PROJET

SmartAudit utilise l'intelligence artificielle pour détecter automatiquement les anomalies fiscales en croisant les données de 5 sources. C'est le cœur du projet qui différencie SmartAudit des systèmes traditionnels.

Types d'Anomalies Détectées

  1. TVA_DISCREPANCY : Écart entre déclarations TVA et factures FNE
  2. UNDECLARED_REVENUE : Revenus non déclarés (banques vs déclarations)
  3. CNPS_INCONSISTENCY : Incohérences cotisations sociales
  4. BANK_TRANSACTION_MISMATCH : Transactions bancaires suspectes
  5. CUSTOMS_DECLARATION_ISSUE : Problèmes déclarations douanières
  6. SUSPICIOUS_PATTERN : Patterns suspects dans les données
  7. LATE_DECLARATION : Déclarations en retard
  8. AMOUNT_THRESHOLD_EXCEEDED : Montants dépassant les seuils

Algorithme de Détection

// Exemple : Détection écart TVA
BigDecimal declaredTVA = declarationService.getTVAForPeriod(company, period);
BigDecimal calculatedTVA = invoiceService.calculateTVAFromFNE(company, period);
BigDecimal discrepancy = calculatedTVA.subtract(declaredTVA);

if (discrepancy.compareTo(threshold) > 0) {
    createAlert(company, AlertType.TVA_DISCREPANCY, discrepancy);
}

🛠️ TÂCHES À IMPLÉMENTER

1. Services de Détection

// Fichier : service/AnomalyDetectionService.java
@Service
@Slf4j
public class AnomalyDetectionService {
    
    @Autowired
    private AlertService alertService;
    
    @Autowired
    private DeclarationService declarationService;
    
    @Autowired
    private InvoiceService invoiceService;
    
    @Autowired
    private TransactionService transactionService;
    
    @Scheduled(cron = "0 0 2 * * ?") // Tous les jours à 2h
    public void detectAnomaliesDaily() {
        log.info("Début de la détection d'anomalies quotidienne...");
        
        List<Company> companies = companyService.findAllActive();
        
        for (Company company : companies) {
            try {
                detectTVAAnomalies(company);
                detectRevenueAnomalies(company);
                detectCNPSAnomalies(company);
                detectBankAnomalies(company);
            } catch (Exception e) {
                log.error("Erreur détection anomalies pour entreprise {}: {}", 
                         company.getIfu(), e.getMessage());
            }
        }
        
        log.info("Détection d'anomalies terminée. {} alertes créées.", 
                alertService.countNewAlerts());
    }
    
    private void detectTVAAnomalies(Company company) {
        // 1. Récupérer déclarations TVA des 12 derniers mois
        List<Declaration> tvaDeclarations = declarationService
            .findByCompanyAndType(company, DeclarationType.TVA);
        
        // 2. Récupérer factures FNE correspondantes
        List<Invoice> invoices = invoiceService
            .findByCompanyAndStatus(company, InvoiceStatus.VALID);
        
        // 3. Calculer TVA théorique depuis FNE
        BigDecimal calculatedTVA = invoices.stream()
            .map(Invoice::getTvaAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        // 4. Calculer TVA déclarée
        BigDecimal declaredTVA = tvaDeclarations.stream()
            .map(Declaration::getDeclaredTVA)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        // 5. Détecter écart significatif
        BigDecimal discrepancy = calculatedTVA.subtract(declaredTVA);
        BigDecimal threshold = new BigDecimal("1000000"); // 1M FCFA
        
        if (discrepancy.abs().compareTo(threshold) > 0) {
            createTVAAlert(company, discrepancy, calculatedTVA, declaredTVA);
        }
    }
    
    private void detectRevenueAnomalies(Company company) {
        // 1. CA déclaré (e-impots)
        BigDecimal declaredRevenue = declarationService
            .getTotalRevenueForYear(company, LocalDate.now().getYear());
        
        // 2. Crédits bancaires (Banques)
        BigDecimal bankCredits = transactionService
            .getTotalCreditsForYear(company, LocalDate.now().getYear());
        
        // 3. Détecter revenus non déclarés
        BigDecimal undeclaredAmount = bankCredits.subtract(declaredRevenue);
        BigDecimal threshold = new BigDecimal("5000000"); // 5M FCFA
        
        if (undeclaredAmount.compareTo(threshold) > 0) {
            createRevenueAlert(company, undeclaredAmount, declaredRevenue, bankCredits);
        }
    }
    
    private void createTVAAlert(Company company, BigDecimal discrepancy, 
                               BigDecimal calculatedTVA, BigDecimal declaredTVA) {
        Alert alert = new Alert();
        alert.setCompany(company);
        alert.setType(AlertType.TVA_DISCREPANCY);
        alert.setPriority(calculatePriority(discrepancy));
        alert.setTitle("Écart TVA détecté - " + company.getCompanyName());
        alert.setDescription(String.format(
            "Écart de %,.0f FCFA entre TVA déclarée (%,.0f) et TVA calculée depuis FNE (%,.0f)",
            discrepancy, declaredTVA, calculatedTVA
        ));
        alert.setPotentialAmount(discrepancy.abs());
        alert.setConfidenceScore(calculateConfidenceScore(discrepancy, calculatedTVA));
        alert.setEvidenceData(createEvidenceJSON(calculatedTVA, declaredTVA, discrepancy));
        
        alertService.save(alert);
        log.info("Alerte TVA créée pour {}: écart de {} FCFA", 
                company.getCompanyName(), discrepancy);
    }
}

2. Service de Gestion des Alertes

// Fichier : service/AlertService.java
@Service
@Transactional
public class AlertService {
    
    @Autowired
    private AlertRepository alertRepository;
    
    @Autowired
    private NotificationService notificationService;
    
    public List<Alert> findNewAlerts() {
        return alertRepository.findByStatus(AlertStatus.NEW);
    }
    
    public List<Alert> findAlertsByAgent(Agent agent) {
        return alertRepository.findByAssignedAgent(agent);
    }
    
    public Alert assignAlertToAgent(Long alertId, Agent agent) {
        Alert alert = alertRepository.findById(alertId)
            .orElseThrow(() -> new AlertNotFoundException("Alerte non trouvée"));
        
        alert.setAssignedAgent(agent);
        alert.setStatus(AlertStatus.ASSIGNED);
        alert.setAssignedAt(LocalDateTime.now());
        
        // Notifier l'agent
        notificationService.notifyNewAlert(agent, alert);
        
        return alertRepository.save(alert);
    }
    
    public Alert resolveAlert(Long alertId, String resolutionNotes) {
        Alert alert = alertRepository.findById(alertId)
            .orElseThrow(() -> new AlertNotFoundException("Alerte non trouvée"));
        
        alert.setStatus(AlertStatus.RESOLVED);
        alert.setResolvedAt(LocalDateTime.now());
        alert.setResolutionNotes(resolutionNotes);
        
        return alertRepository.save(alert);
    }
    
    public BigDecimal calculateConfidenceScore(BigDecimal discrepancy, BigDecimal totalAmount) {
        // Score de confiance basé sur l'ampleur de l'écart
        if (totalAmount.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
        
        BigDecimal percentage = discrepancy.abs()
            .divide(totalAmount, 4, RoundingMode.HALF_UP)
            .multiply(new BigDecimal("100"));
        
        // Plus l'écart est important, plus la confiance est élevée
        return percentage.min(new BigDecimal("100"));
    }
    
    private AlertPriority calculatePriority(BigDecimal amount) {
        if (amount.abs().compareTo(new BigDecimal("10000000")) > 0) {
            return AlertPriority.URGENT;
        } else if (amount.abs().compareTo(new BigDecimal("5000000")) > 0) {
            return AlertPriority.HIGH;
        } else if (amount.abs().compareTo(new BigDecimal("1000000")) > 0) {
            return AlertPriority.MEDIUM;
        } else {
            return AlertPriority.LOW;
        }
    }
}

3. Controllers REST

// Fichier : controller/AlertController.java
@RestController
@RequestMapping("/api/alerts")
@PreAuthorize("hasAnyRole('ADMIN', 'AGENT')")
public class AlertController {
    
    @Autowired
    private AlertService alertService;
    
    @GetMapping
    public ResponseEntity<List<AlertDTO>> getAllAlerts(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) AlertStatus status,
            @RequestParam(required = false) AlertPriority priority) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<Alert> alerts = alertService.findAlertsWithFilters(status, priority, pageable);
        
        List<AlertDTO> alertDTOs = alerts.getContent().stream()
            .map(this::convertToDTO)
            .collect(Collectors.toList());
        
        return ResponseEntity.ok(alertDTOs);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<AlertDetailsDTO> getAlertDetails(@PathVariable Long id) {
        Alert alert = alertService.findById(id);
        AlertDetailsDTO details = convertToDetailsDTO(alert);
        return ResponseEntity.ok(details);
    }
    
    @PostMapping("/{id}/assign")
    @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISEUR')")
    public ResponseEntity<String> assignAlert(@PathVariable Long id, 
                                            @RequestBody AssignAlertDTO dto) {
        Agent agent = agentService.findById(dto.getAgentId());
        alertService.assignAlertToAgent(id, agent);
        return ResponseEntity.ok("Alerte assignée avec succès");
    }
    
    @PostMapping("/{id}/resolve")
    public ResponseEntity<String> resolveAlert(@PathVariable Long id,
                                             @RequestBody ResolveAlertDTO dto) {
        alertService.resolveAlert(id, dto.getResolutionNotes());
        return ResponseEntity.ok("Alerte résolue avec succès");
    }
    
    @PostMapping("/detect")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<String> triggerDetection() {
        anomalyDetectionService.detectAnomaliesDaily();
        return ResponseEntity.ok("Détection d'anomalies lancée");
    }
}

4. DTOs

// Fichier : dto/AlertDTO.java
public class AlertDTO {
    private Long id;
    private String companyName;
    private String companyIfu;
    private AlertType type;
    private AlertStatus status;
    private AlertPriority priority;
    private String title;
    private String description;
    private BigDecimal potentialAmount;
    private BigDecimal confidenceScore;
    private LocalDateTime detectedAt;
    private String assignedAgentName;
}

// Fichier : dto/AlertDetailsDTO.java
public class AlertDetailsDTO extends AlertDTO {
    private String evidenceData;
    private String resolutionNotes;
    private LocalDateTime assignedAt;
    private LocalDateTime resolvedAt;
    private List<DeclarationDTO> relatedDeclarations;
    private List<InvoiceDTO> relatedInvoices;
    private List<TransactionDTO> relatedTransactions;
}

🔧 OUTILS & TECHNOLOGIES

Spring Boot

  • @Scheduled : Détection automatique quotidienne
  • Spring Data JPA : Requêtes complexes
  • @Transactional : Gestion des transactions
  • Jackson : Sérialisation JSON

Algorithmes IA

  • Détection d'anomalies : Comparaison statistique
  • Scoring de confiance : Calcul basé sur l'ampleur
  • Seuils adaptatifs : Basés sur le secteur d'activité
  • Machine Learning simple : Patterns suspects

🎯 CRITÈRES D'ACCEPTATION

Détection Automatique

  • Job quotidien : Exécution automatique à 2h
  • 8 types d'anomalies : Tous les types détectés
  • Scoring confiance : 0-100% basé sur l'ampleur
  • Seuils configurables : Par type d'anomalie
  • Performance : < 5 minutes pour 100 entreprises

Gestion des Alertes

  • Assignation automatique : Par district et spécialisation
  • Workflow complet : NEW → ASSIGNED → RESOLVED
  • Notifications : Agents notifiés des nouvelles alertes
  • Historique : Traçabilité complète des actions

API REST

  • Filtres avancés : Par statut, priorité, agent
  • Pagination : Pour gros volumes d'alertes
  • Détails complets : Données sources incluses
  • Actions : Assignation, résolution, détection manuelle

📁 STRUCTURE DES FICHIERS

src/main/java/ci/dgi/smartaudit/
├── service/
│   ├── AnomalyDetectionService.java
│   ├── AlertService.java
│   └── AlertAssignmentService.java
├── controller/
│   └── AlertController.java
├── repository/
│   └── AlertRepository.java
├── dto/
│   ├── AlertDTO.java
│   ├── AlertDetailsDTO.java
│   ├── AssignAlertDTO.java
│   └── ResolveAlertDTO.java
└── config/
    └── SchedulingConfig.java

🧪 TESTS À IMPLÉMENTER

Tests Unitaires

@ExtendWith(MockitoExtension.class)
class AnomalyDetectionServiceTest {
    
    @Test
    void detectTVAAnomalies_WithSignificantDiscrepancy_ShouldCreateAlert() {
        // Test détection écart TVA
    }
    
    @Test
    void detectRevenueAnomalies_WithUndeclaredRevenue_ShouldCreateAlert() {
        // Test détection revenus non déclarés
    }
    
    @Test
    void calculateConfidenceScore_WithHighDiscrepancy_ShouldReturnHighScore() {
        // Test calcul score confiance
    }
}

Tests d'Intégration

@SpringBootTest
@AutoConfigureTestDatabase
class AlertWorkflowIntegrationTest {
    
    @Test
    void alertWorkflow_FromDetectionToResolution_ShouldWork() {
        // Test workflow complet
    }
}

🚀 DÉMARRAGE RAPIDE

1. Configuration Scheduling

@Configuration
@EnableScheduling
public class SchedulingConfig {
    
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("smartaudit-");
        scheduler.initialize();
        return scheduler;
    }
}

2. Configuration Seuils

# application.yml
smartaudit:
  detection:
    thresholds:
      tva-discrepancy: 1000000
      revenue-anomaly: 5000000
      cnps-inconsistency: 1000000
    confidence:
      min-score: 70
      high-confidence: 90

📊 MÉTRIQUES DE SUCCÈS

  • Précision détection : > 85% des alertes justifiées
  • Performance : < 5 minutes pour 100 entreprises
  • Couverture : 8 types d'anomalies détectés
  • Temps réponse : < 200ms pour API alertes

🔗 INTÉGRATIONS

Avec Issue #2

  • Données sources : Croisement multi-sources
  • Validation : Cohérence des données

Avec Issue #4

  • Création audits : Alertes → Audits
  • Workflow : Assignation agents

Avec Issue #6

  • Notifications : Alertes nouvelles
  • Multi-canal : Email, SMS, in-app

Avec Issue #7

  • Traçabilité : Enregistrement actions blockchain

⚠️ POINTS D'ATTENTION

  1. Performance : Optimiser requêtes pour gros volumes
  2. Faux positifs : Ajuster seuils selon retours terrain
  3. Ressources : Monitoring CPU/mémoire job quotidien
  4. Sécurité : Logs sans données sensibles

ESTIMATION : 2.5 jours
DIFFICULTÉ : Élevée
PRIORITÉ : Critique (cœur du projet)