# 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 { // 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 { 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 { 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