Bot de Telegram para monitoreo de notificaciones judiciales del PJ Paraguay. Multi-tenant con onboarding conversacional, audit log, y polling automático. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
257 lines
9.2 KiB
Python
257 lines
9.2 KiB
Python
"""
|
|
NUEVOS MÉTODOS para agregar a csj_client.py
|
|
Basados en reconocimiento del 22/05/2026.
|
|
|
|
Agregar:
|
|
1. La clase CasoJudicial después de NotificacionPendiente
|
|
2. La clase ActuacionCaso después de CasoJudicial
|
|
3. Los métodos buscar_expedientes() y listar_actuaciones() dentro de CSJClient
|
|
"""
|
|
|
|
# ─── NUEVOS MODELOS (agregar después de NotificacionPendiente) ────────────────
|
|
|
|
class CasoJudicial:
|
|
"""Expediente judicial devuelto por búsqueda."""
|
|
|
|
__slots__ = (
|
|
"cod_caso_judicial",
|
|
"caratula",
|
|
"origen",
|
|
"nro_expediente_numero",
|
|
"nro_expediente_anio",
|
|
"circunscripcion",
|
|
"nombre_circunscripcion",
|
|
"descripcion_despacho_judicial",
|
|
"descripcion_estado_despacho_caso",
|
|
"cod_estado_despacho_caso",
|
|
"es_corte",
|
|
"cantidad_total_elementos",
|
|
)
|
|
|
|
def __init__(self, data: dict) -> None:
|
|
self.cod_caso_judicial: int = data["codCasoJudicial"]
|
|
self.caratula: str = data.get("caratula") or "sin carátula"
|
|
self.origen: int = data.get("origen", 0)
|
|
self.nro_expediente_numero: int | None = data.get("nroExpedienteNumero")
|
|
self.nro_expediente_anio: int | None = data.get("nroExpedienteAnio")
|
|
self.circunscripcion: str | None = data.get("nombreCircunscripcion") or data.get("circunscripcion")
|
|
self.descripcion_despacho_judicial: str | None = data.get("descripcionDespachoJudicial")
|
|
self.descripcion_estado_despacho_caso: str | None = data.get("descripcionEstadoDespachoCaso")
|
|
self.cod_estado_despacho_caso: int = data.get("codEstadoDespachoCaso", 0)
|
|
self.es_corte: int = data.get("esCorte", 0)
|
|
self.cantidad_total_elementos: int = data.get("cantidadTotalElementos", 0)
|
|
|
|
@property
|
|
def expediente_str(self) -> str:
|
|
if self.nro_expediente_numero and self.nro_expediente_anio:
|
|
return f"{self.nro_expediente_numero}/{self.nro_expediente_anio}"
|
|
return "sin número"
|
|
|
|
@property
|
|
def estado(self) -> str:
|
|
return self.descripcion_estado_despacho_caso or "sin estado"
|
|
|
|
@property
|
|
def resumen(self) -> str:
|
|
"""Texto compacto para mostrar en listados."""
|
|
partes = [self.caratula]
|
|
if self.expediente_str != "sin número":
|
|
partes.append(f"Exp. {self.expediente_str}")
|
|
if self.circunscripcion:
|
|
partes.append(self.circunscripcion)
|
|
partes.append(self.estado)
|
|
return " | ".join(partes)
|
|
|
|
|
|
class ActuacionCaso:
|
|
"""Actuación/movimiento dentro de un expediente."""
|
|
|
|
__slots__ = (
|
|
"cod_actuacion_caso",
|
|
"cod_caso_judicial",
|
|
"cod_tipo_actuacion_global",
|
|
"descripcion_actuacion",
|
|
"descripcion_tipo_actuacion",
|
|
"descripcion_despacho",
|
|
"descripcion_proceso_caso",
|
|
"fecha_actuacion",
|
|
"fecha_finalizacion",
|
|
"estado",
|
|
"grupo",
|
|
"origen",
|
|
)
|
|
|
|
def __init__(self, data: dict) -> None:
|
|
self.cod_actuacion_caso: int = data["codActuacionCaso"]
|
|
self.cod_caso_judicial: int = data["codCasoJudicial"]
|
|
self.cod_tipo_actuacion_global: int | None = data.get("codTipoActuacionGlobal")
|
|
self.descripcion_actuacion: str | None = data.get("descripcionActuacion")
|
|
self.descripcion_tipo_actuacion: str | None = data.get("descripcionTipoActuacion")
|
|
self.descripcion_despacho: str | None = data.get("descripcionDespacho")
|
|
self.descripcion_proceso_caso: str | None = data.get("descripcionProcesoCaso")
|
|
self.fecha_actuacion: str | None = data.get("fechaActuacion")
|
|
self.fecha_finalizacion: str | None = data.get("fechaFinalizacion")
|
|
self.estado: str | None = data.get("estado")
|
|
self.grupo: str | None = data.get("grupo")
|
|
self.origen: int | None = data.get("origen")
|
|
|
|
@property
|
|
def fecha_formateada(self) -> str:
|
|
if not self.fecha_actuacion:
|
|
return "sin fecha"
|
|
try:
|
|
from datetime import datetime
|
|
dt = datetime.fromisoformat(self.fecha_actuacion)
|
|
return dt.strftime("%d/%m/%Y")
|
|
except ValueError:
|
|
return self.fecha_actuacion
|
|
|
|
@property
|
|
def tipo(self) -> str:
|
|
return self.descripcion_tipo_actuacion or f"Tipo {self.cod_tipo_actuacion_global}"
|
|
|
|
|
|
# ─── NUEVOS MÉTODOS para la clase CSJClient ──────────────────────────────────
|
|
|
|
async def buscar_expedientes(
|
|
self,
|
|
*,
|
|
caratula: str | None = None,
|
|
numero: int | None = None,
|
|
anio: int | None = None,
|
|
cantidad: int = 10,
|
|
offset: int = 0,
|
|
) -> tuple[list[CasoJudicial], int]:
|
|
"""
|
|
Busca expedientes por carátula y/o número+año.
|
|
Retorna (lista de casos, total de registros).
|
|
|
|
Parámetros confirmados por reconocimiento 22/05/2026:
|
|
- caratula: búsqueda parcial por texto en carátula
|
|
- NroExpedienteNumero: número del expediente (sin año)
|
|
- NroExpedienteAnho: año del expediente (con 'h', no 'ñ')
|
|
- offset/cantidad: paginación
|
|
|
|
Al menos uno de caratula, numero, o (numero+anio) debe proveerse.
|
|
"""
|
|
if not caratula and not numero:
|
|
raise ValueError("Se requiere al menos carátula o número de expediente")
|
|
|
|
session = await self._asegurar_sesion()
|
|
assert self._http is not None
|
|
|
|
params: dict = {
|
|
"offset": offset,
|
|
"cantidad": cantidad,
|
|
"caratula": caratula or "",
|
|
}
|
|
if numero is not None:
|
|
params["NroExpedienteNumero"] = numero
|
|
if anio is not None:
|
|
params["NroExpedienteAnho"] = anio
|
|
|
|
try:
|
|
r = await self._http.get(
|
|
"/api/gestion-partes/CasoJudicial",
|
|
params=params,
|
|
headers=session.auth_headers(),
|
|
)
|
|
except httpx.HTTPError as e:
|
|
raise CSJConnectionError(f"Error al buscar expedientes: {e}") from e
|
|
|
|
if r.status_code == 401:
|
|
self._session = None
|
|
session = await self._asegurar_sesion()
|
|
r = await self._http.get(
|
|
"/api/gestion-partes/CasoJudicial",
|
|
params=params,
|
|
headers=session.auth_headers(),
|
|
)
|
|
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
|
|
items = data.get("resultado") or []
|
|
total = data.get("cantidadTotalRegistros") or 0
|
|
|
|
casos = []
|
|
for item in items:
|
|
try:
|
|
casos.append(CasoJudicial(item))
|
|
except (KeyError, TypeError) as e:
|
|
logger.warning("error_parseando_caso", error=str(e), item=str(item)[:200])
|
|
|
|
logger.info("expedientes_encontrados", total=total, devueltos=len(casos))
|
|
return casos, total
|
|
|
|
async def listar_actuaciones(
|
|
self,
|
|
cod_caso_judicial: int,
|
|
origen: int,
|
|
cantidad: int = 10,
|
|
offset: int = 0,
|
|
incluir_notificaciones: bool = False,
|
|
) -> tuple[list[ActuacionCaso], int]:
|
|
"""
|
|
Retorna las actuaciones (movimientos) de un expediente.
|
|
|
|
URL confirmada por reconocimiento 22/05/2026:
|
|
GET /api/gestion-partes/CasoJudicial/{codCasoJudicial}/{origen}/0/actuaciones
|
|
|
|
El parámetro {origen} es el campo 'origen' del CasoJudicial:
|
|
- origen=2: expediente del sistema viejo integrado
|
|
- origen=0: expediente del sistema nuevo
|
|
|
|
El segundo posicional (/0/) parece fijo en todos los casos observados.
|
|
"""
|
|
session = await self._asegurar_sesion()
|
|
assert self._http is not None
|
|
|
|
url = f"/api/gestion-partes/CasoJudicial/{cod_caso_judicial}/{origen}/0/actuaciones"
|
|
|
|
try:
|
|
r = await self._http.get(
|
|
url,
|
|
params={
|
|
"offset": offset,
|
|
"cantidad": cantidad,
|
|
"incluirNotificaciones": str(incluir_notificaciones).lower(),
|
|
},
|
|
headers=session.auth_headers(),
|
|
)
|
|
except httpx.HTTPError as e:
|
|
raise CSJConnectionError(f"Error al listar actuaciones: {e}") from e
|
|
|
|
if r.status_code == 401:
|
|
self._session = None
|
|
session = await self._asegurar_sesion()
|
|
r = await self._http.get(
|
|
url,
|
|
params={
|
|
"offset": offset,
|
|
"cantidad": cantidad,
|
|
"incluirNotificaciones": str(incluir_notificaciones).lower(),
|
|
},
|
|
headers=session.auth_headers(),
|
|
)
|
|
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
|
|
items = data.get("resultado") or []
|
|
total = data.get("cantidadTotalRegistros") or 0
|
|
|
|
actuaciones = []
|
|
for item in items:
|
|
try:
|
|
actuaciones.append(ActuacionCaso(item))
|
|
except (KeyError, TypeError) as e:
|
|
logger.warning("error_parseando_actuacion", error=str(e), item=str(item)[:200])
|
|
|
|
logger.info(
|
|
"actuaciones_listadas",
|
|
cod_caso=cod_caso_judicial,
|
|
total=total,
|
|
devueltas=len(actuaciones),
|
|
)
|
|
return actuaciones, total |