Skip to main content

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:
  1. RFC Validation - Ensures tax IDs match the profile
  2. Fiscal Regime Validation - Verifies the fiscal regime matches configured regimes
  3. 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>
1

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;
}
2

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);
  }
}
3

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;
}
4

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>
1

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"
);
2

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);
    }
  }
}
3

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",
  },
});
4

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

const config = {
  validarRFCIngresos: true,
  validarRFCGastos: true,
  bloquearSiRFCNoCoincide: true,     // Block if RFC doesn't match
  validarRegimenFiscal: true,
  validarUUIDDuplicado: true,
};

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.

Build docs developers (and LLMs) love