chabjose / cfdi-generator
Generador de CFDI 4.0 (Comprobante Fiscal Digital por Internet) para México, independiente de framework, con sellado digital incluido.
Requires
- php: >=8.1
- ext-dom: *
- ext-libxml: *
- ext-openssl: *
- ext-xsl: *
Requires (Dev)
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
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
IdCCPy cálculo dePesoBrutoTotal/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 (
ComplementoRegistrypara mapeo XML,ComplementoBuilderRegistryyComplementoConceptoBuilderRegistrypara 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:PagoValidatorvalida, por cadaFormaDePagoP, siRfcEmisorCtaOrd/CtaOrdenanteyRfcEmisorCtaBen/CtaBeneficiariaaplican o no (ej. el código06—Dinero electrónico— nunca admite cuenta beneficiaria, aunque sí admite ordenante), el patrón exacto de cada cuenta (CLABE, tarjeta, etc.), la obligatoriedad deNomBancoOrdExtcuando el RFC es el genéricoXEXX010101000, y queTipoCadPago/CertPago/CadPago/SelloPagosolo 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
CfdiGeneratorsin 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 ClaveHYPes igual alClaveProdServdel ConceptoClaveUnidaddel Concepto esLTRSubProductoHYPcorresponde a laClaveHYPindicada (cada clave admite solo ciertos subproductos)NumeroPermisocumple el patrón oficial de suTipoPermiso
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:
CURPcon el formato oficial de 18 caracteresnivelEducativodentro de los 5 valores permitidosnombreAlumnoyautRVOEno vacíosrfcPago, 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 comophpcfdi/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.