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
12 KiB
12 KiB
ET-HR-005: Implementación Integración INFONAVIT
Epic: MAI-007 - RRHH, Asistencias y Nómina RF: RF-HR-005 Tipo: Especificación Técnica Prioridad: Crítica (Cumplimiento Legal) Estado: 🚧 En Implementación Última actualización: 2025-11-17
🔧 Implementación Backend
1. INFONAVITIntegrationService
Archivo: apps/backend/src/modules/hr/integrations/infonavit/infonavit-integration.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Employee } from '../../employees/entities/employee.entity';
import { INFONAVITCredit } from './entities/infonavit-credit.entity';
import { INFONAVITPaymentFile } from './entities/infonavit-payment-file.entity';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class INFONAVITIntegrationService {
private readonly apiUrl: string;
private readonly registroPatronal: string;
private readonly apiKey: string;
private readonly accessToken: string;
constructor(
@InjectRepository(Employee)
private employeeRepo: Repository<Employee>,
@InjectRepository(INFONAVITCredit)
private creditRepo: Repository<INFONAVITCredit>,
@InjectRepository(INFONAVITPaymentFile)
private paymentFileRepo: Repository<INFONAVITPaymentFile>,
private httpService: HttpService,
private configService: ConfigService,
) {
this.apiUrl = configService.get('INFONAVIT_API_URL');
this.registroPatronal = configService.get('INFONAVIT_REGISTRO_PATRONAL');
this.apiKey = configService.get('INFONAVIT_API_KEY');
this.accessToken = configService.get('INFONAVIT_ACCESS_TOKEN');
}
/**
* Calcular aportaciones bimestrales (5% del SBC)
*/
async calcularAportaciones(
constructoraId: string,
periodNumber: number, // 1-6 (bimestre)
periodYear: number,
): Promise<{ totalAportaciones: number; empleados: any[] }> {
// Determinar fechas del bimestre
const { startDate, endDate } = this.getBimestreDates(periodNumber, periodYear);
// Obtener empleados activos en el bimestre
const employees = await this.getActiveEmployeesForPeriod(
constructoraId,
startDate,
endDate,
);
let totalAportaciones = 0;
const detalles = [];
for (const employee of employees) {
// Días cotizados en el bimestre (simplificado: 60 días por bimestre)
const diasCotizados = 60;
// Aportación INFONAVIT = SBC × días × 5%
const aportacion = employee.currentSalary * diasCotizados * 0.05;
totalAportaciones += aportacion;
detalles.push({
employeeId: employee.id,
nss: employee.nss,
nombre: employee.fullName,
sbc: employee.currentSalary,
diasCotizados,
aportacion,
});
}
return {
totalAportaciones,
empleados: detalles,
};
}
/**
* Consultar trabajadores con crédito INFONAVIT
*/
async consultarAcreditados(constructoraId: string): Promise<void> {
const payload = {
registro_patronal: this.registroPatronal,
};
try {
const response = await this.makeINFONAVITRequest('/acreditados/consulta', payload);
// Procesar lista de acreditados
for (const acreditado of response.acreditados || []) {
// Buscar empleado por NSS
const employee = await this.employeeRepo.findOne({
where: {
nss: acreditado.nss,
constructoraId,
},
});
if (!employee) continue;
// Actualizar o crear crédito
let credit = await this.creditRepo.findOne({
where: {
employeeId: employee.id,
creditNumber: acreditado.numero_credito,
},
});
if (credit) {
// Actualizar existente
credit.discountValue = acreditado.descuento_mensual;
credit.outstandingBalance = acreditado.saldo_pendiente;
} else {
// Crear nuevo
credit = this.creditRepo.create({
employeeId: employee.id,
creditNumber: acreditado.numero_credito,
discountType: acreditado.tipo_descuento || 'VSM',
discountValue: acreditado.descuento_mensual,
outstandingBalance: acreditado.saldo_pendiente,
startDate: new Date(acreditado.fecha_inicio),
isActive: true,
});
}
await this.creditRepo.save(credit);
}
} catch (error) {
console.error('Error consultando acreditados INFONAVIT:', error);
throw error;
}
}
/**
* Calcular descuento por crédito INFONAVIT para un empleado
*/
async calcularDescuentoCredito(
employeeId: string,
salarioBruto: number,
): Promise<number> {
const credit = await this.creditRepo.findOne({
where: {
employeeId,
isActive: true,
},
});
if (!credit) {
return 0;
}
let descuento = 0;
switch (credit.discountType) {
case 'VSM': {
// Veces Salario Mínimo
const salarioMinimo = 248.93; // 2025
const diasMes = 30;
descuento = salarioMinimo * credit.discountValue * diasMes;
break;
}
case 'percentage': {
// Porcentaje del salario bruto
descuento = salarioBruto * (credit.discountValue / 100);
break;
}
case 'fixed': {
// Cuota fija
descuento = credit.discountValue;
break;
}
}
// Validar que no exceda 30% del salario bruto
const maxDescuento = salarioBruto * 0.3;
if (descuento > maxDescuento) {
descuento = maxDescuento;
}
return descuento;
}
/**
* Generar archivo de pago bimestral
*/
async generarArchivoPago(
constructoraId: string,
periodNumber: number,
periodYear: number,
generatedBy: string,
): Promise<INFONAVITPaymentFile> {
// Calcular aportaciones
const { totalAportaciones, empleados } = await this.calcularAportaciones(
constructoraId,
periodNumber,
periodYear,
);
// Obtener descuentos por créditos
const creditos = await this.creditRepo.find({
where: { isActive: true },
relations: ['employee'],
});
let totalDescuentos = 0;
const descuentosDetalle = [];
for (const credito of creditos) {
// Calcular descuento mensual × 2 (bimestre)
const descuentoBimestral = (await this.calcularDescuentoCredito(
credito.employeeId,
credito.employee.currentSalary * 30, // Salario mensual estimado
)) * 2;
totalDescuentos += descuentoBimestral;
descuentosDetalle.push({
employeeId: credito.employeeId,
nss: credito.employee.nss,
creditoNumero: credito.creditNumber,
descuento: descuentoBimestral,
});
}
// Construir archivo de pago
let fileContent = '';
// Header
fileContent += `HEADER|${this.registroPatronal}|BIMESTRE ${periodNumber}-${periodYear}\n`;
// Aportaciones
empleados.forEach((emp) => {
fileContent += `APORTACION|${emp.nss}|${emp.aportacion.toFixed(2)}|${emp.diasCotizados}\n`;
});
// Descuentos
descuentosDetalle.forEach((desc) => {
fileContent += `DESCUENTO|${desc.nss}|${desc.creditoNumero}|${desc.descuento.toFixed(2)}\n`;
});
// Total
fileContent += `TOTAL|${empleados.length}|${totalAportaciones.toFixed(2)}|${totalDescuentos.toFixed(2)}\n`;
// Calcular hash
const crypto = require('crypto');
const fileHash = crypto.createHash('sha256').update(fileContent).digest('hex');
// Generar línea de captura (simulado)
const lineaCaptura = this.generarLineaCaptura(
totalAportaciones + totalDescuentos,
periodNumber,
periodYear,
);
// Guardar en BD
const paymentFile = this.paymentFileRepo.create({
constructoraId,
periodNumber,
periodYear,
fileContent,
fileHash,
totalEmployees: empleados.length,
totalContributions: totalAportaciones,
totalDiscounts: totalDescuentos,
paymentReference: lineaCaptura,
paymentStatus: 'pending',
generatedBy,
});
return await this.paymentFileRepo.save(paymentFile);
}
/**
* Request HTTP a API INFONAVIT con OAuth 2.0
*/
private async makeINFONAVITRequest(endpoint: string, payload: any): Promise<any> {
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.accessToken}`,
'X-API-Key': this.apiKey,
},
};
const response = await firstValueFrom(
this.httpService.post(this.apiUrl + endpoint, payload, config),
);
return response.data;
}
/**
* Generar línea de captura bancaria
*/
private generarLineaCaptura(monto: number, bimestre: number, year: number): string {
// Formato simplificado: INFONAVIT + REGISTRO + BIMESTRE + MONTO
const registro = this.registroPatronal.padEnd(10, '0');
const periodo = `${year}${bimestre.toString().padStart(2, '0')}`;
const montoStr = Math.round(monto).toString().padStart(12, '0');
return `INFONAVIT${registro}${periodo}${montoStr}`;
}
/**
* Obtener fechas de bimestre
*/
private getBimestreDates(bimestre: number, year: number): { startDate: Date; endDate: Date } {
const bimestreMap = {
1: { start: new Date(year, 0, 1), end: new Date(year, 1, 28) }, // Ene-Feb
2: { start: new Date(year, 2, 1), end: new Date(year, 3, 30) }, // Mar-Abr
3: { start: new Date(year, 4, 1), end: new Date(year, 5, 30) }, // May-Jun
4: { start: new Date(year, 6, 1), end: new Date(year, 7, 31) }, // Jul-Ago
5: { start: new Date(year, 8, 1), end: new Date(year, 9, 31) }, // Sep-Oct
6: { start: new Date(year, 10, 1), end: new Date(year, 11, 31) }, // Nov-Dic
};
return {
startDate: bimestreMap[bimestre].start,
endDate: bimestreMap[bimestre].end,
};
}
private async getActiveEmployeesForPeriod(
constructoraId: string,
startDate: Date,
endDate: Date,
): Promise<Employee[]> {
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,
})
.andWhere('emp.status = :status', { status: 'active' })
.getMany();
}
}
🔐 Configuración
Variables de Entorno
# .env.production
INFONAVIT_API_URL=https://api.infonavit.org.mx/v1
INFONAVIT_REGISTRO_PATRONAL=1234567890
INFONAVIT_API_KEY=your-api-key-here
INFONAVIT_ACCESS_TOKEN=your-oauth-token-here
OAuth 2.0 Flow
// Obtener access token
async function refreshAccessToken() {
const response = await axios.post('https://api.infonavit.org.mx/oauth/token', {
grant_type: 'client_credentials',
client_id: process.env.INFONAVIT_CLIENT_ID,
client_secret: process.env.INFONAVIT_CLIENT_SECRET,
});
return response.data.access_token;
}
🧪 Tests
describe('INFONAVITIntegrationService', () => {
it('should calculate 5% contributions correctly', async () => {
const result = await service.calcularAportaciones('constructora-id', 1, 2025);
// Empleado con SBC $350/día × 60 días × 5% = $1,050
expect(result.totalAportaciones).toBeGreaterThan(0);
expect(result.empleados).toHaveLength(5);
expect(result.empleados[0].aportacion).toBe(350 * 60 * 0.05);
});
it('should calculate VSM discount correctly', async () => {
const descuento = await service.calcularDescuentoCredito('emp-id', 10500);
// 2.5 VSM = $248.93 × 2.5 × 30 = $18,669.75
expect(descuento).toBeCloseTo(18669.75, 2);
});
it('should not exceed 30% of gross salary', async () => {
const descuento = await service.calcularDescuentoCredito('emp-id', 5000);
expect(descuento).toBeLessThanOrEqual(5000 * 0.3);
});
});
Fecha de creación: 2025-11-17 Versión: 1.0