erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-004-integracion-nomina-externa.md

979 lines
27 KiB
Markdown

# US-HR-004: Integración con Nómina Externa
**Epic:** MAI-007 - RRHH, Asistencias y Nómina
**RF:** No aplica (funcionalidad de integración)
**ET:** No aplica
**Tipo:** Historia de Usuario
**Prioridad:** Media
**Story Points:** 8
**Sprint:** 10-11
**Estado:** 📋 Pendiente
**Última actualización:** 2025-11-17
---
## 📖 Historia de Usuario
**Como** Gerente de RRHH o Contador
**Quiero** exportar automáticamente asistencias, incidencias y nómina a mi sistema de nómina externo (CONTPAQi, Aspel NOI, Excel)
**Para** procesar la nómina sin captura manual, reducir errores y tener trazabilidad completa entre asistencia y pago
---
## 🎯 Criterios de Aceptación
### CA-1: Configuración de Sistema de Nómina 🔧
**Dado que** soy Gerente de RRHH con permisos de configuración
**Cuando** accedo a "Configuración" > "Integración de Nómina"
**Entonces** puedo:
1. **Seleccionar Tipo de Sistema:**
- Opciones disponibles:
- ☐ CONTPAQi Nóminas
- ☐ Aspel NOI
- ☐ Tress Nomipaq
- ☐ Excel personalizado
- ☐ API REST personalizada
- ☐ CSV genérico
- Seleccionar uno
2. **Configurar Mapeo de Campos:**
- Ver tabla de mapeo de campos:
| Campo Interno | Campo Externo | Transformación |
|---------------|---------------|----------------|
| employeeCode | NUMERO_EMPLEADO | Ninguna |
| curp | CURP | Ninguna |
| nss | NSS | Ninguna |
| fullName | NOMBRE_COMPLETO | Concatenar |
| currentSalary | SALARIO_DIARIO | Ninguna |
| daysWorked | DIAS_TRABAJADOS | Suma mensual |
- Poder editar nombres de campos externos
- Poder agregar campos personalizados
3. **Configurar Formato de Exportación:**
- **Si Excel:**
- Seleccionar template (.xlsx) con formato deseado
- Definir hoja donde se escribirán datos
- Fila de inicio: 5 (ejemplo)
- **Si CSV:**
- Delimitador: coma, punto y coma, tab
- Encoding: UTF-8, Latin1
- Incluir headers: Sí/No
- **Si API REST:**
- URL del endpoint: `https://api.nomina.example.com/v1/attendance`
- Método: POST
- Headers personalizados (API Key, etc.)
- Formato del body: JSON, XML
4. **Programar Exportación Automática:**
- Frecuencia:
- ☐ Diaria (lunes a viernes a las 6 PM)
- ☐ Semanal (viernes a las 6 PM)
- ☐ Quincenal (días 15 y último del mes)
- ☐ Mensual (último día del mes)
- Email de notificación al completar
- Email de alerta si falla
**Y** guardar configuración con validación completa
### CA-2: Exportación Manual de Asistencias 📤
**Dado que** soy Gerente de RRHH
**Cuando** accedo a "RRHH" > "Exportar a Nómina"
**Entonces** puedo:
1. **Seleccionar Periodo:**
- Fecha inicio: 2025-11-01
- Fecha fin: 2025-11-15
- O seleccionar: "Última quincena", "Último mes"
- Mostrar preview: "Del 1 al 15 de noviembre (15 días)"
2. **Seleccionar Empleados:**
- Opción: "Todos los empleados activos" (default)
- O filtrar por:
- Obra específica
- Cuadrilla específica
- Lista personalizada (checkboxes)
- Mostrar contador: "52 empleados seleccionados"
3. **Vista Previa de Datos:**
- Ver tabla con datos que se exportarán:
| Empleado | NSS | Días Trabajados | Faltas | Incap. | Salario | Total |
|----------|-----|-----------------|--------|--------|---------|-------|
| Juan Pérez | 12345678901 | 13 | 2 | 0 | $500 | $6,500 |
| María López | 98765432109 | 15 | 0 | 0 | $450 | $6,750 |
| ... | ... | ... | ... | ... | ... | ... |
- Paginación: 20 empleados por página
- Totales en footer:
- Total empleados: 52
- Total días trabajados: 742
- Total neto estimado: $371,000
4. **Validaciones Previas:**
- ✅ Todos los empleados tienen NSS válido
- ✅ No hay asistencias pendientes de aprobar
- ⚠️ 3 empleados con advertencias GPS
- ❌ 1 empleado sin CURP registrado
Si hay errores críticos (❌):
- No permitir exportación
- Mostrar lista de empleados con problemas
- Botón: "Ir a corregir datos"
5. **Exportar:**
- Botón: "Exportar a [CONTPAQi]"
- Spinner: "Generando archivo..."
- Descarga automática del archivo:
- Nombre: `Nomina_2025-11-01_2025-11-15_52empleados.xlsx`
- Tamaño: ~150 KB
- Toast: "✓ Archivo exportado correctamente"
**Y** registrar log de exportación con usuario, fecha y periodo
### CA-3: Formato de Exportación para CONTPAQi 📊
**Dado que** seleccioné CONTPAQi como sistema de nómina
**Cuando** exporto asistencias
**Entonces** el archivo Excel generado debe tener:
1. **Estructura de Archivo:**
```
Hoja: ASISTENCIAS
Fila 1: [Logo]
Fila 2: Reporte de Asistencias
Fila 3: Periodo: 01/11/2025 - 15/11/2025
Fila 4: [Vacío]
Fila 5: [Headers]
Fila 6+: [Datos]
```
2. **Columnas Requeridas:**
- A: NUMERO_EMPLEADO (texto)
- B: NOMBRE_COMPLETO (texto)
- C: NSS (texto, 11 dígitos)
- D: CURP (texto, 18 caracteres)
- E: RFC (texto, 13 caracteres)
- F: DIAS_TRABAJADOS (número, 2 decimales)
- G: FALTAS (número entero)
- H: INCAPACIDADES (número entero)
- I: SALARIO_DIARIO (moneda, 2 decimales)
- J: TOTAL_PERCIBIDO (fórmula: =F*I)
3. **Formato de Celdas:**
- Headers (fila 5): Negrita, fondo azul, texto blanco
- Datos: Arial 10, alineación izquierda para texto, derecha para números
- Columnas de moneda: formato $#,##0.00
4. **Validaciones de Integridad:**
- NSS: 11 dígitos exactos, sin guiones
- CURP: 18 caracteres, mayúsculas
- DIAS_TRABAJADOS: Max 15 en quincena, 31 en mes
- SALARIO_DIARIO: Min $248.93 (salario mínimo 2025)
### CA-4: Formato CSV Genérico 📄
**Dado que** seleccioné CSV genérico
**Cuando** exporto
**Entonces** el archivo CSV debe:
1. **Estructura:**
```csv
EMPLEADO_CODIGO,NOMBRE,NSS,CURP,DIAS_TRAB,FALTAS,SALARIO,TOTAL
EMP-00001,"Pérez García Juan",12345678901,BADD110313HCMLNS09,13,2,500.00,6500.00
EMP-00002,"López Martínez María",98765432109,LOMM900515MDFPRD08,15,0,450.00,6750.00
```
2. **Configuración:**
- Delimitador: Configurable (coma por defecto)
- Encoding: UTF-8 con BOM (para Excel)
- Línea de headers: Sí (configurable)
- Comillas para texto: Sí (para nombres con comas)
3. **Manejo de Caracteres Especiales:**
- Acentos: Preservados con UTF-8
- Ñ: Preservada
- Comas en nombres: Encerrar en comillas dobles
### CA-5: Integración API REST 🔌
**Dado que** seleccioné API REST personalizada
**Cuando** se ejecuta la exportación (manual o automática)
**Entonces** el sistema debe:
1. **Preparar Payload JSON:**
```json
{
"period": {
"startDate": "2025-11-01",
"endDate": "2025-11-15"
},
"company": {
"id": "constructora-uuid",
"name": "Constructora ABC",
"rfc": "CABC850101XYZ"
},
"employees": [
{
"employeeCode": "EMP-00001",
"fullName": "Juan Pérez García",
"nss": "12345678901",
"curp": "BADD110313HCMLNS09",
"rfc": "BADD110313AB1",
"daysWorked": 13,
"absences": 2,
"incapacities": 0,
"dailySalary": 500.00,
"totalGross": 6500.00
}
],
"summary": {
"totalEmployees": 52,
"totalDays": 742,
"totalGross": 371000.00
}
}
```
2. **Enviar Request HTTP:**
```typescript
const response = await axios.post(
config.payrollApiUrl,
payload,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
'X-Company-ID': constructoraId,
},
timeout: 30000, // 30 segundos
}
);
```
3. **Manejo de Respuestas:**
- **200 OK:**
- Mensaje: "✓ Datos enviados correctamente a sistema de nómina"
- Guardar `response.data.batchId` para trazabilidad
- **400 Bad Request:**
- Mostrar errores de validación del sistema externo
- Ejemplo: "Empleado NSS 12345678901 no existe en sistema de nómina"
- **401 Unauthorized:**
- Error: "API Key inválida, contacta al administrador"
- **500 Server Error:**
- Reintentar automáticamente 3 veces con delay (1s, 2s, 4s)
- Si falla: enviar email de alerta y mostrar error
4. **Logs de Integración:**
- Registrar cada llamada API:
- Timestamp
- URL
- Payload (resumen, no datos sensibles)
- Response status
- Tiempo de respuesta
- Retención: 90 días
### CA-6: Exportación Automática Programada ⏰
**Dado que** configuré exportación automática quincenal
**Cuando** llega la fecha programada (día 15 a las 6 PM)
**Entonces** el sistema debe:
1. **Ejecutar Exportación Automáticamente:**
- Detectar que es día 15 del mes
- A las 18:00 hrs ejecutar job
- Calcular periodo automáticamente:
- Si día 15: del 1 al 15
- Si último día del mes: del 16 al último día
2. **Validar Datos:**
- Verificar que no hay asistencias pendientes de aprobar
- Verificar que todos los empleados tienen datos completos
- Si hay problemas críticos:
- NO exportar
- Enviar email a RRHH con lista de problemas
- Programar reintento para mañana
3. **Generar y Enviar:**
- Generar archivo según configuración
- Si es archivo (Excel/CSV):
- Enviar por email a RRHH con archivo adjunto
- Si es API:
- Enviar request
- Guardar archivo en servidor en:
- `/storage/payroll-exports/2025/11/nomina_20251115.xlsx`
4. **Notificar Resultado:**
- Email a RRHH:
```
Asunto: ✓ Exportación automática de nómina completada
Se exportaron 52 empleados del periodo 01/11/2025 - 15/11/2025
Resumen:
- Total días trabajados: 742
- Total neto estimado: $371,000
- Archivo adjunto: nomina_20251115.xlsx
Generado automáticamente el 15/11/2025 a las 18:05 hrs
```
- Si falla:
```
Asunto: ❌ Error en exportación automática de nómina
No se pudo completar la exportación.
Errores:
- 3 empleados sin NSS registrado
- 5 asistencias pendientes de aprobar
Por favor, corrige estos problemas y ejecuta la exportación manualmente.
```
### CA-7: Incluir Incidencias en Exportación 📝
**Dado que** hay incidencias registradas (faltas, incapacidades, permisos)
**Cuando** exporto asistencias
**Entonces** deben incluirse:
1. **Tipos de Incidencias:**
- **Falta:** Día laboral sin asistencia (injustificada)
- **Falta Justificada:** Con documento de respaldo
- **Incapacidad:** Por enfermedad (IMSS)
- **Permiso:** Autorizado por supervisor
- **Vacaciones:** Días de descanso programados
- **Día Festivo:** Día no laboral pagado
2. **Cálculo de Incidencias:**
```
Periodo: 15 días laborales
- Asistencias: 13 días
- Faltas: 2 días
- Incapacidades: 0 días
- Permisos: 0 días
Total verificado: 13 + 2 = 15 ✓
```
3. **Columnas en Exportación:**
- DIAS_TRABAJADOS: 13
- FALTAS: 2
- INCAPACIDADES: 0
- PERMISOS: 0
- VACACIONES: 0
4. **Validación de Consistencia:**
- Suma de incidencias debe = días del periodo
- Si no coincide: marcar con warning
- Permitir ajuste manual antes de exportar
### CA-8: Importación de Resultados de Nómina (Opcional) 📥
**Dado que** la nómina fue procesada en el sistema externo
**Cuando** importo los resultados de vuelta
**Entonces** puedo:
1. **Subir Archivo de Resultados:**
- Formato: Excel o CSV
- Columnas esperadas:
- EMPLEADO_CODIGO
- TOTAL_PERCEPCIONES
- TOTAL_DEDUCCIONES
- NETO_A_PAGAR
- FECHA_PAGO
2. **Mapear Datos:**
- Sistema valida que cada empleado existe
- Valida que periodo coincide
- Crea registros en tabla `payroll_results`:
```json
{
"employeeId": "uuid",
"period": "2025-11-01_2025-11-15",
"totalPerceptions": 6500.00,
"totalDeductions": 1235.00,
"netPay": 5265.00,
"paymentDate": "2025-11-16"
}
```
3. **Visualizar en Empleado:**
- En detalle del empleado, nueva sección: "Historial de Nómina"
- Mostrar tabla con pagos pasados
- Solo lectura (no editable)
### CA-9: Permisos por Rol 🔐
**Roles y Permisos:**
| Acción | Director | Engineer | Resident | HR | Finance |
|--------|----------|----------|----------|-----|---------|
| Configurar integración | ✅ | ❌ | ❌ | ✅ | ❌ |
| Exportar manualmente | ✅ | ❌ | ❌ | ✅ | ✅ |
| Ver exportaciones pasadas | ✅ | ❌ | ❌ | ✅ | ✅ |
| Descargar archivos | ✅ | ❌ | ❌ | ✅ | ✅ |
| Importar resultados | ✅ | ❌ | ❌ | ✅ | ✅ |
---
## 🔧 Detalles Técnicos
### Servicio de Exportación
```typescript
// payroll-export.service.ts
import { Injectable } from '@nestjs/common';
import * as ExcelJS from 'exceljs';
import { createObjectCsvStringifier } from 'csv-writer';
@Injectable()
export class PayrollExportService {
/**
* Exportar asistencias a Excel (CONTPAQi format)
*/
async exportToExcel(
period: { startDate: Date; endDate: Date },
employeeIds: string[]
): Promise<Buffer> {
// 1. Obtener datos de asistencias
const data = await this.getAttendanceData(period, employeeIds);
// 2. Crear workbook
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('ASISTENCIAS');
// 3. Header
worksheet.getCell('A1').value = 'Reporte de Asistencias';
worksheet.getCell('A1').font = { bold: true, size: 14 };
worksheet.getCell('A2').value = `Periodo: ${this.formatDate(period.startDate)} - ${this.formatDate(period.endDate)}`;
// 4. Column headers (row 5)
const headers = [
'NUMERO_EMPLEADO',
'NOMBRE_COMPLETO',
'NSS',
'CURP',
'RFC',
'DIAS_TRABAJADOS',
'FALTAS',
'INCAPACIDADES',
'SALARIO_DIARIO',
'TOTAL_PERCIBIDO',
];
worksheet.getRow(5).values = headers;
worksheet.getRow(5).font = { bold: true };
worksheet.getRow(5).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: '4472C4' },
};
worksheet.getRow(5).font = { color: { argb: 'FFFFFF' }, bold: true };
// 5. Data rows
let rowIndex = 6;
for (const employee of data) {
worksheet.getRow(rowIndex).values = [
employee.employeeCode,
employee.fullName,
employee.nss,
employee.curp,
employee.rfc,
employee.daysWorked,
employee.absences,
employee.incapacities,
employee.dailySalary,
{ formula: `F${rowIndex}*I${rowIndex}` }, // Total
];
// Format salary columns
worksheet.getCell(`I${rowIndex}`).numFmt = '$#,##0.00';
worksheet.getCell(`J${rowIndex}`).numFmt = '$#,##0.00';
rowIndex++;
}
// 6. Auto-fit columns
worksheet.columns.forEach(column => {
column.width = 15;
});
// 7. Return buffer
const buffer = await workbook.xlsx.writeBuffer();
return Buffer.from(buffer);
}
/**
* Exportar a CSV
*/
async exportToCSV(
period: { startDate: Date; endDate: Date },
employeeIds: string[],
config: CSVConfig
): Promise<string> {
const data = await this.getAttendanceData(period, employeeIds);
const csvStringifier = createObjectCsvStringifier({
header: [
{ id: 'employeeCode', title: 'EMPLEADO_CODIGO' },
{ id: 'fullName', title: 'NOMBRE' },
{ id: 'nss', title: 'NSS' },
{ id: 'curp', title: 'CURP' },
{ id: 'daysWorked', title: 'DIAS_TRAB' },
{ id: 'absences', title: 'FALTAS' },
{ id: 'dailySalary', title: 'SALARIO' },
{ id: 'total', title: 'TOTAL' },
],
});
const records = data.map(emp => ({
...emp,
total: emp.daysWorked * emp.dailySalary,
}));
const csv = csvStringifier.getHeaderString() + csvStringifier.stringifyRecords(records);
return csv;
}
/**
* Enviar a API externa
*/
async sendToAPI(
period: { startDate: Date; endDate: Date },
employeeIds: string[],
apiConfig: APIConfig
): Promise<any> {
const data = await this.getAttendanceData(period, employeeIds);
const payload = {
period: {
startDate: period.startDate.toISOString().split('T')[0],
endDate: period.endDate.toISOString().split('T')[0],
},
employees: data.map(emp => ({
employeeCode: emp.employeeCode,
fullName: emp.fullName,
nss: emp.nss,
curp: emp.curp,
rfc: emp.rfc,
daysWorked: emp.daysWorked,
absences: emp.absences,
incapacities: emp.incapacities,
dailySalary: emp.dailySalary,
totalGross: emp.daysWorked * emp.dailySalary,
})),
};
const response = await axios.post(apiConfig.url, payload, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiConfig.apiKey}`,
},
timeout: 30000,
});
// Log the integration
await this.logIntegration({
type: 'api_export',
status: response.status,
responseTime: response.duration,
recordCount: data.length,
});
return response.data;
}
/**
* Obtener datos de asistencias con incidencias
*/
private async getAttendanceData(
period: { startDate: Date; endDate: Date },
employeeIds: string[]
) {
// Query complejo que une asistencias + incidencias
const result = await this.db.query(`
WITH attendance_summary AS (
SELECT
e.id as employee_id,
e.employee_code,
CONCAT(e.first_name, ' ', e.last_name) as full_name,
e.nss,
e.curp,
e.rfc,
e.current_salary as daily_salary,
COALESCE(SUM(lc.days_worked), 0) as days_worked
FROM hr.employees e
LEFT JOIN hr.labor_costs lc ON lc.employee_id = e.id
AND lc.work_date >= $1
AND lc.work_date <= $2
WHERE e.id = ANY($3)
GROUP BY e.id
),
incidences_summary AS (
SELECT
employee_id,
COUNT(*) FILTER (WHERE type = 'absence') as absences,
COUNT(*) FILTER (WHERE type = 'incapacity') as incapacities,
COUNT(*) FILTER (WHERE type = 'permission') as permissions
FROM hr.incidences
WHERE incidence_date >= $1 AND incidence_date <= $2
GROUP BY employee_id
)
SELECT
a.*,
COALESCE(i.absences, 0) as absences,
COALESCE(i.incapacities, 0) as incapacities,
COALESCE(i.permissions, 0) as permissions
FROM attendance_summary a
LEFT JOIN incidences_summary i ON i.employee_id = a.employee_id
ORDER BY a.full_name
`, [period.startDate, period.endDate, employeeIds]);
return result.rows;
}
}
```
### Job Programado
```typescript
// payroll-export.cron.ts
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PayrollExportService } from './payroll-export.service';
@Injectable()
export class PayrollExportCron {
constructor(
private payrollExportService: PayrollExportService,
private emailService: EmailService,
) {}
/**
* Ejecutar exportación automática quincenalmente
* Día 15 y último día del mes a las 6 PM
*/
@Cron('0 18 15,L * *', { // L = último día del mes
name: 'payroll-export-automatic',
timeZone: 'America/Mexico_City',
})
async handleAutomaticExport() {
console.log('🕐 Iniciando exportación automática de nómina...');
const today = new Date();
const period = this.calculatePeriod(today);
try {
// 1. Validar que no hay problemas
const validation = await this.validateExportReady(period);
if (!validation.isValid) {
await this.emailService.send({
to: 'rrhh@constructora.com',
subject: '❌ Exportación automática de nómina: Errores detectados',
body: this.buildErrorEmail(validation.errors),
});
return;
}
// 2. Exportar
const buffer = await this.payrollExportService.exportToExcel(
period,
validation.employeeIds
);
// 3. Guardar archivo
const filename = `nomina_${this.formatDate(today)}.xlsx`;
await this.storageService.save(`payroll-exports/${filename}`, buffer);
// 4. Enviar email con archivo
await this.emailService.send({
to: 'rrhh@constructora.com',
subject: '✓ Exportación automática de nómina completada',
body: this.buildSuccessEmail(validation.summary),
attachments: [
{
filename,
content: buffer,
},
],
});
console.log('✓ Exportación automática completada');
} catch (error) {
console.error('Error en exportación automática:', error);
await this.emailService.send({
to: 'rrhh@constructora.com',
subject: '❌ Error en exportación automática de nómina',
body: `Error técnico: ${error.message}`,
});
}
}
private calculatePeriod(date: Date): { startDate: Date; endDate: Date } {
const day = date.getDate();
const year = date.getFullYear();
const month = date.getMonth();
if (day === 15) {
// Primera quincena: del 1 al 15
return {
startDate: new Date(year, month, 1),
endDate: new Date(year, month, 15),
};
} else {
// Segunda quincena: del 16 al último día
return {
startDate: new Date(year, month, 16),
endDate: new Date(year, month + 1, 0), // Último día del mes
};
}
}
}
```
---
## 🧪 Casos de Prueba
### TC-PAY-001: Exportación Manual a Excel ✅
**Precondiciones:**
- 52 empleados activos
- Periodo: 01/11/2025 - 15/11/2025
- Configuración: CONTPAQi Excel
**Pasos:**
1. Ir a "RRHH" > "Exportar a Nómina"
2. Seleccionar periodo 01/11 - 15/11
3. Seleccionar "Todos los empleados activos"
4. Hacer clic en "Exportar"
**Resultado esperado:**
- Archivo descargado: `Nomina_2025-11-01_2025-11-15_52empleados.xlsx`
- Tamaño: ~150 KB
- Al abrir archivo:
- Hoja "ASISTENCIAS" existe
- Fila 5 tiene headers correcto
- Fila 6 tiene primer empleado
- 52 empleados en total (filas 6-57)
- Columnas formateadas correctamente
- Totales calculados con fórmulas
### TC-PAY-002: Validación Antes de Exportar ❌
**Precondiciones:**
- 3 empleados sin NSS registrado
- 5 asistencias pendientes de aprobar
**Pasos:**
1. Intentar exportar nómina
**Resultado esperado:**
- Validación falla
- Mensaje de error:
```
❌ No se puede exportar
Problemas encontrados:
- 3 empleados sin NSS:
• Juan López (EMP-00045)
• María García (EMP-00051)
• Carlos Ruiz (EMP-00052)
- 5 asistencias pendientes de aprobar
Por favor, corrige estos problemas antes de exportar.
```
- Botón "Ir a corregir" visible
- Botón "Exportar" deshabilitado
### TC-PAY-003: Exportación a CSV ✅
**Precondiciones:**
- Configuración: CSV genérico
- Delimitador: coma
- Encoding: UTF-8
**Pasos:**
1. Exportar nómina a CSV
**Resultado esperado:**
- Archivo: `Nomina_2025-11-01_2025-11-15.csv`
- Contenido:
```csv
EMPLEADO_CODIGO,NOMBRE,NSS,CURP,DIAS_TRAB,FALTAS,SALARIO,TOTAL
EMP-00001,"Pérez García Juan",12345678901,BADD110313HCMLNS09,13,2,500.00,6500.00
EMP-00002,"López Martínez María",98765432109,LOMM900515MDFPRD08,15,0,450.00,6750.00
```
- Acentos preservados correctamente
- Comillas en nombres con comas
### TC-PAY-004: Integración API REST ✅
**Precondiciones:**
- Configuración API:
- URL: `https://api.nomina.test.com/v1/attendance`
- API Key: válida
- Mock server respondiendo 200 OK
**Pasos:**
1. Exportar usando API
**Resultado esperado:**
- Request enviado a API con payload JSON correcto
- Response 200 OK recibida
- `batchId` guardado: `BATCH-2025-11-15-001`
- Toast: "✓ Datos enviados correctamente"
- Log de integración registrado:
- Timestamp: 2025-11-15 18:05:32
- Status: 200
- Response time: 1.2s
- Records: 52
### TC-PAY-005: Exportación Automática Quincenal ⏰
**Precondiciones:**
- Fecha: 15 de noviembre a las 6 PM
- Configuración automática activa
- 52 empleados con datos completos
**Pasos:**
1. Cron job se ejecuta automáticamente
**Resultado esperado:**
- Exportación ejecutada
- Archivo generado y guardado en:
`/storage/payroll-exports/2025/11/nomina_20251115.xlsx`
- Email enviado a RRHH:
- Asunto: "✓ Exportación automática completada"
- Archivo adjunto
- Resumen de 52 empleados, 742 días, $371,000
- Log registrado
### TC-PAY-006: Fallo en Exportación Automática ❌
**Precondiciones:**
- Fecha: 15 de noviembre
- 3 empleados sin datos completos
**Pasos:**
1. Cron job se ejecuta
**Resultado esperado:**
- Validación detecta problemas
- Exportación NO se ejecuta
- Email de alerta enviado:
- Asunto: "❌ Error en exportación automática"
- Lista de problemas
- Instrucciones para corrección manual
- Reintento programado para mañana
### TC-PAY-007: Incluir Incidencias ✅
**Precondiciones:**
- Empleado trabajó 13 días
- 2 faltas registradas
- 0 incapacidades
**Pasos:**
1. Exportar nómina
**Resultado esperado:**
- Fila del empleado muestra:
- DIAS_TRABAJADOS: 13
- FALTAS: 2
- INCAPACIDADES: 0
- Suma: 13 + 2 = 15 (total de días del periodo) ✓
---
## 📦 Dependencias
### Dependencias de Otros US
-**US-HR-001:** Empleados con datos completos (NSS, CURP, RFC)
-**US-HR-002:** Asistencias registradas
-**US-HR-003:** Cálculo de días trabajados
### Librerías Backend
```json
{
"exceljs": "^4.4.0",
"csv-writer": "^1.6.0",
"@nestjs/schedule": "^4.0.0",
"axios": "^1.6.2"
}
```
---
## ⚠️ Riesgos
### R-1: Cambios de Formato del Sistema Externo
**Descripción:** Sistema de nómina cambia estructura sin aviso
**Impacto:** Alto
**Probabilidad:** Media
**Mitigación:**
- Versionado de templates
- Alertas automáticas si validación falla
- Documentar formato esperado
---
## 📊 Métricas de Éxito
**Métricas de Negocio:**
- ✅ 95% de exportaciones exitosas en primer intento
- ✅ Reducción de 80% en tiempo de captura manual
- ✅ 0 errores de captura vs método manual
**Métricas Técnicas:**
- ✅ Exportación de 100 empleados en < 10 segundos
- 100% de exportaciones automáticas ejecutadas a tiempo
- API response time < 5 segundos
---
## 📋 Checklist de Implementación
### Backend
- [ ] Crear PayrollExportService con métodos de exportación
- [ ] Implementar generación de Excel con ExcelJS
- [ ] Implementar generación de CSV
- [ ] Implementar integración API REST
- [ ] Crear PayrollExportCron para ejecución automática
- [ ] Crear endpoints de configuración
- [ ] Implementar validaciones pre-exportación
- [ ] Implementar logs de integración
- [ ] Crear EmailService para notificaciones
### Frontend
- [ ] Crear página PayrollExportConfig
- [ ] Crear componente ExportWizard
- [ ] Crear tabla de preview de datos
- [ ] Implementar validaciones visuales
- [ ] Crear historial de exportaciones
- [ ] Implementar descargas de archivos
### Testing
- [ ] Tests de generación Excel
- [ ] Tests de generación CSV
- [ ] Tests de API integration
- [ ] Tests de cron job
- [ ] Tests de validaciones
---
**Fecha de creación:** 2025-11-17
**Versión:** 1.0