workspace-v1/projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-004-integracion-nomina-externa.md
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -06:00

27 KiB

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:

    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:

    {
      "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:

    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:
      {
        "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

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

// 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:
    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

{
  "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