Saltar al contenido
Finance & Banking
Evolution Program
Inicio / modules / 17-pagos-transfronterizos-remesas-y-liquidacion / solutions

Solución de referencia — Laboratorio 2: motor de rutas

Material docente.

Por qué el orden de los criterios importa

Los tres primeros son filtros; los tres últimos son factores.

FILTRO        elimina rutas. No se compensa con nada.
FACTOR        ordena las rutas que quedan.

SI SE PONDERAN LOS FILTROS
  una ruta muy barata que no soporta un control exigido
  puede ganar la comparación
  → el motor elige una operación no autorizada

Es el mismo principio de la clase 12: en sanciones no hay apetito de riesgo. Un filtro de cumplimiento que se pondera deja de ser un control.

Implementación de referencia

def elegir(pago: Pago, rutas: list[Ruta], pesos: Pesos) -> Decision:
    elegibles = [r for r in rutas if r.admite(pago)]
    if not elegibles:
        return Decision(None, None, motivo="ninguna ruta admite este pago")

    conformes = [r for r in elegibles if r.soporta(pago.controles)]
    if not conformes:
        return Decision(None, None,
                        motivo=f"ninguna ruta soporta {pago.controles}")

    disponibles = [r for r in conformes if r.disponible_ahora()]
    if not disponibles:
        return Decision(None, None, motivo="todas las rutas no disponibles")

    # Regla de la clase 14: el enlace directo gana antes de ponderar.
    enlaces = [r for r in disponibles if r.tipo == "enlace_pagos_inmediatos"]
    if enlaces:
        elegida = min(enlaces, key=lambda r: r.coste_total(pago))
        return Decision(elegida, _siguiente(disponibles, elegida),
                        motivo="enlace de pagos inmediatos disponible")

    ordenadas = sorted(disponibles, key=lambda r: r.puntuacion(pago, pesos))
    return Decision(ordenadas[0], ordenadas[1] if len(ordenadas) > 1 else None,
                    motivo=_explicar(ordenadas[0], ordenadas, pesos))

El campo motivo

BIEN   «enlace de pagos inmediatos disponible»
BIEN   «menor coste total (2,4 % frente a 3,1 %) con plazo dentro
        del compromiso; la alternativa es 6 h más rápida y 0,7 pp más cara»
MAL    «mejor puntuación»
MAL    «ruta preferente»

Un motivo que no permite reconstruir la decisión no es un motivo: es una etiqueta.

Las pruebas que separan un buen motor de uno malo

def test_un_filtro_no_se_compensa_con_precio(motor):
    barata_no_conforme = Ruta(coste=0.001, soporta_travel_rule=False)
    cara_conforme = Ruta(coste=0.05, soporta_travel_rule=True)
    pago = Pago(controles={"travel_rule"})

    decision = motor.elegir(pago, [barata_no_conforme, cara_conforme])

    assert decision.ruta is cara_conforme


def test_cambiar_un_peso_cambia_la_eleccion(motor):
    rapida_cara = Ruta(plazo_h=1, coste=0.04)
    lenta_barata = Ruta(plazo_h=30, coste=0.012)

    con_prisa = motor.elegir(pago, [rapida_cara, lenta_barata],
                             Pesos(tiempo=0.8, coste=0.2))
    con_precio = motor.elegir(pago, [rapida_cara, lenta_barata],
                              Pesos(tiempo=0.2, coste=0.8))

    assert con_prisa.ruta is rapida_cara
    assert con_precio.ruta is lenta_barata

Configuración de pesos

Se versiona y se documenta, como el contrato de una API:

{
  "version": "2026-08-06",
  "pesos": { "tiempo": 0.35, "coste": 0.50, "riesgo": 0.15 },
  "motivo_del_cambio": "el coste pesa mas tras la queja de la asociacion",
  "aprobado_por": "comite de pagos",
  "vigente_desde": "2026-08-06"
}

Sin motivo_del_cambio y aprobado_por, nadie puede explicar dentro de un año por qué el motor elige lo que elige.

Errores frecuentes en la corrección

Error Por qué se penaliza
Ponderar el cumplimiento Convierte un control en una preferencia
Motor sin alternativa Un corredor cae y no hay plan
Pesos codificados en el código No se pueden auditar ni cambiar sin desplegar
Motivo genérico No permite corregir una elección equivocada
Ignorar la frescura del dato de disponibilidad Se enruta hacia una ruta caída

Límites