# Plan: Módulo RRHH — Adición Presentismo (M16–M22)

**Fecha**: 2026-05-18
**Fuente funcional**: `Novasis_ERP_RRHH_Presentismo_v2.pdf` (versión 2.0 — Mayo 2026)
**Documento base**: [`plan-rrhh.md`](./plan-rrhh.md) — módulos M01–M15 ya implementados
**Estándares**: [`PROJECT_STANDARDS.md`](./PROJECT_STANDARDS.md) y [`backend/CLAUDE.md`](../CLAUDE.md)
**Estado**: Fase 1–4 implementadas y validadas (backend/frontend + QA técnico/funcional) — lista para cierre de release

---

## Contexto de partida

El módulo RRHH v1 (M01–M15) está operativo y cuenta con:

- Motor de liquidación en [`src/rrhh/engine/rrhh-engine.service.ts`](../src/rrhh/engine/rrhh-engine.service.ts) que ya consume `rrhh_asistencia_novedades` con `tipo_novedad ∈ { AUSENCIA, TARDANZA }` y aplica los conceptos `DESCUENTO_AUSENCIA` y `DESCUENTO_TARDANZA`.
- Tabla `rrhh_asistencia_novedades` con `preliquidacion_id` y `liquidacion_id` que bloquean edición tras cierre.
- 18 pestañas frontend (incluida `NovedadesTab.jsx`) con guía contextual, prereq checklist y onboarding (ver [`plan-rrhh-ux.md`](./plan-rrhh-ux.md)).
- Parámetros legales versionados en `rrhh_parametros_sistema` con vigencia temporal.

La adición Presentismo amplía esta base sin romper compatibilidad: extiende `rrhh_asistencia_novedades` (no la reemplaza) y agrega tablas nuevas con prefijo `rrhh_`.

---

## Decisiones de diseño

| # | Decisión | Resolución |
|---|---|---|
| 1 | Alcance v1 Presentismo | Implementar M16–M22 priorizando ingesta de marcaciones, motor de novedades y descuento automático en liquidación. |
| 2 | Tabla de novedades | Extender `rrhh_asistencia_novedades` agregando columnas (`turno_id`, `turno_bloque_orden`, `hora_real`, `hora_esperada`, `tolerancia_aplicada_min`, `permiso_id`, `generado_automatico`, `estado`). No se crea `novedades_presentismo` separada — evita duplicar lógica del engine. |
| 3 | Enums en Prisma | Todos los campos de estado/tipo nuevos (`tipo_marcacion`, `estado_importacion`, `tipo_conexion`, `tipo_auth`, `tipo_turno`, `tipo_asignacion_turno`, `tipo_permiso_presentismo`, `tipo_justificacion`, `estado_permiso_presentismo`, `motivo_correccion_marcacion`, `aplica_tolerancia_a`, `tipo_novedad_presentismo`, `estado_novedad_presentismo`) declarados como `enum` Prisma — sin `VARCHAR + // comentario` (regla `PROJECT_STANDARDS.md` punto 2). |
| 4 | Ampliación de `tipo_novedad` | Se amplía el enum para incluir `AUSENCIA_JORNADA`, `AUSENCIA_BLOQUE`, `SALIDA_ANTICIPADA`, `AUSENCIA_JUSTIFICADA`, `TARDANZA_JUSTIFICADA`, manteniendo `AUSENCIA` y `TARDANZA` legacy como sinónimos durante migración. |
| 5 | Identificación de funcionarios | El reloj/planilla siempre envía `cedula_identidad`; el resolver lo mapea contra `rrhh_empleados.cedula_identidad` filtrado por `empresa_id`. Documento no encontrado ⇒ `marcaciones_raw.estado_importacion = ERROR`. |
| 6 | Idempotencia | Motor de deduplicación y motor de novedades son idempotentes: re-ejecutar sobre el mismo período produce el mismo resultado. Reproceso de novedades pasa las existentes a `ANULADA` (no las elimina). |
| 7 | Bloqueo por liquidación cerrada | El motor de novedades no se ejecuta si existe liquidación `CERRADA` del período. Una novedad ya vinculada a `liquidacion_id` no puede anularse hasta anular la liquidación. |
| 8 | Log de correcciones inmutable | `rrhh_marcaciones_log_correcciones` es auditoría permanente — no se expone endpoint `DELETE` ni `UPDATE`, ni siquiera para `SUPER_ADMIN`. Las correcciones manuales viven en `rrhh_marcaciones_ajustes_manuales`. |
| 9 | Multi-empresa | Todas las tablas nuevas operativas llevan `empresa_id` obligatorio con índice; los matchings de documento se hacen siempre `WHERE empresa_id = $1`. |
| 10 | Frontend | Nueva pestaña padre **Presentismo** dentro de `RRHHTemplate.jsx` con sub-tabs: Relojes, Turnos, Marcaciones, Permisos, Novedades, Tolerancias, Log Correcciones. La pestaña Novedades existente se relabel a "Novedades de Presentismo" (mismo store, datos extendidos). |
| 11 | Selectores buscables | Todo dropdown de funcionario, turno, reloj, sucursal o departamento usa `Autocomplete` MUI (regla `feedback_selectores_buscables`). Nada de `<select>` nativo. |
| 12 | Diálogos | Confirmaciones (anular novedad, eliminar asignación de turno, ejecutar motor) usan `ConfirmDialog`/`useConfirmDialog`; nunca `window.confirm`/`alert` (regla `feedback_no_alert_browser`). |
| 13 | Tour y onboarding | Se extiende `rrhhTour.js` con pasos para las 7 nuevas sub-tabs y se agrega slide 4 al `RRHHOnboardingDialog` describiendo Presentismo. |
| 14 | AI Dashboard | Se registra el área `RRHH / Presentismo` en [`src/ai-dashboard/chat/schema-context.ts`](../src/ai-dashboard/chat/schema-context.ts) con las tablas nuevas y ejemplos SQL (regla `PROJECT_STANDARDS.md` punto 15). |
| 15 | Estrategia de entrega | 5 fases incrementales (≈16 semanas) iguales a las del PDF, cada fase entregable y testeable de forma aislada. |

---

## Alcance funcional (M16–M22)

| Módulo | Descripción | Prioridad |
|---|---|---|
| M16 | Integración con relojes marcadores (API REST + importación CSV/Excel) | Alta |
| M17 | Gestión de turnos (simples y cortados; asignación perpetua/ocasional) | Alta |
| M18 | Permisos de presentismo (jornada/turno/salida anticipada/llegada tardía/ausencia turno) | Alta |
| M19 | Motor de novedades de presentismo (comparación marcaciones vs. turnos + permisos) | Crítica |
| M20 | Parámetros de tolerancia (globales + por fecha particular) | Alta |
| M21 | Consulta de marcaciones consumidas (3 vistas + exportación) | Media |
| M22 | Deduplicación de marcaciones y log de correcciones inmutable | Alta |

---

## Parámetros nuevos en `rrhh_parametros_sistema`

Se agregan vía seed con vigencia inicial `2026-05-01`:

| Clave | Default | Categoría | Editable |
|---|---|---|---|
| `PRESENTISMO_TOLERANCIA_ENTRADA_MIN` | `5` | PRESENTISMO | ✅ |
| `PRESENTISMO_TOLERANCIA_SALIDA_MIN` | `5` | PRESENTISMO | ✅ |
| `PRESENTISMO_GENERAR_NOVEDAD_AUTOMATICA` | `true` | PRESENTISMO | ✅ |
| `PRESENTISMO_HORAS_AUSENCIA_MEDIA_JORNADA` | `4` | PRESENTISMO | ✅ |

Reglas no parametrizables (validadas en código):

- Tolerancia especial por fecha siempre prevalece sobre la global (regla 27 PDF).
- `JORNADA_COMPLETA` tiene precedencia sobre cualquier otro permiso del mismo día (regla 26 PDF).
- Descuentos por tardanza/ausencia respetan `LIMITE_DESCUENTO_SALARIO = 0.30` (Art. 240 CT, ya existente).

---

## Modelo de datos propuesto

### Enums Prisma (todos nuevos)

```prisma
enum rrhh_reloj_tipo_conexion { API PLANILLA }
enum rrhh_reloj_tipo_auth { NONE BASIC BEARER API_KEY }
enum rrhh_marcacion_fuente { API PLANILLA }
enum rrhh_marcacion_tipo { ENTRADA SALIDA }
enum rrhh_marcacion_estado { PENDIENTE PROCESADO DUPLICADO ERROR }
enum rrhh_turno_tipo { SIMPLE CORTADO }
enum rrhh_turno_asignacion_tipo { PERPETUA OCASIONAL }
enum rrhh_permiso_presentismo_tipo {
  JORNADA_COMPLETA
  TURNO
  SALIDA_ANTICIPADA
  LLEGADA_TARDIA
  AUSENCIA_TURNO
}
enum rrhh_permiso_justificacion_tipo {
  MEDICA
  PERSONAL
  FUERZA_MAYOR
  INSTITUCIONAL
  CLIMA
  OTRO
}
enum rrhh_permiso_estado { PENDIENTE APROBADO RECHAZADO }
enum rrhh_tolerancia_aplica_a { TODOS SUCURSAL DEPARTAMENTO }
enum rrhh_correccion_motivo {
  DUPLICADO_ENTRADA
  DUPLICADO_SALIDA
  DUPLICADO_AMBOS
}
enum rrhh_novedad_estado { BORRADOR CONFIRMADA ANULADA }
```

Se amplía el enum/columna `tipo_novedad` (hoy `VARCHAR(30)`) a un nuevo enum `rrhh_novedad_tipo` con valores:
`AUSENCIA`, `TARDANZA`, `SALIDA_ANTICIPADA`, `AUSENCIA_JORNADA`, `AUSENCIA_BLOQUE`, `AUSENCIA_JUSTIFICADA`, `TARDANZA_JUSTIFICADA`.
La migración convierte los valores legacy `AUSENCIA` → `AUSENCIA_JORNADA` y los conserva en una columna `tipo_novedad_legacy` por trazabilidad durante 3 meses.

### 1) Configuración de relojes y marcaciones

```sql
CREATE TABLE rrhh_relojes_marcadores (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  sucursal_id UUID REFERENCES sucursales(id),
  nombre VARCHAR(100) NOT NULL,
  tipo_conexion rrhh_reloj_tipo_conexion NOT NULL,
  url_base VARCHAR(255),
  api_key TEXT, -- cifrado en BD (igual que credenciales SIFEN)
  tipo_auth rrhh_reloj_tipo_auth,
  formato_respuesta VARCHAR(20), -- JSON / XML
  campo_documento VARCHAR(50),
  campo_timestamp VARCHAR(50),
  intervalo_polling_min INT, -- NULL = manual
  activo BOOLEAN NOT NULL DEFAULT true,
  creado_por UUID,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE rrhh_marcaciones_raw (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  reloj_id UUID REFERENCES rrhh_relojes_marcadores(id),
  fuente rrhh_marcacion_fuente NOT NULL,
  documento_raw VARCHAR(20) NOT NULL,
  empleado_id UUID REFERENCES rrhh_empleados(id), -- NULL si no matchea
  timestamp_marcacion TIMESTAMP NOT NULL,
  tipo_marcacion rrhh_marcacion_tipo, -- NULL si se infiere
  estado_importacion rrhh_marcacion_estado NOT NULL DEFAULT 'PENDIENTE',
  importado_en TIMESTAMP NOT NULL DEFAULT NOW(),
  importado_por UUID,
  batch_importacion_id UUID NOT NULL,
  error_descripcion TEXT
);
CREATE INDEX ON rrhh_marcaciones_raw (empresa_id, timestamp_marcacion);
CREATE INDEX ON rrhh_marcaciones_raw (batch_importacion_id);
CREATE INDEX ON rrhh_marcaciones_raw (empleado_id, timestamp_marcacion);

CREATE TABLE rrhh_marcaciones_limpias (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  empleado_id UUID NOT NULL REFERENCES rrhh_empleados(id),
  fecha DATE NOT NULL,
  hora_entrada TIME,
  hora_salida TIME,
  turno_id UUID REFERENCES rrhh_turnos(id),
  procesado_novedades BOOLEAN NOT NULL DEFAULT false,
  marcacion_raw_entrada_id UUID REFERENCES rrhh_marcaciones_raw(id),
  marcacion_raw_salida_id UUID REFERENCES rrhh_marcaciones_raw(id),
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (empleado_id, fecha)
);
```

### 2) Turnos y asignaciones

```sql
CREATE TABLE rrhh_turnos (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  nombre VARCHAR(100) NOT NULL,
  descripcion TEXT,
  tipo rrhh_turno_tipo NOT NULL,
  lunes BOOLEAN NOT NULL DEFAULT false,
  martes BOOLEAN NOT NULL DEFAULT false,
  miercoles BOOLEAN NOT NULL DEFAULT false,
  jueves BOOLEAN NOT NULL DEFAULT false,
  viernes BOOLEAN NOT NULL DEFAULT false,
  sabado BOOLEAN NOT NULL DEFAULT false,
  domingo BOOLEAN NOT NULL DEFAULT false,
  activo BOOLEAN NOT NULL DEFAULT true,
  creado_por UUID,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE rrhh_turnos_bloques (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  turno_id UUID NOT NULL REFERENCES rrhh_turnos(id) ON DELETE CASCADE,
  orden INT NOT NULL,
  hora_entrada TIME NOT NULL,
  hora_salida TIME NOT NULL,
  UNIQUE (turno_id, orden)
);

CREATE TABLE rrhh_empleados_turnos (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  empleado_id UUID NOT NULL REFERENCES rrhh_empleados(id),
  turno_id UUID NOT NULL REFERENCES rrhh_turnos(id),
  tipo_asignacion rrhh_turno_asignacion_tipo NOT NULL,
  fecha_inicio DATE,
  fecha_fin DATE,
  prioridad INT NOT NULL DEFAULT 100, -- menor = mayor precedencia
  observacion TEXT,
  creado_por UUID,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX ON rrhh_empleados_turnos (empleado_id, fecha_inicio, fecha_fin);
```

### 3) Permisos y tolerancias

```sql
CREATE TABLE rrhh_permisos_presentismo (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  empleado_id UUID NOT NULL REFERENCES rrhh_empleados(id),
  fecha DATE NOT NULL,
  tipo_permiso rrhh_permiso_presentismo_tipo NOT NULL,
  turno_bloque_orden INT,
  hora_desde TIME,
  hora_hasta TIME,
  minutos_tolerados INT,
  motivo TEXT NOT NULL,
  tipo_justificacion rrhh_permiso_justificacion_tipo NOT NULL,
  afecta_descuento BOOLEAN NOT NULL DEFAULT true,
  estado rrhh_permiso_estado NOT NULL DEFAULT 'PENDIENTE',
  aprobado_por UUID,
  fecha_aprobacion TIMESTAMP,
  rechazado_por UUID,
  fecha_rechazo TIMESTAMP,
  creado_por UUID,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX ON rrhh_permisos_presentismo (empleado_id, fecha);
CREATE INDEX ON rrhh_permisos_presentismo (empresa_id, estado);

CREATE TABLE rrhh_presentismo_tolerancias_especiales (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  fecha DATE NOT NULL,
  tolerancia_entrada_min INT NOT NULL,
  tolerancia_salida_min INT NOT NULL,
  motivo TEXT NOT NULL,
  aplica_a rrhh_tolerancia_aplica_a NOT NULL DEFAULT 'TODOS',
  aplica_a_id UUID,
  creado_por UUID,
  created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX ON rrhh_presentismo_tolerancias_especiales (empresa_id, fecha);
```

### 4) Extensión de `rrhh_asistencia_novedades`

```sql
ALTER TABLE rrhh_asistencia_novedades
  ADD COLUMN turno_id UUID REFERENCES rrhh_turnos(id),
  ADD COLUMN turno_bloque_orden INT,
  ADD COLUMN hora_esperada TIME,
  ADD COLUMN hora_real TIME,
  ADD COLUMN tolerancia_aplicada_min INT,
  ADD COLUMN permiso_id UUID REFERENCES rrhh_permisos_presentismo(id),
  ADD COLUMN generado_automatico BOOLEAN NOT NULL DEFAULT false,
  ADD COLUMN estado rrhh_novedad_estado NOT NULL DEFAULT 'CONFIRMADA',
  ADD COLUMN tipo_novedad_legacy VARCHAR(30); -- backfill durante migración
```

La columna `tipo_novedad` migra a `rrhh_novedad_tipo` con conversión:
`'AUSENCIA' → 'AUSENCIA_JORNADA'`, `'TARDANZA' → 'TARDANZA'`. Los valores nuevos (`SALIDA_ANTICIPADA`, `AUSENCIA_BLOQUE`, `AUSENCIA_JUSTIFICADA`, `TARDANZA_JUSTIFICADA`) sólo los genera el motor M19.

El engine de liquidación existente ([`rrhh-engine.service.ts:296-312`](../src/rrhh/engine/rrhh-engine.service.ts#L296-L312)) se ajusta para tratar `AUSENCIA_JORNADA`, `AUSENCIA_BLOQUE` como ausencia (proporcional al bloque) y `TARDANZA`, `SALIDA_ANTICIPADA` como tardanza por minutos. `AUSENCIA_JUSTIFICADA` y `TARDANZA_JUSTIFICADA` se ignoran (sin descuento).

### 5) Deduplicación y log

```sql
CREATE TABLE rrhh_marcaciones_log_correcciones (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  empleado_id UUID NOT NULL REFERENCES rrhh_empleados(id),
  fecha DATE NOT NULL,
  batch_importacion_id UUID NOT NULL,
  cantidad_marcaciones_raw INT NOT NULL,
  cantidad_duplicados_descartados INT NOT NULL,
  timestamp_entrada_seleccionado TIMESTAMP,
  timestamp_salida_seleccionado TIMESTAMP,
  timestamps_descartados JSONB,
  motivo_correccion rrhh_correccion_motivo NOT NULL,
  procesado_en TIMESTAMP NOT NULL DEFAULT NOW(),
  procesado_por UUID
);
CREATE INDEX ON rrhh_marcaciones_log_correcciones (empresa_id, fecha);
CREATE INDEX ON rrhh_marcaciones_log_correcciones (batch_importacion_id);

CREATE TABLE rrhh_marcaciones_ajustes_manuales (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  marcacion_limpia_id UUID NOT NULL REFERENCES rrhh_marcaciones_limpias(id),
  campo_modificado VARCHAR(30) NOT NULL, -- HORA_ENTRADA / HORA_SALIDA / TURNO
  valor_anterior TEXT,
  valor_nuevo TEXT NOT NULL,
  motivo TEXT NOT NULL,
  ajustado_por UUID NOT NULL,
  ajustado_en TIMESTAMP NOT NULL DEFAULT NOW()
);
```

Se aplica `REVOKE DELETE, UPDATE ON rrhh_marcaciones_log_correcciones FROM <role_app>` en la migración para forzar inmutabilidad a nivel BD (regla 29 PDF).

---

## Reglas de negocio obligatorias (backend, no sólo UI)

1. Dos asignaciones `OCASIONAL` solapadas con misma `prioridad` para el mismo empleado ⇒ `HTTP 409`.
2. En un turno `CORTADO` los bloques no pueden solaparse (`hora_entrada(N+1) > hora_salida(N)`).
3. El motor de novedades **no** puede ejecutarse si la liquidación del período está `CERRADA` ⇒ `HTTP 409`.
4. Una novedad `CONFIRMADA` con `liquidacion_id` no nulo no puede pasar a `ANULADA` sin anular antes la liquidación.
5. `rrhh_marcaciones_raw` es inmutable post-importación; cambios requieren registro en `rrhh_marcaciones_ajustes_manuales`.
6. Deduplicación es idempotente: misma entrada produce mismo `marcaciones_limpias`; no genera filas duplicadas.
7. Permisos `JORNADA_COMPLETA` y `LLEGADA_TARDIA` para misma fecha+empleado: precedencia siempre del primero.
8. Tolerancia especial por fecha siempre prevalece sobre la global.
9. Sin turno vigente ese día ⇒ no se crea `marcaciones_limpias` ni novedad. Las marcaciones quedan en raw pero no procesan.
10. Log de correcciones inmutable (DB-level y API-level).
11. Reproceso de novedades de un período: las existentes pasan a `ANULADA`, nunca se eliminan.
12. Descuentos `TARDANZA` + `AUSENCIA` respetan `LIMITE_DESCUENTO_SALARIO` (30% Art. 240 CT). El motor de liquidación ya valida esto (ver `rrhh-engine.service.ts:378`).
13. Una marcación cuyo `documento_raw` no matchea con ningún `rrhh_empleados.cedula_identidad` de la empresa: queda en `estado_importacion = ERROR` con `error_descripcion` explicativo; no bloquea el lote.
14. `intervalo_polling_min` debe ser ≥ 5 para evitar saturar la API del reloj.

---

## Algoritmo del motor M19 (resumen ejecutable)

```typescript
// Pseudocódigo NestJS — vive en src/rrhh/engine/presentismo-engine.service.ts
async procesarPeriodo(empresaId, fechaDesde, fechaHasta, opts) {
  await this.anularNovedadesPreviasDelPeriodo(empresaId, fechaDesde, fechaHasta);
  const empleados = await this.empleadosConTurno(empresaId);
  for (const dia of rangoDeFechas(fechaDesde, fechaHasta)) {
    for (const emp of empleados) {
      const turno = await this.resolverTurnoVigente(emp.id, dia); // OCASIONAL > PERPETUA
      if (!turno || !diaSemanaAplica(turno, dia)) continue;
      const permisos = await this.permisosAprobadosDelDia(emp.id, dia);
      if (permisos.some(p => p.tipo_permiso === 'JORNADA_COMPLETA')) {
        await this.registrar('AUSENCIA_JUSTIFICADA', { empleado: emp, fecha: dia, permiso });
        continue;
      }
      const tolerancia = await this.tolerancia(empresaId, dia, emp); // especial > global
      const marcacion = await this.marcacionLimpiaDelDia(emp.id, dia);
      for (const bloque of turno.bloques) {
        if (permisos.find(p => p.tipo_permiso === 'AUSENCIA_TURNO' && p.turno_bloque_orden === bloque.orden)) {
          await this.registrar('AUSENCIA_JUSTIFICADA', { ..., turno_bloque_orden: bloque.orden });
          continue;
        }
        // ENTRADA
        if (!marcacion?.hora_entrada) {
          await this.registrar(bloque.orden === 1 ? 'AUSENCIA_JORNADA' : 'AUSENCIA_BLOQUE', { ... });
        } else {
          const tardanzaMin = diferenciaMin(marcacion.hora_entrada, bloque.hora_entrada);
          const permLT = permisos.find(p => p.tipo_permiso === 'LLEGADA_TARDIA' && cubre(p, marcacion.hora_entrada));
          if (tardanzaMin > tolerancia && !permLT) await this.registrar('TARDANZA', { ..., minutos: tardanzaMin });
          else if (permLT) await this.registrar('TARDANZA_JUSTIFICADA', { ..., permiso: permLT });
        }
        // SALIDA
        if (marcacion?.hora_salida) {
          const anticipMin = diferenciaMin(bloque.hora_salida, marcacion.hora_salida);
          const permSA = permisos.find(p => p.tipo_permiso === 'SALIDA_ANTICIPADA' && cubre(p, marcacion.hora_salida));
          if (anticipMin > toleranciaSalida && !permSA) await this.registrar('SALIDA_ANTICIPADA', { ..., minutos: anticipMin });
        }
      }
      await this.marcarProcesado(marcacion);
    }
  }
}
```

Reside en `src/rrhh/engine/presentismo-engine.service.ts`, completamente puro (recibe `PrismaService` por DI, sin side-effects no testeables). Cobertura objetivo: **≥ 90 %** (igual que el engine de liquidación).

---

## API REST propuesta (v1, prefijo `/api/v1/rrhh/presentismo`)

| Método | Ruta | Permiso |
|---|---|---|
| GET | `/relojes` | `RRHH_PRESENTISMO_CONFIG` |
| POST | `/relojes` | `RRHH_PRESENTISMO_CONFIG` |
| PUT | `/relojes/:id` | `RRHH_PRESENTISMO_CONFIG` |
| POST | `/relojes/:id/sincronizar` | `RRHH_PRESENTISMO_INGESTA` |
| POST | `/marcaciones/importar` | `RRHH_PRESENTISMO_INGESTA` |
| GET | `/marcaciones/batches` | `RRHH_PRESENTISMO_CONSULTA` |
| GET | `/marcaciones/batches/:id` | `RRHH_PRESENTISMO_CONSULTA` |
| GET | `/marcaciones` | `RRHH_PRESENTISMO_CONSULTA` |
| GET | `/marcaciones/limpias` | `RRHH_PRESENTISMO_CONSULTA` |
| PATCH | `/marcaciones/limpias/:id` | `RRHH_PRESENTISMO_AJUSTE` |
| GET | `/turnos` | `RRHH_PRESENTISMO_TURNOS` |
| POST | `/turnos` | `RRHH_PRESENTISMO_TURNOS` |
| PUT | `/turnos/:id` | `RRHH_PRESENTISMO_TURNOS` |
| GET | `/empleados/:id/turnos` | `RRHH_PRESENTISMO_TURNOS` |
| POST | `/empleados/:id/turnos` | `RRHH_PRESENTISMO_TURNOS` |
| DELETE | `/empleados/:id/turnos/:asignId` | `RRHH_PRESENTISMO_TURNOS` |
| GET | `/permisos` | `RRHH_PRESENTISMO_PERMISOS` |
| POST | `/permisos` | `RRHH_PRESENTISMO_PERMISOS` |
| PUT | `/permisos/:id/aprobar` | `RRHH_PRESENTISMO_PERMISOS_APROBAR` |
| PUT | `/permisos/:id/rechazar` | `RRHH_PRESENTISMO_PERMISOS_APROBAR` |
| GET | `/tolerancias-especiales` | `RRHH_PRESENTISMO_CONFIG` |
| POST | `/tolerancias-especiales` | `RRHH_PRESENTISMO_CONFIG` |
| POST | `/novedades/procesar` | `RRHH_PRESENTISMO_PROCESAR` |
| GET | `/novedades` | `RRHH_PRESENTISMO_CONSULTA` |
| PUT | `/novedades/:id/anular` | `RRHH_PRESENTISMO_PROCESAR` |
| GET | `/log-correcciones` | `RRHH_AUDITORIA_VER` |
| GET | `/log-correcciones/:id` | `RRHH_AUDITORIA_VER` |

Todos usan el stack `@UseGuards(AuthGuard('jwt'), PermissionGuard)` + `@RequirePermission('RRHH', ...)` siguiendo el patrón de los demás controllers de RRHH.

---

## Permisos nuevos

- `RRHH_PRESENTISMO_CONFIG` (relojes, tolerancias)
- `RRHH_PRESENTISMO_INGESTA` (importar planilla, sincronizar API)
- `RRHH_PRESENTISMO_CONSULTA` (ver marcaciones, batches, novedades)
- `RRHH_PRESENTISMO_TURNOS` (CRUD turnos y asignaciones)
- `RRHH_PRESENTISMO_PERMISOS` (alta de permisos)
- `RRHH_PRESENTISMO_PERMISOS_APROBAR` (aprobar/rechazar)
- `RRHH_PRESENTISMO_PROCESAR` (ejecutar motor, anular novedades)
- `RRHH_PRESENTISMO_AJUSTE` (corrección manual de marcaciones limpias)

Se asignan a los roles existentes (`SUPER_ADMIN`, `GERENTE_RRHH`, `OPERADOR_RRHH`, `SUPERVISOR`, `AUDITOR`) vía seed; el rol `EMPLEADO` no recibe ninguno.

---

## Frontend

### Estructura de pestañas (extensión de `RRHHTemplate.jsx`)

Nueva pestaña padre **Presentismo** con sub-tabs (ordenadas por flujo natural):

1. **Relojes** — CRUD de relojes marcadores con prueba de conexión.
2. **Turnos** — CRUD turno + bloques; vista calendario semanal.
3. **Asignación de Turnos** — sub-vista de Empleados o tab independiente; tabla con perpetuas y ocasionales.
4. **Marcaciones** — 3 vistas (Resumen por día / Detalle raw / Novedades) con filtros (fecha, funcionario `Autocomplete`, sucursal, departamento, estado, fuente, reloj) y exportación Excel/PDF.
5. **Permisos** — flujo PENDIENTE → APROBADO/RECHAZADO con `ConfirmDialog`.
6. **Tolerancias** — tabla de tolerancias especiales por fecha.
7. **Novedades de Presentismo** — relabel + extensión de `NovedadesTab.jsx`; botón "Procesar período" → `ConfirmDialog` → POST `/novedades/procesar`.
8. **Log de Correcciones** — tabla read-only con drill-down a detalle del lote.

### Reglas UX aplicadas (PROJECT_STANDARDS + memorias)

- **Selectores buscables**: `Autocomplete` MUI para empleado (filtra por nombre/CI), turno, reloj, sucursal, departamento, concepto. Nunca `<select>` nativo.
- **Diálogos**: confirmaciones de anular/aprobar/procesar usan `ConfirmDialog` + `useConfirmDialog`. Edición de motivo: `PromptDialog`. Nunca `window.confirm/alert/prompt`.
- **MonedaInput**: no aplica aquí (no hay inputs monetarios directos en presentismo; los descuentos los calcula el motor de liquidación).
- **Feedback inmediato**: toasts `notistack` (existentes) + `InlineValidationBanner` para errores de formulario de turno/permiso.
- **Empty states**: `RRHHEmptyState` en cada sub-tab vacía, con CTA y botón "Ver guía".
- **Guía contextual**: `RRHHGuia` por sub-tab con contenido nuevo en `rrhhGuias.js` (sección `presentismo*`).
- **Prereq checklist**: `RRHHPrereqChecklist` muestra dependencias (`Turnos definidos`, `Reloj configurado`, `Empleados con CI`, `Parámetros vigentes`).
- **Tour**: 7 pasos nuevos en `rrhhTour.js` + slide 4 en `RRHHOnboardingDialog`.
- **Responsive**: tabla de marcaciones con virtualización (>1k filas) y vista compacta en `<600px`.

### Servicios frontend

- Nuevo `src/api/rrhh-presentismo.service.js` con métodos para cada endpoint.
- Extensión de `src/api/rrhh.service.js` para los endpoints `novedades/procesar` y `novedades/:id/anular`.

---

## Integración con módulos existentes

| Módulo | Punto de integración | Detalle |
|---|---|---|
| **M01 — Empleados** | `cedula_identidad` | Identificador de matching con el reloj. Validar formato CI paraguayo (sólo dígitos, 5-9 chars). |
| **M04 — Vacaciones/Permisos** | Coexistencia | Los permisos de vacaciones siguen en `rrhh_vacaciones_solicitudes`; los permisos de presentismo en la tabla nueva. La pestaña "Novedades" se relabel a "Novedades de Presentismo" pero conserva la URL/ruta. |
| **M07 — Conceptos** | `DESCUENTO_TARDANZA`, `DESCUENTO_AUSENCIA` | Ya existen en seed (`src/rrhh/seeds`). Se actualizan fórmulas/descripciones para reflejar SALIDA_ANTICIPADA y AUSENCIA_BLOQUE. |
| **M09 — Pre-liquidación** | Engine extendido | Antes de calcular: si no se procesó M19 para el período, emitir advertencia (no bloquea, pero queda en `advertencias` del response). |
| **M10 — Liquidación mensual** | Engine extendido | Al cerrar, se setea `liquidacion_id` en cada `rrhh_asistencia_novedades` con `impacta_liquidacion = true` del período (ya implementado para AUSENCIA/TARDANZA; se extiende a SALIDA_ANTICIPADA / AUSENCIA_BLOQUE). |
| **M15 — Parámetros** | 4 claves nuevas | Sembradas con vigencia `2026-05-01`; editables desde `ParametrosTab.jsx`. |
| **AI Dashboard** | `schema-context.ts` | Nuevo área `RRHH / Presentismo` con las 9 tablas nuevas y 4-5 ejemplos SQL (tardanzas del mes, ausencias por sucursal, marcaciones duplicadas, etc.). |
| **Auditoría** | `@Auditable('rrhh_presentismo_*')` | Cada entidad nueva auditable; el log de correcciones tiene su propio mecanismo nativo. |
| **Tours frontend** | `rrhhTour.js` | 7 pasos nuevos. |

---

## Plan de implementación

### Fase 1 — Migraciones y configuración base (Semanas 1–3)

- Crear migración Prisma `20260520_rrhh_presentismo_base/` con enums y tablas: `rrhh_relojes_marcadores`, `rrhh_marcaciones_raw`, `rrhh_marcaciones_limpias`, `rrhh_turnos`, `rrhh_turnos_bloques`, `rrhh_empleados_turnos`. Usar `IF NOT EXISTS` y `DO $$ BEGIN ... EXCEPTION WHEN duplicate_object THEN NULL; END $$` (regla de migraciones `backend/CLAUDE.md`).
- Migración separada `20260521_rrhh_presentismo_permisos/` para `rrhh_permisos_presentismo`, `rrhh_presentismo_tolerancias_especiales`, `rrhh_marcaciones_log_correcciones`, `rrhh_marcaciones_ajustes_manuales`.
- Migración `20260522_rrhh_asistencia_novedades_ext/` con extensión de columnas + conversión de `tipo_novedad` a enum.
- Seed de los 4 parámetros nuevos en `rrhh_parametros_sistema` (vigencia 2026-05-01).
- CRUD `rrhh_relojes_marcadores` + endpoint `POST /:id/sincronizar` (placeholder).
- CRUD `rrhh_turnos` + `rrhh_turnos_bloques` con validación de solapamiento de bloques.
- Permisos nuevos sembrados en `seed.service.ts`.
- AI Dashboard: agregar área `RRHH / Presentismo` a `schema-context.ts`.

### Fase 2 — Ingesta de marcaciones (Semanas 4–6)

- Conector de planilla CSV/Excel (reusar `xlsx`/`csv-parse` ya presentes en `src/importaciones`). Validar columnas mínimas, formato CI, fechas.
- Cliente HTTP genérico para API de relojes (NONE/BASIC/BEARER/API_KEY) en `src/rrhh/services/relojes-client.service.ts`.
- BullMQ queue nuevo `rrhh-presentismo-queue` para polling programado (`intervalo_polling_min`) y deduplicación asincrónica.
- Motor de deduplicación en `src/rrhh/engine/marcaciones-dedup.service.ts` (idempotente, agrupa por `empleado_id + fecha`, selecciona min/max timestamp, registra log).
- Endpoint `POST /marcaciones/importar` con multipart.
- Endpoint `GET /marcaciones/batches`, `GET /marcaciones/batches/:id`.
- Endpoint `GET /marcaciones` y `GET /marcaciones/limpias` con filtros.
- Pantalla frontend `MarcacionesTab.jsx` con 3 vistas + exportación.

**Cierre Fase 2 (2026-05-18):**
- ✅ Polling automático operativo con BullMQ (`rrhh-presentismo-queue`) para relojes API activos.
- ✅ Alta/edición/desactivación de reloj reprograma o remueve su job repeatable automáticamente.
- ✅ Endpoint `PATCH /api/v1/rrhh/presentismo/marcaciones/limpias/:id` implementado con auditoría en `rrhh_marcaciones_ajustes_manuales`.
- ✅ Migración de legacy `AUSENCIA -> AUSENCIA_JORNADA` aplicada preservando `tipo_novedad_legacy`.
- ✅ Validación de CI en parser ajustada a 5–9 dígitos.
- ✅ Frontend Presentismo agregado en RRHH con subtabs Fase 2: Relojes, Turnos, Marcaciones y Log de Correcciones.

### Nota técnica — Rollout cola automática

- Dependencia obligatoria: Redis disponible y estable en el entorno (dev/staging/prod).
- La cola `rrhh-presentismo-queue` se reconstruye al iniciar el módulo RRHH (`bootstrap` por relojes API activos).
- Monitoreo mínimo recomendado:
  - cantidad de jobs repeatables registrados,
  - jobs fallidos por reloj y por empresa,
  - latencia promedio de sincronización.
- Ante caída temporal de API externa del reloj, BullMQ aplica reintentos exponenciales sin bloquear operaciones manuales.

### Plan de pruebas operativo Fase 2

- Documento QA ejecutable: [`qa-plan-rrhh-presentismo-f2.md`](./qa-plan-rrhh-presentismo-f2.md)
- Evidencias de ejecución: `backend/docs/qa-presentismo-evidencia-YYYY-MM-DD.md`

### Plan de pruebas operativo Fase 3

- Documento QA ejecutable: [`qa-plan-rrhh-presentismo-f3.md`](./qa-plan-rrhh-presentismo-f3.md)

### Plan de pruebas operativo Fase 4

- Documento QA ejecutable: [`qa-plan-rrhh-presentismo-f4.md`](./qa-plan-rrhh-presentismo-f4.md)

### Fase 3 — Turnos, permisos y tolerancias (Semanas 7–9)

- ✅ Algoritmo de resolución de turno vigente (`src/rrhh/services/turnos-resolver.service.ts`).
- ✅ Validación de asignaciones ocasionales solapadas con misma prioridad (HTTP 409).
- ✅ CRUD permisos con flujo PENDIENTE → APROBADO/RECHAZADO; auditable (`src/rrhh/controllers/permisos-presentismo.controller.ts`).
- ✅ CRUD tolerancias especiales por fecha (`src/rrhh/controllers/tolerancias-presentismo.controller.ts`).
- ✅ Pantallas frontend `TurnosPresentismoTab.jsx`, `AsignacionTurnosPresentismoTab.jsx`, `PermisosPresentismoTab.jsx`, `ToleranciasEspecialesPresentismoTab.jsx`.
- ✅ `RRHHGuia` + `RRHHPrereqChecklist` + `RRHHEmptyState` en subtabs de Presentismo fase 3.

### Fase 4 — Motor de novedades e integración con liquidación (Semanas 10–13)

- ✅ `src/rrhh/engine/presentismo-engine.service.ts` implementado y testeable (`src/rrhh/engine/presentismo-engine.service.spec.ts`).
- ✅ Casos implementados en motor: ausencia jornada, ausencia bloque, tardanza, tardanza justificada, salida anticipada, permiso jornada completa, permiso turno y tolerancia especial vs. global.
- ✅ Endpoint `POST /novedades/procesar` (período + sucursal opcional) — idempotente, anula existentes automáticas del período.
- ✅ Endpoint `PUT /novedades/:id/anular` con guard de `liquidacion_id`.
- ✅ Ajuste de `rrhh-engine.service.ts` para `AUSENCIA_JORNADA`, `AUSENCIA_BLOQUE` y `SALIDA_ANTICIPADA`.
- ✅ Extensión de `NovedadesTab.jsx` con botón "Procesar período" + `ConfirmDialog`, estado y anulación.
- ✅ `LogCorreccionesPresentismoTab.jsx` continúa en modo read-only dentro de Presentismo.
- ✅ Cierre fino aplicado: cobertura ampliada de casos borde del motor + ajuste de tour de Novedades de Presentismo.

### Fase 5 — QA, documentación y rollout (Semanas 14–16)

- Ejecutar plan de pruebas completo (sección siguiente) en empresa de pruebas en **modo dev** (`pnpm start:dev` backend + `pnpm dev` frontend) — regla `PROJECT_STANDARDS.md` punto 14.
- Documentación de API Swagger con ejemplos de payload por endpoint (visible en `/docs`).
- Revisión de seguridad: log de correcciones inmutable (DB + API), control de roles, no logueo de `api_key` en claro.
- Bench de performance: importar 50 000 marcaciones / mes y verificar tiempo de deduplicación < 30 s.
- Verificación AI Dashboard responde consultas como "ausencias del mes en Sucursal X" o "tardanzas por empleado".
- Smoke test producción tras deploy.

---

## Plan de pruebas — modo dev

> Ejecutar siempre con `pnpm start:dev` (backend) + `pnpm dev` (frontend) sobre la empresa **Demo RRHH** con al menos 20 empleados con `cedula_identidad` válida. Validar mensaje funcional ante fallo (no stack trace).

### Datos de prueba reutilizables

| Dato | Valor sugerido |
|---|---|
| Empresa | Demo RRHH |
| Sucursal | Matriz |
| Empleados activos | 20+ con CI única |
| Período base | Mes actual |
| Reloj 1 | "Planta Norte" — PLANILLA |
| Reloj 2 | "Acceso Principal" — API (mock local en `tools/mock-reloj`) |
| Turno A | SIMPLE — 08:00–17:00 — L-V |
| Turno B | CORTADO — 08:00–12:00 / 13:30–17:30 — L-V |
| Tolerancia global | 5 min entrada / 5 min salida |
| Usuario RRHH | Admin RRHH con todos los permisos `RRHH_PRESENTISMO_*` |
| Usuario sin permisos | Operador sin `RRHH_PRESENTISMO_PROCESAR` |

### FASE 1 — Configuración (M16, M17, M20)

#### PRUEBA 1.1 — Alta de reloj marcador modalidad PLANILLA

**Dónde**: RRHH › Presentismo › Relojes › Nuevo
**Pasos**: Crear reloj "Planta Norte" tipo PLANILLA, sucursal Matriz, activo.
**Resultado esperado**: Reloj creado, visible en lista, sin campos de API solicitados (formulario contextual).
**Negativo**: Si solicita `url_base` para PLANILLA → defecto UX.

#### PRUEBA 1.2 — Alta de reloj modalidad API con prueba de conexión

**Dónde**: RRHH › Presentismo › Relojes › Nuevo
**Pasos**: Crear reloj API con URL del mock local, auth BEARER, intervalo 10 min. Click "Probar conexión".
**Resultado esperado**: Botón "Probar conexión" devuelve 200 + cantidad de marcaciones detectadas en el endpoint. `api_key` guardado cifrado (verificar en BD que no aparece en claro).
**Negativo**: Si `api_key` se devuelve en `GET /relojes` ⇒ defecto crítico de seguridad.

#### PRUEBA 1.3 — Alta de turno SIMPLE

**Dónde**: RRHH › Presentismo › Turnos › Nuevo
**Pasos**: Crear Turno A con un bloque 08:00–17:00, L-V activo.
**Resultado esperado**: Turno persistido con un único bloque `orden = 1`.
**Negativo**: Si permite guardar sin marcar al menos un día de la semana → defecto.

#### PRUEBA 1.4 — Alta de turno CORTADO con bloques solapados (debe fallar)

**Dónde**: RRHH › Presentismo › Turnos › Nuevo
**Pasos**: Intentar crear Turno con bloques 08:00–13:00 y 12:00–17:00.
**Resultado esperado**: Error funcional `Los bloques no pueden solaparse` (banner inline, no alert nativo).
**Negativo**: Si permite guardar → defecto crítico.

#### PRUEBA 1.5 — Tolerancia especial por fecha

**Dónde**: RRHH › Presentismo › Tolerancias › Nueva
**Pasos**: Crear tolerancia 30 min para hoy, motivo "Lluvia intensa", aplica a TODOS.
**Resultado esperado**: Tolerancia persiste; al procesar novedades de hoy, se usa 30 min en lugar de 5.
**Negativo**: Si la tolerancia global se aplica igual → defecto crítico del motor.

### FASE 2 — Ingesta de marcaciones (M16, M22)

#### PRUEBA 2.1 — Importar planilla CSV con marcaciones válidas

**Dónde**: RRHH › Presentismo › Marcaciones › Importar
**Pasos**: Subir CSV con 50 filas válidas (formato `documento_funcionario, fecha, hora_entrada, hora_salida`).
**Resultado esperado**: Batch creado, 50 marcaciones raw `PROCESADO`, 25 limpias generadas (una por empleado-día).
**Negativo**: Si filas con CI inexistente bloquean todo el lote → defecto. Deben quedar en estado `ERROR` individual.

#### PRUEBA 2.2 — Importar planilla con CI inválido

**Dónde**: RRHH › Presentismo › Marcaciones › Importar
**Pasos**: Subir CSV con 10 filas, una con CI `99999999`.
**Resultado esperado**: 9 procesadas, 1 en `ERROR` con `error_descripcion = "CI no matchea con empleado de la empresa"`. Banner muestra "1 error de 10".
**Negativo**: Si bloquea todo el lote → defecto.

#### PRUEBA 2.3 — Deduplicación con marcaciones duplicadas

**Dónde**: importar CSV con 3 entradas y 3 salidas para el mismo empleado el mismo día (timestamps distintos).
**Resultado esperado**: `rrhh_marcaciones_limpias` queda con 1 fila (min entrada, max salida); 4 marcaciones en `DUPLICADO`; 1 log de corrección con `motivo = DUPLICADO_AMBOS`.
**Negativo**: Si genera duplicados en limpias → defecto crítico de idempotencia.

#### PRUEBA 2.4 — Re-importar mismo lote (idempotencia)

**Dónde**: subir el mismo CSV de 2.1 dos veces.
**Resultado esperado**: Segunda ejecución no crea duplicados en `rrhh_marcaciones_limpias`; nuevo lote queda con `cantidad_duplicados_descartados = N`. Sin filas extra en limpias.
**Negativo**: Si crea duplicados o falla → defecto.

#### PRUEBA 2.5 — Log de correcciones inmutable

**Dónde**: intentar `DELETE /log-correcciones/:id` con usuario `SUPER_ADMIN`.
**Resultado esperado**: HTTP 405 / 403 con mensaje "Log de correcciones es inmutable".
**Negativo**: Si permite borrar → defecto crítico de auditoría.

### FASE 3 — Asignación de turnos y permisos (M17, M18)

#### PRUEBA 3.1 — Asignación perpetua

**Dónde**: RRHH › Empleados › [Empleado X] › Turnos › Asignar
**Pasos**: Asignar Turno A perpetuo a empleado X desde hoy.
**Resultado esperado**: Asignación persiste; `resolverTurnoVigente(empX, hoy) = Turno A`.

#### PRUEBA 3.2 — Asignación ocasional supera la perpetua

**Pasos**: Sobre el mismo empleado con Turno A perpetuo, asignar Turno B ocasional sólo para mañana.
**Resultado esperado**: Mañana resuelve a Turno B; pasado mañana resuelve a Turno A.
**Negativo**: Si mañana resuelve a Turno A → defecto crítico del resolver.

#### PRUEBA 3.3 — Dos ocasionales solapadas misma prioridad

**Pasos**: Asignar dos ocasionales solapadas con `prioridad = 100`.
**Resultado esperado**: Segunda creación devuelve HTTP 409 con mensaje funcional.

#### PRUEBA 3.4 — Permiso JORNADA_COMPLETA

**Dónde**: RRHH › Presentismo › Permisos › Nuevo
**Pasos**: Crear permiso JORNADA_COMPLETA aprobado para empleado X mañana.
**Resultado esperado**: Al procesar novedades de mañana, se genera `AUSENCIA_JUSTIFICADA` y ninguna otra novedad para ese día.

#### PRUEBA 3.5 — Permiso LLEGADA_TARDIA con minutos_tolerados

**Pasos**: Permiso LLEGADA_TARDIA 30 min para empleado X hoy. Empleado marca 08:25 con turno 08:00.
**Resultado esperado**: Genera `TARDANZA_JUSTIFICADA` (sin descuento) en lugar de `TARDANZA`.

### FASE 4 — Motor de novedades (M19)

#### PRUEBA 4.1 — Procesar período con casos mixtos

**Dónde**: RRHH › Presentismo › Novedades › Procesar período
**Pasos**: Período = mes actual, sucursal = Matriz. Confirmar en `ConfirmDialog`.
**Resultado esperado**: Job ejecuta < 30 s para 20 empleados × 22 días hábiles; genera novedades agrupadas por tipo. Banner muestra resumen: "X tardanzas, Y ausencias, Z justificadas".

#### PRUEBA 4.2 — Re-procesar período (idempotencia)

**Pasos**: Ejecutar 4.1 dos veces seguidas.
**Resultado esperado**: Segunda ejecución pasa novedades previas a `ANULADA` y crea conjunto nuevo. Total final de `CONFIRMADA` igual al de la primera corrida. Nada se elimina.

#### PRUEBA 4.3 — Bloqueo con liquidación cerrada

**Pasos**: Cerrar liquidación mensual de período X. Intentar procesar novedades del mismo período.
**Resultado esperado**: HTTP 409 con mensaje "No se puede procesar: liquidación cerrada".

#### PRUEBA 4.4 — Turno cortado con marcación faltante en segundo bloque

**Pasos**: Empleado con Turno B (08–12 / 13:30–17:30) marca 08:05 entrada, 12:00 salida; no marca el segundo bloque.
**Resultado esperado**: Genera `AUSENCIA_BLOQUE` con `turno_bloque_orden = 2`.

#### PRUEBA 4.5 — Empleado sin turno

**Pasos**: Empleado Z sin asignación de turno marca entrada y salida hoy.
**Resultado esperado**: Marcaciones quedan en raw + limpias pero **no** se genera novedad para Z. Sin error.

#### PRUEBA 4.6 — Tolerancia especial prevalece sobre global

**Pasos**: Configurar tolerancia especial 30 min para hoy. Empleado marca 08:20 con turno 08:00.
**Resultado esperado**: Sin novedad (cae dentro de 30 min). Si la global de 5 se aplicara, generaría TARDANZA.

#### PRUEBA 4.7 — Empleado activo procesa, desvinculado no

**Pasos**: Empleado X tiene `fecha_egreso = ayer`. Procesar hoy.
**Resultado esperado**: X no genera novedades hoy aunque tenga turno vigente.

### FASE 5 — Integración con liquidación (M07, M09, M10)

#### PRUEBA 5.1 — Tardanza impacta descuento

**Pasos**: Procesar novedades del período → calcular pre-liquidación.
**Resultado esperado**: Pre-liquidación muestra concepto `DESCUENTO_TARDANZA` por cada empleado con tardanzas confirmadas. Monto = `salario_base / horas_laborales_mes / 60 * total_min_tardanza`.

#### PRUEBA 5.2 — Ausencia impacta descuento

**Resultado esperado**: Concepto `DESCUENTO_AUSENCIA` aplicado proporcional al día.

#### PRUEBA 5.3 — Justificadas no descuentan

**Resultado esperado**: Empleado con sólo `TARDANZA_JUSTIFICADA` y `AUSENCIA_JUSTIFICADA` no tiene línea de descuento.

#### PRUEBA 5.4 — Tope del 30% Art. 240 CT

**Pasos**: Forzar caso con descuentos que superen 30 % del bruto.
**Resultado esperado**: Liquidación muestra advertencia "descuentos superan 30 %" (mensaje existente del engine, ver `rrhh-engine.service.ts:378`).

#### PRUEBA 5.5 — Cerrar liquidación bloquea anulación de novedad

**Pasos**: Cerrar liquidación. Intentar anular una novedad incluida.
**Resultado esperado**: HTTP 409 "Anular liquidación primero".

### FASE 6 — Consulta, exportación y AI Dashboard (M21)

#### PRUEBA 6.1 — Vista Resumen por día

**Dónde**: RRHH › Presentismo › Marcaciones › Resumen por día
**Resultado esperado**: Una fila por empleado-día con entrada, salida, turno y N° de novedades. Filtros por funcionario funcionan con `Autocomplete` buscable.

#### PRUEBA 6.2 — Exportación Excel y PDF

**Resultado esperado**: Excel y PDF generados con los mismos datos visibles + headers de la empresa.

#### PRUEBA 6.3 — AI Dashboard contesta "tardanzas del mes"

**Dónde**: Dashboard › Consulta IA
**Pasos**: Preguntar "¿Cuántas tardanzas hubo este mes?"
**Resultado esperado**: La IA usa el área `RRHH / Presentismo` del schema-context y devuelve la cifra correcta consultando `rrhh_asistencia_novedades`.
**Negativo**: Si responde "no tengo datos sobre presentismo" → falta registrar el módulo en `schema-context.ts`.

### FASE 7 — Permisos y seguridad

#### PRUEBA 7.1 — Usuario sin `RRHH_PRESENTISMO_PROCESAR`

**Resultado esperado**: Botón "Procesar período" no visible (o disabled con tooltip) y endpoint POST devuelve 403.

#### PRUEBA 7.2 — Aislamiento multi-empresa

**Pasos**: Login con usuario de Empresa A. Pedir `GET /relojes` esperando ver los de Empresa B.
**Resultado esperado**: Solo se ven los de A.

#### PRUEBA 7.3 — Auditoría

**Resultado esperado**: Cada alta/edit de reloj, turno, asignación y permiso queda en `audit_logs` con `entity_type`, `entity_id`, `user_id`, `before/after`.

### FASE 8 — UX y onboarding

#### PRUEBA 8.1 — Onboarding actualizado

**Dónde**: localStorage limpiado, entrar a RRHH.
**Resultado esperado**: `RRHHOnboardingDialog` muestra slide 4 "Presentismo" mencionando los pasos clave.

#### PRUEBA 8.2 — Tour cubre Presentismo

**Resultado esperado**: Tour overview visita las 7 sub-tabs nuevas con explicación.

#### PRUEBA 8.3 — Empty states y prereq checklist

**Pasos**: Entrar a Marcaciones sin reloj configurado.
**Resultado esperado**: `RRHHEmptyState` con CTA "Configurar reloj" + `RRHHPrereqChecklist` lista los faltantes con "Ir a configurar".

#### PRUEBA 8.4 — Diálogos no nativos

**Resultado esperado**: Ninguna acción usa `window.confirm/alert/prompt`. Todas usan `ConfirmDialog`/`PromptDialog` (regla `feedback_no_alert_browser`).

#### PRUEBA 8.5 — Selectores buscables

**Resultado esperado**: Todos los dropdowns de funcionario/turno/reloj son `Autocomplete` con búsqueda por texto (regla `feedback_selectores_buscables`).

#### PRUEBA 8.6 — Responsive

**Resultado esperado**: En viewport `<600px`, tabla de marcaciones colapsa a vista compacta; checklist colapsa a acordeón.

---

## Riesgos y mitigaciones

- **Volumen alto de marcaciones**: 100+ empleados × 30 días × 2 marcaciones ≈ 6 000 filas/mes. Mitigar con BullMQ asincrónico, índices BTREE en `(empleado_id, timestamp_marcacion)` y deduplicación en chunks.
- **Reloj cae o cambia formato**: bitácora de errores por sincronización + reintentos exponenciales en BullMQ.
- **Cambio de tolerancia retroactivo**: las tolerancias especiales son por fecha; el motor de novedades es idempotente y re-procesa correctamente. Para tolerancia global, el cambio de parámetro no afecta períodos ya cerrados.
- **CI duplicado entre empresas** (cuando un trabajador pasa de A a B): el filtro siempre va por `empresa_id`. Validar en seed de pruebas.
- **Cifrado de `api_key`**: usar el mismo helper existente para credenciales SIFEN (`src/common/crypto`).
- **Migración de `tipo_novedad` legacy**: mantener `tipo_novedad_legacy` por 3 meses (auditable) y rollback plan documentado.

---

## Entregables mínimos de v1 Presentismo

- 9 tablas nuevas + extensión de `rrhh_asistencia_novedades` aplicadas con migraciones idempotentes.
- 13 enums Prisma nuevos.
- Motor M19 puro con cobertura ≥ 90 %.
- 27 endpoints REST documentados en Swagger.
- 7 sub-tabs frontend con `RRHHGuia`, `RRHHEmptyState`, `RRHHPrereqChecklist`, tour y onboarding.
- Integración bidireccional con motor de liquidación (descuentos automáticos respetando Art. 240 CT).
- Área `RRHH / Presentismo` registrada en AI Dashboard.
- Plan de pruebas ejecutado en modo dev con evidencias documentadas en `docs/qa-presentismo-evidencia-YYYY-MM-DD.md`.
- 8 permisos nuevos sembrados y asignados a roles existentes.
- Log de correcciones inmutable verificado a nivel BD y API.

---

## Anexo — Checklist de cumplimiento PROJECT_STANDARDS

- [ ] **MonedaInput**: no aplica (sin inputs monetarios; descuentos los calcula el motor de liquidación).
- [ ] **Enums en código y BD**: 13 enums Prisma declarados; sin `VARCHAR + comentario` para estados/tipos.
- [ ] **Navegación clara**: sub-tabs ordenadas por flujo (configurar → ingerir → asignar → procesar → consultar).
- [ ] **Minimizar fricción**: botón "Procesar período" único; prereq checklist guía al usuario.
- [ ] **Interfaces limpias**: guías colapsables; tablas con virtualización para volúmenes grandes.
- [ ] **Consistencia**: reutiliza componentes `_shared` de RRHH (`RRHHGuia`, `RRHHEmptyState`, etc.).
- [ ] **Ayuda contextual**: `FieldHint` en formularios de turno y permiso.
- [ ] **Ejemplos prácticos**: `rrhhGuias.js` con casos reales (Turno Mañana, Turno Partido, etc.).
- [ ] **Feedback inmediato**: `InlineValidationBanner` + toasts; sin alerts nativos.
- [ ] **Responsive**: layout adaptado a móvil/tablet.
- [ ] **Accesibilidad**: `aria-describedby`, focus-visible.
- [ ] **Velocidad**: queries indexadas + procesamiento asíncrono en BullMQ.
- [ ] **Plan de pruebas en modo dev**: documentado en sección anterior.
- [ ] **AI Dashboard**: área `RRHH / Presentismo` registrada en `schema-context.ts`.
