"""Porte de décision métier unifiée. Combine la sortie du parsing (`app.postprocess.process`) et de la qualité image (`app.quality.estimate_image_quality`) pour décider si une lecture peut être validée (règles 3/4/5/7 du cahier des charges) : un paiement ne doit jamais être validé si la photo est floue/illisible, si litres et montant sont incohérents, ou si la confiance du modèle est trop faible. """ from typing import Any, Dict, Optional from .config import RuleConfig, load_config def _compute_confidence_score(parsed: Dict[str, Any], quality: Dict[str, Any]) -> float: ocr_confidence = parsed.get("ocr_confidence") if ocr_confidence is None: ocr_confidence = min(1.0, len(parsed.get("raw_numbers", [])) / 3.0) quality_score = quality.get("quality_score", 0.0) return round(min(1.0, ocr_confidence * 0.6 + quality_score * 0.4), 2) def evaluate(parsed: Dict[str, Any], quality: Dict[str, Any], fuel_price: Optional[float] = None, cfg: Optional[RuleConfig] = None) -> Dict[str, Any]: """Retourne {'success': bool, 'message': str, 'confidence_score': float}. Ordre des vérifications (le premier échec l'emporte) : 1. image non "valid" (floue/sombre/surexposée/peu exploitable) -> bloqué 2. aucune donnée numérique détectée -> bloqué 3. litres ET montant manquants, ou prix manquant -> bloqué 4. is_consistent est False ou None (quand vérifiable) -> bloqué 5. confidence_score sous le seuil -> bloqué 6. sinon -> succès """ if cfg is None: cfg = load_config() confidence_score = _compute_confidence_score(parsed, quality) image_quality = quality.get("image_quality") if image_quality != "valid": messages = { "blurry": "Photo floue, veuillez reprendre la photo.", "dark": "Image trop sombre, veuillez reprendre la photo.", "bright": "Image surexposée (reflet), veuillez reprendre la photo.", "unclear": "Image peu exploitable, veuillez reprendre la photo.", } return { "success": False, "message": messages.get(image_quality, "Image non exploitable, veuillez reprendre la photo."), "confidence_score": confidence_score, } if not parsed.get("raw_numbers"): return { "success": False, "message": "Aucune donnée détectée sur l'écran, veuillez reprendre la photo.", "confidence_score": confidence_score, } liters = parsed.get("detected_liters") amount = parsed.get("detected_amount") price = parsed.get("fuel_price") if liters is None and amount is None: return { "success": False, "message": "Données insuffisantes (litres et montant non lisibles).", "confidence_score": confidence_score, } if price is None: return { "success": False, "message": "Prix du litre manquant, impossible de vérifier la cohérence.", "confidence_score": confidence_score, } if liters is not None and amount is not None: is_consistent = parsed.get("is_consistent") if is_consistent is not True: return { "success": False, "message": "Incohérence détectée entre montant, litres et prix.", "confidence_score": confidence_score, } if confidence_score < cfg.business_confidence_threshold: return { "success": False, "message": "Confiance de lecture insuffisante, vérification manuelle requise.", "confidence_score": confidence_score, } return { "success": True, "message": "Données vérifiées avec succès.", "confidence_score": confidence_score, }