mvvm portal web solventa_1 - martin-hendricks/MISO-PROYECTO-FINAL-SOLVENTA GitHub Wiki

Patrón MVVM para el Portal Web de Solventa (Angular)

Alcance: solo el canal web. No cubre el cliente móvil, el BFF ni los microservicios. Referencias: caso MISW4501-2026 (§5.1 recorridos críticos, §6 atributos de calidad) y wiki del proyecto (Hoja de trabajo, Estrategia de pruebas §4.2).


1. Por qué MVVM y no MVC en el portal

El portal web de Solventa no es un sitio de páginas renderizadas por servidor: es una SPA Angular donde la interfaz cambia continuamente en respuesta a datos que llegan de forma asíncrona, parcial y a veces degradada (cotización en curso, perfilamiento con Open Finance, estado de siniestro). En ese contexto:

Criterio MVC MVVM (elegido)
Acoplamiento Vista↔Modelo El Modelo notifica a la Vista → acoplamiento residual Nulo: el ViewModel es el único intermediario
Encaje con Angular Forzado (Angular no tiene "Controller") Nativo: binding declarativo + signals/RxJS
Testabilidad de la lógica de pantalla Requiere DOM ViewModel se prueba sin DOM (Karma/Jest)
Estados intermedios (loading, degradado) Difíciles de modelar Se modelan como estado explícito del VM

MVVM además habilita directamente dos exigencias del caso: el escenario de dependencia degradada (§6.1) necesita que la vista sepa mostrar "precio referencial en caché" sin que el modelo de dominio conozca la UI; y la facilidad de modificación (§6.5) exige que un cambio de reglas de rating no obligue a tocar componentes visuales.


2. Mapeo del patrón a Angular

View — componentes pasivos

  • Componentes standalone con ChangeDetectionStrategy.OnPush.
  • Cero lógica de negocio, cero HTTP, cero if de reglas de dominio. Solo binding y eventos.
  • Cero cadenas literales: todo texto viene de catálogos i18n (ngx-translate / @angular/localize), requisito explícito de la estrategia de pruebas.
  • Cada elemento interactivo lleva data-testid estable → habilita el PageObject Model exigido para Cucumber, sin locators frágiles.
<!-- cotizacion.view.html -->
<sv-quote-form
  [disabled]="vm.state().status === 'loading'"
  (submit)="vm.cotizar($event)">
</sv-quote-form>

<sv-price-card
  *ngIf="vm.state().status === 'ready' || vm.state().status === 'degraded'"
  [price]="vm.state().data"
  [degraded]="vm.state().status === 'degraded'"
  [degradedMessage]="'quote.degraded_notice' | translate">
</sv-price-card>

ViewModel — estado y lógica de presentación

Una clase por vista, inyectada en el componente. Expone un solo estado observable y un conjunto de intenciones. No conoce HttpClient, ni Router, ni el DOM.

export type VmState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'ready';    data: T; source: 'live' }
  | { status: 'degraded'; data: T; source: 'cache'; reason: DegradeReason }
  | { status: 'error';    error: UiError };

@Injectable()
export class CotizacionViewModel {
  private readonly _state = signal<VmState<PrecioVM>>({ status: 'idle' });
  readonly state = this._state.asReadonly();

  constructor(private readonly obtenerCotizacion: ObtenerCotizacionUseCase) {}

  async cotizar(form: CotizacionFormVM): Promise<void> {
    this._state.set({ status: 'loading' });
    const r = await this.obtenerCotizacion.execute(CotizacionMapper.toDomain(form));
    this._state.set(CotizacionMapper.toVmState(r)); // ready | degraded | error
  }
}

Puntos clave:

  • El estado degradado es de primera clase, no un catch improvisado. Es lo que permite cumplir el escenario "un proveedor supera 700 ms o falla → degradar con caché sin exceder el presupuesto".
  • El ViewModel traduce dominio → ViewModel objects (PrecioVM), nunca expone DTOs ni entidades crudas a la vista.

Model — dominio, casos de uso y puertos

  • Modelo de dominio puro TypeScript: Cotizacion, Poliza, Siniestro, PerfilRiesgo, Consentimiento. Sin decoradores de Angular.
  • Casos de uso (ObtenerCotizacion, EmitirPoliza, PerfilarDeudor, RegistrarSiniestro, OperarPoliza): orquestan dominio + puertos + políticas de resiliencia.
  • Puertos (interfaces): ICotizacionRepo, IPolizaRepo, ISiniestroRepo, IPerfilRepo. Los adaptadores HTTP los implementan. Esto es hexagonal aplicada dentro del front y es lo que hace que cambiar el contrato del BFF no toque ni un ViewModel ni un componente.

3. Los cinco recorridos críticos, en MVVM

Recorrido View ViewModel Caso de uso Presupuesto (§6.1)
HU-10 Cotización embebida CotizacionView CotizacionViewModel ObtenerCotizacion p95 ≤ 250 ms
HU-19 Suscripción y emisión SuscripcionEmisionView SuscripcionViewModel EmitirPoliza p95 ≤ 1,5 s
HU-58 Perfilamiento hipotecario PerfilamientoHipotecarioView PerfilamientoViewModel PerfilarDeudor p95 ≤ 400 ms
Siniestro asistido (back-office) SiniestroAsistidoView SiniestroViewModel RegistrarSiniestro consulta p95 ≤ 150 ms
HU-59–63 Ciclo de vida de póliza PolizaLifecycleView PolizaViewModel OperarPoliza p95 ≤ 150 ms

Cada recorrido es un feature module con lazy loading: es la unidad de despliegue y de cambio. Añadir un ramo nuevo (§6.5 "nuevo ramo de seguro") = añadir un feature + un caso de uso, sin tocar los existentes.


4. Cómo el diseño sostiene cada atributo de calidad

Latencia. El ViewModel emite loading de forma inmediata (feedback < 100 ms percibidos) y el caso de uso aplica timeout duro de 700 ms por dependencia. El adaptador HTTP hace cache-aside: si el BFF excede el presupuesto, devuelve el último valor conocido y el VM pasa a degraded. La telemetría RUM instrumenta p95/p99 por journey, no por endpoint, que es como están escritos los escenarios del caso.

Disponibilidad. La degradación es un camino de diseño, no una excepción. La caída de un módulo no crítico (p. ej. reportes) no tumba venta ni siniestros porque los features son lazy e independientes.

Seguridad. AuthInterceptor (OAuth2/OIDC + PKCE) y ConsentGuard viven en la capa de adaptadores; ningún ViewModel manipula tokens. La revocación de consentimiento (≤ 5 min) invalida caché local y fuerza idle en los VM afectados. El portal no persiste PII en localStorage.

Facilidad de modificación. La regla de dependencia es unidireccional: View → ViewModel → Caso de uso → Puerto ← Adaptador. Cambiar el proveedor detrás del BFF, o versionar el contrato a /v2, se absorbe en el adaptador. Cambiar la presentación no toca dominio.

Facilidad de integración. El portal conoce solo el contrato del BFF Web, versionado. Cumple el escenario "cliente web y móvil sobre la misma API": un cambio no obliga a redesplegar ambos clientes a la vez, porque el mapeo DTO↔dominio está aislado.

Testabilidad (§4.2 de la estrategia).

Nivel Qué se prueba Herramienta
Unitario ViewModels y casos de uso, sin DOM ni HTTP Karma/Jasmine + dobles de los puertos
Componente View con ViewModel fake TestBed
Contrato Adaptador vs. contrato del BFF HttpTestingController / Pact
E2E BDD Recorridos críticos Given/When/Then Cucumber + PageObject Model, esperas dinámicas
Visual Regresión de UI Resemble.js / BackstopJS

5. Convenciones de carpetas

src/app/
  core/          # interceptores, auth, consent, telemetría, i18n, errores
  domain/        # entidades, value objects, puertos, políticas de resiliencia
  application/   # casos de uso (uno por recorrido crítico)
  adapters/      # adaptadores HTTP, mappers DTO↔dominio, cache
  features/
    cotizacion/        { *.view.ts | *.view.html | *.viewmodel.ts | *.spec.ts }
    suscripcion/
    perfilamiento/
    siniestros/
    polizas/
  ui-kit/        # componentes presentacionales reutilizables

Regla de lint (ESLint boundaries): features/** no puede importar de adapters/**. Es la barrera que impide que MVVM se degrade a "componentes con HttpClient adentro".


6. Riesgos y decisiones abiertas

  1. Sobre-ingeniería. MVVM + puertos en un portal pequeño cuesta código extra. Se justifica aquí por los seis atributos de calidad y por el horizonte de 3 países / 30+ socios.
  2. Signals vs. RxJS. La propuesta usa signals para el estado del VM y RxJS solo en los bordes (HTTP, debounce). Conviene fijarlo como ADR para evitar mezclas.
  3. Dónde vive la caché de degradación. Aquí se propone en el cliente. Si el BFF ya expone stale-while-revalidate, parte de esta lógica podría bajarse al backend — decisión a coordinar con el equipo del BFF.
  4. Micro-frontends. No se recomiendan en el MVP: el caso pide velocidad con equipo reducido. La modularidad por features deja la puerta abierta si el equipo crece.
⚠️ **GitHub.com Fallback** ⚠️