Repository navigation
feat(risk): adaptador SanctionsLists con matching difuso (E6-T07) - #15
Merged
Merged
Conversation
- Interfaz SanctionsLists (screen + list_versions) con dominio puro de similitud en risk/domain/screening.py: normalizacion (acentos, mayusculas, puntuacion, orden de tokens) y Jaro-Winkler stdlib, tolerante al orden. - MockSanctionsLists determinista sobre 3 listas ficticias versionadas (OFAC/WORLDCHECK/PEP) en adapters/data/sanctions/*.json, sin PII real. - HttpSanctionsLists: POST /screen y GET /lists con timeout, reintentos con backoff y circuito; API key solo en cabecera Authorization, nunca en URL, cuerpo ni logs (solo hash del sujeto). - Fabrica create_sanctions_lists() (SANCTIONS_PROVIDER, default mock). - 53 pruebas nuevas; suite 2217 passed + 1 skipped; ruff/black limpios. - Sin migracion ni endpoints nuevos (adaptador interno, se consume en E6-T09).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumen para tontos
El banco ahora puede comparar el nombre de una persona o empresa contra listas de sanciones y de personas políticamente expuestas (PEP). Detecta la coincidencia aunque el nombre esté escrito con acentos, en otro orden o con una pequeña falta de ortografía. Por ahora usa listas de prueba con nombres inventados, así que no hay riesgo de señalar a nadie real. No bloquea ninguna operación: solo informa las coincidencias para que el área de cumplimiento las revise.
Cómo probarlo
Entra al backend:
Corre las pruebas de esta tarea:
Resultado esperado:
53 passed.Verifica estilo y formato:
ruff check . black --check .Ambos deben salir limpios (
All checks passed!y488 files would be left unchanged).Prueba el adaptador a mano (coincidencia exacta y difusa):
Quispe Aurelio Mendoza) →OFACconmatched=Trueymatch_score=1.0.Quispien vez deQuispe) →OFACconmatched=Trueymatch_score=0.9818(por encima del umbral0.85).matched=Falsey su score parcial, para que se vea el mejor candidato por lista.Nota: todavía no hay endpoint HTTP. Esto es un adaptador interno; se conectará al cotejo de onboarding y transacciones en
E6-T09.Resumen técnico
SanctionsLists(backend/app/adapters/sanctions_lists.py): dos métodos,screen(subject_type, name, document_number?, country?)ylist_versions().screendevuelve el mejor candidato por lista (siempre unScreeningHitpor lista, conmatched=falsecuando no hay coincidencia), ylist_versionsexpone la versión vigente de cada lista.backend/app/modules/risk/domain/screening.py): normalización de texto (mayúsculas, sin acentos, sin puntuación, espacios colapsados) + Jaro-Winkler implementado en stdlib, sin dependencias nuevas. La comparación es tolerante al orden de los tokens ("QUISPE AURELIO"vs"AURELIO QUISPE"→1.0). Funciones:normalize_text,jaro_winkler,name_similarity,is_match.MockSanctionsLists: determinista, sin red. Carga 3 listas ficticias versionadas desde JSON (backend/app/adapters/data/sanctions/ofac.json,worldcheck.json,pep.json). Soporta modos de falla simulada (timeout,down) para probar el manejo de errores.HttpSanctionsLists: contratoPOST {base_url}/screenyGET {base_url}/lists, con timeout, reintentos con backoff exponencial y circuito (se abre tras N fallas consecutivas y se cierra solo tras un cooldown). La API key viaja únicamente en la cabeceraAuthorization: Bearer ...; nunca en URL, cuerpo ni logs. Los errores se traducen a códigos tipados:SANCTIONS_TIMEOUT(502),SANCTIONS_UNAVAILABLE(503),VALIDATION_ERROR(422).create_sanctions_lists()exportada desdeapp.adapters: elige implementación porSANCTIONS_PROVIDER(mockpor defecto; valor desconocido cae amock).subject_hash), el código de lista, el score y el código de error. Nunca el nombre, el documento ni la API key.Alcance
Incluido:
backend/app/adapters/sanctions_lists.py(nuevo): interfaz, payloads tipados, config por entorno, mock, http y fábrica.backend/app/adapters/data/sanctions/*.json(nuevos): 3 semillas ficticias versionadas (OFAC,WORLDCHECK,PEP).backend/app/modules/risk/domain/screening.py(nuevo): normalización y Jaro-Winkler puros.backend/app/adapters/__init__.py: exportacreate_sanctions_lists.backend/tests/test_e6_t07_sanctions_lists.py(nuevo): 53 pruebas.backend/.env.example: variablesSANCTIONS_*documentadas (sin secretos reales).docs/02-arquitectura.md,docs/modules/README.md,docs/tasks/E6-T07.md: documentación y reporte de la tarea.No incluido: endpoints, casos de uso, tablas, migraciones, listas reales.
Criterios de aceptación
match_score=1.0; un nombre con error de tipeo o tokens en otro orden da un score alto ymatched=truecuando supera el umbral, conmatched_fieldsindicando qué campos coincidieron (name,document_number,country) y sin exponer valores crudos.matched=falseymatched_fields=(), sin falsos positivos.503y caída sostenida se traducen aSANCTIONS_TIMEOUT/SANCTIONS_UNAVAILABLE; el circuito se abre y se recupera tras el cooldown. La API key no aparece en URL, cuerpo ni logs.match_score; los logs solo contienen hash del sujeto y metadatos, nunca PII.ruff checkyblack --checklimpios; suite completa en verde.Pruebas
backend/tests/test_e6_t07_sanctions_lists.py→ 53 passed.ruff check .→All checks passed!black --check .→488 files would be left unchangedSupuestos
SANCTIONS_SCREENING_THRESHOLDcon default0.85, espejo derisk.screening_threshold. El valor límite cuenta como coincidencia (score >= threshold).countrypuede sumar amatched_fields, pero por sí solo nunca crea un hit; la coincidencia se decide por nombre/documento.screendevuelve un hit por lista (el de mayor score), no todos los candidatos. Así el consumidor decide sin recorrer listas completas.FIC-en los documentos) y están marcados como ficticios en el propio JSON. No hay PII real.httprecalibramatched: si el proveedor respondematched, el adaptador lo recalcula localmente con el umbral propio para no depender de la configuración del proveedor.Fuera de alcance
E6-T08: hoy las semillas son JSON versionados en el repo; la ingesta, actualización periódica y persistencia de versiones reales quedan para esa tarea.E6-T09: aquí solo se entrega el adaptador. El caso de uso, el endpoint y la tablascreening_resultsllegan con esa tarea.Riesgos
0.85es un punto de partida; nombres muy cortos o con muchas partículas pueden puntuar distinto. Se mitiga devolviendo siempre el mejor candidato con su score para que cumplimiento calibre con datos reales.weak aliases).E6-T08las reemplaza.httpasumido:POST /screenyGET /listsson una suposición de integración; si el proveedor real difiere, habrá que ajustar el parseo (ya tolerante a alias y adataanidado) enE6-T08.