Overview
The Fiscal Validation service ensures that all uploaded CFDI documents comply with Mexican fiscal regulations and match the profile’s tax information. It performs comprehensive validation of RFC (tax ID), fiscal regime (Régimen Fiscal), and UUID uniqueness.
Validation rules can be configured per profile, allowing flexibility for different use cases while maintaining fiscal compliance.
Validation Types
The service performs three types of validation:
- RFC Validation - Ensures tax IDs match the profile
- Fiscal Regime Validation - Verifies the fiscal regime matches configured regimes
- UUID Validation - Prevents duplicate document uploads
Invoice Validation
For income invoices (facturas de ingreso), the service validates that the issuer (emisor) information matches the profile:
// From fiscal-validation.service.ts:14
async validateFacturaIngreso(
cfdi: CFDI,
profileId: string,
profileRFC: string,
profileRegimenesFiscales: string[],
validacionesHabilitadas: ValidacionesConfig
): Promise<EstadoValidacionCFDI>
RFC Format Validation
Validates that both the issuer’s RFC and profile’s RFC have valid formats.// fiscal-validation.service.ts:31
if (!isValidRFCFormat(cfdi.rfcEmisor)) {
estado.errores.push(`El RFC del emisor (${cfdi.rfcEmisor}) no tiene un formato válido`);
estado.valido = false;
return estado;
}
RFC Matching
Compares the issuer’s RFC with the profile’s RFC.// fiscal-validation.service.ts:46
const rfcCoincide = compareRFCs(cfdi.rfcEmisor, profileRFC);
if (!rfcCoincide) {
if (validacionesHabilitadas.bloquearSiRFCNoCoincide) {
estado.errores.push(mensaje);
estado.valido = false;
} else {
estado.advertencias.push(mensaje);
}
}
Fiscal Regime Validation
Verifies the fiscal regime is in the profile’s configured regimes.// fiscal-validation.service.ts:72
const regimenIncluido = profileRegimenesFiscales.includes(
cfdi.regimenFiscalEmisor ?? ""
);
if (!regimenIncluido) {
estado.errores.push(
`El régimen fiscal de la factura (${cfdi.regimenFiscalEmisor}) ` +
`no está entre los regímenes del perfil`
);
estado.valido = false;
}
UUID Duplicate Check
Ensures the UUID hasn’t been uploaded before.// fiscal-validation.service.ts:86
const uuidDuplicado = await this.checkUUIDDuplicado(
cfdi.uuid, profileId, "invoice"
);
if (uuidDuplicado) {
estado.errores.push(`El UUID (${cfdi.uuid}) ya existe en el sistema`);
estado.valido = false;
}
Expense Validation
For expenses (gastos), the service validates that the receiver (receptor) information matches the profile:
// From fiscal-validation.service.ts:102
async validateGasto(
cfdi: CFDI,
profileId: string,
profileRFC: string,
profileRegimenesFiscales: string[],
validacionesHabilitadas: ValidacionesConfig
): Promise<EstadoValidacionGasto>
The validation logic is similar to invoices, but checks the receiver’s RFC instead of the issuer’s, since in expense documents the profile is the receiver, not the issuer.
Key Difference: Receiver vs Issuer
- Invoices (Ingresos): Profile is the issuer → validate
rfcEmisor
- Expenses (Gastos): Profile is the receiver → validate
rfcReceptor
// fiscal-validation.service.ts:134
const rfcCoincide = compareRFCs(cfdi.rfcReceptor, profileRFC);
Payment Complement Validation
Payment complements require special validation logic since they can relate to both invoices and expenses:
// From fiscal-validation.service.ts:191
async validateComplementoPago(
cfdi: CFDI,
profileId: string,
profileRFC: string
): Promise<EstadoValidacionComplemento>
UUID Duplicate Check (Global)
Payment complement UUIDs are checked globally (not per profile) to prevent duplicate fiscal stamps.// fiscal-validation.service.ts:212
const uuidDuplicado = await this.checkUUIDDuplicado(
cfdi.uuid, profileId, "complement"
);
Extract Related Invoices
Extract all related invoice UUIDs from the payment complement.// fiscal-validation.service.ts:234
const facturasUUIDs: string[] = [];
for (const pago of cfdi.complementoPago.pagos) {
for (const facturaRel of pago.facturasRelacionadas) {
if (facturaRel.uuid) {
facturasUUIDs.push(facturaRel.uuid);
}
}
}
Search Related Invoices
Search for related invoices in both the invoices and expenses tables.// fiscal-validation.service.ts:253
const facturasInvoice = await Invoice.findAll({
where: {
uuid: { [Op.in]: facturasUUIDs },
profile_id: profileId,
tipo: "PPD",
},
});
const facturasAccruedExpense = await AccruedExpense.findAll({
where: {
uuid: { [Op.in]: facturasUUIDs },
profile_id: profileId,
tipo: "PPD",
},
});
Validate RFC Based on Document Type
Validate RFC based on whether the related documents are invoices or expenses.
- For invoices: Profile should be the issuer of the complement
- For expenses: Profile should be the receiver of the complement
// fiscal-validation.service.ts:275
if (facturasInvoice.length > 0) {
const rfcCoincide = compareRFCs(cfdi.rfcEmisor, profileRFC);
}
if (facturasAccruedExpense.length > 0) {
const rfcCoincide = compareRFCs(cfdi.rfcReceptor, profileRFC);
}
If no related invoices are found in the database, the service validates that at least one RFC matches:
// From fiscal-validation.service.ts:330
if (facturasInvoice.length === 0 && facturasAccruedExpense.length === 0) {
const rfcEmisorCoincide = compareRFCs(cfdi.rfcEmisor, profileRFC);
const rfcReceptorCoincide = compareRFCs(cfdi.rfcReceptor, profileRFC);
if (!rfcEmisorCoincide && !rfcReceptorCoincide) {
estado.errores.push(
`El complemento de pago no corresponde al perfil ${profileRFC}. ` +
`No se encontraron facturas relacionadas en la base de datos.`
);
estado.valido = false;
} else {
// Complement belongs to profile, but related invoices not uploaded yet
estado.advertencias.push(
`No se encontraron facturas relacionadas. ` +
`El complemento se guardará pero no se aplicará hasta que se suban las facturas.`
);
}
}
If the payment complement doesn’t belong to the profile (neither RFC matches), it will be rejected. If it belongs but related invoices aren’t found, it will be accepted with a warning.
UUID Duplicate Detection
The service checks for duplicate UUIDs across multiple tables:
// From fiscal-validation.service.ts:372
async checkUUIDDuplicado(
uuid: string,
profileId: string,
tipo: "invoice" | "expense" | "complement" | "both" = "both"
): Promise<boolean> {
if (tipo === "both") {
// Check in invoices, expenses, and complements
const existingInvoice = await Invoice.findOne({
where: { uuid, profile_id: profileId },
});
const existingAccruedExpense = await AccruedExpense.findOne({
where: { uuid, profile_id: profileId },
});
// Complements: global unique constraint (no profile_id)
const existingComplement = await PaymentComplement.findOne({
where: { uuid },
});
return !!(existingInvoice || existingAccruedExpense || existingComplement);
}
// ... specific type checks
}
Important: Payment complements use a global UUID constraint (without profile_id), while invoices and expenses are scoped per profile.
Validation Configuration
Validation rules can be configured per profile:
interface ValidacionesConfig {
validarRFCIngresos?: boolean; // Validate RFC for invoices
validarRFCGastos?: boolean; // Validate RFC for expenses
bloquearSiRFCNoCoincide?: boolean; // Block or warn if RFC doesn't match
validarRegimenFiscal?: boolean; // Validate fiscal regime
validarUUIDDuplicado?: boolean; // Check for duplicate UUIDs
}
Configuration Examples
Strict Validation
Lenient Validation
Minimal Validation
const config = {
validarRFCIngresos: true,
validarRFCGastos: true,
bloquearSiRFCNoCoincide: true, // Block if RFC doesn't match
validarRegimenFiscal: true,
validarUUIDDuplicado: true,
};
const config = {
validarRFCIngresos: true,
validarRFCGastos: true,
bloquearSiRFCNoCoincide: false, // Warn but allow if RFC doesn't match
validarRegimenFiscal: false,
validarUUIDDuplicado: true,
};
const config = {
validarRFCIngresos: false,
validarRFCGastos: false,
bloquearSiRFCNoCoincide: false,
validarRegimenFiscal: false,
validarUUIDDuplicado: true, // Always check for duplicates
};
RFC Validation Utils
The service uses utility functions for RFC validation:
// From utils/rfc.util.ts
// Validate RFC format (12-13 characters, alphanumeric)
export function isValidRFCFormat(rfc: string): boolean {
const rfcPattern = /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/;
return rfcPattern.test(rfc.toUpperCase());
}
// Normalize RFC (uppercase, remove spaces)
export function normalizeRFC(rfc: string): string {
return rfc.toUpperCase().trim().replace(/\s+/g, '');
}
// Compare two RFCs (case-insensitive, normalized)
export function compareRFCs(rfc1: string, rfc2: string): boolean {
return normalizeRFC(rfc1) === normalizeRFC(rfc2);
}
Validation Response Structure
All validation methods return a structured response:
interface EstadoValidacionCFDI {
rfcVerificado: boolean; // RFC validation passed
regimenFiscalVerificado: boolean; // Fiscal regime validation passed
uuidDuplicado: boolean; // UUID already exists
advertencias: string[]; // Non-blocking warnings
errores: string[]; // Blocking errors
valido: boolean; // Overall validation result
}
Usage Examples
Validating an Invoice
import { FiscalValidationService } from './services/fiscal-validation.service';
const service = new FiscalValidationService();
const validacion = await service.validateFacturaIngreso(
cfdi,
profileId,
'XAXX010101000', // Profile RFC
['612', '621'], // Profile fiscal regimes
{
validarRFCIngresos: true,
bloquearSiRFCNoCoincide: true,
validarRegimenFiscal: true,
validarUUIDDuplicado: true,
}
);
if (!validacion.valido) {
console.error('Validation failed:', validacion.errores);
throw new Error('Invalid invoice');
}
if (validacion.advertencias.length > 0) {
console.warn('Validation warnings:', validacion.advertencias);
}
console.log('✓ Invoice validated successfully');
Validating an Expense
const validacion = await service.validateGasto(
cfdi,
profileId,
profileRFC,
profileRegimenesFiscales,
validacionesConfig
);
if (validacion.valido) {
console.log('✓ RFC Verified:', validacion.rfcVerificado);
console.log('✓ Fiscal Regime Verified:', validacion.regimenFiscalVerificado);
console.log('✓ UUID Unique:', !validacion.uuidDuplicado);
}
Validating a Payment Complement
const validacion = await service.validateComplementoPago(
cfdi,
profileId,
profileRFC
);
if (!validacion.valido) {
console.error('Errors:', validacion.errores);
} else if (validacion.advertencias.length > 0) {
console.warn('Warnings:', validacion.advertencias);
// Complement is valid but related invoices not found
}
Error Handling
Common validation errors:
- “El RFC del emisor no tiene un formato válido”: Invalid RFC format
- “El RFC del emisor no coincide con el RFC del perfil”: RFC mismatch
- “El régimen fiscal no está entre los regímenes del perfil”: Fiscal regime not configured
- “El perfil no tiene regímenes fiscales configurados”: Profile missing fiscal regime setup
- “El UUID ya existe en el sistema”: Duplicate document
- “El complemento de pago no corresponde al perfil”: Wrong profile for payment complement
Implementation Details
Source Code References
- Service:
src/services/fiscal-validation.service.ts
- Types:
src/types/validation.types.ts
- RFC Utils:
src/utils/rfc.util.ts
Database Tables
invoices - Income invoices (with profile_id, uuid unique per profile)
accrued_expenses - Expenses (with profile_id, uuid unique per profile)
payment_complements - Payment complements (uuid globally unique)
API Integration
The fiscal validation service is integrated into all document upload endpoints:
POST /api/invoices - Validates before creating invoice
POST /api/expenses - Validates before creating expense
POST /api/payment-complements - Validates before creating complement
Validation occurs before database insertion, preventing invalid documents from being stored.