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
10 KiB
10 KiB
ET-HR-004: Implementación Integración IMSS
Epic: MAI-007 - RRHH, Asistencias y Nómina RF: RF-HR-004 Tipo: Especificación Técnica Prioridad: Crítica (Cumplimiento Legal) Estado: 🚧 En Implementación Última actualización: 2025-11-17
🔧 Implementación Backend
1. IMSSIntegrationService
Archivo: apps/backend/src/modules/hr/integrations/imss/imss-integration.service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from '@nestjs/typeorm';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Employee } from '../../employees/entities/employee.entity';
import { IMSSIntegrationLog } from './entities/imss-integration-log.entity';
import { SUAFile } from './entities/sua-file.entity';
import { firstValueFrom } from 'rxjs';
import * as fs from 'fs';
import * as crypto from 'crypto';
@Injectable()
export class IMSSIntegrationService {
private readonly apiUrl: string;
private readonly registroPatronal: string;
private readonly certificatePath: string;
private readonly privateKeyPath: string;
constructor(
@InjectRepository(Employee)
private employeeRepo: Repository<Employee>,
@InjectRepository(IMSSIntegrationLog)
private logRepo: Repository<IMSSIntegrationLog>,
@InjectRepository(SUAFile)
private suaFileRepo: Repository<SUAFile>,
private httpService: HttpService,
private configService: ConfigService,
) {
this.apiUrl = configService.get('IMSS_API_URL');
this.registroPatronal = configService.get('IMSS_REGISTRO_PATRONAL');
this.certificatePath = configService.get('IMSS_CERTIFICATE_PATH');
this.privateKeyPath = configService.get('IMSS_PRIVATE_KEY_PATH');
}
/**
* Alta de trabajador ante IMSS
*/
async registrarTrabajador(employeeId: string): Promise<any> {
const employee = await this.employeeRepo.findOne({
where: { id: employeeId },
});
if (!employee) {
throw new BadRequestException('Empleado no encontrado');
}
// Validar que tenga NSS, CURP, RFC
if (!employee.nss || !employee.curp || !employee.rfc) {
throw new BadRequestException('Faltan datos fiscales del empleado');
}
const payload = {
registro_patronal: this.registroPatronal,
nss: employee.nss,
curp: employee.curp,
nombre: employee.firstName,
apellido_paterno: employee.lastName.split(' ')[0],
apellido_materno: employee.lastName.split(' ')[1] || '',
fecha_nacimiento: employee.dateOfBirth,
sexo: employee.gender === 'male' ? 'H' : 'M',
salario_base_cotizacion: employee.currentSalary,
fecha_alta: employee.hireDate,
tipo_trabajador: employee.contractType === 'permanent' ? '1' : '2',
tipo_salario: '0', // Fijo
};
try {
const response = await this.makeIMSSRequest('/afiliacion/alta', payload);
// Guardar log
await this.saveLog(employeeId, 'alta', payload, response, 'success', response.folio);
return response;
} catch (error) {
await this.saveLog(employeeId, 'alta', payload, null, 'failed', null, error.message);
throw error;
}
}
/**
* Baja de trabajador ante IMSS
*/
async bajaTrabajador(
employeeId: string,
fechaBaja: Date,
motivoBaja: string,
): Promise<any> {
const employee = await this.employeeRepo.findOne({
where: { id: employeeId },
});
const payload = {
registro_patronal: this.registroPatronal,
nss: employee.nss,
fecha_baja: fechaBaja,
tipo_baja: this.mapBajaType(motivoBaja),
causa_baja: motivoBaja,
};
try {
const response = await this.makeIMSSRequest('/afiliacion/baja', payload);
await this.saveLog(employeeId, 'baja', payload, response, 'success', response.folio);
return response;
} catch (error) {
await this.saveLog(employeeId, 'baja', payload, null, 'failed', null, error.message);
throw error;
}
}
/**
* Modificación salarial ante IMSS
*/
async modificacionSalarial(
employeeId: string,
nuevoSBC: number,
fechaEfectiva: Date,
): Promise<any> {
const employee = await this.employeeRepo.findOne({
where: { id: employeeId },
});
// Solo notificar si cambio > 5% o > 1 UMA (~103 MXN)
const cambio = Math.abs(nuevoSBC - employee.currentSalary);
const porcentajeCambio = (cambio / employee.currentSalary) * 100;
if (porcentajeCambio < 5 && cambio < 103) {
return { skipped: true, reason: 'Cambio menor al 5% y 1 UMA' };
}
const payload = {
registro_patronal: this.registroPatronal,
nss: employee.nss,
nuevo_sbc: nuevoSBC,
fecha_efectiva: fechaEfectiva,
tipo_modificacion: 'Modificación salarial',
};
try {
const response = await this.makeIMSSRequest('/afiliacion/modificacion', payload);
await this.saveLog(
employeeId,
'modificacion',
payload,
response,
'success',
response.folio,
);
return response;
} catch (error) {
await this.saveLog(
employeeId,
'modificacion',
payload,
null,
'failed',
null,
error.message,
);
throw error;
}
}
/**
* Generar archivo SUA mensual
*/
async generarArchivoSUA(
constructoraId: string,
periodMonth: number,
periodYear: number,
generatedBy: string,
): Promise<SUAFile> {
// Obtener empleados activos del mes
const employees = await this.getActiveEmployeesForMonth(
constructoraId,
periodMonth,
periodYear,
);
// Construir archivo SUA
let suaContent = '';
// Header
suaContent += this.buildSUAHeader(periodMonth, periodYear);
// Registro patronal
suaContent += this.buildRegistroPatronal();
// Trabajadores
let totalContributions = 0;
employees.forEach((emp) => {
const line = this.buildEmployeeLine(emp, periodMonth, periodYear);
suaContent += line + '\n';
totalContributions += this.calculateIMSSContributions(emp.currentSalary, 30);
});
// Totales
suaContent += this.buildTotalsLine(employees.length, totalContributions);
// Calcular hash
const fileHash = crypto.createHash('sha256').update(suaContent).digest('hex');
// Guardar en BD
const suaFile = this.suaFileRepo.create({
constructoraId,
periodMonth,
periodYear,
fileContent: suaContent,
fileHash,
totalEmployees: employees.length,
totalContributions,
generatedBy,
});
return await this.suaFileRepo.save(suaFile);
}
/**
* Calcular cuotas IMSS (simplificado)
*/
private calculateIMSSContributions(sbc: number, dias: number): number {
// Promedio de cuotas: ~27% del SBC
const tasaPromedio = 0.27;
const contribucionDiaria = sbc * tasaPromedio;
return contribucionDiaria * dias;
}
/**
* Construir línea de empleado en archivo SUA
*/
private buildEmployeeLine(employee: Employee, month: number, year: number): string {
const nss = employee.nss.padEnd(11, ' ');
const nombre = (employee.lastName + ' ' + employee.firstName)
.substring(0, 50)
.padEnd(50, ' ');
const sbc = employee.currentSalary.toFixed(2).padStart(10, '0');
const dias = '30'; // Días cotizados (simplificado)
// Formato SUA: 80 caracteres
return `${nss}${nombre}${dias.padStart(2, '0')}${sbc}`;
}
/**
* Request HTTP a API IMSS con certificado digital
*/
private async makeIMSSRequest(endpoint: string, payload: any): Promise<any> {
// Cargar certificado y llave privada
const cert = fs.readFileSync(this.certificatePath);
const key = fs.readFileSync(this.privateKeyPath);
const config = {
headers: {
'Content-Type': 'application/json',
},
httpsAgent: new (require('https').Agent)({
cert,
key,
rejectUnauthorized: false, // Solo para sandbox, en prod debe ser true
}),
};
const response = await firstValueFrom(
this.httpService.post(this.apiUrl + endpoint, payload, config),
);
return response.data;
}
private async saveLog(
employeeId: string,
operationType: string,
request: any,
response: any,
status: string,
folio: string | null,
errorMessage: string | null = null,
) {
const log = this.logRepo.create({
employeeId,
operationType,
requestPayload: request,
responsePayload: response,
status,
imssFolio: folio,
errorMessage,
});
await this.logRepo.save(log);
}
private mapBajaType(motivo: string): string {
if (motivo.includes('renuncia')) return '1';
if (motivo.includes('despido')) return '2';
if (motivo.includes('defunción')) return '3';
return '1'; // Default
}
private buildSUAHeader(month: number, year: number): string {
return `*************${month.toString().padStart(2, '0')}${year}${this.registroPatronal}\n`;
}
private buildRegistroPatronal(): string {
return `${this.registroPatronal}CONSTRUCCIONES SA\n`;
}
private buildTotalsLine(totalEmps: number, totalAmount: number): string {
return `99999999999TOTALES ${totalEmps.toString().padStart(5, '0')}${totalAmount.toFixed(2).padStart(15, '0')}\n`;
}
private async getActiveEmployeesForMonth(
constructoraId: string,
month: number,
year: number,
): Promise<Employee[]> {
// Obtener empleados activos durante el mes
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
return await this.employeeRepo
.createQueryBuilder('emp')
.where('emp.constructoraId = :constructoraId', { constructoraId })
.andWhere('emp.hireDate <= :endDate', { endDate })
.andWhere('(emp.terminationDate IS NULL OR emp.terminationDate >= :startDate)', {
startDate,
})
.getMany();
}
}
🔐 Configuración
Variables de Entorno
# .env.production
IMSS_API_URL=https://api.imss.gob.mx/v1
IMSS_REGISTRO_PATRONAL=A1234567890
IMSS_CERTIFICATE_PATH=/path/to/certificate.cer
IMSS_PRIVATE_KEY_PATH=/path/to/private.key
Secrets Management (Kubernetes)
apiVersion: v1
kind: Secret
metadata:
name: imss-certificates
type: Opaque
data:
certificate.cer: <base64-encoded-cert>
private.key: <base64-encoded-key>
Fecha de creación: 2025-11-17 Versión: 1.0