""" Helpers para construir respuestas de scan y commit (formato unificado). """ from typing import Dict, Any, List, Optional ERRORS_PREVIEW_LIMIT = 100 SUMMARY_LIMIT = 10 def _summarize_reasons(items: List[Dict[str, Any]], key_name: str) -> List[Dict[str, Any]]: counts: Dict[str, int] = {} for item in items: reason = str(item.get(key_name, "")).strip() if not reason: continue counts[reason] = counts.get(reason, 0) + 1 ordered = sorted(counts.items(), key=lambda kv: kv[1], reverse=True) return [{"reason": reason, "count": count} for reason, count in ordered[:SUMMARY_LIMIT]] def scan_result( job_id: str, processed_rows: int, error_count: int, errors_detail: List[Dict[str, Any]], total_rows_in_file: Optional[int] = None, message: Optional[str] = None, ) -> Dict[str, Any]: """Respuesta de scan_file (waiting_confirmation). Si total_rows_in_file no se pasa, se usa processed_rows como total (comportamiento anterior). """ total = total_rows_in_file if total_rows_in_file is not None else processed_rows valid_rows = processed_rows - error_count out: Dict[str, Any] = { "status": "waiting_confirmation", "job_id": job_id, "total_rows": total, "error_count": error_count, "valid_rows": valid_rows, # Para no saturar el front: devolvemos solo un preview. # El detalle completo se descarga desde CSV usando el job_id. "errors": errors_detail[:ERRORS_PREVIEW_LIMIT], "error_summary": _summarize_reasons([e for e in errors_detail if not e.get("warning")], "msg"), } if message: out["message"] = message elif valid_rows == 0 and total > 0: out["message"] = ( "Ninguna fila quedó libre de observaciones en el escaneo. " "Revise el reporte de errores (incluye advertencias por línea), " "complete catálogos (clientes/proveedores, transporte, conductores, etc.) " "y vuelva a cargar, o descargue el CSV de errores para corregir el archivo." ) return out def commit_result( status: str, inserted: int, skipped_invalid: int, skipped_missing_fk: int, skipped_duplicate: int, skipped_details: List[Dict[str, Any]], message: Optional[str] = None, error: Optional[str] = None, ) -> Dict[str, Any]: """Respuesta de insert_valid_rows (finished / warning / failed).""" out = { "status": status, "inserted": inserted, "skipped_invalid": skipped_invalid, "skipped_missing_fk": skipped_missing_fk, "skipped_duplicate": skipped_duplicate, "skipped_details": skipped_details, "skipped_summary": _summarize_reasons(skipped_details, "reason"), } if message: out["message"] = message if error: out["error"] = error return out def normalize_commit_status_payload(d: Dict[str, Any]) -> Dict[str, Any]: """ Homogeneiza respuestas de commit CSV (finished / warning) al contrato usado por pedimentos: inserted, updated, skipped_*, skipped_details, skipped_summary, critical_reference_gaps, reference_state_ready. No modifica waiting_confirmation ni otros status. """ if not isinstance(d, dict): return d st = d.get("status") if st not in ("finished", "warning"): return d out = dict(d) out.setdefault("inserted", 0) out.setdefault("updated", 0) out.setdefault("skipped_invalid", 0) out.setdefault("skipped_missing_fk", 0) out.setdefault("skipped_duplicate", 0) out.setdefault("skipped_missing_invoice", 0) details = out.get("skipped_details") if not isinstance(details, list): out["skipped_details"] = [] else: out["skipped_details"] = details out.setdefault("critical_reference_gaps", 0) if "reference_state_ready" not in out: out["reference_state_ready"] = out.get("critical_reference_gaps", 0) == 0 if "skipped_summary" not in out: out["skipped_summary"] = ( _summarize_reasons(out["skipped_details"], "reason") if out["skipped_details"] else [] ) return out