import { Credential } from '@nodecfdi/credentials/node'; import type { Pool } from 'pg'; import { encryptFielCredentials, decryptFielCredentials } from './sat/sat-crypto.service.js'; import type { FielStatus } from '@horux/shared'; export async function uploadFielContribuyente( pool: Pool, contribuyenteId: string, cerBase64: string, keyBase64: string, password: string ): Promise<{ success: boolean; message: string; status?: FielStatus }> { try { const cerData = Buffer.from(cerBase64, 'base64'); const keyData = Buffer.from(keyBase64, 'base64'); let credential: Credential; try { credential = Credential.create(cerData.toString('binary'), keyData.toString('binary'), password); } catch { return { success: false, message: 'Los archivos de la FIEL no son válidos o la contraseña es incorrecta' }; } if (!credential.isFiel()) { return { success: false, message: 'El certificado proporcionado no es una FIEL (e.firma). Parece ser un CSD.' }; } const certificate = credential.certificate(); const rfc = certificate.rfc(); const serialNumber = certificate.serialNumber().bytes(); const validFrom = new Date(String(certificate.validFromDateTime())); const validUntil = new Date(String(certificate.validToDateTime())); if (new Date() > validUntil) { return { success: false, message: 'La FIEL está vencida desde ' + validUntil.toLocaleDateString() }; } const enc = encryptFielCredentials(cerData, keyData, password); // Check whether this contribuyente already had an active FIEL (to decide auto-sync) const { rows: existingRows } = await pool.query( `SELECT 1 FROM fiel_contribuyente WHERE contribuyente_id = $1 AND is_active = true`, [contribuyenteId] ); const isFirstUpload = existingRows.length === 0; await pool.query(` INSERT INTO fiel_contribuyente ( contribuyente_id, rfc, cer_data, key_data, key_password_enc, cer_iv, cer_tag, key_iv, key_tag, password_iv, password_tag, serial_number, valid_from, valid_until, is_active ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, true) ON CONFLICT (contribuyente_id) DO UPDATE SET rfc = $2, cer_data = $3, key_data = $4, key_password_enc = $5, cer_iv = $6, cer_tag = $7, key_iv = $8, key_tag = $9, password_iv = $10, password_tag = $11, serial_number = $12, valid_from = $13, valid_until = $14, is_active = true, updated_at = now() `, [ contribuyenteId, rfc, enc.encryptedCer, enc.encryptedKey, enc.encryptedPassword, enc.cerIv, enc.cerTag, enc.keyIv, enc.keyTag, enc.passwordIv, enc.passwordTag, serialNumber, validFrom, validUntil, ]); // Trigger auto-sync on first upload (fire-and-forget) if (isFirstUpload) { import('./opinion-cumplimiento.service.js').then(async ({ consultarOpinionContribuyente }) => { try { await consultarOpinionContribuyente(pool, contribuyenteId); } catch (err: any) { console.error(`[FIEL first-upload] Opinión falló para contribuyente ${contribuyenteId}:`, err.message || err); } }).catch(() => {}); import('./constancia.service.js').then(async ({ consultarConstanciaContribuyente }) => { try { await consultarConstanciaContribuyente(pool, contribuyenteId); } catch (err: any) { console.error(`[FIEL first-upload] CSF falló para contribuyente ${contribuyenteId}:`, err.message || err); } }).catch(() => {}); } const daysUntilExpiration = Math.ceil((validUntil.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); return { success: true, message: 'FIEL configurada correctamente', status: { configured: true, rfc, serialNumber, validFrom: validFrom.toISOString(), validUntil: validUntil.toISOString(), isExpired: false, daysUntilExpiration }, }; } catch (error: any) { console.error('[FIEL Contribuyente Upload Error]', error); return { success: false, message: error.message || 'Error al procesar la FIEL' }; } } export async function getFielStatusContribuyente(pool: Pool, contribuyenteId: string): Promise { // Try per-contribuyente first (tenant BD) const { rows } = await pool.query(` SELECT rfc, serial_number AS "serialNumber", valid_from AS "validFrom", valid_until AS "validUntil", is_active AS "isActive" FROM fiel_contribuyente WHERE contribuyente_id = $1 `, [contribuyenteId]); if (rows.length === 0 || !rows[0].isActive) { // Fallback: check legacy tenant-level FIEL by matching RFC const { rows: contribRows } = await pool.query('SELECT rfc FROM contribuyentes WHERE entidad_id = $1', [contribuyenteId]); const rfc = contribRows[0]?.rfc; if (rfc) { const { getFielStatus } = await import('./fiel.service.js'); // getFielStatus reads by tenantId — check if the legacy FIEL matches this RFC // We need prisma access, so import it const { prisma } = await import('../config/database.js'); const legacyFiel = await prisma.fielCredential.findFirst({ where: { rfc, isActive: true }, select: { rfc: true, serialNumber: true, validFrom: true, validUntil: true, isActive: true }, }); if (legacyFiel) { const now = new Date(); const isExpired = now > legacyFiel.validUntil; const daysUntilExpiration = Math.ceil((legacyFiel.validUntil.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); return { configured: true, rfc: legacyFiel.rfc, serialNumber: legacyFiel.serialNumber || undefined, validFrom: legacyFiel.validFrom.toISOString(), validUntil: legacyFiel.validUntil.toISOString(), isExpired, daysUntilExpiration: isExpired ? 0 : daysUntilExpiration, }; } } return { configured: false }; } const fiel = rows[0]; const now = new Date(); const isExpired = now > new Date(fiel.validUntil); const daysUntilExpiration = Math.ceil((new Date(fiel.validUntil).getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); return { configured: true, rfc: fiel.rfc, serialNumber: fiel.serialNumber || undefined, validFrom: new Date(fiel.validFrom).toISOString(), validUntil: new Date(fiel.validUntil).toISOString(), isExpired, daysUntilExpiration: isExpired ? 0 : daysUntilExpiration, }; } export async function getDecryptedFielContribuyente(pool: Pool, contribuyenteId: string): Promise<{ cerContent: string; keyContent: string; password: string; rfc: string; } | null> { const { rows } = await pool.query(` SELECT * FROM fiel_contribuyente WHERE contribuyente_id = $1 AND is_active = true `, [contribuyenteId]); if (rows.length === 0) { // Fallback: check legacy FIEL by matching RFC const { rows: contribRows } = await pool.query('SELECT rfc FROM contribuyentes WHERE entidad_id = $1', [contribuyenteId]); const rfc = contribRows[0]?.rfc; if (rfc) { const { prisma } = await import('../config/database.js'); const legacyFiel = await prisma.fielCredential.findFirst({ where: { rfc, isActive: true }, }); if (legacyFiel && new Date() <= legacyFiel.validUntil) { try { const { decryptFielCredentials } = await import('./sat/sat-crypto.service.js'); const { cerData, keyData, password } = decryptFielCredentials( Buffer.from(legacyFiel.cerData), Buffer.from(legacyFiel.keyData), Buffer.from(legacyFiel.keyPasswordEncrypted), Buffer.from(legacyFiel.cerIv), Buffer.from(legacyFiel.cerTag), Buffer.from(legacyFiel.keyIv), Buffer.from(legacyFiel.keyTag), Buffer.from(legacyFiel.passwordIv), Buffer.from(legacyFiel.passwordTag) ); return { cerContent: cerData.toString('binary'), keyContent: keyData.toString('binary'), password, rfc: legacyFiel.rfc }; } catch (err) { console.error('[FIEL Contribuyente] Legacy decrypt failed:', err); return null; } } } return null; } const fiel = rows[0]; if (new Date() > new Date(fiel.valid_until)) return null; try { const { cerData, keyData, password } = decryptFielCredentials( Buffer.from(fiel.cer_data), Buffer.from(fiel.key_data), Buffer.from(fiel.key_password_enc), Buffer.from(fiel.cer_iv), Buffer.from(fiel.cer_tag), Buffer.from(fiel.key_iv), Buffer.from(fiel.key_tag), Buffer.from(fiel.password_iv), Buffer.from(fiel.password_tag) ); return { cerContent: cerData.toString('binary'), keyContent: keyData.toString('binary'), password, rfc: fiel.rfc }; } catch (error) { console.error('[FIEL Contribuyente Decrypt Error]', error); return null; } }