203 lines
8.6 KiB
TypeScript
203 lines
8.6 KiB
TypeScript
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<FielStatus> {
|
|
// 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;
|
|
}
|
|
}
|