# app.py import logging import os from typing import Optional from fastapi import FastAPI, HTTPException, Header, BackgroundTasks from pydantic import BaseModel # Importation de votre logique métier from extract_claim_from_email import extract_claim_info_from_email from extract_retard_from_email import extract_retard_info_from_email from drive_enrichment import executer_enrichissement from finalize_claim import executer_bilan_final from finalize_claim_gcs import executer_bilan_final_gcs from register_claim import executer_enregistrement from appsheet_client import edit_reclamation, edit_retard, find_rows, TABLE_RECLAMATIONS, TABLE_RETARD logger = logging.getLogger("app") app = FastAPI(title="Claim Processing & Extraction API") # Clé de sécurité partagée avec AppSheet API_KEY = os.environ.get("PIPELINE_API_KEY") # Correspondance entre les clés renvoyées par l'extraction et les vraies # colonnes de la table Reclamations (à ajuster si vous renommez des colonnes) CHAMP_MAPPING = { "license_plate": "Vehicule Plaque", "customer_name": "Client Nom Prenom", "vehicle_model": "Vehicule Modele", "claim_message": "Dommage Detaille", "claim_demand": "Demande Reclamation", "localisation_carrosserie": "Localisation Carrosserie", } # Correspondance entre les clés renvoyées par l'extraction et les vraies # colonnes de la table Retard (à ajuster si vous renommez des colonnes) CHAMP_MAPPING_RETARD = { "license_plate": "Vehicule Plaque", "customer_name": "Client Nom Prenom", "vehicle_model": "Vehicule Modele", "description": "Incident Description", } # Modèles de données Pydantic (Validation des requêtes entrantes) class EmailRequest(BaseModel): id_reclamation: str claim_email: str class EnrichRequest(BaseModel): id_reclamation: str license_plate: str class FinalizeRequest(BaseModel): id_reclamation: str class RegisterRequest(BaseModel): id_reclamation: str def _check_api_key(x_api_key: Optional[str]): """Vérification du Header de sécurité""" if API_KEY and x_api_key != API_KEY: raise HTTPException(status_code=401, detail="Clé API invalide ou manquante") def _set_statut(id_reclamation: str, statut: str): """Met à jour la colonne Statut de la table Reclamations, sans jamais lever d'exception : appelée aussi bien en cas de succès que dans un except, elle ne doit jamais masquer l'erreur d'origine.""" try: edit_reclamation(id_reclamation, {"Statut WorkFlow": statut}) except Exception: logger.exception( "Impossible de mettre à jour le Statut de %s en %r", id_reclamation, statut ) def _set_statut_retard(id_retard: str, statut: str): """Équivalent de _set_statut mais pour la table Retard (colonne "Statut Workflow", identifiant "ID Retard").""" try: edit_retard(id_retard, {"Statut Workflow": statut}) except Exception: logger.exception( "Impossible de mettre à jour le Statut de %s en %r", id_retard, statut ) @app.get("/") def health(): return {"status": "ok"} # ----------------------------------RECLAMATION-------------------------------------------------------- # --- ENDPOINT 1 : EXTRACTION DU MAIL BRUT --- @app.post("/extract-claim") def extract_claim(payload: EmailRequest, x_api_key: Optional[str] = Header(default=None)): _check_api_key(x_api_key) if not payload.claim_email or not payload.claim_email.strip(): raise HTTPException(status_code=400, detail="claim_email est requis et ne peut pas être vide") # Garde-fou anti double-traitement : si l'extraction a déjà été faite # pour cette réclamation (ex: webhook rejoué), on ne la relance pas. existant = find_rows( TABLE_RECLAMATIONS, f'Filter(Reclamations, [ID Reclamation] = "{payload.id_reclamation}")', ) if existant and existant[0].get("Client Nom Prenom"): return {"status": "skipped", "reason": "Réclamation déjà extraite (Client Nom Prenom déjà rempli)"} _set_statut(payload.id_reclamation, "Retraitement Mail en cours") try: result = extract_claim_info_from_email(payload.claim_email) except Exception as exc: _set_statut(payload.id_reclamation, "Erreur Retraitement Mail") raise HTTPException(status_code=500, detail=f"Erreur extraction : {exc}") from exc # Traduit les clés JSON (license_plate, ...) vers les vraies colonnes # AppSheet (Vehicule Plaque, ...) et écrit directement dans la table. champs_appsheet = { CHAMP_MAPPING[cle]: valeur for cle, valeur in result.items() if cle in CHAMP_MAPPING } try: edit_reclamation(payload.id_reclamation, champs_appsheet) except Exception as exc: # L'extraction a réussi mais l'écriture AppSheet a échoué : on le # signale clairement plutôt que de faire comme si tout allait bien. _set_statut(payload.id_reclamation, "Erreur Retraitement Mail") raise HTTPException( status_code=502, detail=f"Extraction OK mais écriture AppSheet impossible : {exc}", ) from exc return {"status": "ok", **result} # --- ENDPOINT 2 : ENRICHISSEMENT DES DONNEES --- @app.post("/enrich-claim") def enrich_claim(payload: EnrichRequest, x_api_key: Optional[str] = Header(default=None)): _check_api_key(x_api_key) # Garde-fou anti double-traitement : si l'enrichissement a déjà été fait # pour cette réclamation (ex: webhook rejoué), on ne le relance pas. existant = find_rows( TABLE_RECLAMATIONS, f'Filter(Reclamations, [ID Reclamation] = "{payload.id_reclamation}")', ) if existant and existant[0].get("Claim Internal ID"): return {"status": "skipped", "reason": "Réclamation déjà enrichie (Claim Internal ID déjà rempli)"} _set_statut(payload.id_reclamation, "Ajout des Informations complémentaires") try: result = executer_enrichissement(payload.id_reclamation, payload.license_plate) except Exception as exc: _set_statut(payload.id_reclamation, "Erreur Ajout Informations") raise HTTPException(status_code=500, detail=f"Erreur enrichissement : {exc}") from exc return {"status": "ok", **result} # --- ENDPOINT 3 : BILAN FINAL --- @app.post("/finalize-claim", status_code=202) def finalize_claim(payload: FinalizeRequest, background_tasks: BackgroundTasks, x_api_key: Optional[str] = Header(default=None)): _check_api_key(x_api_key) background_tasks.add_task(executer_bilan_final, payload.id_reclamation) return {"status": "accepted"} # --- ENDPOINT 3bis : BILAN FINAL (variante GCS — médias via "URL Media") --- # Coexiste avec /finalize-claim, ne le remplace pas : voir le docstring de # finalize_claim_gcs.py pour les différences (source des médias, pas de # copie Preuves/Parties Visibles). @app.post("/finalize-claim-gcs", status_code=202) def finalize_claim_gcs(payload: FinalizeRequest, background_tasks: BackgroundTasks, x_api_key: Optional[str] = Header(default=None)): _check_api_key(x_api_key) background_tasks.add_task(executer_bilan_final_gcs, payload.id_reclamation) return {"status": "accepted"} # --- ENDPOINT 4 : ENREGISTREMENT DE LA RECLAMATION DANS EXCEL (DRIVE) --- @app.post("/register-claim", status_code=202) def register_claim(payload: RegisterRequest, background_tasks: BackgroundTasks, x_api_key: Optional[str] = Header(default=None)): _check_api_key(x_api_key) background_tasks.add_task(executer_enregistrement, payload.id_reclamation) return {"status": "accepted"} # -----------------------------------------------RETARD----------------------------------------------------------------------- # --- ENDPOINT 1 : EXTRACTION DU MAIL BRUT --- @app.post("/extract-retard") def extract_retard(payload: EmailRequest, x_api_key: Optional[str] = Header(default=None)): _check_api_key(x_api_key) if not payload.claim_email or not payload.claim_email.strip(): raise HTTPException(status_code=400, detail="claim_email est requis et ne peut pas être vide") # Garde-fou anti double-traitement : si l'extraction a déjà été faite # pour cette réclamation (ex: webhook rejoué), on ne la relance pas. existant = find_rows( TABLE_RETARD, f'Filter(Retard, [ID Retard] = "{payload.id_reclamation}")', ) if existant and existant[0].get("Client Nom Prenom"): return {"status": "skipped", "reason": "Réclamation déjà extraite (Client Nom Prenom déjà rempli)"} _set_statut_retard(payload.id_reclamation, "Retraitement Mail en cours") try: result = extract_retard_info_from_email(payload.claim_email) except Exception as exc: _set_statut_retard(payload.id_reclamation, "Erreur Retraitement Mail") raise HTTPException(status_code=500, detail=f"Erreur extraction : {exc}") from exc # Traduit les clés JSON (license_plate, ...) vers les vraies colonnes # AppSheet (Vehicule Plaque, ...) et écrit directement dans la table. champs_appsheet = { CHAMP_MAPPING_RETARD[cle]: valeur for cle, valeur in result.items() if cle in CHAMP_MAPPING_RETARD } try: edit_retard(payload.id_reclamation, champs_appsheet) except Exception as exc: # L'extraction a réussi mais l'écriture AppSheet a échoué : on le # signale clairement plutôt que de faire comme si tout allait bien. _set_statut_retard(payload.id_reclamation, "Erreur Retraitement Mail") raise HTTPException( status_code=502, detail=f"Extraction OK mais écriture AppSheet impossible : {exc}", ) from exc return {"status": "ok", **result}