DHLBC · Hallazgos de las pruebas E2E

Backlog de correcciones

Registro histórico de los bugs que encontraron las pruebas E2E, con su trazabilidad completa: dónde estaba (archivo y línea), por qué pasaba, cómo se corrigió (Work Item y PR) y cómo se verificó la corrección en QA.

17/17corregidos y verificados
4severidad alta
8severidad media
5severidad baja
Cómo leer esto. Las severidades y la «Corrección sugerida» reflejan el análisis original del hallazgo (documento histórico). El cierre real de cada ficha está en el tag verde y en sus «Notas»: fix mergeado + verificación en QA. Los de tipo config se resolvieron en el ambiente (datos), no tocando código. El detalle en markdown para agentes está en backlog-correcciones.md.
H01

Login no valida User.Active: usuarios desactivados (Active=0) pueden iniciar sesión

alto seguridad confirmado en código ✅ corregido · verificado en QA
Work Item
#1253 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN.API/_workitems/edit/1253
Ubicación
src/Desgravamen.Application/Features/Authentication/Login/AuthenticationLoginCommandHandler.cs:116-120 (falta el chequeo justo después del null-check del usuario, antes de generar token en L124-151)
Causa raíz
El handler valida credenciales contra LDAP (ValidateUser, L108) y que el usuario exista en la tabla User (GetByUserNameAsync, L116-120), pero nunca evalúa la bandera User.Active. Un usuario con Active=0 pasa ambas validaciones y el flujo genera token y sesión igual. El campo Active existe en el dominio (Desgravamen.Domain/Users/User.cs:104, bool) y ya hay un error de dominio dedicado sin usar (Errors.Authentication.InactiveCredentials, Errors.Authentication.cs:96) — la validación simplemente nunca se cableó.
Cómo reproducirlo
Con un usuario que exista en LDAP y en la tabla User pero con Active=0 (p.ej. 'test'), hacer POST al endpoint de login con credenciales válidas. Devuelve 200 con AuthenticationResult + token en vez de rechazar. Confirmable por lectura: entre L120 y L124 no hay ninguna condición sobre user.Active.
Corrección sugerida
Agregar, inmediatamente después del null-check (tras L120 y antes de armar roles en L124), el guard: `if (!user.Active) { return Errors.Authentication.InactiveCredentials; }`. Reutiliza el Error ya existente (Errors.Authentication.cs:96, ya localizado en español "Credenciales no activas."). No hace falta crear error nuevo. Verificar que el mapeo Error.Validation→HTTP del endpoint devuelva un status apropiado (idealmente 401/403); si InactiveCredentials mapea a 400 y se prefiere 401, alinearlo con InvalidCredentials, pero eso es secundario al fix de seguridad.
Riesgo al corregir
Bajo. El guard solo bloquea usuarios ya marcados inactivos; no afecta el path de usuarios activos ni la creación de sesión. Cuidar únicamente: (1) que no exista ningún flujo legítimo que dependa de que un inactivo obtenga token (no lo hay en este handler); (2) actualizar/añadir tests unitarios del handler para el caso Active=false (hoy no cubierto, por eso pasó a E2E); (3) confirmar el status code del mapeo si UX espera 401.
Notas
Bug de BACK confirmado por lectura de código, no comportamiento intencional: la infraestructura para rechazarlo (propiedad Active + error InactiveCredentials) ya está construida pero desconectada, lo que indica un chequeo olvidado, no una decisión. Existe además un Errors.User.UserInactive (Errors.User.cs:86) como alternativa, pero InactiveCredentials es el correcto por dominio (Authentication) y por estar ya localizado. La validación no puede delegarse a LDAP porque Active es estado a nivel aplicación, no del directorio. VERIFICADO EN QA (2026-07-13, post-deploy): FV (sonda API): login de «test» (Active=0) → 400 «Inactive Credentials»; F01 re-corrido OK, 0 hallazgos.
H02

GET /api/v1/app/File/{path} sirve documentos sin autenticación ([AllowAnonymous])

alto seguridad confirmado en código ✅ corregido · verificado en QA
Work Item
#1254 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN.API/_workitems/edit/1254
Ubicación
LBC.DESGRAVAMEN.API/Code/api/src/Desgravamen.Api/Endpoints/Documents/FileController.cs:82-83
Causa raíz
El endpoint GetFileFromRepository lleva [AllowAnonymous] (línea 83), que sobreescribe el [Authorize] a nivel de controller (línea 29). El handler devuelve el contenido completo del documento (base64 del archivo) sin verificar sesión ni pertenencia del archivo al usuario, exponiendo PII sensible (certificados de defunción, cédulas, comprobantes de pago) a cualquiera que conozca o adivine el id de ruta.
Cómo reproducirlo
Sin autenticarse, hacer GET a https://{host}/api/v1/app/File/{path} donde {path} es un id válido (en dev el token base64url es forjable como base64url de "{folder}/{filename}"; en prod es el id opaco de SharePoint que puede filtrarse por logs/historial/referer). Responde 200 con el JSON {base64, fileName} del documento. Contrastar con cualquier otro endpoint bajo [Authorize] que responde 401 sin token.
Corrección sugerida
Eliminar la línea 83 [AllowAnonymous] en FileController.cs. El [Authorize] del controller (línea 29) vuelve a aplicar y exige token válido — coherente con el resto del File/Documents controller y con el uso real del front, que ya envía Authorization: Bearer en downloadSignedPayment (claims.service.ts:1596) y viewFilePath (claims.service.ts:2281). Opcional endurecer con [Authorize(Roles = "Analyst,Supervisor,Treasurer")] si se quiere acotar por perfil, pero el mínimo imprescindible es exigir autenticación.
Riesgo al corregir
Bajo. Los dos únicos consumidores del front usan HttpClient con Bearer, así que restaurar auth no rompe el flujo de la UI. Antes de mergear, confirmar que no exista un consumidor anónimo legítimo fuera del front (deep-links en emails de notificación, webhooks de SharePoint, o el visor PDF cargando la URL directo en un iframe/img sin headers) — no se detectó ninguno en el repo front, pero conviene validarlo en QA. La autorización sigue sin verificar que el archivo pertenezca al claim del usuario (IDOR entre usuarios autenticados); eso es una mejora aparte, no bloquea este fix.
Notas
No es comportamiento intencional: el comentario en LocalFileRepositoryProvider.cs:242 reconoce explícitamente que "decodificar no es, por sí solo, una frontera de seguridad" porque el endpoint es [AllowAnonymous] — el hardening anti-traversal existente asume justamente que el endpoint queda expuesto. La opacidad del id (base64url en dev, drive-item en prod) reduce la probabilidad de adivinación pero es seguridad por oscuridad, no un control de acceso. El endpoint hermano [HttpPost("upload")] sí está protegido con [Authorize(Roles="Analyst,Supervisor")], reforzando que el AllowAnonymous del GET es una inconsistencia, no una decisión de diseño. VERIFICADO EN QA (2026-07-13, post-deploy): FV (sonda API): GET /File/{path} sin token → 401; sonda defensiva de F10 en verde.
H15

POST /File/upload de Ecofuturo falla porque el banco no tiene analistas activos vinculados en QA

alto config confirmado en código ✅ corregido · verificado en QA
Work Item
#1255 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN.API/_workitems/edit/1255
Ubicación
Origen del mensaje: RoundRobinAssigner.cs:42 (FindNextUser lanza InvalidOperationException); disparado desde UploadCommandHandler.cs:278. Query de datos: UserRepository.cs:140-155 (GetAnalystsWithActiveClaimsAsync). Chequeo inefectivo: UploadCommandHandler.cs:222-227.
Causa raíz
Es un problema de DATOS del ambiente QA, no un bug de código: no hay ningún User con rol Analyst, Active=true y vinculado al banco Ecofuturo (relación Banks). GetAnalystsWithActiveClaimsAsync (UserRepository.cs:145-153) filtra por Active + rol + banco y devuelve lista VACÍA (nunca null, porque ToListAsync jamás retorna null). El chequeo `if (analysts == null)` de UploadCommandHandler.cs:223 no atrapa la lista vacía, así que el flujo sigue, construye un RoundRobinAssigner con heap vacío y en el primer registro FindNextUser() lanza la excepción "No hay usuarios disponibles para asignar Claims" (RoundRobinAssigner.cs:42), que el catch de línea 192 envuelve en un Error.Failure genérico.
Cómo reproducirlo
En QA: subir un archivo Excel de intermediario con Banco=Ecofuturo vía POST /File/upload estando el banco sin ningún analista activo vinculado. La request falla con "Error al procesar archivo" cuyo detalle es "No hay usuarios disponibles para asignar Claims". Verificable en Seq/Serilog: no aparece el log "Analysts not found" (línea 225) porque la lista no es null; el error viene del catch general (línea 194).
Corrección sugerida
Corrección primaria (DATOS/config del ambiente, sin tocar código): en QA vincular al menos un usuario con rol Analyst y Active=true al banco Ecofuturo — insertar/asegurar la fila en la tabla puente User-Banks (BankUser) para el BankId de Ecofuturo, con el usuario marcado Active=true y con rol Analyst asignado. Tras eso el round-robin encuentra analista y el upload asigna Claims normalmente. Corrección secundaria RECOMENDADA (hardening de código, opcional): en UploadCommandHandler.cs cambiar el chequeo de línea 223 de `if (analysts == null)` a `if (analysts == null || analysts.Count == 0)` para devolver un DomainError claro (p.ej. Errors.User.NotFound con mensaje "No hay analistas activos para el banco X") en vez de dejar que reviente como InvalidOperationException genérica en el primer registro.
Riesgo al corregir
La corrección de datos es de bajo riesgo pero cuidar: el analista debe quedar vinculado SOLO al/los banco(s) que corresponda (la relación Banks segrega la asignación por banco); no vincular un analista de prueba a bancos que no debe operar. Para el hardening de código: es puramente defensivo (mueve el fallo de excepción a DomainError controlado) y no cambia el happy-path, pero conviene alinear el mensaje/tipo de error con lo que el front espera mostrar.
Notas
El mensaje reportado en el hallazgo ("No hay usuarios disponibles para asignar Claims") es literal de RoundRobinAssigner.cs:42, lo que confirma la cadena. No es comportamiento intencional deseable: la validación de línea 222-227 fue escrita con la intención de cubrir el caso "sin analistas", pero es inefectiva porque compara contra null cuando EF devuelve lista vacía. Severidad alto correcta: bloquea por completo la carga de siniestros de cualquier banco que quede sin analista activo vinculado (no solo Ecofuturo). Recomiendo además una verificación de datos preventiva en QA/PROD para todos los bancos activos: cada banco operativo debe tener >=1 analista activo vinculado. VERIFICADO EN QA (2026-07-13, post-deploy): Verificado en sesión previa: tras vincular analista activo a Ecofuturo, el ciclo FB-ECO corrió completo (Excel→cierre).
H17

Operadores == y != de Entity y ValueObject con semántica de null invertida: los null-guards no atajan y el 404 se vuelve 500

alto back confirmado en código ✅ corregido · verificado en QA
Work Item
#1271 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN.API/_workitems/edit/1271
Ubicación
LBC.DESGRAVAMEN.API/Code/api/src/Desgravamen.Domain/Abstractions/Entity.cs (operator==) + ValueObject.cs (operator!=)
Causa raíz
Entity<T>.operator== devuelve `left is not null && right is not null && left.Equals(right)` → null==null da FALSE, así que `if (entidad == null)` con entidad null NO entra al guard y el código sigue hasta un NullReferenceException (HTTP 500 en vez del 400/404 de dominio). ValueObject.operator!= tiene las ramas de null invertidas: `x != null` (x no-null) da FALSE y `null != null` da TRUE — afecta a todos los EntityId (ClaimId, BankId, UserId, ...). El == de ValueObject es correcto; solo el != está invertido.
Cómo reproducirlo
Descubierto durante el TDD del fix H16: GroupClaimsForAcceptanceCommandHandler:113 con claim null (ID inexistente) no ataja en `if (claim == null)` y revienta con NPE en :121 (claim.Bank.Id) — verificado con unit test + Moq. Además, el matcher `It.Is<ClaimId?>(id => id != null)` con un ClaimId real evaluaba false por el != invertido (bisección It.IsAny pasa / It.Is falla).
Corrección sugerida
Entity<T>.operator==: `ReferenceEquals(left, right) || (left is not null && left.Equals(right))` (y != como su negación). ValueObject.operator!=: `!(left == right)`. TDD con la tabla de verdad completa (null==null, x==null, null==x, x==y) en tests de dominio para ambos tipos. Revisión posterior de los `== null`/`!= null` sobre entidades en handlers (los `is null` no están afectados).
Riesgo al corregir
Bajo el fix en sí (semántica estándar de igualdad). OJO: código que hoy dependa accidentalmente de la semántica rota (p.ej. una rama `if (x != null)` que nunca corre) puede cambiar de comportamiento al arreglarse — correr la suite completa.
Notas
No proviene de un flujo E2E sino del TDD del fix H16 (PR #83). Evidencia con tablas de verdad adjunta al WI #1271 (hijo del Feature #1268). FIX (2026-07-13): PR #85 back MERGEADO — Entity.== con ReferenceEquals (null==null true) y ValueObject.!= como negación exacta del ==; 15 tests de tabla de verdad; regresión completa 311 unit + 219 functional en verde. WI #1271 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): FV (sonda API): POST group-claims con ULID inexistente → 400 «Claims not found» (error de dominio, ya no 500/NPE).
H04

Lápiz "Analista responsable" visible para rol Medical en caso-page (falta guard de rol)

medio front confirmado en código ✅ corregido · verificado en QA
Work Item
#1256 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1256
Ubicación
LBC.DESGRAVAMEN/Code/front/src/app/pages/caso-page/caso-page.component.html:214
Causa raíz
El `<span class="btn-edit">` que dispara `editarAnalista()` (líneas 214-220) es el único de los cuatro lápices de la card que no tiene `*ngIf`. Los otros tres (SISE l.24, Solicitud l.171, Monto l.194) llevan `*ngIf="datasource?.claimId && roleActual !== 'Medical'"`. No existe contenedor padre que filtre por rol, por lo que el lápiz se renderiza incondicionalmente para todos los roles, incluido Medical.
Cómo reproducirlo
Loguearse con un usuario de rol Medical y abrir caso-page (#/home-page/gestion-page o la ruta del caso). En la card izquierda, bloque "Analista responsable", aparece el ícono de lápiz "edit" (los lápices de N° SISE, N° de Solicitud de Pago y Monto y Tipo NO aparecen para ese rol). Clickearlo abre el diálogo EditarAnalistaComponent, permitiendo al médico reasignar el analista responsable.
Corrección sugerida
Agregar la misma condición que los otros tres lápices al span de línea 214: `*ngIf="datasource?.claimId && roleActual !== 'Medical'"`. Queda: `<span *ngIf="datasource?.claimId && roleActual !== 'Medical'" class="btn-edit material-symbols-outlined" matTooltip="Editar" matTooltipPosition="above" (click)="editarAnalista()">edit</span>`. Con eso el lápiz queda consistente con SISE/Solicitud/Monto y se oculta para Medical.
Riesgo al corregir
Bajo. Cambio puramente aditivo de visibilidad UI, idéntico al patrón ya usado por los tres lápices hermanos; no afecta otros roles (Analyst sigue viéndolo porque roleActual !== 'Medical'). Nota de defensa en profundidad: este guard es solo UI; conviene confirmar que el endpoint que respalda EditarAnalistaComponent también rechace el rol Medical en el backend, ya que un lápiz oculto no impide invocar la API directamente.
Notas
editarAnalista() (caso-page.component.ts:261) abre EditarAnalistaComponent con {id, claimNumber, responsibleId}. roleActual se setea en l.538 vía authService.getRole(). El bug es una inconsistencia de guard, no comportamiento intencional: los tres lápices análogos evidencian que Medical NO debe editar estos campos. Podría reclasificarse como 'seguridad' si el back no valida el rol, pero como hallazgo de front es una fuga de acción no autorizada en la UI. FIX (2026-07-13): PR #86 front MERGEADO — *ngIf de rol en el lápiz de analista responsable; back ya rechazaba a Medical en /assign-responsible. Validado E2E (auditor.medico1 sin lápiz / analista.extra4 con lápiz, capturas en el WI). WI #1256 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): F06: auditor.medico1 ya no ve ningún lápiz de edición en caso-page (la sonda condicional no disparó).
H05

Toast del médico muestra "para el caso undefined" al solicitar informe médico

medio front confirmado en código ✅ corregido · verificado en QA
Work Item
#1257 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1257
Ubicación
LBC.DESGRAVAMEN/Code/front/src/app/pages/medical-home-page/medical-home-page.component.ts:271 (también :268 y :274); interface en interfaces/medical-Claim.ts:7
Causa raíz
El backend serializa el número de caso como `claimNumber` (contrato `CaseForMedicalDto`, JsonPropertyName "claimNumber", CaseForMedicalDto.cs:34), y el número SISE como `siseNumber`. En el front, `handleAction()` pasa `element.caseNumber` a los diálogos/toasts, pero ese campo NO existe en la respuesta → llega `undefined`, y el toast interpola "...para el caso undefined". La interface `Case` declara mal el campo como `caseNumber` (nunca presente en runtime), lo que oculta el error en compilación.
Cómo reproducirlo
Entrar como perfil médico a medical-home-page, abrir el menú de acciones de un caso y ejecutar "Realizar informe médico" (icono local_hospital). Al confirmar el diálogo InformeComponent, el toast de éxito dice literal "Se realizo la solicitud de informe médico para el caso undefined". Idéntico defecto en "Solicitar documentación adicional" (línea 138) y "Devolver caso" (línea 177), porque los tres reciben el mismo `number` proveniente de `element.caseNumber`. Contraste: la tabla SÍ muestra bien el número porque el HTML usa `element.claimNumber` (template línea 75), no `caseNumber`.
Corrección sugerida
1) En medical-home-page.component.ts, cambiar en `handleAction()` las tres referencias `element.caseNumber` → `element.claimNumber` (líneas 268, 271, 274). 2) En interfaces/medical-Claim.ts corregir el campo de la interface `Case`: reemplazar `caseNumber: string;` por `claimNumber: string;` y agregar `siseNumber: string;` para alinear con el contrato real. Con esto los tres toasts (documento adicional, informe médico, devolver caso) interpolan el número correcto. Opcional: si el número que se quiere mostrar en el mensaje es el SISE en vez del interno, usar `element.siseNumber` — validar con negocio cuál corresponde; el resto de las pantallas (casos-en-proceso, etc.) usa el número de caso interno, así que `claimNumber` es lo consistente.
Riesgo al corregir
bajo. Es un cambio de nombre de propiedad puramente en el front; no toca backend ni contrato. Cuidar solo que `claimNumber` esté efectivamente presente en la respuesta (confirmado en el DTO) y que si se habilitara strictTemplates el HTML ya usa `claimNumber`/`siseNumber` que hoy no están en la interface — conviene actualizar la interface igual para evitar warnings futuros.
Notas
El bug afecta a los TRES toasts del médico, no solo al de informe médico. El hallazgo lo reportó como campo "casenumber", pero el nombre real en el JSON es `claimNumber` (camelCase). No es comportamiento intencional: es un desalineamiento interface-vs-contrato que TypeScript no detecta porque la interface está mal tipada. Severidad medio: es cosmético (no rompe el flujo funcional, la acción se ejecuta bien), pero degrada la confianza del usuario al mostrar "undefined". FIX (2026-07-13): PR #87 front MERGEADO — element.claimNumber en handleAction ×3 + interface Case alineada al contrato (claimNumber+siseNumber). WI #1257 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): F06: el toast de informe médico dice «…para el caso 1062» — número real, no «undefined».
H07

mat-paginator de la bandeja del médico muestra total 0 y no pagina porque loadCases() reemplaza la instancia de dataSource y rompe el cableado del paginador

medio front confirmado en código ✅ corregido · verificado en QA
Work Item
#1258 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1258
Ubicación
medical-home-page.component.ts:201 (causa raíz) — cableado en :60; template medical-home-page.component.html:155-158
Causa raíz
En ngAfterViewInit (línea 60) se hace `this.dataSource.paginator = this.paginator` sobre la instancia inicial vacía, pero acto seguido loadCases() (línea 201) hace `this.dataSource = new MatTableDataSource<Case>(response.cases)`, creando una instancia NUEVA que nunca recibe `.paginator`. Como MatTableDataSource es quien sincroniza automáticamente `paginator.length` con `data.length`, al perderse ese vínculo el paginador queda huérfano: su `length` nunca se actualiza (queda en 0) y la paginación no se aplica, por lo que la tabla renderiza todas las filas sin paginar. El diagnóstico del hallazgo ("falta setear [length]") es un síntoma: con MatTableDataSource NO hay que setear [length] a mano, el problema es la reasignación que rompe el binding.
Cómo reproducirlo
Loguearse como médico y abrir la bandeja (medical-home-page). Con una respuesta que traiga 1 caso: la tabla muestra ese 1 caso pero el paginador dice "1 - 10 de 0". Con >10 casos: se listan todos sin paginar (no recorta a 10) y el total sigue en 0. El mismo defecto se dispara en cada refresh porque loadCases() se llama tras cada acción (solicitarInfo/realizarInforme/onButtonClick) y siempre recrea el dataSource.
Corrección sugerida
En loadCases() no recrear la instancia: reemplazar la línea 201 `this.dataSource = new MatTableDataSource<Case>(response.cases);` por `this.dataSource.data = response.cases;`. Así se conserva la instancia cableada en ngAfterViewInit y MatTableDataSource actualiza solo `paginator.length`. Alternativa equivalente si se quiere seguir instanciando: tras crear el nuevo dataSource, reasignar `this.dataSource.paginator = this.paginator;` (y `.filterPredicate`). NO agregar `[length]` manual en el template (html:155-158): con MatTableDataSource sería redundante y quedaría desincronizado del filtro. Nota aparte: el `mat-sort-header` del template no funciona porque no se importa MatSortModule ni se cablea MatSort — fuera del alcance de H07 pero mismo componente.
Riesgo al corregir
Bajo. Usar `this.dataSource.data = ...` es el patrón canónico de Angular Material y además preserva el `filterPredicate` seteado en applyFilter(), que con la reasignación actual también se perdía en cada recarga. Verificar que ningún otro lugar dependa de recibir una instancia nueva de dataSource (no lo hay en este componente). Si se opta por la alternativa de reasignar `.paginator`, hacerlo antes de que Angular corra el change detection para evitar ExpressionChangedAfterItHasBeenChecked.
Notas
Hallazgo real y reproducible, pero la causa NO es "falta [length]" sino la reasignación de la instancia de dataSource que desconecta el paginador. El array ELEMENT_DATA hardcodeado (líneas 293-318) es data muerta no usada. La severidad reportada era "bajo"; la subo a "medio" porque la paginación queda totalmente inoperante (no recorta filas) y el contador es incorrecto, no es solo cosmético. AMPLIACIÓN (2026-07-13, comentario en el WI #1258): el mismo defecto está replicado en 8 componentes más (casos-en-pronunciamiento :198/:298, casos-en-proceso :317/:277, casos-en-gestion-medica :240/:388, casos-en-aceptacion-del-tomador :68/:128, casos-en-tesoreria :103/:197, agrupados-en-tesoreria :88/:225, casos-por-cerrar :92/:162, tesorery-home-page :56/:154). Patrón correcto de referencia: casos-por-gestionar.component.ts:326-329. Corregir los 9 en el mismo PR. FIX (2026-07-13): PR #84 front MERGEADO — rebind de dataSource.paginator tras las 11 reasignaciones en los 9 componentes; 5 specs de regresión; validado E2E contra QA (bandejas con total real). WI #1258 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): Sonda UI: paginator de «En proceso» muestra «1 - 2 de 2» — total real tras el rebind del dataSource.
H08

Casos CERRADOS (ClosedApproved/ClosedRejected) siguen ofreciendo Reasignar y Eliminar al supervisor en repositorio-page

medio back confirmado en código ✅ corregido · verificado en QA
Work Item
#1259 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN.API/_workitems/edit/1259
Ubicación
LBC.DESGRAVAMEN.API/Code/api/src/Desgravamen.Application/Features/Claims/Repository/List/GetCases/ClaimRepoListRequestHandler.cs:82-89 (armado) y :114 (asignación a cada caso)
Causa raíz
La lista `actions` se construye UNA sola vez ANTES del `foreach` (líneas 82-89) según el rol del usuario logueado (Supervisor → agrega Reassign y Delete). Luego se copia idéntica a TODOS los casos con `Actions: [.. actions]` (línea 114), sin importar el estado del claim. La `condition` por caso (que distingue ClosedApproved/ClosedRejected, líneas 93-98) se calcula pero NO se usa para filtrar las acciones. Resultado: un supervisor ve reassign/delete hasta en casos cerrados.
Cómo reproducirlo
Loguearse como usuario con rol Supervisor, ir a repositorio-page (vista global). En la columna Acciones, un caso en etapa/condición ClosedApproved o ClosedRejected muestra igualmente los íconos Reasignar y Eliminar (el front en repositorio-page.component.html:251 hace `*ngFor` sobre `element.actions` sin filtrar, así que renderiza tal cual lo que emite el back). El menú `toAction` (repositorio-page.component.ts:161-174) los ejecuta normalmente.
Corrección sugerida
Mover el armado de acciones al interior del `foreach` y condicionarlo por estado del caso. Calcular `bool isClosed = claim.ClaimStatus.Name.Equals(ClaimStatusEnum.ClosedApproved.ToString()) || claim.ClaimStatus.Name.Equals(ClaimStatusEnum.ClosedRejected.ToString());` (reutilizando la misma comparación que ya existe para `condition`), y sólo agregar Reassign/Delete cuando `isSupervisor && !isClosed`. View queda siempre. Concretamente: sacar el bloque 82-89, precomputar `bool isSupervisor = currentUser.Roles.Any(r => r.Name == RoleEnum.Supervisor.ToString());` fuera del loop, y dentro del loop construir `var actions = new List<string> { ClaimActionsEnum.View.ToString().ToLower() }; if (isSupervisor && !isClosed) { actions.Add(Reassign...); actions.Add(Delete...); }`.
Riesgo al corregir
Bajo/acotado. El armado de actions es local a este handler (ClaimRepoListRequestHandler), no compartido: no impacta otras vistas (caso-page, gestión, etc. arman sus acciones por otro lado). Antes de implementar conviene CONFIRMAR con negocio la regla exacta: (a) ¿bloquear ambas acciones o sólo Reassign? Reasignar un caso cerrado casi nunca tiene sentido; Eliminar podría querer conservarse para corrección administrativa. (b) Verificar que no exista un requisito de "reabrir/corregir" cerrados que dependa de estas acciones. Además, si se quiere defensa en profundidad, evaluar validar el estado también en ReassignCommandHandler y en el handler de Delete, ya que ocultar el botón no impide un POST directo.
Notas
Podría argumentarse comportamiento intencional (supervisor con poder total), pero la evidencia del código contradice eso: el handler YA calcula la `condition` cerrada por caso y no la aplica a las acciones, lo que delata que el gating por estado se omitió, no que se decidió permitirlo. El front no filtra nada (confía 100% en actions[] del back), así que la corrección DEBE ir en el back; tocar sólo el front dejaría la puerta abierta vía API. No existe un estado plano "Closed" en ClaimStatusEnum: los cerrados son ClosedApproved y ClosedRejected (Desgravamen.Shared/Enums/ClaimStatusEnum.cs:93,98). FIX (2026-07-13): PR #88 back MERGEADO a develop (pipeline 2604 verde: build, unit, integration, smoke y functional) — acciones por caso en ClaimRepoListRequestHandler: view siempre, reassign/delete solo supervisor con caso abierto; 5 unit tests. WI #1259 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): FV (sonda API): GET /Claims/Repository como supervisora — todos los casos cerrados exponen únicamente actions=[view].
H09

Fechas de comentarios del caso se muestran en UTC (+4h) en vez de hora local de Bolivia (UTC-4)

medio back confirmado en código ✅ corregido · verificado en QA
Work Item
#1260 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN.API/_workitems/edit/1260
Ubicación
Render (síntoma): LBC.DESGRAVAMEN/Code/front/src/app/pages/caso-page/caso-page.component.html:259-260. Causa raíz (back): LBC.DESGRAVAMEN.API/Code/api/src/Desgravamen.Application/Features/Claims/Comments/GetClaimCommentsCommandHandler.cs:108 (proyecta comment.CreationDate) + Desgravamen.Domain/Claims/ClaimComment.cs:51 (CreationDate = DateTime.UtcNow).
Causa raíz
El back guarda CreationDate con DateTime.UtcNow (instante UTC), pero EF Core lo re-materializa desde SQL Server (datetime2) con DateTimeKind.Unspecified. System.Text.Json (config default en Program.cs:52-63, sin converter de DateTime) serializa un Unspecified SIN sufijo 'Z' ni offset (ej. "2026-07-10T04:16:00"). El pipe date de Angular (caso-page.component.html:259-260, sin timezone) parsea ese ISO naive como hora LOCAL del navegador, así que el wall-clock UTC 04:16 se muestra tal cual como 04:16 en vez de convertirlo a 00:16 de Bolivia. No es solo un pipe mal usado: el front no puede saber que el string es UTC porque el back no lo marca.
Cómo reproducirlo
Crear un comentario en un caso (gestion/caso-page) a una hora local conocida en Bolivia (UTC-4). Abrir el detalle del caso y mirar el timestamp del comentario (caso-page.component.html:259-260): se muestra +4h respecto de la hora local (creado 00:16 aparece 04:16). Confirmable inspeccionando la respuesta del GET de comentarios: el campo date llega como "...T04:16:00" SIN 'Z'.
Corrección sugerida
Fix de raíz en el back (beneficia a todo consumidor): en GetClaimCommentsCommandHandler.cs:108 envolver el valor con DateTime.SpecifyKind(comment.CreationDate, DateTimeKind.Utc) al construir el ClaimCommentDto, para que System.Text.Json emita el 'Z' y el JSON sea "...T04:16:00Z". Con el 'Z' presente, el pipe date en Bolivia (sin DST) ya renderiza 00:16 correcto sin tocar el front. Si además el ambiente de pruebas/servidor corre en un timezone distinto de Bolivia (contenedores en UTC), pinnear la salida en el front para determinismo: caso-page.component.html:259-260 -> {{ comentario.date | date : "fullDate" : "-0400" }} {{ comentario.date | date : "shortTime" : "-0400" }} (esto solo funciona bien SI el string ya trae el 'Z'; el offset del pipe ajusta la salida, no arregla el parseo de un string naive). Alternativa front-only sin tocar back: normalizar el string agregando 'Z' antes de renderizar (mapear comentario.date a un Date UTC en el .ts) y luego aplicar el offset -0400. Recomendado: back (SpecifyKind) como fuente de verdad y opcionalmente pinnear -0400 en el front para tests deterministas.
Riesgo al corregir
Bajo si el fix es targeteado (SpecifyKind solo en la proyección de comentarios). NO aplicar un ValueConverter global DateTime->Utc en EF ni un converter global de JSON sin auditar: hay fechas que ya son UTC (DateTimeProvider hace ToUniversalTime) y forzar Kind global podría doble-corregir. El front-only con offset '-0400' sobre un string SIN 'Z' no corrige nada (el parseo naive ya salió mal); requiere primero normalizar a UTC. La misma clase de bug aplica a los comentarios de tesorería (ViewCommentsCommandHandler / ClaimTreasuryCommentDto, mismo patrón CreationDate UTC sin 'Z'): si se arregla acá, alinear allá para no dejar dos criterios distintos.
Notas
Bug real, no comportamiento intencional: no hay config de timezone deliberada, es el default de System.Text.Json chocando con Kind=Unspecified de EF y el parseo local del pipe. Severidad medio confirmada: correctness de display (engaña al analista sobre CUÁNDO se dejó un comentario, 4h de desfase) sin corrupción de datos. El hallazgo lo tipeó como "front"; el pipe sin timezone es el punto visible pero la causa raíz es back (timestamp UTC enviado sin marca de zona), por eso lo clasifico back. Fix de menor blast-radius: SpecifyKind(Utc) en el DTO. Bolivia no tiene horario de verano, UTC-4 es fijo, sin preocupación por DST. FIX (2026-07-13): PR #89 back MERGEADO a develop (pipeline 2606 verde: build, unit, integration, smoke y functional) — DateTime.SpecifyKind(Utc) en las proyecciones de comentarios (caso y tesorería); 2 unit tests. WI #1260 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): FV (sonda API): date de comments llega «2026-07-13T16:57:42…Z» — UTC con marca de zona; el front ya convierte a hora local.
H11

Toast de validación al aprobar análisis no indica qué documento/campo falta

medio ux confirmado en código ✅ corregido · verificado en QA
Work Item
#1261 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1261
Ubicación
LBC.DESGRAVAMEN/Code/front/src/app/pages/gestion-page/gestion-page.component.ts:1274 (mensaje hardcodeado); validación en verificarDocumento() :1250-1269, con checkAllDocumentsValid :1305-1310 y checkAllDocumentsValidAtributes :1311-1316
Causa raíz
verificarDocumento() valida con dos predicados booleanos que colapsan todo a un every(...) === true, descartando cuáles atributos/documentos fallaron. Cuando falla, llama mostrarMensaje() que hardcodea 'Verifique la informacion para continuar.' (línea 1274) sin recibir ni computar qué falta. El detalle existe en el modelo (Document.name, Attribute.name/displayName) pero nunca se recolecta.
Cómo reproducirlo
En gestion-page (/#/home-page/gestion-page/{claimId}), dejar al menos un atributo de un documento con isValid=false (campo sin completar/validar) y presionar el botón que dispara verificarDocumento() (aprobar/continuar análisis). Aparece el toastr.error con título 'Error' y cuerpo 'Verifique la informacion para continuar.', sin mencionar el documento ni el campo faltante.
Corrección sugerida
Reemplazar el flujo booleano por uno que recolecte los faltantes y arme el mensaje. Concretamente: en verificarDocumento() (o en un helper nuevo getInvalidItems()) recorrer this.documents y juntar, por cada document con isValid !== true, los attr con isValid !== true usando (attr.displayName || attr.name) y agrupándolos por document.name. Luego pasar ese detalle a mostrarMensaje(detalle: string): cambiar la firma para aceptar el texto y construir this.Mensaje = 'Complete o verifique: ' + detalle (ej. 'Certificado de Defunción: País de ocurrencia, Provincia'). Si la lista queda vacía por algún borde, caer al string genérico actual. No cambiar la lógica de qué se considera válido (isValid), solo enriquecer el mensaje. Nota: hoy la condición de éxito es (allValid || allvalidAtributes) — al construir el detalle usar el mismo criterio de 'inválido' que dispara el else, para no listar campos que el flujo igual aceptaría.
Riesgo al corregir
Bajo. Es puramente presentacional (no toca la decisión de guardar ni el backend). Cuidar: (1) truncar/limitar la lista si hay muchos campos para no romper el layout del toast; (2) respetar el criterio OR existente (allValid || allvalidAtributes) al decidir qué marcar como faltante, para que el mensaje no contradiga el gate real; (3) usar displayName con fallback a name porque algunos atributos podrían no tener displayName.
Notas
No es comportamiento intencional: la genericidad viene de que mostrarMensaje() no recibe parámetros, no de una decisión de diseño. Severidad medio es adecuada: no bloquea la operación pero degrada UX y alarga el ciclo de corrección del analista (prueba y error). Existe mostrarMensajeError(text) (1280-1284) que ya acepta texto — se puede reutilizar ese patrón en vez de crear otro método. checkAllDocumentsValid no guardea contra arreglo vacío (el comentario 'Retorna false si el arreglo está vacío' miente: every([]) === true), pero eso no afecta este bug. FIX (2026-07-13): PR #90 front MERGEADO — detalleFaltantes() agrupa por documento los atributos sin validar y el toast nombra qué falta; 2 tests. WI #1261 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): F02: al aprobar con la Hoja de Riesgo incompleta el toast nombró documento y campos exactos («Complete o verifique: PaymentRisk: InterestRate, LastPaymentDate, …»).
H14

Descarga de documento con base64 vacío no valida contenido ni da feedback al usuario

medio front confirmado en código ✅ corregido · verificado en QA
Work Item
#1262 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1262
Ubicación
LBC.DESGRAVAMEN/Code/front/src/app/pages/caso-page/caso-page.component.ts:449 (método downloadDocument, líneas 436-470; helper downloadBase64File 508-535)
Causa raíz
El handler downloadDocument valida únicamente response.fileName (línea 449) antes de llamar a downloadBase64File, pero NO valida que response.base64 tenga contenido. Con un base64 vacío pero fileName presente, atob('') devuelve '' → Blob de 0 bytes → link.click() sobre un anchor no adjunto al DOM: no se percibe descarga y no se muestra ningún mensaje. La guarda correcta (!archivo || archivo.trim() === '') SÍ existe en el método hermano pfdVisualizer (línea 403) pero fue omitida en la descarga.
Cómo reproducirlo
En caso-page, sobre un documento cuya API viewCaseDocumentDownload devuelve 200 con base64 de longitud 0 y fileName no vacío (caso real: archivos no restaurados en QA), tocar el botón de descarga (caso-page.component.html:298 y :346). El botón "no hace nada": no baja archivo (o baja uno de 0 bytes/corrupto según navegador) y no aparece toast de error. Comparar con el botón de visualizar (pfdVisualizer), que en el mismo escenario sí muestra "No se ha podido visualizar el pdf".
Corrección sugerida
En downloadDocument reemplazar la condición de la línea 449 por una que valide también el contenido: cambiar `if(response.fileName){ this.downloadBase64File(archivo, fileName); } else { ... }` por algo como `if (!archivo || archivo.trim() === '' || !response.fileName) { this.mostrarErrorConMensaje("No se ha podido descargar el documento"); return; } this.downloadBase64File(archivo, fileName);`. Así se cubre tanto fileName ausente como base64 vacío, reutilizando el mismo patrón/guarda ya presente en pfdVisualizer (línea 403) y el método mostrarErrorConMensaje ya existente (línea 369).
Riesgo al corregir
Bajo. Solo agrega una validación previa y un toast; no altera el camino feliz. Cuidar de no romper el else existente que ya mostraba error cuando falta fileName (mantener ese mensaje). Es un fix puramente de front, sin impacto en back ni en otros flujos.
Notas
Hallazgos colaterales detectados al verificar (fuera del alcance de H14, pero conviene registrarlos): (1) BUG LATENTE de MIME: downloadBase64File (508-535), que es el helper usado para descargar documentos, arma el Blob con type 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' (XLSX), mientras que existe un downloadBase64File2 (476-505) con MIME 'application/pdf' que NO se usa (código muerto). Los documentos del caso son PDFs, así que la descarga real puede salir con MIME incorrecto. (2) El anchor link no se hace document.body.appendChild antes del click ni se libera con URL.revokeObjectURL — funciona en Chrome pero es frágil. El scope de ambiente (archivos no restaurados en QA que producen base64 vacío) es correcto: el defecto de front es la falta de validación+feedback, y está confirmado en código. VERIFICADO EN QA (2026-07-13, post-deploy): Repro dirigida: con base64 completo la descarga se concreta (informe médico, 684 KB); el camino de base64 vacío queda cubierto por el guard + toast (specs del PR #91).
H16

group-claims no valida el estado: un caso EN PROCESO sin pronunciamiento puede agruparse y cerrarse como ClosedRejected

medio back confirmado en código ✅ corregido · verificado en QA
Work Item
#1270 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN.API/_workitems/edit/1270
Ubicación
LBC.DESGRAVAMEN.API/Code/api/src/Desgravamen.Application/Features/Claims/GroupClaimsForAcceptance/GroupClaimsForAcceptanceCommandHandler.cs:110-135 (+ ApproveGroupClaimCommandHandler.cs:96)
Causa raíz
El foreach de GroupClaimsForAcceptance solo valida existencia del claim y mismo banco; NO valida ClaimState/ClaimStatus y sobrescribe incondicionalmente a PolicyholderAcceptance (:132-133). Al aprobar el tomador, el branch Treasury vs ClosedRejected depende de ClaimAcceptanceGroupDetail.StatementApproved, congelado al momento de AGRUPAR (ClaimAcceptanceGroupDetail.cs:88-95): un caso InProcess da false y ToClosure(ClosedRejected) acepta el origen PolicyholderAcceptance, así que cierra sin error.
Cómo reproducirlo
Caso en InProcess (análisis aprobado, sin pronunciamiento): POST /v1/app/Claims/group-claims {claims:[{id}]} → 200; PUT group-claims/{g}/claims/{id}/approve → 200; el caso queda Closure/ClosedRejected sin carta ni motivo. Reproducido 3 veces por los flujos E2E FA-RECHAZO (Sol/Fie/Eco, 2026-07-13) cuando statement-rejected falló con 400 y el ciclo siguió igual.
Corrección sugerida
En GroupClaimsForAcceptanceCommandHandler validar por claim State == Statement y Status ∈ {StatementApproved, StatementRejected} antes de agrupar (DomainError claro, p.ej. Claim.InvalidStateForAcceptanceGrouping). Defensa en profundidad: en ApproveGroupClaimCommandHandler decidir Treasury vs ClosedRejected con el status real del pronunciamiento, no con el flag congelado.
Riesgo al corregir
Medio-bajo. Validación aditiva; confirmar que el front solo agrupa desde la bandeja Pronunciamiento (no hay flujo legítimo agrupando desde otros estados).
Notas
CAUSA DEL 400 CONFIRMADA (2026-07-13, repro API): el cliente E2E enviaba {description, comments} en vez del campo requerido 'considerations' → validación de model binding rechazaba la carta; statement-rejected caía en cascada (RejectedLetterNotFound). Corregido ciclo_api → carta 200 (PDF), rechazo 200, el caso aparece en state/Statement como StatementRejected y el badge cuenta bien. El bug de este WI (group-claims sin validación) es INDEPENDIENTE y quedó re-reproducido en la misma repro (caso 1297: InProcess → agrupado → Close sin pronunciamiento). Colateral: el runner E2E ahora vuelca los 4xx de ciclo_api como hallazgos y fuerza estado CON HALLAZGOS. FIX EN CURSO (2026-07-13): PR #83 (fix/1270-group-claims-state-validation) con validación en dos pasadas + 6 unit tests + 1 functional test negativo. PR #83 MERGEADO a develop con merge commit (pipeline 2596 verde); WI #1270 Resolved. Validación final en develop consolidado: build Release + 324 unit + 220 functional, 0 fallos. VERIFICADO EN QA (2026-07-13, post-deploy): FV (sonda API): POST group-claims con caso InProcess → 400 «…is not in Statement state…cannot be grouped…»; el caso no muta.
H03

Rutas legacy del template daxa montadas sin guard ni lazy-loading en producción

bajo seguridad confirmado en código ✅ corregido · verificado en QA
Work Item
#1263 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1263
Ubicación
/Users/claudiojara/Workspace/work/neosoltec/LBC/LBC.DESGRAVAMEN/Code/front/src/app/app.routes.ts:313-600 (bloque legacy que arranca en la línea 313 `{ path: '', component: EcommerceComponent }` hasta la 599; wildcard NotFound en 600). Imports eager asociados: líneas 3-190.
Causa raíz
El array `routes` mezcla las rutas reales de LBC (líneas 218-311, todas con `canActivate: [privateGuard]` + `data.roles`) con TODAS las rutas del template daxa original (líneas 313-599: `crm`, `ecommerce-page`, `project-management-page`, `lms-page`, `help-desk-page`, `settings`, `internal-error`, etc.), que quedaron montadas SIN `canActivate` y con componentes importados de forma eager (no lazy). Con el routing por hash cualquiera puede navegar a `/#/crm`, `/#/ecommerce-page`, `/#/internal-error` sin sesión.
Cómo reproducirlo
Sin loguearse, ir a `http://localhost:4200/#/crm`, `/#/ecommerce-page`, `/#/settings` o `/#/internal-error`: renderizan las pantallas demo del template sin pasar por `privateGuard`. Contraste: `/#/home-page` o `/#/dashboard-page` redirigen al login (tienen guard). Nota: `/#/` cae en LoginPage porque la ruta `''` de LBC (línea 218) precede a la `''` duplicada del template (línea 313) y el router toma la primera coincidencia.
Corrección sugerida
Eliminar el bloque legacy completo (líneas 313 a 599) del array `routes`, dejando solo las rutas LBC (218-311) y el wildcard `{ path: '**', component: NotFoundComponent }` (línea 600). Al borrar las rutas, quitar también los imports eager ya sin uso (líneas 3-190) para bajar el bundle de prod. Como mínimo viable, si no se quiere borrar aún, envolver esas rutas con `canActivate: [privateGuard]`, pero lo correcto es removerlas: son pantallas muertas sin valor de negocio. No tocar `NotFoundComponent`/`InternalErrorComponent` si el manejo de errores global los referencia por componente (ver riesgo).
Riesgo al corregir
Antes de borrar: (1) verificar con `rg "routerLink|navigate.*(crm|ecommerce-page|settings|internal-error|not-found)"` en `src/app` que ninguna pantalla LBC enlace a esas rutas; (2) `internal-error`/`not-found` podrían usarse por un interceptor/handler de errores que navegue a `/#/internal-error` — si es así, conservar SOLO esas dos rutas (con o sin guard según el caso) y borrar el resto; (3) borrar la ruta NO borra el componente: los componentes UI compartidos que las pantallas LBC importan directamente (no vía router) no se ven afectados. Recompilar y correr el smoke de Playwright del login tras el cambio.
Notas
Severidad real BAJA, no media: son pantallas demo con data hardcodeada del template, no exponen siniestros ni datos reales del negocio (no hay fuga de PII/claims). El impacto es higiene/superficie de ataque y peso de bundle en prod (código muerto eager-loaded), más que una vulnerabilidad de datos. NO es comportamiento intencional: es residuo del scaffolding daxa que nunca se limpió. Detalle a favor de que es bug y no feature: la ruta `''` está duplicada (218 y 313), señal clara de merge sin limpieza. Tratarlo como deuda técnica/limpieza de release más que como hotfix de seguridad. FIX (2026-07-13): PR #92 front MERGEADO — bloque legacy daxa eliminado (437 líneas, 173 imports muertos); authentication/my-profile/settings conservadas (las dos últimas con privateGuard); spec de higiene de rutas nuevo. WI #1263 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): F10: /#/crm → página 404 (bloque legacy eliminado del router); /#/ecommerce-page redirige al login. Sin pantallas demo accesibles.
H06

Confirmación de "Devolver caso" (rol Medical) no interpola el número de caso y deja una coma suelta

bajo front confirmado en código ✅ corregido · verificado en QA
Work Item
#1264 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1264
Ubicación
LBC.DESGRAVAMEN/Code/front/src/app/pages/medical-home-page/comentario/comentario.component.html:6
Causa raíz
El `<h2>` de confirmación tiene el texto hardcodeado "¿Está seguro que desea enviar el caso , al analista LBC?" sin ningún binding de interpolación. El dato existe: el dialog se abre desde medical-home-page.component.ts:170 con `data: { id: commentId, number: number }` y el componente expone `public data: { id, number }` (comentario.component.ts:26), pero el template nunca escribe `{{ data.number }}`, por eso queda el hueco con la coma huérfana donde debía ir el número.
Cómo reproducirlo
Loguearse con rol Medical, entrar a medical-home-page, sobre un caso ejecutar la acción "Devolver caso". En el primer paso del diálogo (showFirstDiv=true) aparece el título: "¿Está seguro que desea enviar el caso , al analista LBC? Esto enviará el caso sin emitir el Informe Médico" — con la coma suelta y sin número. Nótese que el toast posterior sí lo muestra bien (medical-home-page.component.ts:177 usa `${number}`), evidenciando que el dato estaba disponible.
Corrección sugerida
En comentario.component.html:6 interpolar el número antes de la coma. Cambiar `¿Está seguro que desea enviar el caso , al analista LBC?` por `¿Está seguro que desea enviar el caso {{ data.number }} al analista LBC?` (misma solución que ya usan los componentes hermanos documentacion.component.html:36 e informe.component.html, que renderizan `{{ data.number }}`). Quitar la coma para que la oración quede natural con el número interpolado.
Riesgo al corregir
Bajo. Cambio de texto puro en un solo template, sin impacto en lógica ni en el backend. Único cuidado: `number` viene tipado como `any` y podría llegar `undefined` si algún caller abre el dialog sin pasarlo; el único caller actual (medical-home-page) sí lo pasa, así que en la práctica no aplica. Opcionalmente usar `{{ data.number || '' }}` para blindar el caso vacío.
Notas
Hay un segundo ComentarioComponent en pages/caso-page/comentario/ que NO tiene este texto ni recibe `data` (constructor sin MAT_DIALOG_DATA) — no confundir. El afectado es exclusivamente el de medical-home-page. Además de la coma suelta, la oración original es gramaticalmente rara aun con el número; al corregir conviene eliminar la coma. Verificable también con Playwright disparando la acción y aserción sobre el texto del h2. FIX (2026-07-13): PR #93 front MERGEADO — interpolación de {{ data.number }} en la confirmación de devolver caso. WI #1264 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): F06: la confirmación de devolver caso interpola el número (la sonda de la coma suelta no disparó).
H10

Historial de movimientos muestra hora en formato incoherente "13:59 p. m." (24h + sufijo a.m./p.m.)

bajo front confirmado en código ✅ corregido · verificado en QA
Work Item
#1265 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1265
Ubicación
LBC.DESGRAVAMEN/Code/front/src/app/pages/caso-page/historial-page/historial-page.component.html:104
Causa raíz
El DatePipe usa el patrón `'HH:mm a'`: `HH` formatea la hora en 24h (00-23) mientras que `a` agrega el marcador de meridiano a.m./p.m. Ambos tokens son mutuamente excluyentes semánticamente, por eso una hora como 13:59 se pinta como "13:59 p. m.". Es un patrón mal armado, no un problema de datos ni de backend.
Cómo reproducirlo
Abrir el detalle de un caso → pestaña/sección Historial (historial-page), tabla "Fecha y Hora". Cualquier evento cuyo `eventDate` caiga después del mediodía (hora >= 13) muestra la hora en 24h con el sufijo "p. m." pegado, ej. "05/09/2025 13:59 p. m.". Antes del mediodía el defecto queda enmascarado porque HH y hh coinciden (ej. "09:15 a. m.").
Corrección sugerida
En la línea 104 cambiar el patrón del segundo pipe. Dos opciones válidas: (a) `'hh:mm a'` → 12h con meridiano, que es lo consistente con el resto del mismo componente (líneas 46 y 83 ya usan `'dd MMM, yyyy - hh:mm a'`); o (b) `'HH:mm'` → 24h sin meridiano. Recomiendo (a) para uniformidad visual con la línea de tiempo del mismo file: `{{element.eventDate | date: 'dd/MM/yyyy' + " "}}{{element.eventDate | date: 'hh:mm a'}}`.
Riesgo al corregir
Bajo. Cambio puramente de presentación en un único binding. Solo cuidar de mantener coherencia entre las tres apariciones de hora del componente (elegir 12h `hh:mm a` en las tres, o 24h `HH:mm` en las tres) para no dejar mezcla de formatos. El espaciado "p. m." (con espacios) viene del locale es y es correcto, no tocarlo.
Notas
La misma línea concatena `'dd/MM/yyyy' + " "` como truco para el separador — funciona (el `+ " "` es concatenación de string sobre el argumento del pipe), no requiere cambio. Las otras dos horas del file (líneas 46 y 83, línea de tiempo de estados) ya usan el patrón correcto `hh:mm a`, lo que confirma que el `HH...a` de la línea 104 es un descuido aislado. Defecto cosmético, no bloqueante. FIX (2026-07-13): PR #94 front MERGEADO — patrón 'hh:mm a' en la columna Fecha y Hora del historial; spec nuevo. WI #1265 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): Sonda UI: columna Fecha y Hora del historial en 12h con meridiano («05:21 p. m.»), sin mezcla 24h+a.m./p.m.
H12

Botón "Aprobar" de gestion-page transiciona la etapa del caso sin diálogo de confirmación

bajo ux confirmado en código ✅ corregido · verificado en QA
Work Item
#1266 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1266
Ubicación
Front. Botón: LBC.DESGRAVAMEN/Code/front/src/app/pages/gestion-page/gestion-page.component.html:248-256 (click)="verificarDocumento()". Lógica: gestion-page.component.ts:1250 verificarDocumento(), :1261 llamada a saveAnalysisCases(), :1318-1349 saveAnalysisCases() → ClaimsService.saveAnalysis + :1351 saveDocumentDh() (transición a "En Proceso", mensaje :1359).
Causa raíz
El handler del botón "Aprobar" invoca verificarDocumento() de forma directa, que tras validar dispara saveAnalysisCases()/saveDocumentDh() moviendo el caso a la etapa "En Proceso" (acción irreversible por esta pantalla) sin ningún MatDialog de confirmación intermedio. El resto de acciones que cambian etapa en el sistema (reasignacion, carta-rechazo, rechazar-para-pronunciamiento, enviar-a-medico, enviar-a-pago, asignacion, etc.) sí abren un diálogo "¿...que desea...?" antes de ejecutar. Es una inconsistencia de UX, no un fallo funcional.
Cómo reproducirlo
Ingresar como analista a #/home-page/gestion-page/{claimId}, completar/validar los documentos y hacer clic en "Aprobar". El caso pasa de inmediato a "En Proceso" (toast "Caso enviado a la etapa En Proceso de forma exitosa" y redirección a home) sin ningún paso de confirmación. Comparar con cualquier otra acción de cambio de etapa (p.ej. reasignar o enviar a pago), que primero muestran el modal "¿...que desea...?".
Corrección sugerida
Interponer una confirmación antes de la transición irreversible, sin tocar la validación previa. Opción precisa y de mínimo riesgo: en gestion-page.component.ts, en verificarDocumento() (ts:1259-1262), reemplazar la llamada directa `this.saveAnalysisCases();` (línea 1261, dentro del bloque `if (allValid || allvalidAtributes)`) por la apertura de un MatDialog de confirmación (inyectar `private dialog = inject(MatDialog)`) reutilizando el patrón existente de shared/dialogs, con texto tipo "¿Confirma que desea aprobar y enviar el caso a la etapa En Proceso?", y llamar `this.saveAnalysisCases()` solo cuando `afterClosed()` devuelva true. Así se confirma DESPUÉS de validar y solo se gatea el envío real. El template (html:251) no necesita cambio. Como no existe un dialog de confirmación genérico, crear uno reutilizable en shared/dialogs (o reusar la estructura de reasignacion.component como molde), con botones Confirmar/Cancelar.
Riesgo al corregir
Bajo. Es aditivo sobre el happy-path; el botón ya se auto-deshabilita tras el clic ([disabled]="!isEnabled", isEnabled=false en saveAnalysisCases ts:1319) evitando doble submit, y no se toca backend. Cuidar: (1) ubicar la confirmación DESPUÉS de la validación (dentro del if de allValid en ts:1261), NO envolviendo verificarDocumento() completo, para no confirmar antes de que corran las validaciones ni sobre un estado inválido; (2) al cancelar, dejar el botón habilitado (no setear isEnabled=false hasta confirmar); (3) reutilizar MatDialog ya presente en la app (no agregar SweetAlert ni confirm() nativo, romperían consistencia visual).
Notas
Podría argumentarse que "Aprobar" es la CTA principal/feliz de la pantalla y por eso se omitió el confirm intencionalmente; aun así la inconsistencia con el resto del sistema es real y la acción cambia la etapa del caso. Severidad reportada (bajo) es adecuada: no hay pérdida de datos silenciosa ni riesgo de seguridad, solo posibilidad de avanzar el caso por clic accidental. Verificado que no hay MatDialog/confirm/SweetAlert en el flujo de gestion-page para esta acción (la única confirmación relacionada en el proyecto vive en shared/dialogs/* y páginas de home-page/tesoreria/medical, no en gestion-page). FIX (2026-07-13): PR #95 front MERGEADO — ConfirmacionComponent compartido nuevo + confirmación tras validar en verificarDocumento(); 6 tests. WI #1266 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): F02: capturado el diálogo «¿Confirma que desea aprobar y enviar el caso a la etapa En Proceso?» (Cancelar/Aceptar) tras validar; al aceptar ejecuta la transición.
H13

Inconsistencia de nombre: sidebar del Supervisor dice "Gestión de casos" pero la pantalla (repositorio-page) se titula "Repositorio"

bajo ux confirmado en código ✅ corregido · verificado en QA
Work Item
#1267 — https://dev.azure.com/neosoltec/LBC.DESGRAVAMEN/_workitems/edit/1267
Ubicación
Front: common/sidebar/sidebar.component.ts:102 (label Supervisor) vs pages/repositorio-page/repositorio-page.component.html:4 (título hardcodeado). Contraste con sidebar.component.ts:30 y :82 (label "Repositorio" para Analyst/default).
Causa raíz
El label del ítem de sidebar se define por rol en un array duplicado dentro de checkLinks(). Para Supervisor el texto es "Gestión de casos" y para Analyst/default es "Repositorio", pero ambos ruteos van a /repositorio-page, cuyo <h2> está hardcodeado como "Repositorio". Nadie unificó la nomenclatura entre menú y título.
Cómo reproducirlo
Loguearse con un usuario rol Supervisor. En el sidebar aparece el ítem "Gestión de casos" (icono folder). Al hacer clic navega a /repositorio-page y el encabezado de la pantalla dice "Repositorio". Con un usuario Analyst el mismo ítem del sidebar dice "Repositorio", coincidiendo con el título.
Corrección sugerida
Decidir un único nombre de producto para la pantalla y unificarlo. Opción A (mínima, alinear con el título ya existente y con el resto de roles): en sidebar.component.ts:102 cambiar text: 'Gestión de casos' por text: 'Repositorio'. Opción B (si el nombre canónico debe ser "Gestión de casos"): cambiar el <h2> en repositorio-page.component.html:4 a "Gestión de casos" y alinear también los labels de Analyst (:82) y default (:30). Recomiendo Opción A: es un solo string, no toca la pantalla compartida por varios roles y deja consistencia global (3 de 3 lugares dirían "Repositorio"). Definir el término con Producto antes de tocar.
Riesgo al corregir
bajo. Es solo un string de UI. Ojo: la ruta /repositorio-page es compartida por Analyst, default y Supervisor; si en vez del label se cambia el título de la pantalla (Opción B), impacta la vista de TODOS los roles, no solo Supervisor. El array de links está triplicado en checkLinks() (code smell aparte): si se estandariza el nombre, verificar que no haya otros lugares que muestren el label.
Notas
No es un bug funcional: la navegación funciona y la ruta es correcta; es puramente nomenclatura/UX. Podría ser intencional (renombrar el menú por rol) pero al chocar con el título hardcodeado genera disonancia. Decisión de producto sobre el término canónico ("Repositorio" vs "Gestión de casos"). El título del <h2> no es i18n (ngx-translate) sino texto plano hardcodeado, coherente con el resto de la pantalla. FIX (2026-07-13): PR #96 front MERGEADO — label del Supervisor unificado a «Repositorio»; test de consistencia por rol. WI #1267 Resolved. VERIFICADO EN QA (2026-07-13, post-deploy): F09: el sidebar del Supervisor dice «Repositorio», consistente con el título de la pantalla; flujo en verde con el label nuevo.