Saltar a contenido

Auditoría SV Standards — Travel (2026-07-18)

Alcance: delta post-maratón del 2026-07-14 (122 archivos, commits 2026-07-15→18: billing checkout propio, dominio platform nuevo, traducción DeepL, paleta de marca, tema claro del admin, rework de marketing-site) contra SV_Standard_Backend.md, SV_Standard_Frontend.md y documentation_standard.md + re-verificación de invariantes globales por comando. La base pre-07-14 NO se re-auditó (verde hace 4 días, ver tasks_active.md de esa fecha).

Método: 1 agente auditor de backend (checklist de 75 reglas destiladas) + auditoría mecánica propia de frontends (18 Líneas Rojas grep-ables + lectura dirigida) y de documentación (checks estructurales); cada hallazgo verificado contra el código real antes de actuar — dos agentes murieron a mitad de camino por límite de sesión y sus áreas se re-ejecutaron a mano.

Veredicto por estándar

Estándar Veredicto Detalle
SV_Standard_Backend 🟡 → ✅ tras fixes 1 🔴 + 10 🟡 en el delta; el 🔴 y 6 🟡 corregidos en la sesión, 4 🟡 declarados como deuda con decisión
SV_Standard_Frontend 🟡 → ✅ tras fixes 1 LR13 real + 2 LR2 + 1 LR8 sin justificar + 1 LR5; todos corregidos o con excepción documentada salvo LR5 (deuda declarada)
documentation_standard ✅ (1 drift menor corregido) AGENTS ≤150 todos, Diátaxis 4/4, auto-generados al día, CODEOWNERS ✓; único drift: MAP.md sin el dominio platform

Hallazgos y disposición

🔴 CORREGIDO — POST /billing/charge sin idempotencia (LR#11)

charge_with_source generaba un nonce aleatorio por llamada y creaba una transacción Wompi nueva contra la tarjeta guardada en cada POST: doble click o retry por timeout = doble cobro real. Fix: guard in-process anti doble-cobro (_charges_in_flight + ventana de 60s _recent_charges; un fallo del gateway libera el guard para reintentar) → DuplicateChargeError → 409. 2 tests nuevos, incluido "una sola transacción creada en Wompi". Deuda declarada: v1 in-process válido con 1 worker (mismo criterio que el rate limiter); multi-worker exige Idempotency-Key DB-backed (= Fase 1b ya planificada).

🟡 CORREGIDOS (backend)

  • mypy --strict roto en main (5 errores en 3 archivos, entraron con el merge remoto): wompi.py no-any-return ×2, asignación Any|None→str en billing, pseudo-Protocol de platform (clase plana que mypy rechazaba en el router — ahora hereda typing.Protocol y el contrato se valida estructuralmente), anotación faltante. + 1 error ruff. Gate en 0 de nuevo.
  • Webhook de payments sin las lecciones de billing: (a) referencia ajena/desconocida → lanzaba 404 — con UN comercio Wompi y UNA URL de eventos para todas las verticales, todo evento de Gastro/suscripciones habría hecho reintentar a Wompi en bucle → ahora ACK 200 sin efectos (propio-sin-Payment se loguea como error y ACKea); (b) sin guard de orden — un PENDING rezagado regresaba un pago APPROVED a pending con la reserva ya completed → ahora se ignora el downgrade. 3 tests nuevos.
  • /platform/audit-log: instanciaba el repo y mapeaba schemas en el router (LR#1), devolvía {"items": []} con 200 para property inexistente (inconsistente con el resto) y aceptaba le=200 (tope estándar 100). Movido a PlatformService.list_audit_log con 404 consistente y le=100.
  • Logging (LR#9): payments/service.py usaba stdlib logging con %-format y billing/service.py mezclaba extra={...} (idioma stdlib que en structlog anida un dict no consultable) — todo convertido a structlog con kwargs planos.
  • Tipado: session: AnyAsyncSession en translation_service; # type: ignore de platform/repository con justificación textual.

🟡 CORREGIDOS (frontend)

  • LR13 real: TrialBanner (features/billing) importaba usePropertyProfile de features/property. Fix FSD canónico: el widget Layout (capa que SÍ compone features) llama al hook y baja subscriptionStatus/trialEndsAt por props; el banner quedó presentacional. Tests reescritos a props (sin mock del hook).
  • LR2 con excepción documentada: CardCheckoutForm (4 useState) y NequiCheckoutPane (1) — datos de pago PCI-sensibles y efímeros que viajan del navegador directo a Wompi (jamás a nuestro backend); mismo criterio que las 2 excepciones de FE-1.3. Comentario de justificación añadido en ambos.
  • LR8 con excepción documentada: WompiCheckout.loadWompiWidget inyecta el script third-party del Widget en <body> (lo exige Wompi) — justificado en comentario, aislado en función con nombre (mismo patrón que useSocialMeta/theme.ts).

🟡 CORREGIDO (documentación)

  • backend/MAP.md no mencionaba el dominio platform — fila añadida (responsabilidad, endpoints, auth interna, tests).

🟡 DEUDA DECLARADA (no corregida a propósito — ver TECH_DEBT de cada unidad)

  • billing/service.py 701 líneas (partir en subpaquete en sprint propio — mueve dinero).
  • platform importa 3 dominios hermanos (LR#22, decisión consciente; extraer la primitiva de audit a core/ cuando haya tercer consumidor).
  • Tests de ruta faltantes (LR#10) en mutaciones de platform y payment-sources/charge.
  • X-Operator-Actor sin verificar (aceptable con el BFF como único caller).
  • MembresiaTab.tsx 294 líneas (LR5) — partir en sprint de billing UI.
  • Stories de features/billing (LR10 — consistente con la práctica del repo: Stories solo en design-system; anotado por transparencia).

ℹ️ Sin acción (verificados como aceptables)

  • int(data["id"]) sin guard en billing (Wompi cambiaría contrato → 500 visible en Sentry).
  • travel_public_url con default de producción en config (patrón existente).
  • Posible PII en raw truncado de un warning del webhook (500 chars, ya truncado).

✅ Zonas verificadas limpias (evidencia, no suposición)

  • Webhook de billing: firma antes de parsear, compare_digest, anti-replay 300s, idempotencia tx+status, validación de monto contra catálogo, ERROR≠DECLINED, property ajena→200, fail-closed sin secreto — con 8 tests de integración contra PG real.
  • core/wompi.py: timeouts en las 3 llamadas externas, fail-closed.
  • Migraciones 0025×2/0026×2/0027: solo columnas sobre tablas existentes (ADR-010 no aplica y lo documentan); merge revision correcta; un solo head.
  • translation_service + hooks: timeout DeepL, tenant context antes de la primera query, filtro property_id en el UPDATE, referencias fuertes anti-GC, test cross-tenant.
  • Delta frontend: cero any/@ts-ignore/console.log/<div onClick>/colores hardcodeados en los archivos nuevos; features/billing con index.ts como API pública y 5 archivos de test.
  • Documentación: AGENTS.md ≤150 líneas en las 5 unidades (backend 132, el mayor), Diátaxis 4/4 en todas, references auto-generados sin drift (billing/platform/DEEPL presentes), CODEOWNERS existente, llms.txt sin links muertos.

Gates finales (post-fixes)

  • Backend: pytest suite completa + ruff + mypy --strict (ver cierre de sesión en tasks_active.md — todos en verde al momento del commit de esta auditoría).
  • Frontends: admin-panel 162+ tests · public-hotel 38 · marketing-site 5; tsc/lint/build verdes (línea base re-verificada al inicio y gates del pre-commit en el cierre).