Search by

chabjose / cfdi-generator

Chab-Jose

Generador de CFDI 4.0 (Comprobante Fiscal Digital por Internet) para México, independiente de framework, con sellado digital incluido.

Package info

github.com/Chab-Jose/cfdi-generator

pkg:composer/chabjose/cfdi-generator

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.6.0 2026-10-20 21:39 UTC

This package is auto-updated.

Last update: 2026-10-20 21:48:07 UTC


README

Tests Latest Version License PHP Version

Generador de CFDI 4.0 (Comprobante Fiscal Digital por Internet) para México, independiente de framework, con una API fluida para construir, calcular y sellar comprobantes fiscales conforme al esquema del SAT.

Nota sobre alcance: este paquete genera y sella el XML del CFDI. El timbrado (envío a un PAC) no está incluido — cada proyecto conecta su propio Proveedor Autorizado de Certificación. Ver Alcance más abajo.

Características

  • ✅ API fluida para construir comprobantes: Comprobante → Emisor → Receptor → Conceptos
  • ✅ Cálculo automático de Importes, Impuestos (Traslados/Retenciones), SubTotal, Descuento y Total
  • ✅ Mapeo a XML válido según el esquema CFDI 4.0 del SAT
  • ✅ Sellado digital completo (cadena original + firma SHA256) usando el XSLT oficial del SAT
  • ✅ Carga de CSD (.cer/.key) con conversión DER→PEM incluida
  • ✅ Complemento de Pagos (REP) 2.0, con cálculo automático de impuestos agrupados y conversión de moneda a MXN en el nodo Totales
  • ✅ Complemento de Nómina 1.2, con cálculo automático de Percepciones/Deducciones/Totales, incluyendo la regla oficial de exclusión mutua entre Sueldos y Jubilación/Pensión/Retiro
  • ✅ Complemento de Carta Porte 3.1, soportando los 4 medios de transporte (Autotransporte, Marítimo, Aéreo, Ferroviario) como complemento opcional y acoplable sobre un CFDI de Ingreso/Traslado, con generación automática de IdCCP y cálculo de PesoBrutoTotal/NumTotalMercancias/TotalDistRec
  • ✅ Complemento de Hidrocarburos y Petrolíferos (HidroYPetro) 1.0, a nivel Concepto, con validación contra los catálogos oficiales del SAT (permisos, claves y subproductos)
  • ✅ Complemento de Instituciones Educativas Privadas (IEDU) 1.0, a nivel Concepto, con validación de CURP, nivel educativo y RFC de quien paga
  • ✅ Arquitectura extensible: cada pieza (cálculo, mapeo, sellado, validación) es sustituible vía contratos, y nuevos complementos, tanto de Comprobante como de Concepto, se agregan sin modificar el core (ComplementoRegistry para mapeo XML, ComplementoBuilderRegistry y ComplementoConceptoBuilderRegistry para cálculo y validación)
  • ✅ Catálogos del SAT (RegimenFiscal, UsoCfdi, FormaPago, MetodoPago, TipoDeComprobante, Impuesto, TipoFactor, ObjetoImp, Exportacion) como enums nativos de PHP, opcionales — con autocompletado en el IDE, sin bloquear el uso del código crudo del SAT
  • ✅ Cobertura de tests amplia, incluyendo verificación criptográfica real del sello

Requisitos

  • PHP >= 8.1
  • Extensiones: ext-dom, ext-openssl, ext-xsl, ext-libxml

Instalación

composer require chabjose/cfdi-generator

Uso básico: CFDI de Ingreso/Egreso

use ChabJose\CfdiGenerator\CfdiGenerator;
use ChabJose\CfdiGenerator\Services\CsdLoader;
use ChabJose\CfdiGenerator\Services\Sellador;
use ChabJose\CfdiGenerator\Services\CadenaOriginalService;

$csd = (new CsdLoader())->cargar(
    rutaCer: '/ruta/certificado.cer',
    rutaKey: '/ruta/llave.key',
    password: 'contraseña_csd',
);

$sellador = new Sellador(new CadenaOriginalService());

$xmlSellado = CfdiGenerator::make(sellador: $sellador)
    ->comprobante(
        tipoDeComprobante: 'I',
        moneda: 'MXN',
        lugarExpedicion: '24090',
    )
    ->emisor(
        rfc: 'XAXX010101000',
        nombre: 'ACME SA DE CV',
        regimenFiscal: '601',
    )
    ->receptor(
        rfc: 'XEXX010101000',
        nombre: 'PUBLICO EN GENERAL',
        domicilioFiscalReceptor: '24090',
        regimenFiscalReceptor: '616',
        usoCFDI: 'S01',
    )
    ->addConcepto($concepto) // ver ejemplo completo abajo
    ->sellar($csd)
    ->buildXml();

Armando un Concepto con impuestos

use ChabJose\CfdiGenerator\Models\ComprobanteConcepto;
use ChabJose\CfdiGenerator\Models\ComprobanteConceptoImpuestos;
use ChabJose\CfdiGenerator\Models\ComprobanteConceptoImpuestosTraslado;

$concepto = new ComprobanteConcepto();
$concepto->ClaveProdServ = '01010101';
$concepto->Descripcion = 'Producto de prueba';
$concepto->Cantidad = 1.0;
$concepto->ClaveUnidad = 'H87';
$concepto->ValorUnitario = 1000.00;
$concepto->ObjetoImp = '02';

$traslado = new ComprobanteConceptoImpuestosTraslado();
$traslado->Base = 1000.00;
$traslado->Impuesto = '002'; // IVA
$traslado->TipoFactor = 'Tasa';
$traslado->TasaOCuota = 0.16;

$impuestos = new ComprobanteConceptoImpuestos();
$impuestos->addTraslado($traslado);
$concepto->Impuestos = $impuestos;

El paquete calcula automáticamente Importe del concepto y de cada impuesto, así como SubTotal, Descuento y Total del comprobante — no es necesario calcularlos manualmente, aunque si ya traes un valor desde otro sistema (ERP, POS), el paquete lo respeta y no lo sobreescribe.

Complemento de Pagos (REP) 2.0

Para CFDIs de tipo "P" (Pago), usa el facade dedicado PagoGenerator — no CfdiGenerator — ya que un comprobante de Pago tiene reglas estructurales propias (Concepto fijo, SubTotal/Total en cero, Moneda "XXX") que este facade configura automáticamente.

use ChabJose\CfdiGenerator\PagoGenerator;
use ChabJose\CfdiGenerator\Models\Complementos\Pagos\PagosPago;
use ChabJose\CfdiGenerator\Models\Complementos\Pagos\PagosDoctoRelacionado;
use ChabJose\CfdiGenerator\Models\Complementos\Pagos\PagosImpuestosDR;
use ChabJose\CfdiGenerator\Models\Complementos\Pagos\PagosTrasladoDR;

$traslado = new PagosTrasladoDR();
$traslado->BaseDR = 1000.00;
$traslado->ImpuestoDR = '002';
$traslado->TipoFactorDR = 'Tasa';
$traslado->TasaOCuotaDR = 0.16;

$impuestosDR = new PagosImpuestosDR();
$impuestosDR->addTrasladoDR($traslado);

$docto = new PagosDoctoRelacionado();
$docto->IdDocumento = '11111111-2222-3333-4444-555555555555'; // UUID de la factura pagada
$docto->MonedaDR = 'MXN';
$docto->NumParcialidad = 1;
$docto->ImpSaldoAnt = 1160.00;
$docto->ImpPagado = 1160.00;
$docto->ImpSaldoInsoluto = 0.0;
$docto->ObjetoImpDR = '02';
$docto->ImpuestosDR = $impuestosDR;

$pago = new PagosPago();
$pago->FechaPago = '2026-09-11T12:00:00';
$pago->FormaDePagoP = '03';
$pago->MonedaP = 'MXN';
$pago->Monto = 1160.00;
$pago->addDoctoRelacionado($docto);

$xmlSellado = PagoGenerator::make(sellador: $sellador)
    ->comprobante(lugarExpedicion: '24090')
    ->emisor(rfc: 'XAXX010101000', nombre: 'ACME SA DE CV', regimenFiscal: '601')
    ->receptor(
        rfc: 'XEXX010101000',
        nombre: 'PUBLICO EN GENERAL',
        domicilioFiscalReceptor: '24090',
        regimenFiscalReceptor: '616',
        usoCFDI: 'CP01',
    )
    ->pago($pago)
    ->sellar($csd)
    ->buildXml();

El paquete calcula automáticamente los tres niveles de agregación del complemento (impuestos por documento relacionado → impuestos agregados por pago → totales del complemento), incluyendo conversión automática a MXN cuando un pago viene en moneda extranjera.

✅ Validación completa del catálogo c_FormaPago: PagoValidator valida, por cada FormaDePagoP, si RfcEmisorCtaOrd/CtaOrdenante y RfcEmisorCtaBen/CtaBeneficiaria aplican o no (ej. el código 06 —Dinero electrónico— nunca admite cuenta beneficiaria, aunque sí admite ordenante), el patrón exacto de cada cuenta (CLABE, tarjeta, etc.), la obligatoriedad de NomBancoOrdExt cuando el RFC es el genérico XEXX010101000, y que TipoCadPago/CertPago/CadPago/SelloPago solo apliquen a transferencias (03) — todo transcrito directo del catálogo oficial del SAT (FormaPagoMatriz), no inferido.

Complemento de Nómina 1.2

Para CFDIs de tipo "N" (Nómina), usa el facade dedicado NominaGenerator. Igual que con Pagos, este facade configura automáticamente las reglas fijas del SAT para este tipo de comprobante (TipoDeComprobante="N", Moneda="MXN", FormaPago="99", MetodoPago="PUE", Receptor.RegimenFiscalReceptor="605", Receptor.UsoCFDI="CN01", y el Concepto fijo ClaveProdServ="84111505"), por lo que receptor() en este facade no pide regimenFiscalReceptor ni usoCFDI — ya están fijados según la guía oficial del SAT.

use ChabJose\CfdiGenerator\NominaGenerator;
use ChabJose\CfdiGenerator\Models\Complementos\Nomina\Nomina;
use ChabJose\CfdiGenerator\Models\Complementos\Nomina\NominaReceptor;
use ChabJose\CfdiGenerator\Models\Complementos\Nomina\NominaPercepcion;
use ChabJose\CfdiGenerator\Models\Complementos\Nomina\NominaPercepciones;
use ChabJose\CfdiGenerator\Models\Complementos\Nomina\NominaDeduccion;
use ChabJose\CfdiGenerator\Models\Complementos\Nomina\NominaDeducciones;

$nomina = new Nomina();
$nomina->TipoNomina = 'O';
$nomina->FechaPago = '2026-09-15';
$nomina->FechaInicialPago = '2026-09-01';
$nomina->FechaFinalPago = '2026-09-15';
$nomina->NumDiasPagados = 15.0;

$receptor = new NominaReceptor();
$receptor->Curp = 'PEJJ800101HDFRRN01';
$receptor->TipoContrato = '01';
$receptor->TipoRegimen = '02';
$receptor->NumEmpleado = '001';
$receptor->PeriodicidadPago = '04';
$receptor->ClaveEntFed = 'CMX';
$nomina->Receptor = $receptor;

$percepciones = new NominaPercepciones();
$sueldo = new NominaPercepcion();
$sueldo->TipoPercepcion = '001'; // Sueldos, Salarios Rayas y Jornales
$sueldo->Clave = 'P001';
$sueldo->Concepto = 'Sueldos y salarios';
$sueldo->ImporteGravado = 3030.51;
$sueldo->ImporteExento = 0.0;
$percepciones->addPercepcion($sueldo);
$nomina->Percepciones = $percepciones;

$deducciones = new NominaDeducciones();
$isr = new NominaDeduccion();
$isr->TipoDeduccion = '002'; // ISR
$isr->Clave = 'D002';
$isr->Concepto = 'ISR';
$isr->Importe = 400.00;
$deducciones->addDeduccion($isr);
$nomina->Deducciones = $deducciones;

$xmlSellado = NominaGenerator::make(sellador: $sellador)
    ->comprobante(lugarExpedicion: '06600')
    ->emisor(rfc: 'AAA010101AAA', nombre: 'MI EMPRESA SA DE CV', regimenFiscal: '601')
    ->receptor(rfc: 'XAXX010101000', nombre: 'JUAN PEREZ', domicilioFiscalReceptor: '01000')
    ->nomina($nomina)
    ->sellar($csd)
    ->buildXml();

El paquete calcula automáticamente TotalGravado/TotalExento/TotalSueldos por percepciones, TotalImpuestosRetenidos/TotalOtrasDeducciones por deducciones, los totales a nivel Nomina, y propaga esos totales al Concepto fijo del Comprobante (ValorUnitario = TotalPercepciones + TotalOtrosPagos, Descuento = TotalDeducciones), de donde el resto del cálculo del CFDI (SubTotal, Total) sigue la misma aritmética estándar del paquete.

ℹ️ Soporta percepciones especiales como Horas Extra, Acciones o Títulos, Jubilación/Pensión/Retiro (con la regla oficial de exclusión mutua respecto a TotalSueldos), Separación e Indemnización, así como Subsidio al Empleo, Compensación de Saldos a Favor e Incapacidades.

Complemento de Carta Porte 3.1

A diferencia de Pagos y Nómina, Carta Porte no es un tipo de comprobante propio — es un complemento opcional que se adjunta a un CFDI normal (típicamente de Ingreso o Traslado) cuando además de facturar hay que amparar el transporte de mercancías. Por eso usa el facade genérico CfdiGenerator, con un método adicional ->cartaPorte(), en vez de un facade dedicado:

use ChabJose\CfdiGenerator\CfdiGenerator;
use ChabJose\CfdiGenerator\Models\Complementos\CartaPorte\CartaPorte;
use ChabJose\CfdiGenerator\Models\Complementos\CartaPorte\CartaPorteUbicacion;
use ChabJose\CfdiGenerator\Models\Complementos\CartaPorte\CartaPorteMercancias;
use ChabJose\CfdiGenerator\Models\Complementos\CartaPorte\CartaPorteMercancia;
use ChabJose\CfdiGenerator\Models\Complementos\CartaPorte\Autotransporte\CartaPorteAutotransporte;
use ChabJose\CfdiGenerator\Models\Complementos\CartaPorte\CartaPorteFiguraTransporte;
use ChabJose\CfdiGenerator\Models\Complementos\CartaPorte\CartaPorteTipoFigura;

$cartaPorte = new CartaPorte();
$cartaPorte->TranspInternac = 'No';

$origen = new CartaPorteUbicacion();
$origen->TipoUbicacion = '01'; // Origen
$origen->IDUbicacion = 'OR000001';
$origen->RFCRemitenteDestinatario = 'XAXX010101000';
$origen->FechaHoraSalidaLlegada = '2026-09-15T08:00:00';
$cartaPorte->addUbicacion($origen);

$destino = new CartaPorteUbicacion();
$destino->TipoUbicacion = '02'; // Destino
$destino->IDUbicacion = 'DE000001';
$destino->RFCRemitenteDestinatario = 'XEXX010101000';
$destino->FechaHoraSalidaLlegada = '2026-09-15T18:00:00';
$destino->DistanciaRecorrida = 350.0;
$cartaPorte->addUbicacion($destino);

$mercancia = new CartaPorteMercancia();
$mercancia->BienesTransp = '10101501';
$mercancia->Descripcion = 'Mercancía de prueba';
$mercancia->Cantidad = 10.0;
$mercancia->ClaveUnidad = 'H87';
$mercancia->PesoEnKg = 500.0;

$mercancias = new CartaPorteMercancias();
$mercancias->UnidadPeso = 'KGM';
$mercancias->addMercancia($mercancia);
$mercancias->Autotransporte = $autotransporte; // ver documentación por medio de transporte
$cartaPorte->Mercancias = $mercancias;

$figuraTransporte = new CartaPorteFiguraTransporte();
$tipoFigura = new CartaPorteTipoFigura();
$tipoFigura->TipoFigura = '01'; // Operador
$tipoFigura->RFCFigura = 'XAXX010101000';
$tipoFigura->NombreFigura = 'Juan Pérez';
$figuraTransporte->addTipoFigura($tipoFigura);
$cartaPorte->addFiguraTransporte($figuraTransporte);

$xmlSellado = CfdiGenerator::make(sellador: $sellador)
    ->comprobante(tipoDeComprobante: 'I', moneda: 'XXX', lugarExpedicion: '24090')
    ->emisor(rfc: 'XAXX010101000', nombre: 'ACME SA DE CV', regimenFiscal: '601')
    ->receptor(
        rfc: 'XEXX010101000',
        nombre: 'PUBLICO EN GENERAL',
        domicilioFiscalReceptor: '24090',
        regimenFiscalReceptor: '616',
        usoCFDI: 'S01',
    )
    ->addConcepto($concepto)
    ->cartaPorte($cartaPorte)
    ->sellar($csd)
    ->buildXml();

El paquete genera automáticamente IdCCP si no se asigna uno, valida que se haya definido exactamente un medio de transporte (Autotransporte, Marítimo, Aéreo o Ferroviario — vía MedioTransporteValidator, patrón Strategy) y calcula PesoBrutoTotal/NumTotalMercancias a partir de las Mercancías y TotalDistRec como la suma de DistanciaRecorrida únicamente de las Ubicaciones con TipoUbicacion="02" (Destino), conforme a la guía de llenado oficial del SAT.

ℹ️ Al ser un complemento opcional, un CFDI construido con CfdiGenerator sin llamar a ->cartaPorte() sigue funcionando exactamente igual que antes — adjuntarlo no cambia el comportamiento del resto del comprobante.

Complemento HidroYPetro 1.0 (Hidrocarburos y Petrolíferos)

A diferencia de Pagos, Nómina y Carta Porte, HidroYPetro vive dentro del Concepto (cfdi:Concepto/cfdi:ComplementoConcepto), no en cfdi:Complemento. Aplica a conceptos de combustible (diésel, gasolina regular y gasolina premium) en comprobantes de Ingreso o Egreso. Se adjunta directo al Concepto:

use ChabJose\CfdiGenerator\Catalogs\ClaveHYP;
use ChabJose\CfdiGenerator\Catalogs\SubProductoHYP;
use ChabJose\CfdiGenerator\Catalogs\TipoPermiso;
use ChabJose\CfdiGenerator\Models\Complementos\HidroYPetro\HidroYPetro;

$concepto = new ComprobanteConcepto();
$concepto->ClaveProdServ = '15101505';   // debe coincidir con ClaveHYP
$concepto->ClaveUnidad = 'LTR';          // obligatoria para este complemento
$concepto->Descripcion = 'Diésel automotriz';
$concepto->Cantidad = 100.0;
$concepto->ValorUnitario = 25.0;
$concepto->ObjetoImp = '02';

$concepto->addComplementoConcepto(new HidroYPetro(
    TipoPermiso::Per01,
    'PL/12345/EXP/ES/2026',
    ClaveHYP::CombustibleDiesel,
    SubProductoHYP::Sp18,
));

$cfdi->addConcepto($concepto);

Los tres campos de catálogo también aceptan el código crudo del SAT ('PER01', '15101505', 'SP18'). Al construir el comprobante, el paquete valida:

  • el comprobante es de tipo Ingreso (I) o Egreso (E)
  • el Concepto no tiene ACuentaTerceros
  • ClaveHYP es igual al ClaveProdServ del Concepto
  • ClaveUnidad del Concepto es LTR
  • SubProductoHYP corresponde a la ClaveHYP indicada (cada clave admite solo ciertos subproductos)
  • NumeroPermiso cumple el patrón oficial de su TipoPermiso

Si alguna regla falla se lanza HidroYPetroValidationException con el motivo. El xmlns y el schemaLocation del complemento se declaran automáticamente en la raíz del comprobante.

ℹ️ El SAT todavía no lo soporta en su portal de facturación gratuito; este paquete lo genera conforme al XSLT y a los catálogos oficiales publicados.

Complemento IEDU 1.0 (Instituciones Educativas Privadas)

Igual que HidroYPetro, IEDU vive dentro del Concepto (cfdi:Concepto/cfdi:ComplementoConcepto). Lo usan las instituciones educativas privadas para expedir comprobantes por servicios educativos, indicando el alumno al que corresponde el pago:

use ChabJose\CfdiGenerator\Catalogs\NivelEducativo;
use ChabJose\CfdiGenerator\Models\Complementos\Iedu\InstEducativas;

$concepto->addComplementoConcepto(new InstEducativas(
    nombreAlumno: 'JUAN PEREZ LOPEZ',
    CURP: 'GOMC800101HDFRRL09',
    nivelEducativo: NivelEducativo::Primaria,
    autRVOE: '1234567890',
    rfcPago: 'XAXX010101000',   // opcional: solo si paga alguien distinto al alumno
));

nivelEducativo también acepta el texto tal como lo publica el SAT ('Primaria', 'Profesional técnico', etc.). Al construir el comprobante, el paquete valida:

  • CURP con el formato oficial de 18 caracteres
  • nivelEducativo dentro de los 5 valores permitidos
  • nombreAlumno y autRVOE no vacíos
  • rfcPago, si se indica, con formato de RFC de 12 o 13 caracteres

Si alguna regla falla se lanza IeduValidationException. Las propiedades llevan los mismos nombres que el esquema oficial (nombreAlumno, CURP, autRVOE, rfcPago).

ℹ️ La validación cubre lo que definen el XSD y el XSLT oficiales del complemento. Las reglas de la guía de llenado que dependen del resto del comprobante no están incluidas.

Catálogos del SAT

El paquete incluye como enums nativos de PHP (enum ... : string) los catálogos del SAT que son pequeños y estables — no cambian con el SAT publicando decenas de miles de filas, sino vía Resolución Miscelánea Fiscal con poca frecuencia:

RegimenFiscal, UsoCfdi, FormaPago, MetodoPago, TipoDeComprobante, Impuesto, TipoFactor, ObjetoImp, Exportacion, y los del complemento HidroYPetro (TipoPermiso, ClaveHYP, SubProductoHYP) e IEDU (NivelEducativo) — namespace ChabJose\CfdiGenerator\Catalogs.

ℹ️ Por qué no catálogos grandes (c_ClaveProdServ ~52,000 claves, c_ClaveUnidad ~3,000+, códigos postales, etc.): embeberlos ataría las actualizaciones de esos catálogos al ciclo de releases de este paquete, y obligaría a cargar listas enormes en memoria aunque no se necesiten. Para esos, conecta tu propia fuente (base de datos, caché, o un paquete dedicado como phpcfdi/sat-catalogos).

Uso — son completamente opcionales, las propiedades de los modelos siguen aceptando el string crudo del SAT:

use ChabJose\CfdiGenerator\Catalogs\FormaPago;

// Con autocompletado del IDE, cero errores de tipeo:
$comprobante->FormaPago = FormaPago::TransferenciaElectronicaDeFondos;

// O el código crudo — por ejemplo, si el SAT publicó una clave nueva que
// el enum todavía no incluye, o si el valor viene de otro sistema (ERP, POS):
$comprobante->FormaPago = '03';

// Validar un código que no sabes si existe en el catálogo:
if (FormaPago::tryFrom($codigo) === null) {
    throw new \InvalidArgumentException("FormaPago inválido: {$codigo}");
}

El paquete normaliza internamente (en el mapeo a XML y en los cálculos que agrupan por catálogo, como Impuestos) para que ambas formas — enum o string — funcionen de manera idéntica en cualquier punto del flujo.

Alcance

Este paquete cubre la generación y sellado de CFDI 4.0. Explícitamente no incluye:

Fuera de alcance Por qué
Timbrado (conexión a PAC) Cada PAC tiene su propia API; conecta el tuyo implementando tu propio cliente sobre el XML que este paquete genera
Otros complementos (INE, Comercio Exterior, Donatarias) Roadmap futuro — ver Roadmap
Representación impresa (PDF) Fuera del alcance de esta versión

Arquitectura

CfdiGenerator / PagoGenerator / NominaGenerator
    (facades fluidos, extienden AbstractCfdiGenerator)
    └── ComprobanteBuilder (orquesta el cálculo del CFDI base)
            ├── ConceptoImpuestosCalculator
            ├── ConceptoCalculator
            ├── ComprobanteImpuestosCalculator
            └── ComprobanteTotalesCalculator

    └── PagosBuilder (orquesta el cálculo del complemento de Pagos)
            ├── DoctoRelacionadoImpuestosCalculator
            ├── PagoImpuestosCalculator
            └── PagosTotalesCalculator (con conversión de moneda a MXN)

    └── NominaBuilder (orquesta el cálculo del complemento de Nómina)
            ├── NominaPercepcionesCalculator
            ├── NominaDeduccionesCalculator
            └── NominaTotalesCalculator

    └── CartaPorteBuilder (orquesta el cálculo del complemento de Carta Porte)
            ├── IdCcpGenerator (genera IdCCP si no se provee)
            ├── MedioTransporteValidator (Strategy: exactamente 1 medio)
            ├── CartaPorteMercanciasCalculator (PesoBrutoTotal, NumTotalMercancias)
            └── CartaPorteTotalesCalculator (TotalDistRec)

Despacho de complementos (multi-complemento, opcional por comprobante):
    ├── ComplementoRegistry          → despacha cada complemento a su XmlMapper
    │     ├── PagosXmlMapper         → pago20:Pagos
    │     ├── NominaXmlMapper        → nomina12:Nomina
    │     └── CartaPorteXmlMapper    → cartaporte31:CartaPorte
    │           └── MedioTransporteXmlMapperInterface (Strategy por medio:
    │               Autotransporte / Marítimo / Aéreo / Ferroviario)
    └── ComplementoBuilderRegistry   → despacha cada complemento a su Builder
          └── CartaPorteComplementoBuilderAdapter → CartaPorteBuilder
          (devuelve null si el complemento no requiere cálculo, p. ej. Pagos/Nómina
           ya calculan los suyos desde su propio facade dedicado)

Despacho de complementos de Concepto (mismo patrón, instancias propias):
    ├── ComplementoRegistry (de concepto)       → despacha a su XmlMapper
    │     ├── HidroYPetroXmlMapper              → hidrocarburospetroliferos:HidroYPetro
    │     └── InstEducativasXmlMapper           → iedu:instEducativas
    └── ComplementoConceptoBuilderRegistry      → despacha a su Builder, con el
          │                                       Concepto y el Comprobante como contexto
          ├── HidroYPetroComplementoBuilderAdapter → HidroYPetroValidator (HidroYPetroMatriz)
          └── InstEducativasComplementoBuilderAdapter → InstEducativasValidator

XmlMapper          → Comprobante (modelo) → XML
CadenaOriginalService → XML → Cadena Original (XSLT oficial SAT)
Sellador           → Cadena Original + CSD → Sello digital
CsdLoader          → .cer/.key → CsdCredential

Cada pieza está definida por un contrato en Contracts/ — puedes sustituir cualquier implementación sin tocar el resto del paquete. Nuevos complementos se agregan implementando ComplementoXmlMapperInterface (mapeo a XML, vía ComplementoRegistry) y, si requieren cálculo propio, ComplementoBuilderInterface (vía ComplementoBuilderRegistry), sin modificar XmlMapper ni los facades existentes.

Testing

El paquete incluye tests unitarios y de integración. Los tests que requieren un CSD real (sellado, cadena original, complementos de Pagos y Nómina) necesitan un CSD de pruebas del SAT:

composer install
vendor/bin/phpunit

Ver tests/Fixtures/csd/README.md para instrucciones de cómo obtener un CSD de pruebas.

Roadmap

  • Complementos adicionales (INE, Comercio Exterior, Donatarias)
  • Contrato TimbradoInterface (opcional, sin implementación propia)
  • Representación impresa (PDF)

Reportar un problema

¿Encontraste un bug? Abre un Issue usando la plantilla correspondiente. Para vulnerabilidades de seguridad, revisa SECURITY.md en vez de abrir un Issue público.

Contribuir

Los pull requests son bienvenidos. Revisa CONTRIBUTING.md para las convenciones del proyecto antes de abrir uno.

Licencia

MIT