feat: sistema de 3 estados (success/warning/error) para envíos TNS
- _parse_tns_resp retorna 'success'|'warning'|'error' en lugar de bool - 'ya existe/registrado/autorización' → warning (sin reenvío, no es error real) - errores reales → error (con reenvío) - Guarda JSON crudo completo en respuesta_api - logs.html: badge amarillo para warning, verde para success, rojo para error - Filtro de historial incluye opción 'warning' - reenvío en logs.py solo disponible para status='error' - reenvío en logs.py usa misma lógica _parse_tns_resp y soporta ventas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a37c1bb7d6
commit
6a452cd950
+46
-53
@@ -587,27 +587,22 @@ async def run_automation(
|
||||
f"{TNS_BASE}/v2/tablas/Tercero/Crear",
|
||||
json=tercero_json, headers=headers,
|
||||
)
|
||||
body = resp.json()
|
||||
ok = bool(body.get("status")) or bool((body.get("data") or {}).get("success"))
|
||||
msg = body.get("message") or (body.get("data") or {}).get("response") or resp.text[:200]
|
||||
# "ya registrado" no es error real — el tercero existe en TNS
|
||||
if not ok and msg and any(s in msg.lower() for s in ("ya esta registrado", "ya existe", "already exist")):
|
||||
ok = True
|
||||
status, msg = _parse_tns_resp(resp)
|
||||
except Exception as e:
|
||||
ok = False
|
||||
msg = str(e)
|
||||
status, msg = "error", str(e)
|
||||
|
||||
if ok:
|
||||
resultado["paso1_terceros"]["enviados"] += 1
|
||||
else:
|
||||
if status == "error":
|
||||
resultado["paso1_terceros"]["errores"] += 1
|
||||
terceros_fallidos.add(codigo_pac)
|
||||
else:
|
||||
resultado["paso1_terceros"]["enviados"] += 1
|
||||
|
||||
resultado["paso1_terceros"]["detalle"].append({
|
||||
"codigo": codigo_pac, "doc": doc, "nombre": nombre, "ok": ok, "msg": msg,
|
||||
"codigo": codigo_pac, "doc": doc, "nombre": nombre,
|
||||
"ok": status != "error", "status": status, "msg": msg,
|
||||
})
|
||||
|
||||
_guardar_envio(user["user_id"], "terceros", fecha, tercero_json, msg, ok,
|
||||
_guardar_envio(user["user_id"], "terceros", fecha, tercero_json, msg, status,
|
||||
cedula=str(row.get("DOCIDENT") or "").strip())
|
||||
|
||||
# ── Sync paralelo a WhatsApp Lab (silencioso) ─────────────────────────────
|
||||
@@ -656,24 +651,24 @@ async def run_automation(
|
||||
factura = num_override or str(id_recepcion)
|
||||
try:
|
||||
resp = await client.post(endpoint, json=rda_json, headers=headers)
|
||||
ok, msg = _parse_tns_resp(resp)
|
||||
status, msg = _parse_tns_resp(resp)
|
||||
except Exception as e:
|
||||
ok, msg = False, str(e)
|
||||
status, msg = "error", str(e)
|
||||
|
||||
if ok:
|
||||
resultado["paso2_rda"]["enviados"] += 1
|
||||
else:
|
||||
if status == "error":
|
||||
resultado["paso2_rda"]["errores"] += 1
|
||||
else:
|
||||
resultado["paso2_rda"]["enviados"] += 1
|
||||
|
||||
resultado["paso2_rda"]["detalle"].append({
|
||||
"idrecepcion": id_recepcion,
|
||||
"factura": factura,
|
||||
"paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
|
||||
"examenes": len(grupo_rows),
|
||||
"ok": ok, "msg": msg,
|
||||
"ok": status != "error", "status": status, "msg": msg,
|
||||
})
|
||||
|
||||
_guardar_envio(user["user_id"], "transaccion", factura, rda_json, msg, ok,
|
||||
_guardar_envio(user["user_id"], "transaccion", factura, rda_json, msg, status,
|
||||
fecha_inicio=fecha, fecha_fin=fecha, servicios=len(grupo_rows),
|
||||
idrecepcion=id_recepcion, contrato=str(grupo_rows[0].get("CODCONTRATO") or "").strip(),
|
||||
cedula=pac_map.get(str(grupo_rows[0].get("COD_PACIENTE", "")), ""))
|
||||
@@ -710,24 +705,24 @@ async def run_automation(
|
||||
factura_ps = f"{ps_prefijo}-{ps_numero.zfill(5)}"
|
||||
try:
|
||||
resp = await client.post(endpoint, json=rda_json, headers=headers)
|
||||
ok, msg = _parse_tns_resp(resp)
|
||||
status, msg = _parse_tns_resp(resp)
|
||||
except Exception as e:
|
||||
ok, msg = False, str(e)
|
||||
status, msg = "error", str(e)
|
||||
|
||||
if ok:
|
||||
resultado["paso3_preserv"]["enviados"] += 1
|
||||
else:
|
||||
if status == "error":
|
||||
resultado["paso3_preserv"]["errores"] += 1
|
||||
else:
|
||||
resultado["paso3_preserv"]["enviados"] += 1
|
||||
|
||||
resultado["paso3_preserv"]["detalle"].append({
|
||||
"idrecepcion": id_ps,
|
||||
"factura": factura_ps,
|
||||
"paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
|
||||
"examenes": len(grupo_rows),
|
||||
"ok": ok, "msg": msg,
|
||||
"ok": status != "error", "status": status, "msg": msg,
|
||||
})
|
||||
|
||||
_guardar_envio(user["user_id"], "transaccion", factura_ps, rda_json, msg, ok,
|
||||
_guardar_envio(user["user_id"], "transaccion", factura_ps, rda_json, msg, status,
|
||||
fecha_inicio=fecha, fecha_fin=fecha, servicios=len(grupo_rows),
|
||||
idrecepcion=id_ps, contrato=str(grupo_rows[0].get("CODCONTRATO") or "").strip(),
|
||||
cedula=pac_map.get(str(grupo_rows[0].get("COD_PACIENTE", "")), ""))
|
||||
@@ -752,24 +747,24 @@ async def run_automation(
|
||||
contrato_vta = str(grupo_rows[0].get("CODCONTRATO") or "").strip()
|
||||
try:
|
||||
resp = await client.post(endpoint_venta, json=venta_json, headers=headers)
|
||||
ok, msg = _parse_tns_resp(resp)
|
||||
status, msg = _parse_tns_resp(resp)
|
||||
except Exception as e:
|
||||
ok, msg = False, str(e)
|
||||
status, msg = "error", str(e)
|
||||
|
||||
if ok:
|
||||
resultado["paso4_ventas"]["enviados"] += 1
|
||||
else:
|
||||
if status == "error":
|
||||
resultado["paso4_ventas"]["errores"] += 1
|
||||
else:
|
||||
resultado["paso4_ventas"]["enviados"] += 1
|
||||
|
||||
resultado["paso4_ventas"]["detalle"].append({
|
||||
"idrecepcion": id_rec,
|
||||
"factura": factura_display,
|
||||
"paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
|
||||
"examenes": len(grupo_rows),
|
||||
"ok": ok, "msg": msg,
|
||||
"ok": status != "error", "status": status, "msg": msg,
|
||||
})
|
||||
|
||||
_guardar_envio(user["user_id"], "ventas", factura_display, venta_json, msg, ok,
|
||||
_guardar_envio(user["user_id"], "ventas", factura_display, venta_json, msg, status,
|
||||
fecha_inicio=fecha, fecha_fin=fecha, servicios=len(grupo_rows),
|
||||
idrecepcion=id_rec, contrato=contrato_vta,
|
||||
cedula=pac_map.get(str(grupo_rows[0].get("COD_PACIENTE", "")), ""))
|
||||
@@ -787,36 +782,34 @@ async def run_automation(
|
||||
return JSONResponse({"success": True, "resultado": resultado})
|
||||
|
||||
|
||||
_YA_EXISTE = ("ya esta registrado", "ya existe", "already exist", "ya existe una autorización")
|
||||
_YA_WARNING = (
|
||||
"ya esta registrado", "ya existe", "already exist",
|
||||
"ya existe una autorización", "ya existe un rda",
|
||||
)
|
||||
|
||||
def _parse_tns_resp(resp) -> tuple[bool, str]:
|
||||
"""Parsea respuesta TNS: retorna (ok, msg). Trata 'ya existe' como OK."""
|
||||
def _parse_tns_resp(resp) -> tuple[str, str]:
|
||||
"""Parsea respuesta TNS. Retorna (status, msg) donde status es 'success'|'warning'|'error'."""
|
||||
raw_body = resp.text
|
||||
try:
|
||||
body = resp.json()
|
||||
raw_status = body.get("status")
|
||||
# TNS usa status=true/false (booleano); ASP.NET usa status=400 (entero HTTP) → error
|
||||
if isinstance(raw_status, bool):
|
||||
ok = raw_status
|
||||
elif isinstance(raw_status, int):
|
||||
ok = raw_status < 400
|
||||
else:
|
||||
ok = bool(raw_status) or bool((body.get("data") or {}).get("success"))
|
||||
# Mensaje: TNS normal vs error de validación ASP.NET
|
||||
errors = body.get("errors")
|
||||
if errors and isinstance(errors, dict):
|
||||
msgs = [f"{k}: {v[0] if isinstance(v, list) else v}" for k, v in errors.items()]
|
||||
msg = " | ".join(msgs)
|
||||
else:
|
||||
msg = body.get("message") or (body.get("data") or {}).get("response") or body.get("title") or resp.text[:200]
|
||||
msg = raw_body # siempre guardamos el JSON crudo completo
|
||||
except Exception:
|
||||
ok = resp.is_success
|
||||
msg = resp.text[:200]
|
||||
if not ok and msg and any(s in str(msg).lower() for s in _YA_EXISTE):
|
||||
ok = True
|
||||
return ok, str(msg or "")
|
||||
msg = raw_body[:500]
|
||||
|
||||
if not ok and any(s in msg.lower() for s in _YA_WARNING):
|
||||
return "warning", msg
|
||||
return ("success" if ok else "error"), msg
|
||||
|
||||
|
||||
def _guardar_envio(user_id, tipo, factura, json_data, respuesta, ok,
|
||||
def _guardar_envio(user_id, tipo, factura, json_data, respuesta, status,
|
||||
fecha_inicio=None, fecha_fin=None, servicios=0,
|
||||
idrecepcion=None, contrato=None, cedula=None):
|
||||
try:
|
||||
@@ -830,10 +823,10 @@ def _guardar_envio(user_id, tipo, factura, json_data, respuesta, ok,
|
||||
user_id, tipo, factura, idrecepcion, contrato, cedula,
|
||||
fecha_inicio, fecha_fin,
|
||||
1, servicios,
|
||||
"success" if ok else "error",
|
||||
status,
|
||||
json_lib.dumps(json_data, indent=2, ensure_ascii=False)[:10000],
|
||||
respuesta[:1000] if respuesta else "",
|
||||
respuesta[:300] if respuesta else "",
|
||||
respuesta[:5000] if respuesta else "",
|
||||
respuesta[:500] if respuesta else "",
|
||||
datetime.now().isoformat(),
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
+12
-21
@@ -8,6 +8,7 @@ from app.auth import get_current_user
|
||||
from app.services.json_generator import _clean_times
|
||||
from app.services.api_client import get_tns_token, TNS_BASE
|
||||
from app.utils.activity import log_activity, get_ip
|
||||
from app.routes.automation import _parse_tns_resp
|
||||
|
||||
router = APIRouter(prefix="/logs", tags=["logs"])
|
||||
|
||||
@@ -144,8 +145,8 @@ async def reenviar_envio(envio_id: int, request: Request, user: dict = Depends(g
|
||||
conn.close()
|
||||
if not row:
|
||||
return JSONResponse({"success": False, "message": "Registro no encontrado"})
|
||||
if row["status"] == "success":
|
||||
return JSONResponse({"success": False, "message": "Ya fue enviado exitosamente"})
|
||||
if row["status"] in ("success", "warning"):
|
||||
return JSONResponse({"success": False, "message": "No aplica reenvío para este registro"})
|
||||
if not row["json_enviado"]:
|
||||
return JSONResponse({"success": False, "message": "Sin JSON guardado para reenviar"})
|
||||
|
||||
@@ -170,42 +171,32 @@ async def reenviar_envio(envio_id: int, request: Request, user: dict = Depends(g
|
||||
endpoint = (
|
||||
f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
|
||||
if tipo == "transaccion"
|
||||
else f"{TNS_BASE}/v2/facturacion/Ventas/Crear?codigosucursal={api_sucursal}"
|
||||
if tipo == "ventas"
|
||||
else f"{TNS_BASE}/v2/tablas/Tercero/Crear"
|
||||
)
|
||||
|
||||
ok = False
|
||||
new_status = "error"
|
||||
msg = ""
|
||||
raw_resp = ""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
r = await client.post(endpoint, json=rda_json, headers=headers)
|
||||
raw_resp = r.text
|
||||
try:
|
||||
data = r.json()
|
||||
if tipo == "transaccion":
|
||||
ok = bool(data.get("status") or (data.get("data") or {}).get("success", False))
|
||||
msg = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300])
|
||||
else:
|
||||
ok = r.is_success
|
||||
msg = data.get("message", "") or raw_resp[:200]
|
||||
except Exception:
|
||||
ok = r.status_code < 300
|
||||
msg = raw_resp[:300]
|
||||
new_status, msg = _parse_tns_resp(r)
|
||||
except Exception as ex:
|
||||
msg = str(ex)
|
||||
|
||||
if ok:
|
||||
if new_status != "error":
|
||||
conn = get_connection()
|
||||
conn.execute(
|
||||
"UPDATE envios SET status='success', mensaje_tns=?, respuesta_api=?, created_at=? WHERE id=?",
|
||||
(msg, raw_resp[:2000], datetime.now().isoformat(), envio_id)
|
||||
"UPDATE envios SET status=?, mensaje_tns=?, respuesta_api=?, created_at=? WHERE id=?",
|
||||
(new_status, msg[:500], msg[:5000], datetime.now().isoformat(), envio_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
log_activity(user["user_id"], user["username"], "reenvio_ok",
|
||||
f"Envío #{envio_id} factura {row['factura']} reenvío exitoso", get_ip(request))
|
||||
f"Envío #{envio_id} factura {row['factura']} reenvío {new_status}", get_ip(request))
|
||||
|
||||
return JSONResponse({"success": ok, "message": msg})
|
||||
return JSONResponse({"success": new_status != "error", "message": msg})
|
||||
|
||||
|
||||
@router.get("/detalle/{envio_id}")
|
||||
|
||||
+15
-12
@@ -12,10 +12,10 @@
|
||||
{% if s.tipo == 'transaccion' %}bg-purple-100 text-purple-700
|
||||
{% elif s.tipo == 'ventas' %}bg-emerald-100 text-emerald-700
|
||||
{% else %}bg-blue-100 text-blue-700{% endif %}">{{ s.tipo }}</span>
|
||||
<span class="{% if s.status == 'success' %}text-green-600{% else %}text-red-600{% endif %} font-bold text-sm">
|
||||
<span class="{% if s.status == 'success' %}text-green-600{% elif s.status == 'warning' %}text-yellow-600{% else %}text-red-600{% endif %} font-bold text-sm">
|
||||
{{ s.cnt }}
|
||||
</span>
|
||||
<span class="text-gray-400 text-xs">{{ 'exitosos' if s.status == 'success' else 'errores' }}</span>
|
||||
<span class="text-gray-400 text-xs">{{ 'enviados' if s.status == 'success' else ('ya existe' if s.status == 'warning' else 'errores') }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="bg-white rounded-lg border border-gray-200 px-4 py-2.5 shadow-sm flex items-center gap-2 text-gray-500 text-sm">
|
||||
@@ -42,8 +42,9 @@
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">Estado</label>
|
||||
<select name="status" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm min-w-[120px]">
|
||||
<option value="">Todos</option>
|
||||
<option value="success" {{ 'selected' if filtro_status == 'success' }}>Exitoso</option>
|
||||
<option value="error" {{ 'selected' if filtro_status == 'error' }}>Error</option>
|
||||
<option value="success" {{ 'selected' if filtro_status == 'success' }}>✓ Enviado</option>
|
||||
<option value="warning" {{ 'selected' if filtro_status == 'warning' }}>! Ya existe</option>
|
||||
<option value="error" {{ 'selected' if filtro_status == 'error' }}>✗ Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -109,7 +110,7 @@
|
||||
<tbody>
|
||||
{% for e in envios %}
|
||||
<tr class="border-b border-gray-50 hover:bg-gray-50 transition-colors
|
||||
{% if e.status == 'success' %}bg-green-50/25{% elif e.status == 'error' %}bg-red-50/25{% endif %}">
|
||||
{% if e.status == 'success' %}bg-green-50/25{% elif e.status == 'warning' %}bg-yellow-50/40{% elif e.status == 'error' %}bg-red-50/25{% endif %}">
|
||||
<td class="px-4 py-2.5 text-gray-400 font-mono">{{ e.id }}</td>
|
||||
<td class="px-2 py-2.5">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium
|
||||
@@ -123,11 +124,13 @@
|
||||
<td class="px-2 py-2.5 text-gray-500 font-mono">{{ e.idrecepcion or '-' }}</td>
|
||||
<td class="px-2 py-2.5 text-gray-500">{{ e.contrato or '-' }}</td>
|
||||
<td class="px-2 py-2.5">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium
|
||||
{% if e.status == 'success' %}bg-green-100 text-green-700
|
||||
{% else %}bg-red-100 text-red-700{% endif %}">
|
||||
{{ '✓ OK' if e.status == 'success' else '✗ Error' }}
|
||||
</span>
|
||||
{% if e.status == 'success' %}
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">✓ Enviado</span>
|
||||
{% elif e.status == 'warning' %}
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-700">! Ya existe</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">✗ Error</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-2 py-2.5 text-gray-500">
|
||||
{% if e.mensaje_tns %}
|
||||
@@ -247,9 +250,9 @@ async function verDetalle(id) {
|
||||
const data = await resp.json();
|
||||
if (data.error) { showToast(data.error, 'error'); return; }
|
||||
|
||||
const estadoLabel = data.status === 'success' ? '✓ Exitoso' : '✗ Error';
|
||||
const estadoLabel = data.status === 'success' ? '✓ Enviado' : data.status === 'warning' ? '! Ya existe' : '✗ Error';
|
||||
document.getElementById('modal-titulo').innerHTML =
|
||||
`<i class="fas fa-file-alt mr-2 text-blue-500"></i>Envío #${data.id} — <b>${data.tipo}</b> — <span class="${data.status === 'success' ? 'text-green-600' : 'text-red-600'}">${estadoLabel}</span>`;
|
||||
`<i class="fas fa-file-alt mr-2 text-blue-500"></i>Envío #${data.id} — <b>${data.tipo}</b> — <span class="${data.status === 'success' ? 'text-green-600' : data.status === 'warning' ? 'text-yellow-600' : 'text-red-600'}">${estadoLabel}</span>`;
|
||||
|
||||
const info = [
|
||||
data.factura ? `<span><b>Factura:</b> ${data.factura}</span>` : '',
|
||||
|
||||
Reference in New Issue
Block a user