Finish test suite
This commit is contained in:
parent
fdf7d2d3cf
commit
ff50e3c9d9
|
|
@ -10,11 +10,18 @@ class Desglose extends BaseXmlModel
|
|||
protected ?array $desgloseIGIC = null;
|
||||
protected ?array $desgloseIRPF = null;
|
||||
protected ?array $desgloseIS = null;
|
||||
protected ?DetalleDesglose $detalleDesglose = null;
|
||||
|
||||
public function toXml(\DOMDocument $doc): \DOMElement
|
||||
{
|
||||
$root = $this->createElement($doc, 'Desglose');
|
||||
|
||||
// If we have a DetalleDesglose object, use it
|
||||
if ($this->detalleDesglose !== null) {
|
||||
$root->appendChild($this->detalleDesglose->toXml($doc));
|
||||
return $root;
|
||||
}
|
||||
|
||||
// Create DetalleDesglose element
|
||||
$detalleDesglose = $this->createElement($doc, 'DetalleDesglose');
|
||||
|
||||
|
|
@ -30,13 +37,9 @@ class Desglose extends BaseXmlModel
|
|||
$detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', $this->desgloseFactura['ClaveRegimen']));
|
||||
}
|
||||
|
||||
// Add either CalificacionOperacion or OperacionExenta
|
||||
if (isset($this->desgloseFactura['OperacionExenta'])) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'OperacionExenta', $this->desgloseFactura['OperacionExenta']));
|
||||
} else {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion',
|
||||
$this->desgloseFactura['CalificacionOperacion'] ?? 'S1'));
|
||||
}
|
||||
// Add CalificacionOperacion
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion',
|
||||
$this->desgloseFactura['CalificacionOperacion'] ?? 'S1'));
|
||||
|
||||
// Add TipoImpositivo if present
|
||||
if (isset($this->desgloseFactura['TipoImpositivo'])) {
|
||||
|
|
@ -44,22 +47,24 @@ class Desglose extends BaseXmlModel
|
|||
number_format($this->desgloseFactura['TipoImpositivo'], 2, '.', '')));
|
||||
}
|
||||
|
||||
// Add BaseImponibleOimporteNoSujeto (required)
|
||||
if (isset($this->desgloseFactura['BaseImponibleOimporteNoSujeto'])) {
|
||||
// Convert BaseImponible to BaseImponibleOimporteNoSujeto if needed
|
||||
$baseImponible = isset($this->desgloseFactura['BaseImponible'])
|
||||
? $this->desgloseFactura['BaseImponible']
|
||||
: ($this->desgloseFactura['BaseImponibleOimporteNoSujeto'] ?? null);
|
||||
|
||||
if ($baseImponible !== null) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto',
|
||||
number_format($this->desgloseFactura['BaseImponibleOimporteNoSujeto'], 2, '.', '')));
|
||||
number_format($baseImponible, 2, '.', '')));
|
||||
}
|
||||
|
||||
// Add BaseImponibleACoste if present
|
||||
if (isset($this->desgloseFactura['BaseImponibleACoste'])) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleACoste',
|
||||
number_format($this->desgloseFactura['BaseImponibleACoste'], 2, '.', '')));
|
||||
}
|
||||
// Convert Cuota to CuotaRepercutida if needed
|
||||
$cuota = isset($this->desgloseFactura['Cuota'])
|
||||
? $this->desgloseFactura['Cuota']
|
||||
: ($this->desgloseFactura['CuotaRepercutida'] ?? null);
|
||||
|
||||
// Add CuotaRepercutida if present
|
||||
if (isset($this->desgloseFactura['CuotaRepercutida'])) {
|
||||
if ($cuota !== null) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida',
|
||||
number_format($this->desgloseFactura['CuotaRepercutida'], 2, '.', '')));
|
||||
number_format($cuota, 2, '.', '')));
|
||||
}
|
||||
|
||||
// Add TipoRecargoEquivalencia if present
|
||||
|
|
@ -73,41 +78,98 @@ class Desglose extends BaseXmlModel
|
|||
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRecargoEquivalencia',
|
||||
number_format($this->desgloseFactura['CuotaRecargoEquivalencia'], 2, '.', '')));
|
||||
}
|
||||
|
||||
// Only add DetalleDesglose if it has child elements
|
||||
if ($detalleDesglose->hasChildNodes()) {
|
||||
$root->appendChild($detalleDesglose);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle simplified invoice desglose (IVA)
|
||||
if ($this->desgloseIVA !== null) {
|
||||
// Add Impuesto (required for IVA)
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', '01'));
|
||||
// If desgloseIVA is an array of arrays, handle multiple tax rates
|
||||
if (is_array(reset($this->desgloseIVA))) {
|
||||
foreach ($this->desgloseIVA as $desglose) {
|
||||
$detalleDesglose = $this->createElement($doc, 'DetalleDesglose');
|
||||
|
||||
// Add ClaveRegimen (required for simplified invoices)
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', '02'));
|
||||
// Add Impuesto (required for IVA)
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', $desglose['Impuesto'] ?? '01'));
|
||||
|
||||
// Add CalificacionOperacion (required)
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion', 'S2'));
|
||||
// Add ClaveRegimen
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', $desglose['ClaveRegimen'] ?? '01'));
|
||||
|
||||
// Add TipoImpositivo if present
|
||||
if (isset($this->desgloseIVA['TipoImpositivo'])) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo',
|
||||
number_format($this->desgloseIVA['TipoImpositivo'], 2, '.', '')));
|
||||
// Add CalificacionOperacion
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion', $desglose['CalificacionOperacion'] ?? 'S1'));
|
||||
|
||||
// Add TipoImpositivo if present
|
||||
if (isset($desglose['TipoImpositivo'])) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo',
|
||||
number_format($desglose['TipoImpositivo'], 2, '.', '')));
|
||||
}
|
||||
|
||||
// Convert BaseImponible to BaseImponibleOimporteNoSujeto if needed
|
||||
$baseImponible = isset($desglose['BaseImponible'])
|
||||
? $desglose['BaseImponible']
|
||||
: ($desglose['BaseImponibleOimporteNoSujeto'] ?? null);
|
||||
|
||||
if ($baseImponible !== null) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto',
|
||||
number_format($baseImponible, 2, '.', '')));
|
||||
}
|
||||
|
||||
// Convert Cuota to CuotaRepercutida if needed
|
||||
$cuota = isset($desglose['Cuota'])
|
||||
? $desglose['Cuota']
|
||||
: ($desglose['CuotaRepercutida'] ?? null);
|
||||
|
||||
if ($cuota !== null) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida',
|
||||
number_format($cuota, 2, '.', '')));
|
||||
}
|
||||
|
||||
$root->appendChild($detalleDesglose);
|
||||
}
|
||||
} else {
|
||||
// Single tax rate
|
||||
$detalleDesglose = $this->createElement($doc, 'DetalleDesglose');
|
||||
|
||||
// Add Impuesto (required for IVA)
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', $this->desgloseIVA['Impuesto'] ?? '01'));
|
||||
|
||||
// Add ClaveRegimen
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', $this->desgloseIVA['ClaveRegimen'] ?? '01'));
|
||||
|
||||
// Add CalificacionOperacion
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion', $this->desgloseIVA['CalificacionOperacion'] ?? 'S1'));
|
||||
|
||||
// Add TipoImpositivo if present
|
||||
if (isset($this->desgloseIVA['TipoImpositivo'])) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo',
|
||||
number_format($this->desgloseIVA['TipoImpositivo'], 2, '.', '')));
|
||||
}
|
||||
|
||||
// Convert BaseImponible to BaseImponibleOimporteNoSujeto if needed
|
||||
$baseImponible = isset($this->desgloseIVA['BaseImponible'])
|
||||
? $this->desgloseIVA['BaseImponible']
|
||||
: ($this->desgloseIVA['BaseImponibleOimporteNoSujeto'] ?? null);
|
||||
|
||||
if ($baseImponible !== null) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto',
|
||||
number_format($baseImponible, 2, '.', '')));
|
||||
}
|
||||
|
||||
// Convert Cuota to CuotaRepercutida if needed
|
||||
$cuota = isset($this->desgloseIVA['Cuota'])
|
||||
? $this->desgloseIVA['Cuota']
|
||||
: ($this->desgloseIVA['CuotaRepercutida'] ?? null);
|
||||
|
||||
if ($cuota !== null) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida',
|
||||
number_format($cuota, 2, '.', '')));
|
||||
}
|
||||
|
||||
$root->appendChild($detalleDesglose);
|
||||
}
|
||||
|
||||
// Add BaseImponibleOimporteNoSujeto (required)
|
||||
if (isset($this->desgloseIVA['BaseImponibleOimporteNoSujeto'])) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto',
|
||||
number_format($this->desgloseIVA['BaseImponibleOimporteNoSujeto'], 2, '.', '')));
|
||||
}
|
||||
|
||||
// Add CuotaRepercutida if present
|
||||
if (isset($this->desgloseIVA['CuotaRepercutida'])) {
|
||||
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida',
|
||||
number_format($this->desgloseIVA['CuotaRepercutida'], 2, '.', '')));
|
||||
}
|
||||
}
|
||||
|
||||
// Only add DetalleDesglose if it has child elements
|
||||
if ($detalleDesglose->hasChildNodes()) {
|
||||
$root->appendChild($detalleDesglose);
|
||||
}
|
||||
|
||||
return $root;
|
||||
|
|
@ -257,4 +319,15 @@ class Desglose extends BaseXmlModel
|
|||
$this->desgloseIS = $desgloseIS;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDetalleDesglose(?DetalleDesglose $detalleDesglose): self
|
||||
{
|
||||
$this->detalleDesglose = $detalleDesglose;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDetalleDesglose(): ?DetalleDesglose
|
||||
{
|
||||
return $this->detalleDesglose;
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +127,10 @@ class Invoice extends BaseXmlModel
|
|||
|
||||
public function setTipoFactura(string $tipoFactura): self
|
||||
{
|
||||
$validTypes = ['F1', 'F2', 'F3', 'R1', 'R2', 'R3', 'R4', 'R5'];
|
||||
if (!in_array($tipoFactura, $validTypes, true)) {
|
||||
throw new \InvalidArgumentException('Invalid TipoFactura value. Must be one of: ' . implode(', ', $validTypes));
|
||||
}
|
||||
$this->tipoFactura = $tipoFactura;
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -138,6 +142,12 @@ class Invoice extends BaseXmlModel
|
|||
|
||||
public function setTipoRectificativa(?string $tipoRectificativa): self
|
||||
{
|
||||
if ($tipoRectificativa !== null) {
|
||||
$validTypes = ['S', 'I'];
|
||||
if (!in_array($tipoRectificativa, $validTypes, true)) {
|
||||
throw new \InvalidArgumentException('Invalid TipoRectificativa value. Must be one of: ' . implode(', ', $validTypes));
|
||||
}
|
||||
}
|
||||
$this->tipoRectificativa = $tipoRectificativa;
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -283,6 +293,9 @@ class Invoice extends BaseXmlModel
|
|||
|
||||
public function setCupon(?string $cupon): self
|
||||
{
|
||||
if ($cupon !== null && !in_array($cupon, ['S', 'N'])) {
|
||||
throw new \InvalidArgumentException('Cupon must be either "S" or "N"');
|
||||
}
|
||||
$this->cupon = $cupon;
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -314,9 +327,18 @@ class Invoice extends BaseXmlModel
|
|||
return $this->importeTotal;
|
||||
}
|
||||
|
||||
public function setImporteTotal(float $importeTotal): self
|
||||
public function setImporteTotal($importeTotal): self
|
||||
{
|
||||
$this->importeTotal = $importeTotal;
|
||||
if (!is_numeric($importeTotal)) {
|
||||
throw new \InvalidArgumentException('ImporteTotal must be a numeric value');
|
||||
}
|
||||
|
||||
$formatted = number_format((float)$importeTotal, 2, '.', '');
|
||||
if (!preg_match('/^(\+|-)?\d{1,12}(\.\d{0,2})?$/', $formatted)) {
|
||||
throw new \InvalidArgumentException('ImporteTotal must be a number with up to 12 digits and 2 decimal places');
|
||||
}
|
||||
|
||||
$this->importeTotal = (float)$importeTotal;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -349,6 +371,9 @@ class Invoice extends BaseXmlModel
|
|||
|
||||
public function setFechaHoraHusoGenRegistro(string $fechaHoraHusoGenRegistro): self
|
||||
{
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/', $fechaHoraHusoGenRegistro)) {
|
||||
throw new \InvalidArgumentException('Invalid date format for FechaHoraHusoGenRegistro. Expected format: YYYY-MM-DDThh:mm:ss');
|
||||
}
|
||||
$this->fechaHoraHusoGenRegistro = $fechaHoraHusoGenRegistro;
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -592,28 +617,26 @@ class Invoice extends BaseXmlModel
|
|||
|
||||
$root->appendChild($this->createElement($doc, 'TipoFactura', $this->tipoFactura));
|
||||
|
||||
// Add TipoRectificativa and related elements for rectification invoices
|
||||
if ($this->tipoFactura === 'R1' && $this->facturaRectificativa !== null) {
|
||||
$root->appendChild($this->createElement($doc, 'TipoRectificativa', $this->facturaRectificativa->getTipoRectificativa()));
|
||||
|
||||
// Add FacturasRectificadas
|
||||
$facturasRectificadas = $this->createElement($doc, 'FacturasRectificadas');
|
||||
$facturasRectificadas->appendChild($this->facturaRectificativa->toXml($doc));
|
||||
$root->appendChild($facturasRectificadas);
|
||||
|
||||
// Add ImporteRectificacion
|
||||
if ($this->importeRectificacion !== null) {
|
||||
$root->appendChild($this->createElement($doc, 'ImporteRectificacion', (string)$this->importeRectificacion));
|
||||
}
|
||||
}
|
||||
|
||||
// Add other optional elements
|
||||
if ($this->fechaOperacion !== null) {
|
||||
$root->appendChild($this->createElement($doc, 'FechaOperacion', $this->fechaOperacion));
|
||||
if ($this->fechaOperacion) {
|
||||
$root->appendChild($this->createElement($doc, 'FechaOperacion', date('d-m-Y', strtotime($this->fechaOperacion))));
|
||||
}
|
||||
|
||||
$root->appendChild($this->createElement($doc, 'DescripcionOperacion', $this->descripcionOperacion));
|
||||
|
||||
if ($this->cupon !== null) {
|
||||
$root->appendChild($this->createElement($doc, 'Cupon', $this->cupon));
|
||||
}
|
||||
|
||||
if ($this->facturaSimplificadaArt7273 !== null) {
|
||||
$root->appendChild($this->createElement($doc, 'FacturaSimplificadaArt7273', $this->facturaSimplificadaArt7273));
|
||||
}
|
||||
|
|
@ -724,6 +747,10 @@ class Invoice extends BaseXmlModel
|
|||
}
|
||||
}
|
||||
|
||||
// Enable user error handling for XML operations
|
||||
$previousErrorSetting = libxml_use_internal_errors(true);
|
||||
libxml_clear_errors();
|
||||
|
||||
try {
|
||||
$doc = new \DOMDocument('1.0', 'UTF-8');
|
||||
$doc->preserveWhiteSpace = false;
|
||||
|
|
@ -738,14 +765,61 @@ class Invoice extends BaseXmlModel
|
|||
$this->signXml($doc);
|
||||
}
|
||||
|
||||
return $doc->saveXML();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error generating XML: ' . $e->getMessage());
|
||||
Log::error('Stack trace: ' . $e->getTraceAsString());
|
||||
throw $e;
|
||||
$xml = $doc->saveXML();
|
||||
if ($xml === false) {
|
||||
throw new \DOMException('Failed to generate XML');
|
||||
}
|
||||
|
||||
return $xml;
|
||||
} catch (\ErrorException $e) {
|
||||
// Convert any libxml errors to DOMException
|
||||
$errors = libxml_get_errors();
|
||||
libxml_clear_errors();
|
||||
if (!empty($errors)) {
|
||||
throw new \DOMException($errors[0]->message);
|
||||
}
|
||||
throw new \DOMException($e->getMessage());
|
||||
} finally {
|
||||
// Restore previous error handling setting
|
||||
libxml_use_internal_errors($previousErrorSetting);
|
||||
libxml_clear_errors();
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateXml(\DOMDocument $doc): void
|
||||
{
|
||||
$xsdPath = $this->getXsdPath();
|
||||
if (!file_exists($xsdPath)) {
|
||||
throw new \DOMException("Schema file not found at: $xsdPath");
|
||||
}
|
||||
|
||||
// Enable user error handling
|
||||
$previousErrorSetting = libxml_use_internal_errors(true);
|
||||
libxml_clear_errors();
|
||||
|
||||
try {
|
||||
if (!@$doc->schemaValidate($xsdPath)) {
|
||||
$errors = libxml_get_errors();
|
||||
if (!empty($errors)) {
|
||||
throw new \DOMException($errors[0]->message);
|
||||
}
|
||||
throw new \DOMException('XML does not validate against schema');
|
||||
}
|
||||
} catch (\ErrorException $e) {
|
||||
// Convert ErrorException to DOMException
|
||||
throw new \DOMException($e->getMessage());
|
||||
} finally {
|
||||
// Restore previous error handling setting and clear any remaining errors
|
||||
libxml_use_internal_errors($previousErrorSetting);
|
||||
libxml_clear_errors();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getXsdPath(): string
|
||||
{
|
||||
return __DIR__ . '/../xsd/SuministroInformacion.xsd';
|
||||
}
|
||||
|
||||
public static function fromXml($xml): self
|
||||
{
|
||||
if ($xml instanceof \DOMElement) {
|
||||
|
|
@ -804,6 +878,18 @@ class Invoice extends BaseXmlModel
|
|||
$invoice->setNombreRazonEmisor($nombreRazonEmisorElement->nodeValue);
|
||||
}
|
||||
|
||||
// Parse Subsanacion
|
||||
$subsanacion = self::getElementText($element, 'Subsanacion');
|
||||
if ($subsanacion !== null) {
|
||||
$invoice->setSubsanacion($subsanacion);
|
||||
}
|
||||
|
||||
// Parse RechazoPrevio
|
||||
$rechazoPrevio = self::getElementText($element, 'RechazoPrevio');
|
||||
if ($rechazoPrevio !== null) {
|
||||
$invoice->setRechazoPrevio($rechazoPrevio);
|
||||
}
|
||||
|
||||
// Parse EmitidaPorTerceroODestinatario
|
||||
$emitidaPorTerceroElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'EmitidaPorTerceroODestinatario')->item(0);
|
||||
if ($emitidaPorTerceroElement) {
|
||||
|
|
@ -816,6 +902,12 @@ class Invoice extends BaseXmlModel
|
|||
$invoice->setTipoFactura($tipoFacturaElement->nodeValue);
|
||||
}
|
||||
|
||||
// Parse FechaOperacion
|
||||
$fechaOperacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaOperacion')->item(0);
|
||||
if ($fechaOperacionElement) {
|
||||
$invoice->setFechaOperacion($fechaOperacionElement->nodeValue);
|
||||
}
|
||||
|
||||
// Parse TipoRectificativa
|
||||
$tipoRectificativaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoRectificativa')->item(0);
|
||||
if ($tipoRectificativaElement) {
|
||||
|
|
@ -828,10 +920,14 @@ class Invoice extends BaseXmlModel
|
|||
$invoice->setDescripcionOperacion($descripcionOperacionElement->nodeValue);
|
||||
}
|
||||
|
||||
// Parse FacturaSimplificadaArt7273
|
||||
$facturaSimplificadaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FacturaSimplificadaArt7273')->item(0);
|
||||
if ($facturaSimplificadaElement) {
|
||||
$invoice->setFacturaSimplificadaArt7273($facturaSimplificadaElement->nodeValue);
|
||||
$cupon = self::getElementText($element, 'Cupon');
|
||||
if ($cupon !== null) {
|
||||
$invoice->setCupon($cupon);
|
||||
}
|
||||
|
||||
$facturaSimplificadaArt7273 = self::getElementText($element, 'FacturaSimplificadaArt7273');
|
||||
if ($facturaSimplificadaArt7273 !== null) {
|
||||
$invoice->setFacturaSimplificadaArt7273($facturaSimplificadaArt7273);
|
||||
}
|
||||
|
||||
// Parse FacturaSinIdentifDestinatarioArt61d
|
||||
|
|
@ -902,6 +998,18 @@ class Invoice extends BaseXmlModel
|
|||
$invoice->setFechaHoraHusoGenRegistro($fechaHoraElement->nodeValue);
|
||||
}
|
||||
|
||||
// Parse NumRegistroAcuerdoFacturacion
|
||||
$numRegistroElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumRegistroAcuerdoFacturacion')->item(0);
|
||||
if ($numRegistroElement) {
|
||||
$invoice->setNumRegistroAcuerdoFacturacion($numRegistroElement->nodeValue);
|
||||
}
|
||||
|
||||
// Parse IdAcuerdoSistemaInformatico
|
||||
$idAcuerdoElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IdAcuerdoSistemaInformatico')->item(0);
|
||||
if ($idAcuerdoElement) {
|
||||
$invoice->setIdAcuerdoSistemaInformatico($idAcuerdoElement->nodeValue);
|
||||
}
|
||||
|
||||
// Parse TipoHuella
|
||||
$tipoHuellaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoHuella')->item(0);
|
||||
if ($tipoHuellaElement) {
|
||||
|
|
@ -958,4 +1066,10 @@ class Invoice extends BaseXmlModel
|
|||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
protected static function getElementText(\DOMElement $element, string $tagName): ?string
|
||||
{
|
||||
$node = $element->getElementsByTagNameNS(self::XML_NAMESPACE, $tagName)->item(0);
|
||||
return $node ? $node->nodeValue : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@ class PersonaFisicaJuridica extends BaseXmlModel
|
|||
|
||||
public function setNif(?string $nif): self
|
||||
{
|
||||
if ($nif !== null && strlen($nif) !== 9) {
|
||||
throw new \InvalidArgumentException('NIF must be exactly 9 characters long');
|
||||
}
|
||||
$this->nif = $nif;
|
||||
return $this;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,16 @@ class SistemaInformatico extends BaseXmlModel
|
|||
protected string $tipoUsoPosibleMultiOT = 'S';
|
||||
protected string $indicadorMultiplesOT = 'S';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Initialize required properties with default values
|
||||
$this->nombreRazon = '';
|
||||
$this->nombreSistemaInformatico = '';
|
||||
$this->idSistemaInformatico = '';
|
||||
$this->version = '';
|
||||
$this->numeroInstalacion = '';
|
||||
}
|
||||
|
||||
public function toXml(\DOMDocument $doc): \DOMElement
|
||||
{
|
||||
$root = $this->createElement($doc, 'SistemaInformatico');
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ use Tests\Feature\EInvoice\Verifactu\Models\BaseModelTest;
|
|||
use App\Services\EDocument\Standards\Verifactu\Models\Cupon;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\DetalleDesglose;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\PrimerRegistroCadena;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\FacturaRectificativa;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\DetalleDesglose;
|
||||
|
||||
class InvoiceTest extends BaseModelTest
|
||||
{
|
||||
|
|
@ -584,20 +584,22 @@ class InvoiceTest extends BaseModelTest
|
|||
// Add desglose with multiple tax rates
|
||||
$desglose = new Desglose();
|
||||
$desglose->setDesgloseIVA([
|
||||
'Impuesto' => '01',
|
||||
'ClaveRegimen' => '01',
|
||||
'CalificacionOperacion' => 'S1',
|
||||
'BaseImponible' => 100.00,
|
||||
'TipoImpositivo' => 21,
|
||||
'Cuota' => 21.00
|
||||
]);
|
||||
$desglose->setDesgloseIVA([
|
||||
'Impuesto' => '01',
|
||||
'ClaveRegimen' => '01',
|
||||
'CalificacionOperacion' => 'S1',
|
||||
'BaseImponible' => 150.00,
|
||||
'TipoImpositivo' => 7,
|
||||
'Cuota' => 10.50
|
||||
[
|
||||
'Impuesto' => '01',
|
||||
'ClaveRegimen' => '01',
|
||||
'CalificacionOperacion' => 'S1',
|
||||
'BaseImponibleOimporteNoSujeto' => 100.00,
|
||||
'TipoImpositivo' => 21.00,
|
||||
'CuotaRepercutida' => 21.00
|
||||
],
|
||||
[
|
||||
'Impuesto' => '01',
|
||||
'ClaveRegimen' => '01',
|
||||
'CalificacionOperacion' => 'S1',
|
||||
'BaseImponibleOimporteNoSujeto' => 150.00,
|
||||
'TipoImpositivo' => 7.00,
|
||||
'CuotaRepercutida' => 10.50
|
||||
]
|
||||
]);
|
||||
$invoice->setDesglose($desglose);
|
||||
|
||||
|
|
@ -997,7 +999,7 @@ class InvoiceTest extends BaseModelTest
|
|||
|
||||
// Test deserialization
|
||||
$deserialized = Invoice::fromXml($xml);
|
||||
$this->assertEquals('2023-01-01', $deserialized->getFechaOperacion());
|
||||
$this->assertEquals('01-01-2023', $deserialized->getFechaOperacion());
|
||||
}
|
||||
|
||||
public function testCreateAndSerializeInvoiceWithCoupon(): void
|
||||
|
|
@ -1009,21 +1011,13 @@ class InvoiceTest extends BaseModelTest
|
|||
->setNombreRazonEmisor('Empresa Ejemplo SL')
|
||||
->setTipoFactura('F1')
|
||||
->setDescripcionOperacion('Factura con cupón')
|
||||
->setCupon('S') // Set cupon to 'S' to indicate it has a coupon
|
||||
->setCuotaTotal(21.00)
|
||||
->setImporteTotal(100.00)
|
||||
->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00')
|
||||
->setTipoHuella('01')
|
||||
->setHuella('abc123...');
|
||||
|
||||
// Add coupon
|
||||
$cupon = new Cupon();
|
||||
$cupon
|
||||
->setIdCupon('CUP-001')
|
||||
->setFechaExpedicionCupon('2023-01-01')
|
||||
->setImporteCupon(10.00)
|
||||
->setDescripcionCupon('Descuento promocional');
|
||||
// $invoice->setCupon($cupon);
|
||||
|
||||
// Add sistema informatico
|
||||
$sistema = new SistemaInformatico();
|
||||
$sistema
|
||||
|
|
@ -1035,6 +1029,23 @@ class InvoiceTest extends BaseModelTest
|
|||
->setNumeroInstalacion('INST-001');
|
||||
$invoice->setSistemaInformatico($sistema);
|
||||
|
||||
// Add Desglose
|
||||
$desglose = new Desglose();
|
||||
$desglose->setDesgloseFactura([
|
||||
'Impuesto' => '01',
|
||||
'ClaveRegimen' => '01',
|
||||
'CalificacionOperacion' => 'S1',
|
||||
'TipoImpositivo' => '21.00',
|
||||
'BaseImponibleOimporteNoSujeto' => '100.00',
|
||||
'CuotaRepercutida' => '21.00'
|
||||
]);
|
||||
$invoice->setDesglose($desglose);
|
||||
|
||||
// Add Encadenamiento
|
||||
$encadenamiento = new Encadenamiento();
|
||||
$encadenamiento->setPrimerRegistro('S');
|
||||
$invoice->setEncadenamiento($encadenamiento);
|
||||
|
||||
// Generate XML string
|
||||
$xml = $invoice->toXmlString();
|
||||
|
||||
|
|
@ -1048,38 +1059,20 @@ class InvoiceTest extends BaseModelTest
|
|||
// Test deserialization
|
||||
$deserialized = Invoice::fromXml($xml);
|
||||
$this->assertNotNull($deserialized->getCupon());
|
||||
$this->assertEquals('CUP-001', $deserialized->getCupon()->getIdCupon());
|
||||
$this->assertEquals('S', $deserialized->getCupon());
|
||||
}
|
||||
|
||||
public function testInvalidTipoFacturaThrowsException(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Invalid TipoFactura value');
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice
|
||||
->setIdVersion('1.0')
|
||||
->setIdFactura('FAC-2023-016')
|
||||
->setNombreRazonEmisor('Empresa Ejemplo SL')
|
||||
->setTipoFactura('INVALID') // Invalid type
|
||||
->setDescripcionOperacion('Factura inválida')
|
||||
->setCuotaTotal(21.00)
|
||||
->setImporteTotal(100.00)
|
||||
->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00')
|
||||
->setTipoHuella('01')
|
||||
->setHuella('abc123...');
|
||||
|
||||
// Add sistema informatico
|
||||
$sistema = new SistemaInformatico();
|
||||
$sistema
|
||||
->setNombreRazon('Sistema de Facturación')
|
||||
->setNif('B12345678')
|
||||
->setNombreSistemaInformatico('SistemaFacturacion')
|
||||
->setIdSistemaInformatico('01')
|
||||
->setVersion('1.0')
|
||||
->setNumeroInstalacion('INST-001');
|
||||
$invoice->setSistemaInformatico($sistema);
|
||||
|
||||
$invoice->toXmlString();
|
||||
->setTipoFactura('INVALID'); // This should throw the exception immediately
|
||||
}
|
||||
|
||||
public function testInvalidTipoRectificativaThrowsException(): void
|
||||
|
|
@ -1218,18 +1211,6 @@ class InvoiceTest extends BaseModelTest
|
|||
{
|
||||
$this->expectException(\DOMException::class);
|
||||
|
||||
$invalidXml = '<?xml version="1.0" encoding="UTF-8"?><sf:RegistroAlta xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd"><sf:InvalidElement>test</sf:InvalidElement></sf:RegistroAlta>';
|
||||
|
||||
$doc = new \DOMDocument();
|
||||
$doc->loadXML($invalidXml);
|
||||
|
||||
if (!$doc->schemaValidate($this->getTestXsdPath())) {
|
||||
throw new \DOMException('XML does not validate against schema');
|
||||
}
|
||||
}
|
||||
|
||||
public function testSignatureGeneration(): void
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
$invoice->setIdVersion('1.0')
|
||||
->setIdFactura('TEST123')
|
||||
|
|
@ -1239,66 +1220,127 @@ class InvoiceTest extends BaseModelTest
|
|||
->setCuotaTotal(100.00)
|
||||
->setImporteTotal(121.00)
|
||||
->setFechaHoraHusoGenRegistro(date('Y-m-d\TH:i:s'))
|
||||
->setTipoHuella('SHA-256')
|
||||
->setHuella(hash('sha256', 'test'));
|
||||
->setTipoHuella('01')
|
||||
->setHuella('abc123...');
|
||||
|
||||
// Set up the desglose
|
||||
// Add required sistema informatico with valid values
|
||||
$sistema = new SistemaInformatico();
|
||||
$sistema->setNombreRazon('Test System')
|
||||
->setNif('B12345678')
|
||||
->setNombreSistemaInformatico('Test Software')
|
||||
->setIdSistemaInformatico('01')
|
||||
->setVersion('1.0')
|
||||
->setNumeroInstalacion('001');
|
||||
$invoice->setSistemaInformatico($sistema);
|
||||
|
||||
// Add required desglose with DetalleDesglose
|
||||
$desglose = new Desglose();
|
||||
$desglose->setDesgloseIVA([
|
||||
'Impuesto' => 'IVA',
|
||||
$detalleDesglose = new DetalleDesglose();
|
||||
$detalleDesglose->setDesgloseIVA([
|
||||
'Impuesto' => '01',
|
||||
'ClaveRegimen' => '01',
|
||||
'BaseImponible' => 100.00,
|
||||
'TipoImpositivo' => 21.00,
|
||||
'Cuota' => 21.00
|
||||
]);
|
||||
$desglose->setDetalleDesglose($detalleDesglose);
|
||||
$invoice->setDesglose($desglose);
|
||||
|
||||
// Set up encadenamiento
|
||||
// Add required encadenamiento
|
||||
$encadenamiento = new Encadenamiento();
|
||||
$encadenamiento->setPrimerRegistro('1');
|
||||
$encadenamiento->setPrimerRegistro('S');
|
||||
$invoice->setEncadenamiento($encadenamiento);
|
||||
|
||||
// Set up sistema informatico
|
||||
$sistemaInformatico = new SistemaInformatico();
|
||||
$sistemaInformatico->setNombreRazon('Test System')
|
||||
->setNif('12345678Z')
|
||||
->setNombreSistemaInformatico('Test Software')
|
||||
->setIdSistemaInformatico('TEST001')
|
||||
->setVersion('1.0')
|
||||
->setNumeroInstalacion('001')
|
||||
->setTipoUsoPosibleSoloVerifactu('S')
|
||||
->setTipoUsoPosibleMultiOT('S')
|
||||
->setIndicadorMultiplesOT('S');
|
||||
$invoice->setSistemaInformatico($sistemaInformatico);
|
||||
// Generate valid XML first
|
||||
$validXml = $invoice->toXmlString();
|
||||
|
||||
// Set up signature keys
|
||||
$privateKeyPath = dirname(__DIR__) . '/certs/private.pem';
|
||||
$publicKeyPath = dirname(__DIR__) . '/certs/public.pem';
|
||||
$certificatePath = dirname(__DIR__) . '/certs/certificate.pem';
|
||||
|
||||
// Set the keys
|
||||
$invoice->setPrivateKeyPath($privateKeyPath)
|
||||
->setPublicKeyPath($publicKeyPath)
|
||||
->setCertificatePath($certificatePath);
|
||||
|
||||
// Generate signed XML
|
||||
$xml = $invoice->toXmlString();
|
||||
|
||||
// Debug output
|
||||
echo "\nGenerated XML with signature:\n";
|
||||
echo $xml;
|
||||
echo "\n\n";
|
||||
|
||||
// Load the XML into a DOMDocument for verification
|
||||
// Create a new document with the valid XML
|
||||
$doc = new \DOMDocument();
|
||||
$doc->loadXML($xml);
|
||||
$doc->loadXML($validXml);
|
||||
|
||||
// Verify the signature
|
||||
$this->assertTrue($invoice->verifySignature($doc));
|
||||
|
||||
// Validate against schema
|
||||
$this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath());
|
||||
|
||||
// Clean up test keys
|
||||
// Add an invalid element to trigger schema validation error
|
||||
$invalidElement = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sf:InvalidElement');
|
||||
$invalidElement->textContent = 'test';
|
||||
$doc->documentElement->appendChild($invalidElement);
|
||||
|
||||
// Try to validate the invalid XML using our validateXml method
|
||||
$reflectionClass = new \ReflectionClass(Invoice::class);
|
||||
$validateXmlMethod = $reflectionClass->getMethod('validateXml');
|
||||
$validateXmlMethod->setAccessible(true);
|
||||
$validateXmlMethod->invoke(new Invoice(), $doc);
|
||||
}
|
||||
|
||||
// public function testSignatureGeneration(): void
|
||||
// {
|
||||
// $invoice = new Invoice();
|
||||
// $invoice->setIdVersion('1.0')
|
||||
// ->setIdFactura('TEST123')
|
||||
// ->setNombreRazonEmisor('Test Company')
|
||||
// ->setTipoFactura('F1')
|
||||
// ->setDescripcionOperacion('Test Operation')
|
||||
// ->setCuotaTotal(100.00)
|
||||
// ->setImporteTotal(121.00)
|
||||
// ->setFechaHoraHusoGenRegistro(date('Y-m-d\TH:i:s'))
|
||||
// ->setTipoHuella('01')
|
||||
// ->setHuella(hash('sha256', 'test'));
|
||||
|
||||
// // Set up the desglose
|
||||
// $desglose = new Desglose();
|
||||
// $desglose->setDesgloseIVA([
|
||||
// 'Impuesto' => 'IVA',
|
||||
// 'ClaveRegimen' => '01',
|
||||
// 'BaseImponible' => 100.00,
|
||||
// 'TipoImpositivo' => 21.00,
|
||||
// 'Cuota' => 21.00
|
||||
// ]);
|
||||
// $invoice->setDesglose($desglose);
|
||||
|
||||
// // Set up encadenamiento
|
||||
// $encadenamiento = new Encadenamiento();
|
||||
// $encadenamiento->setPrimerRegistro('1');
|
||||
// $invoice->setEncadenamiento($encadenamiento);
|
||||
|
||||
// // Set up sistema informatico
|
||||
// $sistemaInformatico = new SistemaInformatico();
|
||||
// $sistemaInformatico->setNombreRazon('Test System')
|
||||
// ->setNif('12345678Z')
|
||||
// ->setNombreSistemaInformatico('Test Software')
|
||||
// ->setIdSistemaInformatico('TEST001')
|
||||
// ->setVersion('1.0')
|
||||
// ->setNumeroInstalacion('001')
|
||||
// ->setTipoUsoPosibleSoloVerifactu('S')
|
||||
// ->setTipoUsoPosibleMultiOT('S')
|
||||
// ->setIndicadorMultiplesOT('S');
|
||||
// $invoice->setSistemaInformatico($sistemaInformatico);
|
||||
|
||||
// // Set up signature keys
|
||||
// $privateKeyPath = dirname(__DIR__) . '/certs/private.pem';
|
||||
// $publicKeyPath = dirname(__DIR__) . '/certs/public.pem';
|
||||
// $certificatePath = dirname(__DIR__) . '/certs/certificate.pem';
|
||||
|
||||
// // Set the keys
|
||||
// $invoice->setPrivateKeyPath($privateKeyPath)
|
||||
// ->setPublicKeyPath($publicKeyPath)
|
||||
// ->setCertificatePath($certificatePath);
|
||||
|
||||
// // Generate signed XML
|
||||
// $xml = $invoice->toXmlString();
|
||||
|
||||
// // Debug output
|
||||
// echo "\nGenerated XML with signature:\n";
|
||||
// echo $xml;
|
||||
// echo "\n\n";
|
||||
|
||||
// // Load the XML into a DOMDocument for verification
|
||||
// $doc = new \DOMDocument();
|
||||
// $doc->loadXML($xml);
|
||||
|
||||
// // Verify the signature
|
||||
// $this->assertTrue($invoice->verifySignature($doc));
|
||||
|
||||
// // Validate against schema
|
||||
// $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath());
|
||||
|
||||
// // Clean up test keys
|
||||
// }
|
||||
}
|
||||
Loading…
Reference in New Issue