feat: Add poblacion_apta service to docker-compose, enhance API gateway for new service integration, and update README with service details. Introduce new database tables for poblacion_apta_jobs and poblacion_apta_rows, and implement functionality for handling cop/mod calculations in the cleaning service.

This commit is contained in:
erik
2026-07-28 04:29:14 -05:00
parent 447359166b
commit e73e425e4b
47 changed files with 6978 additions and 84 deletions
+35 -8
View File
@@ -6,7 +6,11 @@ from typing import Any, Optional
import pandas as pd
from .cop_mod import _apply_cop_mod_split, merge_moderadora_config
from .cop_mod import (
_apply_cop_mod_bruto_neto,
_apply_cop_mod_split,
merge_moderadora_config,
)
from .documents import _apply_doc_id_split_columns
from .notas import _apply_nota_extract_columns
from .rules import get_rule_fn, get_rule_name
@@ -20,20 +24,24 @@ def apply_rules(
*,
cop_mod_fecha_column: Optional[dict[str, str]] = None,
cop_mod_config: Optional[dict[str, Any]] = None,
cop_mod_bruto_column: Optional[dict[str, str]] = None,
cop_mod_neto_column: Optional[dict[str, str]] = None,
) -> tuple[pd.DataFrame, list[str]]:
"""Aplica las reglas seleccionadas a cada columna.
`rules_per_column` es un dict de `columna -> lista de IDs de reglas en el
orden a aplicar`. La regla `cop_mod_split` se difiere a una segunda pasada
tras el resto, para que la columna de fecha pueda normalizarse antes.
orden a aplicar`. Las reglas `cop_mod_split` y `cop_mod_bruto_neto` se
difieren a una segunda pasada tras el resto.
`cop_mod_fecha_column`: mapa columna de importe -> nombre de columna Fecha Servicio.
`cop_mod_config`: opcional; puede incluir ``moderadora_by_year`` (dict año → lista).
`cop_mod_fecha_column`: mapa columna ancla -> Fecha Servicio (para split).
`cop_mod_bruto_column` / `cop_mod_neto_column`: mapas ancla -> bruto / neto.
`cop_mod_config`: opcional; puede incluir ``moderadora_by_year``.
"""
result = df.copy()
log: list[str] = []
moderadora_years = merge_moderadora_config(cop_mod_config)
deferred = {"cop_mod_split", "cop_mod_bruto_neto"}
def _run_rules_for_column(col: str, rule_ids: list[str]) -> None:
if not rule_ids:
@@ -74,12 +82,12 @@ def apply_rules(
logger.exception("Error aplicando regla %s a %s", rule_id, col)
log.append(f"[ERROR] '{col}' regla '{rule_id}': {exc}")
# Fase 1: todas las reglas excepto cop_mod_split
# Fase 1: todas las reglas excepto COP/MOD diferidas
for col, rule_ids in rules_per_column.items():
filtered = [r for r in rule_ids if r != "cop_mod_split"]
filtered = [r for r in rule_ids if r not in deferred]
_run_rules_for_column(col, filtered)
# Fase 2: Separa COP/MOD sobre el DataFrame ya transformado
# Fase 2a: Separa COP/MOD (cuotas por año)
fecha_map = cop_mod_fecha_column or {}
for col, rule_ids in rules_per_column.items():
if "cop_mod_split" not in rule_ids:
@@ -102,4 +110,23 @@ def apply_rules(
logger.exception("Error aplicando COP/MOD a %s", col)
log.append(f"[ERROR] '{col}' regla 'cop_mod_split': {exc}")
# Fase 2b: COP/MOD = bruto − neto
bruto_map = cop_mod_bruto_column or {}
neto_map = cop_mod_neto_column or {}
for col, rule_ids in rules_per_column.items():
if "cop_mod_bruto_neto" not in rule_ids:
continue
bruto_col = (bruto_map.get(col) or col).strip()
neto_col = (neto_map.get(col) or "").strip()
if not neto_col:
log.append(
f"[WARN] COP/MOD (bruto−neto) en '{col}': indique la columna «Valor neto»; se omite."
)
continue
try:
_apply_cop_mod_bruto_neto(result, bruto_col, neto_col, log)
except Exception as exc: # noqa: BLE001
logger.exception("Error aplicando COP/MOD bruto−neto a %s", col)
log.append(f"[ERROR] '{col}' regla 'cop_mod_bruto_neto': {exc}")
return result, log
@@ -160,3 +160,59 @@ def _apply_cop_mod_split(
f"[OK] '{amount_col}' -> '{ccol}', '{mcol}' <- Separa COP/MOD "
f"(fecha: '{fecha_column}')"
)
def _alloc_cop_mod_diff_column_name(existing: set[str]) -> str:
"""Nombre único para la columna resultado (preferido: cop/mod)."""
preferred = "cop/mod"
if preferred not in existing:
return preferred
for i in range(2, 200):
name = f"cop/mod_{i}"
if name not in existing:
return name
raise ValueError("No se pudo asignar nombre de columna único para cop/mod.")
def _apply_cop_mod_bruto_neto(
result: pd.DataFrame,
bruto_col: str,
neto_col: str,
log: list[str],
) -> None:
"""Crea columna ``cop/mod`` = valor bruto − valor neto."""
if bruto_col not in result.columns:
log.append(
f"[WARN] COP/MOD (bruto−neto): columna bruto '{bruto_col}' no existe; se omite."
)
return
if neto_col not in result.columns:
log.append(
f"[WARN] COP/MOD (bruto−neto): columna neto '{neto_col}' no existe; se omite."
)
return
if bruto_col == neto_col:
log.append(
f"[WARN] COP/MOD (bruto−neto): bruto y neto son la misma columna '{bruto_col}'; se omite."
)
return
bruto_ser = result[bruto_col]
neto_ser = result[neto_col]
bruto_hint = _infer_decimal_hint_from_column(bruto_ser.astype(str))
neto_hint = _infer_decimal_hint_from_column(neto_ser.astype(str))
out_col = _alloc_cop_mod_diff_column_name(set(result.columns))
values: list[Any] = []
for i in range(len(result)):
bruto_i = _amount_cell_to_optional_int(bruto_ser.iloc[i], bruto_hint)
neto_i = _amount_cell_to_optional_int(neto_ser.iloc[i], neto_hint)
if bruto_i is None or neto_i is None:
values.append("")
continue
values.append(bruto_i - neto_i)
result[out_col] = values
log.append(
f"[OK] '{out_col}' = '{bruto_col}' − '{neto_col}' <- COP/MOD (bruto − neto)"
)
@@ -121,6 +121,15 @@ register_rule(
),
fn=special._rule_passthrough_series,
)
register_rule(
"cop_mod_bruto_neto",
name="COP/MOD (bruto − neto)",
description=(
"Calcula la columna «cop/mod» como Valor bruto − Valor neto. "
"Elija ambas columnas en la tarjeta (se detectan automáticamente si existen)."
),
fn=special._rule_passthrough_series,
)
register_rule(
"nota_paciente_autorizacion",
name="Extraer datos de notas (documento y autorización)",
@@ -101,7 +101,7 @@ def _applicable_rules_for_type(inferred_type: str) -> list[str]:
"""Identificadores de reglas a mostrar según el tipo inferido (sin redundancias)."""
base = ["strip_special_spaces", "strip_special_keep_date_seps", "strip_spaces"]
if inferred_type == TYPE_INTEGER:
return base + ["cop_mod_split"]
return base + ["cop_mod_split", "cop_mod_bruto_neto"]
if inferred_type == TYPE_DECIMAL:
return base + [
"to_integer",
@@ -109,6 +109,7 @@ def _applicable_rules_for_type(inferred_type: str) -> list[str]:
"doc_id_split_tipo_numero",
"doc_type_homologate",
"cop_mod_split",
"cop_mod_bruto_neto",
]
if inferred_type == TYPE_DATE:
return base + [
@@ -125,6 +126,7 @@ def _applicable_rules_for_type(inferred_type: str) -> list[str]:
"doc_type_homologate",
"date_to_ddmmyyyy",
"cop_mod_split",
"cop_mod_bruto_neto",
"nota_paciente_autorizacion",
]
# Texto / categórico / boolean / empty → todas las de limpieza de texto + las
@@ -136,6 +138,7 @@ def _applicable_rules_for_type(inferred_type: str) -> list[str]:
"doc_type_homologate",
"date_to_ddmmyyyy",
"cop_mod_split",
"cop_mod_bruto_neto",
"nota_paciente_autorizacion",
]