diff --git a/app/Services/EDocument/Standards/Verifactu/Models/BaseTypes.php b/app/Services/EDocument/Standards/Verifactu/Models/BaseTypes.php new file mode 100644 index 0000000000..d23334aeca --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/BaseTypes.php @@ -0,0 +1,36 @@ +createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':' . $name); + if ($value !== null) { + $element->nodeValue = $value; + } + foreach ($attributes as $attrName => $attrValue) { + $element->setAttribute($attrName, $attrValue); + } + return $element; + } + + protected function createDsElement(\DOMDocument $doc, string $name, ?string $value = null, array $attributes = []): \DOMElement + { + $element = $doc->createElementNS(self::XML_DS_NAMESPACE, self::XML_DS_NAMESPACE_PREFIX . ':' . $name); + if ($value !== null) { + $element->nodeValue = $value; + } + foreach ($attributes as $attrName => $attrValue) { + $element->setAttribute($attrName, $attrValue); + } + return $element; + } + + protected function getElementValue(\DOMElement $parent, string $name, string $namespace = self::XML_NAMESPACE): ?string + { + $elements = $parent->getElementsByTagNameNS($namespace, $name); + if ($elements->length > 0) { + return $elements->item(0)->nodeValue; + } + return null; + } + + abstract public function toXml(): string; + + public static function fromXml($xml): self + { + if ($xml instanceof \DOMElement) { + return static::fromDOMElement($xml); + } + + if (!is_string($xml)) { + throw new \InvalidArgumentException('Input must be either a string or DOMElement'); + } + + $doc = new \DOMDocument(); + if (!$doc->loadXML($xml)) { + throw new \DOMException('Failed to load XML: Invalid XML format'); + } + return static::fromDOMElement($doc->documentElement); + } + + abstract public static function fromDOMElement(\DOMElement $element): self; +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/Cupon.php b/app/Services/EDocument/Standards/Verifactu/Models/Cupon.php new file mode 100644 index 0000000000..5267161c74 --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/Cupon.php @@ -0,0 +1,91 @@ +formatOutput = true; + + $root = $this->createElement($doc, 'Cupon'); + $doc->appendChild($root); + + // Add required elements + $root->appendChild($this->createElement($doc, 'IDCupon', $this->idCupon)); + $root->appendChild($this->createElement($doc, 'FechaExpedicionCupon', $this->fechaExpedicionCupon)); + $root->appendChild($this->createElement($doc, 'ImporteCupon', (string)$this->importeCupon)); + + // Add optional description + if ($this->descripcionCupon !== null) { + $root->appendChild($this->createElement($doc, 'DescripcionCupon', $this->descripcionCupon)); + } + + return $doc->saveXML(); + } + + public static function fromDOMElement(\DOMElement $element): self + { + $cupon = new self(); + $cupon->setIdCupon($cupon->getElementValue($element, 'IDCupon')); + $cupon->setFechaExpedicionCupon($cupon->getElementValue($element, 'FechaExpedicionCupon')); + $cupon->setImporteCupon((float)$cupon->getElementValue($element, 'ImporteCupon')); + + $descripcionCupon = $cupon->getElementValue($element, 'DescripcionCupon'); + if ($descripcionCupon !== null) { + $cupon->setDescripcionCupon($descripcionCupon); + } + + return $cupon; + } + + public function getIdCupon(): string + { + return $this->idCupon; + } + + public function setIdCupon(string $idCupon): self + { + $this->idCupon = $idCupon; + return $this; + } + + public function getFechaExpedicionCupon(): string + { + return $this->fechaExpedicionCupon; + } + + public function setFechaExpedicionCupon(string $fechaExpedicionCupon): self + { + $this->fechaExpedicionCupon = $fechaExpedicionCupon; + return $this; + } + + public function getImporteCupon(): float + { + return $this->importeCupon; + } + + public function setImporteCupon(float $importeCupon): self + { + $this->importeCupon = $importeCupon; + return $this; + } + + public function getDescripcionCupon(): ?string + { + return $this->descripcionCupon; + } + + public function setDescripcionCupon(?string $descripcionCupon): self + { + $this->descripcionCupon = $descripcionCupon; + return $this; + } +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/Desglose.php b/app/Services/EDocument/Standards/Verifactu/Models/Desglose.php new file mode 100644 index 0000000000..57d53e977f --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/Desglose.php @@ -0,0 +1,260 @@ +formatOutput = true; + + $root = $this->createElement($doc, 'Desglose'); + $doc->appendChild($root); + + // Create DetalleDesglose element + $detalleDesglose = $this->createElement($doc, 'DetalleDesglose'); + + // Handle regular invoice desglose + if ($this->desgloseFactura !== null) { + // Add Impuesto if present + if (isset($this->desgloseFactura['Impuesto'])) { + $detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', $this->desgloseFactura['Impuesto'])); + } + + // Add ClaveRegimen if present + if (isset($this->desgloseFactura['ClaveRegimen'])) { + $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 TipoImpositivo if present + if (isset($this->desgloseFactura['TipoImpositivo'])) { + $detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo', + number_format($this->desgloseFactura['TipoImpositivo'], 2, '.', ''))); + } + + // Add BaseImponibleOimporteNoSujeto (required) + $detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto', + number_format($this->desgloseFactura['BaseImponible'], 2, '.', ''))); + + // Add BaseImponibleACoste if present + if (isset($this->desgloseFactura['BaseImponibleACoste'])) { + $detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleACoste', + number_format($this->desgloseFactura['BaseImponibleACoste'], 2, '.', ''))); + } + + // Add CuotaRepercutida if present + if (isset($this->desgloseFactura['Cuota'])) { + $detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida', + number_format($this->desgloseFactura['Cuota'], 2, '.', ''))); + } + + // Add TipoRecargoEquivalencia if present + if (isset($this->desgloseFactura['TipoRecargoEquivalencia'])) { + $detalleDesglose->appendChild($this->createElement($doc, 'TipoRecargoEquivalencia', + number_format($this->desgloseFactura['TipoRecargoEquivalencia'], 2, '.', ''))); + } + + // Add CuotaRecargoEquivalencia if present + if (isset($this->desgloseFactura['CuotaRecargoEquivalencia'])) { + $detalleDesglose->appendChild($this->createElement($doc, 'CuotaRecargoEquivalencia', + number_format($this->desgloseFactura['CuotaRecargoEquivalencia'], 2, '.', ''))); + } + } + + // Handle simplified invoice desglose (IVA) + if ($this->desgloseIVA !== null) { + // Add Impuesto (required for IVA) + $detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', '01')); + + // Add ClaveRegimen (required for simplified invoices) + $detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', '02')); + + // Add CalificacionOperacion (required) + $detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion', 'S2')); + + // Add TipoImpositivo if present + if (isset($this->desgloseIVA['TipoImpositivo'])) { + $detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo', + number_format($this->desgloseIVA['TipoImpositivo'], 2, '.', ''))); + } + + // Add BaseImponibleOimporteNoSujeto (required) + $detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto', + number_format($this->desgloseIVA['BaseImponible'], 2, '.', ''))); + + // Add CuotaRepercutida if present + if (isset($this->desgloseIVA['Cuota'])) { + $detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida', + number_format($this->desgloseIVA['Cuota'], 2, '.', ''))); + } + } + + // Only add DetalleDesglose if it has child elements + if ($detalleDesglose->hasChildNodes()) { + $root->appendChild($detalleDesglose); + } + + return $doc->saveXML(); + } + + public static function fromDOMElement(\DOMElement $element): self + { + $desglose = new self(); + + // Parse DesgloseFactura + $desgloseFacturaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseFactura')->item(0); + if ($desgloseFacturaElement) { + $desgloseFactura = []; + foreach ($desgloseFacturaElement->childNodes as $child) { + if ($child instanceof \DOMElement) { + $desgloseFactura[$child->localName] = $child->nodeValue; + } + } + $desglose->setDesgloseFactura($desgloseFactura); + } + + // Parse DesgloseTipoOperacion + $desgloseTipoOperacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseTipoOperacion')->item(0); + if ($desgloseTipoOperacionElement) { + $desgloseTipoOperacion = []; + foreach ($desgloseTipoOperacionElement->childNodes as $child) { + if ($child instanceof \DOMElement) { + $desgloseTipoOperacion[$child->localName] = $child->nodeValue; + } + } + $desglose->setDesgloseTipoOperacion($desgloseTipoOperacion); + } + + // Parse DesgloseIVA + $desgloseIvaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseIVA')->item(0); + if ($desgloseIvaElement) { + $desgloseIva = []; + foreach ($desgloseIvaElement->childNodes as $child) { + if ($child instanceof \DOMElement) { + $desgloseIva[$child->localName] = $child->nodeValue; + } + } + $desglose->setDesgloseIVA($desgloseIva); + } + + // Parse DesgloseIGIC + $desgloseIgicElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseIGIC')->item(0); + if ($desgloseIgicElement) { + $desgloseIgic = []; + foreach ($desgloseIgicElement->childNodes as $child) { + if ($child instanceof \DOMElement) { + $desgloseIgic[$child->localName] = $child->nodeValue; + } + } + $desglose->setDesgloseIGIC($desgloseIgic); + } + + // Parse DesgloseIRPF + $desgloseIrpfElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseIRPF')->item(0); + if ($desgloseIrpfElement) { + $desgloseIrpf = []; + foreach ($desgloseIrpfElement->childNodes as $child) { + if ($child instanceof \DOMElement) { + $desgloseIrpf[$child->localName] = $child->nodeValue; + } + } + $desglose->setDesgloseIRPF($desgloseIrpf); + } + + // Parse DesgloseIS + $desgloseIsElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseIS')->item(0); + if ($desgloseIsElement) { + $desgloseIs = []; + foreach ($desgloseIsElement->childNodes as $child) { + if ($child instanceof \DOMElement) { + $desgloseIs[$child->localName] = $child->nodeValue; + } + } + $desglose->setDesgloseIS($desgloseIs); + } + + return $desglose; + } + + public function getDesgloseFactura(): ?array + { + return $this->desgloseFactura; + } + + public function setDesgloseFactura(?array $desgloseFactura): self + { + $this->desgloseFactura = $desgloseFactura; + return $this; + } + + public function getDesgloseTipoOperacion(): ?array + { + return $this->desgloseTipoOperacion; + } + + public function setDesgloseTipoOperacion(?array $desgloseTipoOperacion): self + { + $this->desgloseTipoOperacion = $desgloseTipoOperacion; + return $this; + } + + public function getDesgloseIVA(): ?array + { + return $this->desgloseIVA; + } + + public function setDesgloseIVA(?array $desgloseIVA): self + { + $this->desgloseIVA = $desgloseIVA; + return $this; + } + + public function getDesgloseIGIC(): ?array + { + return $this->desgloseIGIC; + } + + public function setDesgloseIGIC(?array $desgloseIGIC): self + { + $this->desgloseIGIC = $desgloseIGIC; + return $this; + } + + public function getDesgloseIRPF(): ?array + { + return $this->desgloseIRPF; + } + + public function setDesgloseIRPF(?array $desgloseIRPF): self + { + $this->desgloseIRPF = $desgloseIRPF; + return $this; + } + + public function getDesgloseIS(): ?array + { + return $this->desgloseIS; + } + + public function setDesgloseIS(?array $desgloseIS): self + { + $this->desgloseIS = $desgloseIS; + return $this; + } +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/Encadenamiento.php b/app/Services/EDocument/Standards/Verifactu/Models/Encadenamiento.php new file mode 100644 index 0000000000..5c32331984 --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/Encadenamiento.php @@ -0,0 +1,233 @@ +createElement($doc, 'Encadenamiento'); + $doc->appendChild($root); + + if ($this->primerRegistro !== null) { + $root->appendChild($this->createElement($doc, 'PrimerRegistro', 'S')); + } + + if ($this->registroAnterior !== null) { + $registroAnteriorXml = $this->registroAnterior->toXml(); + $registroAnteriorDoc = new \DOMDocument(); + $registroAnteriorDoc->loadXML($registroAnteriorXml); + $registroAnteriorNode = $doc->importNode($registroAnteriorDoc->documentElement, true); + $root->appendChild($registroAnteriorNode); + } + + if ($this->registroPosterior !== null) { + $registroPosteriorXml = $this->registroPosterior->toXml(); + $registroPosteriorDoc = new \DOMDocument(); + $registroPosteriorDoc->loadXML($registroPosteriorXml); + $registroPosteriorNode = $doc->importNode($registroPosteriorDoc->documentElement, true); + $root->appendChild($registroPosteriorNode); + } + + // Add namespace declaration to the root element + $root->setAttribute('xmlns:sf', self::XML_NAMESPACE); + $root->setAttribute('xmlns:ds', self::XML_DS_NAMESPACE); + + return $doc->saveXML(); + } + + public static function fromXml($xml): BaseXmlModel + { + $encadenamiento = new self(); + + if (is_string($xml)) { + error_log("Loading XML in Encadenamiento::fromXml: " . $xml); + $dom = new \DOMDocument(); + if (!$dom->loadXML($xml)) { + error_log("Failed to load XML in Encadenamiento::fromXml"); + throw new \DOMException('Invalid XML'); + } + $element = $dom->documentElement; + } else { + $element = $xml; + } + + try { + // Handle PrimerRegistro + $primerRegistro = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'PrimerRegistro')->item(0); + if ($primerRegistro) { + $encadenamiento->setPrimerRegistro($primerRegistro->nodeValue); + } + + // Handle RegistroAnterior + $registroAnterior = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RegistroAnterior')->item(0); + if ($registroAnterior) { + $encadenamiento->setRegistroAnterior(EncadenamientoFacturaAnterior::fromDOMElement($registroAnterior)); + } + + return $encadenamiento; + } catch (\Exception $e) { + error_log("Error parsing XML in Encadenamiento::fromXml: " . $e->getMessage()); + throw new \InvalidArgumentException('Error parsing XML: ' . $e->getMessage()); + } + } + + public static function fromDOMElement(\DOMElement $element): self + { + $encadenamiento = new self(); + + // Handle PrimerRegistro + $primerRegistro = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'PrimerRegistro')->item(0); + if ($primerRegistro) { + $encadenamiento->setPrimerRegistro($primerRegistro->nodeValue); + } + + // Handle RegistroAnterior + $registroAnterior = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RegistroAnterior')->item(0); + if ($registroAnterior) { + $encadenamiento->setRegistroAnterior(EncadenamientoFacturaAnterior::fromDOMElement($registroAnterior)); + } + + return $encadenamiento; + } + + public function getPrimerRegistro(): ?string + { + return $this->primerRegistro; + } + + public function setPrimerRegistro(?string $primerRegistro): self + { + if ($primerRegistro !== null && $primerRegistro !== 'S') { + throw new \InvalidArgumentException('PrimerRegistro must be "S" or null'); + } + $this->primerRegistro = $primerRegistro; + return $this; + } + + public function getRegistroAnterior(): ?EncadenamientoFacturaAnterior + { + return $this->registroAnterior; + } + + public function setRegistroAnterior(?EncadenamientoFacturaAnterior $registroAnterior): self + { + $this->registroAnterior = $registroAnterior; + return $this; + } + + public function getRegistroPosterior(): ?EncadenamientoFacturaAnterior + { + return $this->registroPosterior; + } + + public function setRegistroPosterior(?EncadenamientoFacturaAnterior $registroPosterior): self + { + $this->registroPosterior = $registroPosterior; + return $this; + } +} + +class EncadenamientoFacturaAnterior extends BaseXmlModel +{ + protected string $idEmisorFactura; + protected string $numSerieFactura; + protected string $fechaExpedicionFactura; + protected string $huella; + + public function toXml(): string + { + $doc = new \DOMDocument('1.0', 'UTF-8'); + $doc->formatOutput = true; + + $root = $this->createElement($doc, 'RegistroAnterior'); + $doc->appendChild($root); + + $root->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->idEmisorFactura)); + $root->appendChild($this->createElement($doc, 'NumSerieFactura', $this->numSerieFactura)); + $root->appendChild($this->createElement($doc, 'FechaExpedicionFactura', $this->fechaExpedicionFactura)); + $root->appendChild($this->createElement($doc, 'Huella', $this->huella)); + + return $doc->saveXML(); + } + + public static function fromDOMElement(\DOMElement $element): self + { + $registroAnterior = new self(); + + // Handle IDEmisorFactura + $idEmisorFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDEmisorFactura')->item(0); + if ($idEmisorFactura) { + $registroAnterior->setIdEmisorFactura($idEmisorFactura->nodeValue); + } + + // Handle NumSerieFactura + $numSerieFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumSerieFactura')->item(0); + if ($numSerieFactura) { + $registroAnterior->setNumSerieFactura($numSerieFactura->nodeValue); + } + + // Handle FechaExpedicionFactura + $fechaExpedicionFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaExpedicionFactura')->item(0); + if ($fechaExpedicionFactura) { + $registroAnterior->setFechaExpedicionFactura($fechaExpedicionFactura->nodeValue); + } + + // Handle Huella + $huella = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Huella')->item(0); + if ($huella) { + $registroAnterior->setHuella($huella->nodeValue); + } + + return $registroAnterior; + } + + public function getIdEmisorFactura(): string + { + return $this->idEmisorFactura; + } + + public function setIdEmisorFactura(string $idEmisorFactura): self + { + $this->idEmisorFactura = $idEmisorFactura; + return $this; + } + + public function getNumSerieFactura(): string + { + return $this->numSerieFactura; + } + + public function setNumSerieFactura(string $numSerieFactura): self + { + $this->numSerieFactura = $numSerieFactura; + return $this; + } + + public function getFechaExpedicionFactura(): string + { + return $this->fechaExpedicionFactura; + } + + public function setFechaExpedicionFactura(string $fechaExpedicionFactura): self + { + $this->fechaExpedicionFactura = $fechaExpedicionFactura; + return $this; + } + + public function getHuella(): string + { + return $this->huella; + } + + public function setHuella(string $huella): self + { + $this->huella = $huella; + return $this; + } +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/FacturaRectificativa.php b/app/Services/EDocument/Standards/Verifactu/Models/FacturaRectificativa.php new file mode 100644 index 0000000000..510fe9842a --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/FacturaRectificativa.php @@ -0,0 +1,79 @@ +tipoRectificativa = $tipoRectificativa; + $this->baseRectificada = $baseRectificada; + $this->cuotaRectificada = $cuotaRectificada; + $this->cuotaRecargoRectificado = $cuotaRecargoRectificado; + $this->facturasRectificadas = []; + } + + public function getTipoRectificativa(): string + { + return $this->tipoRectificativa; + } + + public function getBaseRectificada(): float + { + return $this->baseRectificada; + } + + public function getCuotaRectificada(): float + { + return $this->cuotaRectificada; + } + + public function getCuotaRecargoRectificado(): ?float + { + return $this->cuotaRecargoRectificado; + } + + public function addFacturaRectificada(string $nif, string $numSerie, string $fecha): void + { + $this->facturasRectificadas[] = [ + 'nif' => $nif, + 'numSerie' => $numSerie, + 'fecha' => $fecha + ]; + } + + public function getFacturasRectificadas(): array + { + return $this->facturasRectificadas; + } + + public function toXml(\DOMDocument $doc): \DOMElement + { + $idFacturaRectificada = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sf:IDFacturaRectificada'); + + // Add required elements in order with proper namespace + $idEmisorFactura = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sf:IDEmisorFactura'); + $idEmisorFactura->nodeValue = $this->facturasRectificadas[0]['nif']; + $idFacturaRectificada->appendChild($idEmisorFactura); + + $numSerieFactura = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sf:NumSerieFactura'); + $numSerieFactura->nodeValue = $this->facturasRectificadas[0]['numSerie']; + $idFacturaRectificada->appendChild($numSerieFactura); + + $fechaExpedicionFactura = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sf:FechaExpedicionFactura'); + $fechaExpedicionFactura->nodeValue = $this->facturasRectificadas[0]['fecha']; + $idFacturaRectificada->appendChild($fechaExpedicionFactura); + + return $idFacturaRectificada; + } +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/Invoice.php b/app/Services/EDocument/Standards/Verifactu/Models/Invoice.php new file mode 100644 index 0000000000..cad78f9bc1 --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/Invoice.php @@ -0,0 +1,841 @@ +desglose = new Desglose(); + $this->encadenamiento = new Encadenamiento(); + $this->sistemaInformatico = new SistemaInformatico(); + $this->tipoFactura = 'F1'; // Default to normal invoice + } + + // Getters and setters for all properties + public function getIdVersion(): string + { + return $this->idVersion; + } + + public function setIdVersion(string $idVersion): self + { + $this->idVersion = $idVersion; + return $this; + } + + public function getIdFactura(): string + { + return $this->idFactura; + } + + public function setIdFactura(string $idFactura): self + { + $this->idFactura = $idFactura; + return $this; + } + + public function getRefExterna(): ?string + { + return $this->refExterna; + } + + public function setRefExterna(?string $refExterna): self + { + $this->refExterna = $refExterna; + return $this; + } + + public function getNombreRazonEmisor(): string + { + return $this->nombreRazonEmisor; + } + + public function setNombreRazonEmisor(string $nombreRazonEmisor): self + { + $this->nombreRazonEmisor = $nombreRazonEmisor; + return $this; + } + + public function getSubsanacion(): ?string + { + return $this->subsanacion; + } + + public function setSubsanacion(?string $subsanacion): self + { + $this->subsanacion = $subsanacion; + return $this; + } + + public function getRechazoPrevio(): ?string + { + return $this->rechazoPrevio; + } + + public function setRechazoPrevio(?string $rechazoPrevio): self + { + $this->rechazoPrevio = $rechazoPrevio; + return $this; + } + + public function getTipoFactura(): string + { + return $this->tipoFactura; + } + + public function setTipoFactura(string $tipoFactura): self + { + $this->tipoFactura = $tipoFactura; + return $this; + } + + public function getTipoRectificativa(): ?string + { + return $this->tipoRectificativa; + } + + public function setTipoRectificativa(?string $tipoRectificativa): self + { + $this->tipoRectificativa = $tipoRectificativa; + return $this; + } + + public function getFacturasRectificadas(): ?array + { + return $this->facturasRectificadas; + } + + public function setFacturasRectificadas(?array $facturasRectificadas): self + { + $this->facturasRectificadas = $facturasRectificadas; + return $this; + } + + public function getFacturasSustituidas(): ?array + { + return $this->facturasSustituidas; + } + + public function setFacturasSustituidas(?array $facturasSustituidas): self + { + $this->facturasSustituidas = $facturasSustituidas; + return $this; + } + + public function getImporteRectificacion(): ?float + { + return $this->importeRectificacion; + } + + public function setImporteRectificacion(?float $importeRectificacion): self + { + $this->importeRectificacion = $importeRectificacion; + return $this; + } + + public function getFechaOperacion(): ?string + { + return $this->fechaOperacion; + } + + public function setFechaOperacion(?string $fechaOperacion): self + { + $this->fechaOperacion = $fechaOperacion; + return $this; + } + + public function getDescripcionOperacion(): string + { + return $this->descripcionOperacion; + } + + public function setDescripcionOperacion(string $descripcionOperacion): self + { + $this->descripcionOperacion = $descripcionOperacion; + return $this; + } + + public function getFacturaSimplificadaArt7273(): ?string + { + return $this->facturaSimplificadaArt7273; + } + + public function setFacturaSimplificadaArt7273(?string $facturaSimplificadaArt7273): self + { + $this->facturaSimplificadaArt7273 = $facturaSimplificadaArt7273; + return $this; + } + + public function getFacturaSinIdentifDestinatarioArt61d(): ?string + { + return $this->facturaSinIdentifDestinatarioArt61d; + } + + public function setFacturaSinIdentifDestinatarioArt61d(?string $facturaSinIdentifDestinatarioArt61d): self + { + $this->facturaSinIdentifDestinatarioArt61d = $facturaSinIdentifDestinatarioArt61d; + return $this; + } + + public function getMacrodato(): ?string + { + return $this->macrodato; + } + + public function setMacrodato(?string $macrodato): self + { + $this->macrodato = $macrodato; + return $this; + } + + public function getEmitidaPorTerceroODestinatario(): ?string + { + return $this->emitidaPorTerceroODestinatario; + } + + public function setEmitidaPorTerceroODestinatario(?string $emitidaPorTerceroODestinatario): self + { + $this->emitidaPorTerceroODestinatario = $emitidaPorTerceroODestinatario; + return $this; + } + + public function getTercero(): ?PersonaFisicaJuridica + { + return $this->tercero; + } + + public function setTercero(?PersonaFisicaJuridica $tercero): self + { + $this->tercero = $tercero; + return $this; + } + + public function getDestinatarios(): ?array + { + return $this->destinatarios; + } + + public function setDestinatarios(?array $destinatarios): self + { + if ($destinatarios !== null && count($destinatarios) > 1000) { + throw new \InvalidArgumentException('Maximum number of recipients (1000) exceeded'); + } + + // Ensure all elements are PersonaFisicaJuridica instances + if ($destinatarios !== null) { + foreach ($destinatarios as $destinatario) { + if (!($destinatario instanceof PersonaFisicaJuridica)) { + throw new \InvalidArgumentException('All recipients must be instances of PersonaFisicaJuridica'); + } + } + } + + $this->destinatarios = $destinatarios; + return $this; + } + + public function getCupon(): ?string + { + return $this->cupon; + } + + public function setCupon(?string $cupon): self + { + $this->cupon = $cupon; + return $this; + } + + public function getDesglose(): Desglose + { + return $this->desglose; + } + + public function setDesglose(Desglose $desglose): self + { + $this->desglose = $desglose; + return $this; + } + + public function getCuotaTotal(): float + { + return $this->cuotaTotal; + } + + public function setCuotaTotal(float $cuotaTotal): self + { + $this->cuotaTotal = $cuotaTotal; + return $this; + } + + public function getImporteTotal(): float + { + return $this->importeTotal; + } + + public function setImporteTotal(float $importeTotal): self + { + $this->importeTotal = $importeTotal; + return $this; + } + + public function getEncadenamiento(): Encadenamiento + { + return $this->encadenamiento; + } + + public function setEncadenamiento(Encadenamiento $encadenamiento): self + { + $this->encadenamiento = $encadenamiento; + return $this; + } + + public function getSistemaInformatico(): SistemaInformatico + { + return $this->sistemaInformatico; + } + + public function setSistemaInformatico(SistemaInformatico $sistemaInformatico): self + { + $this->sistemaInformatico = $sistemaInformatico; + return $this; + } + + public function getFechaHoraHusoGenRegistro(): string + { + return $this->fechaHoraHusoGenRegistro; + } + + public function setFechaHoraHusoGenRegistro(string $fechaHoraHusoGenRegistro): self + { + $this->fechaHoraHusoGenRegistro = $fechaHoraHusoGenRegistro; + return $this; + } + + public function getNumRegistroAcuerdoFacturacion(): ?string + { + return $this->numRegistroAcuerdoFacturacion; + } + + public function setNumRegistroAcuerdoFacturacion(?string $numRegistroAcuerdoFacturacion): self + { + $this->numRegistroAcuerdoFacturacion = $numRegistroAcuerdoFacturacion; + return $this; + } + + public function getIdAcuerdoSistemaInformatico(): ?string + { + return $this->idAcuerdoSistemaInformatico; + } + + public function setIdAcuerdoSistemaInformatico(?string $idAcuerdoSistemaInformatico): self + { + $this->idAcuerdoSistemaInformatico = $idAcuerdoSistemaInformatico; + return $this; + } + + public function getTipoHuella(): string + { + return $this->tipoHuella; + } + + public function setTipoHuella(string $tipoHuella): self + { + $this->tipoHuella = $tipoHuella; + return $this; + } + + public function getHuella(): string + { + return $this->huella; + } + + public function setHuella(string $huella): self + { + $this->huella = $huella; + return $this; + } + + public function getSignature(): ?string + { + return $this->signature; + } + + public function setSignature(?string $signature): self + { + $this->signature = $signature; + return $this; + } + + public function getFacturaRectificativa(): ?FacturaRectificativa + { + return $this->facturaRectificativa; + } + + public function setFacturaRectificativa(FacturaRectificativa $facturaRectificativa): void + { + $this->facturaRectificativa = $facturaRectificativa; + } + + public function toXml(): string + { + // Validate required fields first, outside of try-catch + $requiredFields = [ + 'idVersion' => 'IDVersion', + 'idFactura' => 'NumSerieFactura', + 'nombreRazonEmisor' => 'NombreRazonEmisor', + 'tipoFactura' => 'TipoFactura', + 'descripcionOperacion' => 'DescripcionOperacion', + 'cuotaTotal' => 'CuotaTotal', + 'importeTotal' => 'ImporteTotal', + 'fechaHoraHusoGenRegistro' => 'FechaHoraHusoGenRegistro', + 'tipoHuella' => 'TipoHuella', + 'huella' => 'Huella' + ]; + + foreach ($requiredFields as $property => $fieldName) { + if (!isset($this->$property)) { + throw new \InvalidArgumentException("Missing required field: $fieldName"); + } + } + + try { + $doc = new \DOMDocument('1.0', 'UTF-8'); + $doc->preserveWhiteSpace = false; + $doc->formatOutput = true; + + // Create root element with proper namespaces + $root = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':RegistroAlta'); + $root->setAttribute('xmlns:ds', self::XML_DS_NAMESPACE); + $doc->appendChild($root); + + // Add required elements in exact order according to schema + $root->appendChild($this->createElement($doc, 'IDVersion', $this->idVersion)); + + // Create IDFactura structure + $idFactura = $this->createElement($doc, 'IDFactura'); + $idFactura->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->tercero?->getNif() ?? 'B12345678')); + $idFactura->appendChild($this->createElement($doc, 'NumSerieFactura', $this->idFactura)); + $idFactura->appendChild($this->createElement($doc, 'FechaExpedicionFactura', date('d-m-Y'))); + $root->appendChild($idFactura); + + if ($this->refExterna !== null) { + $root->appendChild($this->createElement($doc, 'RefExterna', $this->refExterna)); + } + + $root->appendChild($this->createElement($doc, 'NombreRazonEmisor', $this->nombreRazonEmisor)); + + if ($this->subsanacion !== null) { + $root->appendChild($this->createElement($doc, 'Subsanacion', $this->subsanacion)); + } + + if ($this->rechazoPrevio !== null) { + $root->appendChild($this->createElement($doc, 'RechazoPrevio', $this->rechazoPrevio)); + } + + $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 + $importeRectificacion = $this->createElement($doc, 'ImporteRectificacion'); + $importeRectificacion->appendChild($this->createElement($doc, 'BaseRectificada', + number_format($this->facturaRectificativa->getBaseRectificada(), 2, '.', ''))); + $importeRectificacion->appendChild($this->createElement($doc, 'CuotaRectificada', + number_format($this->facturaRectificativa->getCuotaRectificada(), 2, '.', ''))); + + if ($this->facturaRectificativa->getCuotaRecargoRectificado() !== null) { + $importeRectificacion->appendChild($this->createElement($doc, 'CuotaRecargoRectificado', + number_format($this->facturaRectificativa->getCuotaRecargoRectificado(), 2, '.', ''))); + } + + $root->appendChild($importeRectificacion); + } + + $root->appendChild($this->createElement($doc, 'DescripcionOperacion', $this->descripcionOperacion)); + + if ($this->facturaSimplificadaArt7273 !== null) { + $root->appendChild($this->createElement($doc, 'FacturaSimplificadaArt7273', $this->facturaSimplificadaArt7273)); + } + + if ($this->facturaSinIdentifDestinatarioArt61d !== null) { + $root->appendChild($this->createElement($doc, 'FacturaSinIdentifDestinatarioArt61d', $this->facturaSinIdentifDestinatarioArt61d)); + } + + if ($this->macrodato !== null) { + $root->appendChild($this->createElement($doc, 'Macrodato', $this->macrodato)); + } + + if ($this->emitidaPorTerceroODestinatario !== null) { + $root->appendChild($this->createElement($doc, 'EmitidaPorTerceroODestinatario', $this->emitidaPorTerceroODestinatario)); + } + + // Add tercero if present + if ($this->tercero !== null) { + $terceroElement = $this->createElement($doc, 'Tercero'); + $terceroElement->appendChild($this->createElement($doc, 'NombreRazon', $this->tercero->getRazonSocial())); + $terceroElement->appendChild($this->createElement($doc, 'NIF', $this->tercero->getNif())); + $root->appendChild($terceroElement); + } + + // Add destinatarios if present + if ($this->destinatarios !== null && count($this->destinatarios) > 0) { + $destinatariosElement = $this->createElement($doc, 'Destinatarios'); + foreach ($this->destinatarios as $destinatario) { + $idDestinatarioElement = $this->createElement($doc, 'IDDestinatario'); + $idDestinatarioElement->appendChild($this->createElement($doc, 'NombreRazon', $destinatario->getNombreRazon() ?? $destinatario->getRazonSocial())); + + // Handle either NIF or IDOtro + if ($destinatario->getNif() !== null) { + $idDestinatarioElement->appendChild($this->createElement($doc, 'NIF', $destinatario->getNif())); + } else { + $idOtroElement = $this->createElement($doc, 'IDOtro'); + if ($destinatario->getPais() !== null) { + $idOtroElement->appendChild($this->createElement($doc, 'CodigoPais', $destinatario->getPais())); + } + $idOtroElement->appendChild($this->createElement($doc, 'IDType', $destinatario->getTipoIdentificacion())); + $idOtroElement->appendChild($this->createElement($doc, 'ID', $destinatario->getIdOtro())); + $idDestinatarioElement->appendChild($idOtroElement); + } + + $destinatariosElement->appendChild($idDestinatarioElement); + } + $root->appendChild($destinatariosElement); + } + + // Add desglose + try { + $desgloseXml = $this->desglose->toXml(); + $desgloseDoc = new \DOMDocument(); + if (!$desgloseDoc->loadXML($desgloseXml)) { + error_log("Failed to load desglose XML"); + throw new \DOMException('Failed to load desglose XML'); + } + $desgloseNode = $doc->importNode($desgloseDoc->documentElement, true); + // Remove any existing namespace declarations + foreach (['xmlns:sf', 'xmlns:ds'] as $attr) { + if ($desgloseNode->hasAttribute($attr)) { + $desgloseNode->removeAttribute($attr); + } + } + $root->appendChild($desgloseNode); + } catch (\Exception $e) { + error_log("Error in desglose: " . $e->getMessage()); + throw $e; + } + + // Add CuotaTotal and ImporteTotal + $root->appendChild($this->createElement($doc, 'CuotaTotal', number_format($this->cuotaTotal, 2, '.', ''))); + $root->appendChild($this->createElement($doc, 'ImporteTotal', number_format($this->importeTotal, 2, '.', ''))); + + // Add encadenamiento + try { + $encadenamientoXml = $this->encadenamiento->toXml(); + $encadenamientoDoc = new \DOMDocument(); + if (!$encadenamientoDoc->loadXML($encadenamientoXml)) { + error_log("Failed to load encadenamiento XML"); + throw new \DOMException('Failed to load encadenamiento XML'); + } + $encadenamientoNode = $doc->importNode($encadenamientoDoc->documentElement, true); + $root->appendChild($encadenamientoNode); + } catch (\Exception $e) { + error_log("Error in encadenamiento: " . $e->getMessage()); + throw $e; + } + + // Add sistema informatico + $sistemaElement = $this->createElement($doc, 'SistemaInformatico'); + $sistemaElement->appendChild($this->createElement($doc, 'NombreRazon', $this->sistemaInformatico->getNombreRazon())); + $sistemaElement->appendChild($this->createElement($doc, 'NIF', $this->sistemaInformatico->getNif())); + $sistemaElement->appendChild($this->createElement($doc, 'NombreSistemaInformatico', $this->sistemaInformatico->getNombreSistemaInformatico())); + $sistemaElement->appendChild($this->createElement($doc, 'IdSistemaInformatico', $this->sistemaInformatico->getIdSistemaInformatico())); + $sistemaElement->appendChild($this->createElement($doc, 'Version', $this->sistemaInformatico->getVersion())); + $sistemaElement->appendChild($this->createElement($doc, 'NumeroInstalacion', $this->sistemaInformatico->getNumeroInstalacion())); + $sistemaElement->appendChild($this->createElement($doc, 'TipoUsoPosibleSoloVerifactu', $this->sistemaInformatico->getTipoUsoPosibleSoloVerifactu())); + $sistemaElement->appendChild($this->createElement($doc, 'TipoUsoPosibleMultiOT', $this->sistemaInformatico->getTipoUsoPosibleMultiOT())); + $sistemaElement->appendChild($this->createElement($doc, 'IndicadorMultiplesOT', $this->sistemaInformatico->getIndicadorMultiplesOT())); + $root->appendChild($sistemaElement); + + // Add remaining required fields + $root->appendChild($this->createElement($doc, 'FechaHoraHusoGenRegistro', $this->fechaHoraHusoGenRegistro)); + + if ($this->numRegistroAcuerdoFacturacion !== null) { + $root->appendChild($this->createElement($doc, 'NumRegistroAcuerdoFacturacion', $this->numRegistroAcuerdoFacturacion)); + } + + if ($this->idAcuerdoSistemaInformatico !== null) { + $root->appendChild($this->createElement($doc, 'IDAcuerdoSistemaInformatico', $this->idAcuerdoSistemaInformatico)); + } + + $root->appendChild($this->createElement($doc, 'TipoHuella', $this->tipoHuella)); + $root->appendChild($this->createElement($doc, 'Huella', $this->huella)); + + // Add signature if present + if ($this->signature !== null) { + $signatureElement = $this->createDsElement($doc, 'Signature', $this->signature); + $root->appendChild($signatureElement); + } + + $xml = $doc->saveXML($root); + // error_log("Generated XML: " . $xml); + return $xml; + } catch (\Exception $e) { + error_log("Error in toXml: " . $e->getMessage()); + throw $e; + } + } + + public static function fromXml($xml): self + { + if ($xml instanceof \DOMElement) { + return static::fromDOMElement($xml); + } + + if (!is_string($xml)) { + throw new \InvalidArgumentException('Input must be either a string or DOMElement'); + } + + // Enable user error handling for XML parsing + $previousErrorSetting = libxml_use_internal_errors(true); + + try { + $doc = new \DOMDocument(); + if (!$doc->loadXML($xml)) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + throw new \DOMException('Failed to load XML: ' . ($errors ? $errors[0]->message : 'Invalid XML format')); + } + return static::fromDOMElement($doc->documentElement); + } finally { + // Restore previous error handling setting + libxml_use_internal_errors($previousErrorSetting); + } + } + + public static function fromDOMElement(\DOMElement $element): self + { + $invoice = new self(); + + // Parse IDVersion + $idVersionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDVersion')->item(0); + if ($idVersionElement) { + $invoice->setIDVersion($idVersionElement->nodeValue); + } + + // Parse IDFactura + $idFacturaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDFactura')->item(0); + if ($idFacturaElement) { + $numSerieFacturaElement = $idFacturaElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumSerieFactura')->item(0); + if ($numSerieFacturaElement) { + $invoice->setIdFactura($numSerieFacturaElement->nodeValue); + } + } + + // Parse RefExterna + $refExternaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RefExterna')->item(0); + if ($refExternaElement) { + $invoice->setRefExterna($refExternaElement->nodeValue); + } + + // Parse NombreRazonEmisor + $nombreRazonEmisorElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreRazonEmisor')->item(0); + if ($nombreRazonEmisorElement) { + $invoice->setNombreRazonEmisor($nombreRazonEmisorElement->nodeValue); + } + + // Parse EmitidaPorTerceroODestinatario + $emitidaPorTerceroElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'EmitidaPorTerceroODestinatario')->item(0); + if ($emitidaPorTerceroElement) { + $invoice->setEmitidaPorTerceroODestinatario($emitidaPorTerceroElement->nodeValue); + } + + // Parse TipoFactura + $tipoFacturaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoFactura')->item(0); + if ($tipoFacturaElement) { + $invoice->setTipoFactura($tipoFacturaElement->nodeValue); + } + + // Parse TipoRectificativa + $tipoRectificativaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoRectificativa')->item(0); + if ($tipoRectificativaElement) { + $invoice->setTipoRectificativa($tipoRectificativaElement->nodeValue); + } + + // Parse DescripcionOperacion + $descripcionOperacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DescripcionOperacion')->item(0); + if ($descripcionOperacionElement) { + $invoice->setDescripcionOperacion($descripcionOperacionElement->nodeValue); + } + + // Parse FacturaSimplificadaArt7273 + $facturaSimplificadaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FacturaSimplificadaArt7273')->item(0); + if ($facturaSimplificadaElement) { + $invoice->setFacturaSimplificadaArt7273($facturaSimplificadaElement->nodeValue); + } + + // Parse FacturaSinIdentifDestinatarioArt61d + $facturaSinIdentifElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FacturaSinIdentifDestinatarioArt61d')->item(0); + if ($facturaSinIdentifElement) { + $invoice->setFacturaSinIdentifDestinatarioArt61d($facturaSinIdentifElement->nodeValue); + } + + // Parse Macrodato + $macrodatoElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Macrodato')->item(0); + if ($macrodatoElement) { + $invoice->setMacrodato($macrodatoElement->nodeValue); + } + + // Parse Tercero + $terceroElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Tercero')->item(0); + if ($terceroElement) { + $tercero = new PersonaFisicaJuridica(); + + // Get NombreRazon + $nombreRazonElement = $terceroElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreRazon')->item(0); + if ($nombreRazonElement) { + $tercero->setRazonSocial($nombreRazonElement->nodeValue); + } + + // Get NIF + $nifElement = $terceroElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'NIF')->item(0); + if ($nifElement) { + $tercero->setNif($nifElement->nodeValue); + } + + $invoice->setTercero($tercero); + } + + // Parse Desglose + $desgloseElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Desglose')->item(0); + if ($desgloseElement) { + $invoice->setDesglose(Desglose::fromDOMElement($desgloseElement)); + } + + // Parse CuotaTotal + $cuotaTotalElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'CuotaTotal')->item(0); + if ($cuotaTotalElement) { + $invoice->setCuotaTotal((float)$cuotaTotalElement->nodeValue); + } + + // Parse ImporteTotal + $importeTotalElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'ImporteTotal')->item(0); + if ($importeTotalElement) { + $invoice->setImporteTotal((float)$importeTotalElement->nodeValue); + } + + // Parse Encadenamiento + $encadenamientoElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Encadenamiento')->item(0); + if ($encadenamientoElement) { + $invoice->setEncadenamiento(Encadenamiento::fromDOMElement($encadenamientoElement)); + } + + // Parse SistemaInformatico + $sistemaInformaticoElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'SistemaInformatico')->item(0); + if ($sistemaInformaticoElement) { + $invoice->setSistemaInformatico(SistemaInformatico::fromDOMElement($sistemaInformaticoElement)); + } + + // Parse FechaHoraHusoGenRegistro + $fechaHoraElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaHoraHusoGenRegistro')->item(0); + if ($fechaHoraElement) { + $invoice->setFechaHoraHusoGenRegistro($fechaHoraElement->nodeValue); + } + + // Parse TipoHuella + $tipoHuellaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoHuella')->item(0); + if ($tipoHuellaElement) { + $invoice->setTipoHuella($tipoHuellaElement->nodeValue); + } + + // Parse Huella + $huellaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Huella')->item(0); + if ($huellaElement) { + $invoice->setHuella($huellaElement->nodeValue); + } + + // Parse Destinatarios + $destinatariosElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Destinatarios')->item(0); + if ($destinatariosElement) { + $destinatarios = []; + $idDestinatarioElements = $destinatariosElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDDestinatario'); + foreach ($idDestinatarioElements as $idDestinatarioElement) { + $destinatario = new PersonaFisicaJuridica(); + + // Get NombreRazon + $nombreRazonElement = $idDestinatarioElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreRazon')->item(0); + if ($nombreRazonElement) { + $destinatario->setNombreRazon($nombreRazonElement->nodeValue); + } + + // Get either NIF or IDOtro + $nifElement = $idDestinatarioElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'NIF')->item(0); + if ($nifElement) { + $destinatario->setNif($nifElement->nodeValue); + } else { + $idOtroElement = $idDestinatarioElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDOtro')->item(0); + if ($idOtroElement) { + $codigoPaisElement = $idOtroElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'CodigoPais')->item(0); + $idTypeElement = $idOtroElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDType')->item(0); + $idElement = $idOtroElement->getElementsByTagNameNS(self::XML_NAMESPACE, 'ID')->item(0); + + if ($codigoPaisElement) { + $destinatario->setPais($codigoPaisElement->nodeValue); + } + if ($idTypeElement) { + $destinatario->setTipoIdentificacion($idTypeElement->nodeValue); + } + if ($idElement) { + $destinatario->setIdOtro($idElement->nodeValue); + } + } + } + + $destinatarios[] = $destinatario; + } + $invoice->setDestinatarios($destinatarios); + } + + return $invoice; + } +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/PersonaFisicaJuridica.php b/app/Services/EDocument/Standards/Verifactu/Models/PersonaFisicaJuridica.php new file mode 100644 index 0000000000..b4d98bd333 --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/PersonaFisicaJuridica.php @@ -0,0 +1,211 @@ +nif; + } + + public function setNif(?string $nif): self + { + $this->nif = $nif; + return $this; + } + + public function getNombreRazon(): ?string + { + return $this->nombreRazon; + } + + public function setNombreRazon(?string $nombreRazon): self + { + $this->nombreRazon = $nombreRazon; + return $this; + } + + public function getApellidos(): ?string + { + return $this->apellidos; + } + + public function setApellidos(?string $apellidos): self + { + $this->apellidos = $apellidos; + return $this; + } + + public function getNombre(): ?string + { + return $this->nombre; + } + + public function setNombre(?string $nombre): self + { + $this->nombre = $nombre; + return $this; + } + + public function getRazonSocial(): ?string + { + return $this->razonSocial; + } + + public function setRazonSocial(?string $razonSocial): self + { + $this->razonSocial = $razonSocial; + return $this; + } + + public function getTipoIdentificacion(): ?string + { + return $this->tipoIdentificacion; + } + + public function setTipoIdentificacion(?string $tipoIdentificacion): self + { + $this->tipoIdentificacion = $tipoIdentificacion; + return $this; + } + + public function getIdOtro(): ?string + { + return $this->idOtro; + } + + public function setIdOtro(?string $idOtro): self + { + $this->idOtro = $idOtro; + return $this; + } + + public function getPais(): ?string + { + return $this->pais; + } + + public function setPais(?string $pais): self + { + $this->pais = $pais; + return $this; + } + + public function toXml(): string + { + $doc = new \DOMDocument('1.0', 'UTF-8'); + $doc->formatOutput = true; + + $root = $this->createElement($doc, 'PersonaFisicaJuridica'); + $doc->appendChild($root); + + if ($this->nif !== null) { + $root->appendChild($this->createElement($doc, 'NIF', $this->nif)); + } + + if ($this->nombreRazon !== null) { + $root->appendChild($this->createElement($doc, 'NombreRazon', $this->nombreRazon)); + } + + if ($this->apellidos !== null) { + $root->appendChild($this->createElement($doc, 'Apellidos', $this->apellidos)); + } + + if ($this->nombre !== null) { + $root->appendChild($this->createElement($doc, 'Nombre', $this->nombre)); + } + + if ($this->razonSocial !== null) { + $root->appendChild($this->createElement($doc, 'RazonSocial', $this->razonSocial)); + } + + if ($this->tipoIdentificacion !== null) { + $root->appendChild($this->createElement($doc, 'TipoIdentificacion', $this->tipoIdentificacion)); + } + + if ($this->idOtro !== null) { + $root->appendChild($this->createElement($doc, 'IDOtro', $this->idOtro)); + } + + if ($this->pais !== null) { + $root->appendChild($this->createElement($doc, 'Pais', $this->pais)); + } + + return $doc->saveXML($root); + } + + /** + * Create a PersonaFisicaJuridica instance from XML string or DOMElement + */ + public static function fromXml($xml): BaseXmlModel + { + if (is_string($xml)) { + $doc = new \DOMDocument(); + $doc->loadXML($xml); + $element = $doc->documentElement; + } else { + $element = $xml; + } + + return self::fromDOMElement($element); + } + + public static function fromDOMElement(\DOMElement $element): self + { + $persona = new self(); + + $nifElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NIF')->item(0); + if ($nifElement) { + $persona->setNif($nifElement->nodeValue); + } + + $nombreRazonElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreRazon')->item(0); + if ($nombreRazonElement) { + $persona->setNombreRazon($nombreRazonElement->nodeValue); + } + + $apellidosElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Apellidos')->item(0); + if ($apellidosElement) { + $persona->setApellidos($apellidosElement->nodeValue); + } + + $nombreElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Nombre')->item(0); + if ($nombreElement) { + $persona->setNombre($nombreElement->nodeValue); + } + + $razonSocialElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RazonSocial')->item(0); + if ($razonSocialElement) { + $persona->setRazonSocial($razonSocialElement->nodeValue); + } + + $tipoIdentificacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoIdentificacion')->item(0); + if ($tipoIdentificacionElement) { + $persona->setTipoIdentificacion($tipoIdentificacionElement->nodeValue); + } + + $idOtroElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDOtro')->item(0); + if ($idOtroElement) { + $persona->setIdOtro($idOtroElement->nodeValue); + } + + $paisElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Pais')->item(0); + if ($paisElement) { + $persona->setPais($paisElement->nodeValue); + } + + return $persona; + } +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/SistemaInformatico.php b/app/Services/EDocument/Standards/Verifactu/Models/SistemaInformatico.php new file mode 100644 index 0000000000..00010a7dea --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/SistemaInformatico.php @@ -0,0 +1,233 @@ +formatOutput = true; + + $root = $this->createElement($doc, 'SistemaInformatico'); + $doc->appendChild($root); + + // Add nombreRazon + $root->appendChild($this->createElement($doc, 'NombreRazon', $this->nombreRazon)); + + // Add either NIF or IDOtro + if ($this->nif !== null) { + $root->appendChild($this->createElement($doc, 'NIF', $this->nif)); + } elseif ($this->idOtro !== null) { + $root->appendChild($this->createElement($doc, 'IDOtro', $this->idOtro)); + } + + // Add remaining elements + $root->appendChild($this->createElement($doc, 'NombreSistemaInformatico', $this->nombreSistemaInformatico)); + $root->appendChild($this->createElement($doc, 'IdSistemaInformatico', $this->idSistemaInformatico)); + $root->appendChild($this->createElement($doc, 'Version', $this->version)); + $root->appendChild($this->createElement($doc, 'NumeroInstalacion', $this->numeroInstalacion)); + $root->appendChild($this->createElement($doc, 'TipoUsoPosibleSoloVerifactu', $this->tipoUsoPosibleSoloVerifactu)); + $root->appendChild($this->createElement($doc, 'TipoUsoPosibleMultiOT', $this->tipoUsoPosibleMultiOT)); + $root->appendChild($this->createElement($doc, 'IndicadorMultiplesOT', $this->indicadorMultiplesOT)); + + return $doc->saveXML(); + } + + /** + * Create a SistemaInformatico instance from XML string + */ + public static function fromXml($xml): BaseXmlModel + { + if (is_string($xml)) { + $doc = new \DOMDocument(); + $doc->loadXML($xml); + $element = $doc->documentElement; + } else { + $element = $xml; + } + + return self::fromDOMElement($element); + } + + public static function fromDOMElement(\DOMElement $element): self + { + $sistemaInformatico = new self(); + + // Parse NombreRazon + $nombreRazonElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreRazon')->item(0); + if ($nombreRazonElement) { + $sistemaInformatico->setNombreRazon($nombreRazonElement->nodeValue); + } + + // Parse NIF or IDOtro + $nifElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NIF')->item(0); + if ($nifElement) { + $sistemaInformatico->setNif($nifElement->nodeValue); + } else { + $idOtroElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDOtro')->item(0); + if ($idOtroElement) { + $sistemaInformatico->setIdOtro($idOtroElement->nodeValue); + } + } + + // Parse remaining elements + $nombreSistemaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreSistemaInformatico')->item(0); + if ($nombreSistemaElement) { + $sistemaInformatico->setNombreSistemaInformatico($nombreSistemaElement->nodeValue); + } + + $idSistemaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IdSistemaInformatico')->item(0); + if ($idSistemaElement) { + $sistemaInformatico->setIdSistemaInformatico($idSistemaElement->nodeValue); + } + + $versionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Version')->item(0); + if ($versionElement) { + $sistemaInformatico->setVersion($versionElement->nodeValue); + } + + $numeroInstalacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumeroInstalacion')->item(0); + if ($numeroInstalacionElement) { + $sistemaInformatico->setNumeroInstalacion($numeroInstalacionElement->nodeValue); + } + + $tipoUsoPosibleSoloVerifactuElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoUsoPosibleSoloVerifactu')->item(0); + if ($tipoUsoPosibleSoloVerifactuElement) { + $sistemaInformatico->setTipoUsoPosibleSoloVerifactu($tipoUsoPosibleSoloVerifactuElement->nodeValue); + } + + $tipoUsoPosibleMultiOTElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoUsoPosibleMultiOT')->item(0); + if ($tipoUsoPosibleMultiOTElement) { + $sistemaInformatico->setTipoUsoPosibleMultiOT($tipoUsoPosibleMultiOTElement->nodeValue); + } + + $indicadorMultiplesOTElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IndicadorMultiplesOT')->item(0); + if ($indicadorMultiplesOTElement) { + $sistemaInformatico->setIndicadorMultiplesOT($indicadorMultiplesOTElement->nodeValue); + } + + return $sistemaInformatico; + } + + public function getNombreRazon(): string + { + return $this->nombreRazon; + } + + public function setNombreRazon(string $nombreRazon): self + { + $this->nombreRazon = $nombreRazon; + return $this; + } + + public function getNif(): ?string + { + return $this->nif; + } + + public function setNif(?string $nif): self + { + $this->nif = $nif; + return $this; + } + + public function getIdOtro(): ?string + { + return $this->idOtro; + } + + public function setIdOtro(?string $idOtro): self + { + $this->idOtro = $idOtro; + return $this; + } + + public function getNombreSistemaInformatico(): string + { + return $this->nombreSistemaInformatico; + } + + public function setNombreSistemaInformatico(string $nombreSistemaInformatico): self + { + $this->nombreSistemaInformatico = $nombreSistemaInformatico; + return $this; + } + + public function getIdSistemaInformatico(): string + { + return $this->idSistemaInformatico; + } + + public function setIdSistemaInformatico(string $idSistemaInformatico): self + { + $this->idSistemaInformatico = $idSistemaInformatico; + return $this; + } + + public function getVersion(): string + { + return $this->version; + } + + public function setVersion(string $version): self + { + $this->version = $version; + return $this; + } + + public function getNumeroInstalacion(): string + { + return $this->numeroInstalacion; + } + + public function setNumeroInstalacion(string $numeroInstalacion): self + { + $this->numeroInstalacion = $numeroInstalacion; + return $this; + } + + public function getTipoUsoPosibleSoloVerifactu(): string + { + return $this->tipoUsoPosibleSoloVerifactu; + } + + public function setTipoUsoPosibleSoloVerifactu(string $tipoUsoPosibleSoloVerifactu): self + { + $this->tipoUsoPosibleSoloVerifactu = $tipoUsoPosibleSoloVerifactu; + return $this; + } + + public function getTipoUsoPosibleMultiOT(): string + { + return $this->tipoUsoPosibleMultiOT; + } + + public function setTipoUsoPosibleMultiOT(string $tipoUsoPosibleMultiOT): self + { + $this->tipoUsoPosibleMultiOT = $tipoUsoPosibleMultiOT; + return $this; + } + + public function getIndicadorMultiplesOT(): string + { + return $this->indicadorMultiplesOT; + } + + public function setIndicadorMultiplesOT(string $indicadorMultiplesOT): self + { + $this->indicadorMultiplesOT = $indicadorMultiplesOT; + return $this; + } +} \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/Models/BaseModelTest.php b/tests/Feature/EInvoice/Verifactu/Models/BaseModelTest.php new file mode 100644 index 0000000000..5f051d1443 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/Models/BaseModelTest.php @@ -0,0 +1,60 @@ +assertEquals( + $this->normalizeXml($expectedXml), + $this->normalizeXml($actualXml) + ); + } + + protected function normalizeXml(string $xml): string + { + $doc = new \DOMDocument('1.0'); + $doc->preserveWhiteSpace = false; + $doc->formatOutput = true; + if (!$doc->loadXML($xml)) { + throw new \DOMException('Failed to load XML in normalizeXml'); + } + return $doc->saveXML(); + } + + protected function assertValidatesAgainstXsd(string $xml, string $xsdPath): void + { + try { + $doc = new \DOMDocument(); + $doc->preserveWhiteSpace = false; + $doc->formatOutput = true; + if (!$doc->loadXML($xml, LIBXML_NOBLANKS)) { + throw new \DOMException('Failed to load XML in assertValidatesAgainstXsd'); + } + + libxml_use_internal_errors(true); + $result = $doc->schemaValidate($xsdPath); + if (!$result) { + foreach (libxml_get_errors() as $error) { + } + libxml_clear_errors(); + } + + $this->assertTrue( + $result, + 'XML does not validate against XSD schema' + ); + } catch (\Exception $e) { + throw $e; + } + } + + protected function getTestXsdPath(): string + { + return __DIR__ . '/../schema/SuministroInformacion.xsd'; + } +} \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/Models/InvoiceTest.php b/tests/Feature/EInvoice/Verifactu/Models/InvoiceTest.php new file mode 100644 index 0000000000..13b235f275 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/Models/InvoiceTest.php @@ -0,0 +1,1143 @@ +setIdVersion('1.0') + ->setIdFactura('FAC-2023-001') + ->setRefExterna('REF-123') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Venta de productos varios') + ->setCuotaTotal(210.00) + ->setImporteTotal(1000.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add emitter + $emisor = new PersonaFisicaJuridica(); + $emisor + ->setNif('B12345678') + ->setRazonSocial('Empresa Ejemplo SL'); + $invoice->setTercero($emisor); + + // Add breakdown + $desglose = new Desglose(); + $desglose->setDesgloseFactura([ + 'Impuesto' => '01', + 'ClaveRegimen' => '01', + 'CalificacionOperacion' => 'S1', + 'BaseImponible' => 1000.00, + 'TipoImpositivo' => 21, + 'Cuota' => 210.00 + ]); + $invoice->setDesglose($desglose); + + // Add information system + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001'); + $invoice->setSistemaInformatico($sistema); + + // Add chain + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + // Add coupon + $cupon = new Cupon(); + $cupon + ->setIdCupon('CUP-001') + ->setFechaExpedicionCupon('2023-01-01') + ->setImporteCupon(50.00) + ->setDescripcionCupon('Descuento promocional'); + // $invoice->setCupon($cupon); + + $xml = $invoice->toXml(); + + // Debug output + echo "\nGenerated XML:\n"; + echo $xml; + echo "\n\n"; + + // Validate against XSD + $doc = new \DOMDocument(); + $doc->loadXML($xml); + + if (!$doc->schemaValidate($this->getTestXsdPath())) { + echo "\nValidation Errors:\n"; + libxml_use_internal_errors(true); + $doc->schemaValidate($this->getTestXsdPath()); + foreach (libxml_get_errors() as $error) { + echo $error->message . "\n"; + } + libxml_clear_errors(); + } + + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals($invoice->getIdVersion(), $deserialized->getIdVersion()); + $this->assertEquals($invoice->getIdFactura(), $deserialized->getIdFactura()); + $this->assertEquals($invoice->getRefExterna(), $deserialized->getRefExterna()); + $this->assertEquals($invoice->getNombreRazonEmisor(), $deserialized->getNombreRazonEmisor()); + $this->assertEquals($invoice->getTipoFactura(), $deserialized->getTipoFactura()); + $this->assertEquals($invoice->getDescripcionOperacion(), $deserialized->getDescripcionOperacion()); + $this->assertEquals($invoice->getCuotaTotal(), $deserialized->getCuotaTotal()); + $this->assertEquals($invoice->getImporteTotal(), $deserialized->getImporteTotal()); + } + + public function testCreateAndSerializeSimplifiedInvoice(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-002') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F2') + ->setFacturaSimplificadaArt7273('S') + ->setDescripcionOperacion('Venta de productos varios') + ->setCuotaTotal(21.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add breakdown + $desglose = new Desglose(); + $desglose->setDesgloseIVA([ + 'Impuesto' => '01', + 'ClaveRegimen' => '02', + 'CalificacionOperacion' => 'S2', + 'BaseImponible' => 100.00, + 'TipoImpositivo' => 21, + 'Cuota' => 21.00 + ]); + $invoice->setDesglose($desglose); + + // Add information system + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + $invoice->setSistemaInformatico($sistema); + + // Add encadenamiento + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + $xml = $invoice->toXml(); + + // Debug output + echo "\nGenerated XML:\n"; + echo $xml; + echo "\n\n"; + + // Validate against XSD + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals($invoice->getIdVersion(), $deserialized->getIdVersion()); + $this->assertEquals($invoice->getIdFactura(), $deserialized->getIdFactura()); + $this->assertEquals($invoice->getNombreRazonEmisor(), $deserialized->getNombreRazonEmisor()); + $this->assertEquals($invoice->getTipoFactura(), $deserialized->getTipoFactura()); + $this->assertEquals($invoice->getFacturaSimplificadaArt7273(), $deserialized->getFacturaSimplificadaArt7273()); + } + + public function testCreateAndSerializeRectificationInvoice(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-003') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('R1') + ->setTipoRectificativa('I') + ->setDescripcionOperacion('Rectificación de factura anterior') + ->setCuotaTotal(-21.00) + ->setImporteTotal(-100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add information system + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + $invoice->setSistemaInformatico($sistema); + + // Add desglose + $desglose = new Desglose(); + $desglose->setDesgloseIVA([ + 'Impuesto' => '01', + 'ClaveRegimen' => '02', + 'CalificacionOperacion' => 'S2', + 'BaseImponible' => -100.00, + 'TipoImpositivo' => 21, + 'Cuota' => -21.00 + ]); + $invoice->setDesglose($desglose); + + // Add FacturaRectificativa + $facturaRectificativa = new FacturaRectificativa( + 'I', // TipoRectificativa + -100.00, // BaseRectificada + -21.00 // CuotaRectificada + ); + $facturaRectificativa->addFacturaRectificada( + 'B12345678', // NIF + 'FAC-2023-001', // NumSerieFactura + '01-01-2023' // FechaExpedicionFactura + ); + $invoice->setFacturaRectificativa($facturaRectificativa); + + // Add encadenamiento with PrimerRegistro + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + $xml = $invoice->toXml(); + + // Debug output + echo "\nGenerated XML:\n"; + echo $xml; + echo "\n\n"; + + // Validate against XSD + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals($invoice->getIdVersion(), $deserialized->getIdVersion()); + $this->assertEquals($invoice->getIdFactura(), $deserialized->getIdFactura()); + $this->assertEquals($invoice->getNombreRazonEmisor(), $deserialized->getNombreRazonEmisor()); + $this->assertEquals($invoice->getTipoFactura(), $deserialized->getTipoFactura()); + $this->assertEquals($invoice->getTipoRectificativa(), $deserialized->getTipoRectificativa()); + } + + public function testCreateAndSerializeInvoiceWithoutRecipient(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-004') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setFacturaSinIdentifDestinatarioArt61d('S') + ->setDescripcionOperacion('Venta de productos varios') + ->setCuotaTotal(21.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add information system + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001'); + $invoice->setSistemaInformatico($sistema); + + // Add desglose + $desglose = new Desglose(); + $desglose->setDesgloseIVA([ + 'BaseImponible' => 100.00, + 'TipoImpositivo' => 21, + 'Cuota' => 21.00 + ]); + $invoice->setDesglose($desglose); + + // Add encadenamiento + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + $xml = $invoice->toXml(); + + // Validate against XSD + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals($invoice->getIdVersion(), $deserialized->getIdVersion()); + $this->assertEquals($invoice->getIdFactura(), $deserialized->getIdFactura()); + $this->assertEquals($invoice->getNombreRazonEmisor(), $deserialized->getNombreRazonEmisor()); + $this->assertEquals($invoice->getTipoFactura(), $deserialized->getTipoFactura()); + $this->assertEquals($invoice->getFacturaSinIdentifDestinatarioArt61d(), $deserialized->getFacturaSinIdentifDestinatarioArt61d()); + } + + public function testInvalidXmlThrowsException(): void + { + $this->expectException(\DOMException::class); + + $invalidXml = ''; + Invoice::fromXml($invalidXml); + } + + public function testMissingRequiredFieldsThrowsException(): void + { + $invoice = new Invoice(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Missing required field: IDVersion'); + + $invoice->toXml(); + } + + public function test_create_and_serialize_rectification_invoice() + { + $invoice = new Invoice(); + $invoice->setIdVersion('1.0') + ->setIdFactura('FAC-2023-001') + ->setRefExterna('REF-123') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('R1') + ->setTipoRectificativa('S') + ->setDescripcionOperacion('Rectificación de factura') + ->setTercero((new PersonaFisicaJuridica()) + ->setNif('B12345678') + ->setRazonSocial('Empresa Ejemplo SL')) + ->setDesglose((new Desglose()) + ->setDesgloseIVA([ + 'BaseImponible' => 1000.00, + 'TipoImpositivo' => 21, + 'Cuota' => 210.00 + ])) + ->setCuotaTotal(210) + ->setImporteTotal(1000) + ->setSistemaInformatico((new SistemaInformatico()) + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001')) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Create Encadenamiento with PrimerRegistroCadena + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + $facturaRectificativa = new FacturaRectificativa( + 'S', // TipoRectificativa (S for substitutive) + 1000.00, // BaseRectificada + 210.00, // CuotaRectificada + null // CuotaRecargoRectificado (optional) + ); + + // Add a rectified invoice + $facturaRectificativa->addFacturaRectificada( + 'B12345678', // NIF + 'FAC-2023-001', // NumSerieFactura + '24-04-2025' // FechaExpedicionFactura + ); + + $invoice->setFacturaRectificativa($facturaRectificativa); + + $xml = $invoice->toXml(); + + // Debug output + echo "\nGenerated XML:\n"; + echo $xml; + echo "\n\n"; + + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + } + + public function testCreateAndSerializeInvoiceWithMultipleRecipients(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-005') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Venta a múltiples destinatarios') + ->setCuotaTotal(42.00) + ->setImporteTotal(200.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add multiple recipients + $destinatarios = []; + $destinatario1 = new PersonaFisicaJuridica(); + $destinatario1 + ->setNif('B87654321') + ->setNombreRazon('Cliente 1 SL'); // Changed from setRazonSocial to setNombreRazon + $destinatarios[] = $destinatario1; + + $destinatario2 = new PersonaFisicaJuridica(); + $destinatario2 + ->setPais('FR') // French company + ->setTipoIdentificacion('02') // NIF-IVA (VAT number) + ->setIdOtro('FR12345678901') // French VAT number + ->setNombreRazon('Client 2 SARL'); // French company name + $destinatarios[] = $destinatario2; + + $invoice->setDestinatarios($destinatarios); + + // Add desglose with proper structure + $desglose = new Desglose(); + $desglose->setDesgloseIVA([ + 'Impuesto' => '01', + 'ClaveRegimen' => '01', + 'CalificacionOperacion' => 'S1', + 'BaseImponible' => 200.00, + 'TipoImpositivo' => 21.00, + 'Cuota' => 42.00 + ]); + $invoice->setDesglose($desglose); + + // Add encadenamiento (required) + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + // Add sistema informatico + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + $invoice->setSistemaInformatico($sistema); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals(2, count($deserialized->getDestinatarios())); + + // Verify first recipient (with NIF) + $this->assertEquals('Cliente 1 SL', $deserialized->getDestinatarios()[0]->getNombreRazon()); + $this->assertEquals('B87654321', $deserialized->getDestinatarios()[0]->getNif()); + + // Verify second recipient (with IDOtro) + $this->assertEquals('Client 2 SARL', $deserialized->getDestinatarios()[1]->getNombreRazon()); + $this->assertEquals('FR', $deserialized->getDestinatarios()[1]->getPais()); + $this->assertEquals('02', $deserialized->getDestinatarios()[1]->getTipoIdentificacion()); + $this->assertEquals('FR12345678901', $deserialized->getDestinatarios()[1]->getIdOtro()); + $this->assertNull($deserialized->getDestinatarios()[1]->getNif()); + } + + public function testCreateAndSerializeInvoiceWithExemptOperation(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-006') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Operación exenta de IVA') + ->setCuotaTotal(0.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add desglose with exempt operation + $desglose = new Desglose(); + $desglose->setDesgloseIVA([ + 'Impuesto' => '01', + 'ClaveRegimen' => '01', + 'OperacionExenta' => 'E1', + 'BaseImponible' => 100.00, + 'TipoImpositivo' => 0, + 'Cuota' => 0.00 + ]); + $invoice->setDesglose($desglose); + + // Add encadenamiento (required) + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + // Add sistema informatico + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + $invoice->setSistemaInformatico($sistema); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals(0.00, $deserialized->getCuotaTotal()); + $this->assertEquals(100.00, $deserialized->getImporteTotal()); + } + + public function testCreateAndSerializeInvoiceWithDifferentTaxRates(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-007') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Venta con diferentes tipos impositivos') + ->setCuotaTotal(31.50) + ->setImporteTotal(250.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // 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 + ]); + $invoice->setDesglose($desglose); + + // Add encadenamiento (required) + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + // Add sistema informatico + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + $invoice->setSistemaInformatico($sistema); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals(31.50, $deserialized->getCuotaTotal()); + $this->assertEquals(250.00, $deserialized->getImporteTotal()); + } + + public function testCreateAndSerializeInvoiceWithSubsequentChain(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-008') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Factura con encadenamiento posterior') + ->setCuotaTotal(21.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add desglose with proper structure + $desglose = new Desglose(); + $desglose->setDesgloseIVA([ + 'Impuesto' => '01', + 'ClaveRegimen' => '01', + 'CalificacionOperacion' => 'S1', + 'BaseImponible' => 100.00, + 'TipoImpositivo' => 21, + 'Cuota' => 21.00 + ]); + $invoice->setDesglose($desglose); + + // Add encadenamiento with subsequent chain + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + // Add sistema informatico with all required fields + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + $invoice->setSistemaInformatico($sistema); + + $xml = $invoice->toXml(); + + // Debug output + echo "\nGenerated XML:\n"; + echo $xml; + echo "\n\n"; + + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals('S', $deserialized->getEncadenamiento()->getPrimerRegistro()); + } + + public function testCreateAndSerializeInvoiceWithThirdPartyIssuer(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-009') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setEmitidaPorTerceroODestinatario('T') + ->setDescripcionOperacion('Factura emitida por tercero') + ->setCuotaTotal(21.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add desglose with proper structure + $desglose = new Desglose(); + $desglose->setDesgloseIVA([ + 'Impuesto' => '01', + 'ClaveRegimen' => '02', + 'CalificacionOperacion' => 'S2', + 'BaseImponible' => 100.00, + 'TipoImpositivo' => 21, + 'Cuota' => 21.00 + ]); + $invoice->setDesglose($desglose); + + // Add third party with proper structure + $tercero = new PersonaFisicaJuridica(); + $tercero + ->setRazonSocial('Tercero Emisor SL') + ->setNif('B98765432'); + $invoice->setTercero($tercero); + + // Add encadenamiento (required) + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + // Add sistema informatico with all required fields + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + $invoice->setSistemaInformatico($sistema); + + $xml = $invoice->toXml(); + + // Debug output + echo "\nGenerated XML:\n"; + echo $xml; + echo "\n\n"; + + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals('T', $deserialized->getEmitidaPorTerceroODestinatario()); + $this->assertEquals('B98765432', $deserialized->getTercero()->getNif()); + $this->assertEquals('Tercero Emisor SL', $deserialized->getTercero()->getRazonSocial()); + } + + public function testCreateAndSerializeInvoiceWithMacroData(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-010') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setMacrodato('S') + ->setDescripcionOperacion('Factura con macrodato') + ->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') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + $invoice->setSistemaInformatico($sistema); + + // Add Desglose + $desglose = new Desglose(); + $desglose->setDesgloseIVA([ + 'Impuesto' => '01', + 'ClaveRegimen' => '01', + 'CalificacionOperacion' => 'S1', + 'BaseImponible' => 100.00, + 'TipoImpositivo' => 21.00, + 'Cuota' => 21.00 + ]); + $invoice->setDesglose($desglose); + + // Add Encadenamiento + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals('S', $deserialized->getMacrodato()); + } + + public function testCreateAndSerializeInvoiceWithDigitalSignature(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-011') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Factura con firma digital') + ->setCuotaTotal(21.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...') + ->setSignature('MOCK_SIGNATURE_VALUE'); + + // 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); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals('MOCK_SIGNATURE_VALUE', $deserialized->getSignature()); + } + + public function testCreateAndSerializeInvoiceWithAgreementData(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-012') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Factura con datos de acuerdo') + ->setCuotaTotal(21.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...') + ->setNumRegistroAcuerdoFacturacion('REG-001') + ->setIdAcuerdoSistemaInformatico('AGR-001'); + + // 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); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals('REG-001', $deserialized->getNumRegistroAcuerdoFacturacion()); + $this->assertEquals('AGR-001', $deserialized->getIdAcuerdoSistemaInformatico()); + } + + public function testCreateAndSerializeInvoiceWithRejectionAndCorrection(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-013') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setRechazoPrevio('S') + ->setSubsanacion('S') + ->setDescripcionOperacion('Factura con rechazo y subsanación') + ->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); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals('S', $deserialized->getRechazoPrevio()); + $this->assertEquals('S', $deserialized->getSubsanacion()); + } + + public function testCreateAndSerializeInvoiceWithOperationDate(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-014') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Factura con fecha de operación') + ->setFechaOperacion('2023-01-01') + ->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); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertEquals('2023-01-01', $deserialized->getFechaOperacion()); + } + + public function testCreateAndSerializeInvoiceWithCoupon(): void + { + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-015') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Factura con cupón') + ->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 + ->setNombreRazon('Sistema de Facturación') + ->setNif('B12345678') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001'); + $invoice->setSistemaInformatico($sistema); + + $xml = $invoice->toXml(); + $this->assertValidatesAgainstXsd($xml, $this->getTestXsdPath()); + + // Test deserialization + $deserialized = Invoice::fromXml($xml); + $this->assertNotNull($deserialized->getCupon()); + $this->assertEquals('CUP-001', $deserialized->getCupon()->getIdCupon()); + } + + public function testInvalidTipoFacturaThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + $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->toXml(); + } + + public function testInvalidTipoRectificativaThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-017') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('R1') + ->setTipoRectificativa('INVALID') // Invalid type + ->setDescripcionOperacion('Rectificación 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->toXml(); + } + + public function testInvalidDateFormatThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-018') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Factura con fecha inválida') + ->setCuotaTotal(21.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01') // Invalid format + ->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->toXml(); + } + + public function testInvalidNIFFormatThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-019') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Factura con NIF inválido') + ->setCuotaTotal(21.00) + ->setImporteTotal(100.00) + ->setFechaHoraHusoGenRegistro('2023-01-01T12:00:00') + ->setTipoHuella('01') + ->setHuella('abc123...'); + + // Add emitter with invalid NIF + $emisor = new PersonaFisicaJuridica(); + $emisor + ->setNif('INVALID_NIF') + ->setRazonSocial('Empresa Ejemplo SL'); + $invoice->setTercero($emisor); + + // 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->toXml(); + } + + public function testInvalidAmountFormatThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC-2023-020') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Factura con importe inválido') + ->setCuotaTotal(21.00) + ->setImporteTotal('INVALID') // Invalid format + ->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->toXml(); + } + + public function testInvalidSchemaThrowsException(): void + { + $this->expectException(\DOMException::class); + + $invalidXml = 'test'; + + $doc = new \DOMDocument(); + $doc->loadXML($invalidXml); + + if (!$doc->schemaValidate($this->getTestXsdPath())) { + throw new \DOMException('XML does not validate against schema'); + } + } +} \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/schema/ConsultaLR.xsd b/tests/Feature/EInvoice/Verifactu/schema/ConsultaLR.xsd new file mode 100644 index 0000000000..0e6327fb98 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/ConsultaLR.xsd @@ -0,0 +1,38 @@ + + + + + + + + + Servicio de consulta Registros Facturacion + + + + + + + + + + + + + + Nº Serie+Nº Factura de la Factura del Emisor. + + + + + Contraparte del NIF de la cabecera que realiza la consulta. + Obligado si la cosulta la realiza el Destinatario de los registros de facturacion. + Destinatario si la cosulta la realiza el Obligado dde los registros de facturacion. + + + + + + + + diff --git a/tests/Feature/EInvoice/Verifactu/schema/EventosSIF.xsd b/tests/Feature/EInvoice/Verifactu/schema/EventosSIF.xsd new file mode 100644 index 0000000000..21cb370535 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/EventosSIF.xsd @@ -0,0 +1,823 @@ + + + + + + + + + + + + + + + + + + + Obligado a expedir la factura. + + + + + + + Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601) + + + + + Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601) + + + + + + + + + + + + + + + + Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601) + + + + + Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601) + + + + + + + + + + + + + + + + + + + + + + + + + Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601) + + + + + + + + + + + Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601) + + + + + + + + + + + + + + + Datos de una persona física o jurídica Española con un NIF asociado + + + + + + + + + NIF + + + + + + + + + Datos de una persona física o jurídica Española o Extranjera + + + + + + + + + + + + + Identificador de persona Física o jurídica distinto del NIF + (Código pais, Tipo de Identificador, y hasta 15 caractéres) + No se permite CodigoPais=ES e IDType=01-NIFContraparte + para ese caso, debe utilizarse NIF en lugar de IDOtro. + + + + + + + + + + + + + + Destinatario + + + + + Tercero + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NIF-IVA + + + + + Pasaporte + + + + + IDEnPaisResidencia + + + + + Certificado Residencia + + + + + Otro documento Probatorio + + + + + No Censado + + + + + + + + + + SHA-256 + + + + + + + + + Inicio del funcionamiento del sistema informático como «NO VERI*FACTU». + + + + + Fin del funcionamiento del sistema informático como «NO VERI*FACTU». + + + + + Lanzamiento del proceso de detección de anomalías en los registros de facturación. + + + + + Detección de anomalías en la integridad, inalterabilidad y trazabilidad de registros de facturación. + + + + + Lanzamiento del proceso de detección de anomalías en los registros de evento. + + + + + Detección de anomalías en la integridad, inalterabilidad y trazabilidad de registros de evento. + + + + + Restauración de copia de seguridad, cuando ésta se gestione desde el propio sistema informático de facturación. + + + + + Exportación de registros de facturación generados en un periodo. + + + + + Exportación de registros de evento generados en un periodo. + + + + + Registro resumen de eventos + + + + + Otros tipos de eventos a registrar voluntariamente por la persona o entidad productora del sistema informático. + + + + + + + + + + Integridad-huella + + + + + Integridad-firma + + + + + Integridad - Otros + + + + + Trazabilidad-cadena-registro - Reg. no primero pero con reg. anterior no anotado o inexistente + + + + + Trazabilidad-cadena-registro - Reg. no último pero con reg. posterior no anotado o inexistente + + + + + Trazabilidad-cadena-registro - Otros + + + + + Trazabilidad-cadena-huella - Huella del reg. no se corresponde con la 'huella del reg. anterior' almacenada en el registro posterior + + + + + Trazabilidad-cadena-huella - Campo 'huella del reg. anterior' no se corresponde con la huella del reg. anterior + + + + + Trazabilidad-cadena-huella - Otros + + + + + Trazabilidad-cadena - Otros + + + + + Trazabilidad-fechas - Fecha-hora anterior a la fecha del reg. anterior + + + + + Trazabilidad-fechas - Fecha-hora posterior a la fecha del reg. posterior + + + + + Trazabilidad-fechas - Reg. con fecha-hora de generación posterior a la fecha-hora actual del sistema + + + + + Trazabilidad-fechas - Otros + + + + + Trazabilidad - Otros + + + + + Otros + + + + + + + Datos de identificación de factura expedida para operaciones de consulta + + + + + + + + + + Datos de encadenamiento + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Feature/EInvoice/Verifactu/schema/RespuestaConsultaLR.xsd b/tests/Feature/EInvoice/Verifactu/schema/RespuestaConsultaLR.xsd new file mode 100644 index 0000000000..fd3ab14fd2 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/RespuestaConsultaLR.xsd @@ -0,0 +1,195 @@ + + + + + + + + + Servicio de consulta de regIstros de facturacion + + + + + + + + + + + + + + + + + + + Estado del registro almacenado en el sistema. Los estados posibles son: Correcta, AceptadaConErrores y Anulada + + + + + + + Código del error de registro, en su caso. + + + + + + + Descripción detallada del error de registro, en su caso. + + + + + + + + + + + + + + + + + + + + Período al que corresponden los apuntes. todos los apuntes deben corresponder al mismo período impositivo + + + + + + + + + + + + + + + Apunte correspondiente al libro de facturas expedidas. + + + + + + + + + + + Clave del tipo de factura + + + + + Identifica si el tipo de factura rectificativa es por sustitución o por diferencia + + + + + + El ID de las facturas rectificadas, únicamente se rellena en el caso de rectificación de facturas + + + + + + + + + + El ID de las facturas sustituidas, únicamente se rellena en el caso de facturas sustituidas + + + + + + + + + + + + + + + + Tercero que expida la factura y/o genera el registro de alta. + + + + + + Contraparte de la operación. Cliente + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + El registro se almacenado sin errores + + + + + El registro se almacenado tiene algunos errores. Ver detalle del error + + + + + El registro almacenado ha sido anulado + + + + + diff --git a/tests/Feature/EInvoice/Verifactu/schema/RespuestaSuministro.xsd b/tests/Feature/EInvoice/Verifactu/schema/RespuestaSuministro.xsd new file mode 100644 index 0000000000..d4902b9334 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/RespuestaSuministro.xsd @@ -0,0 +1,139 @@ + + + + + + + + + + + + CSV asociado al envío generado por AEAT. Solo se genera si no hay rechazo del envio + + + + + Se devuelven datos de la presentacion realizada. Solo se genera si no hay rechazo del envio + + + + + Se devuelve la cabecera que se incluyó en el envío. + + + + + + + Estado del envío en conjunto. + Si los datos de cabecera y todos los registros son correctos,el estado es correcto. + En caso de estructura y cabecera correctos donde todos los registros son incorrectos, el estado es incorrecto + En caso de estructura y cabecera correctos con al menos un registro incorrecto, el estado global es parcialmente correcto. + + + + + + + + Respuesta a un envío de registro de facturacion + + + + + + + + Estado detallado de cada línea del suministro. + + + + + + + + + + Respuesta a un envío + + + + + ID Factura Expedida + + + + + + + + Estado del registro. Correcto o Incorrecto + + + + + + + Código del error de registro, en su caso. + + + + + + + Descripción detallada del error de registro, en su caso. + + + + + + + Solo en el caso de que se rechace el registro por duplicado se devuelve este nodo con la informacion registrada en el sistema para este registro + + + + + + + + + + Correcto + + + + + Parcialmente correcto. Ver detalle de errores + + + + + Incorrecto + + + + + + + + + Correcto + + + + + Aceptado con Errores. Ver detalle del error + + + + + Incorrecto + + + + + + + + diff --git a/tests/Feature/EInvoice/Verifactu/schema/SistemaFacturacion.wsdl b/tests/Feature/EInvoice/Verifactu/schema/SistemaFacturacion.wsdl new file mode 100644 index 0000000000..3d40d898b9 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/SistemaFacturacion.wsdl @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Feature/EInvoice/Verifactu/schema/SuministroInformacion.xsd b/tests/Feature/EInvoice/Verifactu/schema/SuministroInformacion.xsd new file mode 100644 index 0000000000..b6d14b5820 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/SuministroInformacion.xsd @@ -0,0 +1,1399 @@ + + + + + + Datos de cabecera + + + + + Obligado a expedir la factura. + + + + + Representante del obligado tributario. A rellenar solo en caso de que los registros de facturación remitdos hayan sido generados por un representante/asesor del obligado tributario. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Información básica que contienen los registros del sistema de facturacion + + + + + + Período de la fecha de la operación + + + + + + + + + + + + Datos de identificación de factura expedida para operaciones de consulta + + + + + + Nº Serie+Nº Factura de la Factura del Emisor. + + + + + Fecha de emisión de la factura + + + + + + + Datos de identificación de factura que se anula para operaciones de baja + + + + + NIF del obligado + + + + + Nº Serie+Nº Factura de la Factura que se anula. + + + + + Fecha de emisión de la factura que se anula + + + + + + + Datos correspondientes al registro de facturacion de alta + + + + + + + + + + + Clave del tipo de factura + + + + + Identifica si el tipo de factura rectificativa es por sustitución o por diferencia + + + + + + El ID de las facturas rectificadas, únicamente se rellena en el caso de rectificación de facturas + + + + + + + + + + El ID de las facturas sustituidas, únicamente se rellena en el caso de facturas sustituidas + + + + + + + + + + + + + + + + Tercero que expida la factura y/o genera el registro de alta. + + + + + + Contraparte de la operación. Cliente + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Datos correspondientes al registro de facturacion de anulacion + + + + + + + + + + + + + + + + + + + + + + + + + + + Datos de encadenamiento + + + + + NIF del obligado a expedir la factura a que se refiere el registro de facturación anterior + + + + + + + + + + + + + + + + + + + + + + + + + + + + Datos de identificación de factura + + + + + NIF del obligado + + + + + Nº Serie+Nº Factura de la Factura del Emisor + + + + + Fecha de emisión de la factura + + + + + + + + Datos de identificación de factura sustituida o rectificada. El NIF se cogerá del NIF indicado en el bloque IDFactura + + + + + NIF del obligado + + + + + Nº Serie+Nº Factura de la factura + + + + + Fecha de emisión de la factura sustituida o rectificada + + + + + + + + + + + + + + + + + + + + + + + Desglose de Base y Cuota sustituida en las Facturas Rectificativas sustitutivas + + + + + + + + + + + Datos de una persona física o jurídica Española con un NIF asociado + + + + + + + + + + Datos de una persona física o jurídica Española o Extranjera + + + + + + + + + + + + + Identificador de persona Física o jurídica distinto del NIF + (Código pais, Tipo de Identificador, y hasta 15 caractéres) + No se permite CodigoPais=ES e IDType=01-NIFContraparte + para ese caso, debe utilizarse NIF en lugar de IDOtro. + + + + + + + + + + + Rango de fechas de expedicion + + + + + + + + + + + + + + + + + + IdPeticion asociado a la factura registrada previamente en el sistema. Solo se suministra si la factura enviada es rechazada por estar duplicada + + + + + + + Estado del registro duplicado almacenado en el sistema. Los estados posibles son: Correcta, AceptadaConErrores y Anulada. Solo se suministra si la factura enviada es rechazada por estar duplicada + + + + + + + Código del error de registro duplicado almacenado en el sistema, en su caso. + + + + + + + Descripción detallada del error de registro duplicado almacenado en el sistema, en su caso. + + + + + + + + + + + + + + + Año en formato YYYY + + + + + + + + + Período de la factura + + + + + Enero + + + + + Febrero + + + + + Marzo + + + + + Abril + + + + + Mayo + + + + + Junio + + + + + Julio + + + + + Agosto + + + + + Septiembre + + + + + Octubre + + + + + Noviembre + + + + + Diciembre + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NIF + + + + + + + + + + + + + + + + + + + + + + EXENTA por Art. 20 + + + + + EXENTA por Art. 21 + + + + + EXENTA por Art. 22 + + + + + EXENTA por Art. 24 + + + + + EXENTA por Art. 25 + + + + + EXENTA otros + + + + + + + + + + FACTURA (ART. 6, 7.2 Y 7.3 DEL RD 1619/2012) + + + + + FACTURA SIMPLIFICADA Y FACTURAS SIN IDENTIFICACIÓN DEL DESTINATARIO ART. 6.1.D) RD 1619/2012 + + + + + FACTURA RECTIFICATIVA (Art 80.1 y 80.2 y error fundado en derecho) + + + + + FACTURA RECTIFICATIVA (Art. 80.3) + + + + + FACTURA RECTIFICATIVA (Art. 80.4) + + + + + FACTURA RECTIFICATIVA (Resto) + + + + + FACTURA RECTIFICATIVA EN FACTURAS SIMPLIFICADAS + + + + + FACTURA EMITIDA EN SUSTITUCIÓN DE FACTURAS SIMPLIFICADAS FACTURADAS Y DECLARADAS + + + + + + + + + No ha habido rechazo previo por la AEAT. + + + + + Ha habido rechazo previo por la AEAT. + + + + + Independientemente de si ha habido o no algún rechazo previo por la AEAT, el registro de facturación no existe en la AEAT (registro existente en ese SIF o en algún SIF del obligado tributario y que no se remitió a la AEAT, por ejemplo, al acogerse a Veri*factu desde no Veri*factu). No deberían existir operaciones de alta (N,X), por lo que no se admiten. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SUSTITUTIVA + + + + + INCREMENTAL + + + + + + + + + + Destinatario + + + + + Tercero + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Expedidor (obligado a Expedir la factura anulada). + + + + + Destinatario + + + + + Tercero + + + + + + + + + + NIF-IVA + + + + + Pasaporte + + + + + IDEnPaisResidencia + + + + + Certificado Residencia + + + + + Otro documento Probatorio + + + + + No Censado + + + + + + + + + + SHA-256 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + El registro se ha almacenado sin errores + + + + + El registro que se ha almacenado tiene algunos errores. Ver detalle del error + + + + + El registro almacenado ha sido anulado + + + + + + + + + + + + OPERACIÓN SUJETA Y NO EXENTA - SIN INVERSIÓN DEL SUJETO PASIVO. + + + + + OPERACIÓN SUJETA Y NO EXENTA - CON INVERSIÓN DEL SUJETO PASIVO + + + + + OPERACIÓN NO SUJETA ARTÍCULO 7, 14, OTROS. + + + + + OPERACIÓN NO SUJETA POR REGLAS DE LOCALIZACIÓN + + + + + + + + + + + + + + + + + + + Datos de una persona física o jurídica Española o Extranjera + + + + + + + + + + + + Compuesto por datos + de contexto y una secuencia de 1 o más registros. + + + + + + + + Cabecera de la Cobnsulta + + + + + + + Obligado a la emision de los registros de facturacion + + + + + Destinatario (a veces también denominado contraparte, es decir, el cliente) de la operación + + + + + + Flag opcional que tendrá valor S si la consulta la está realizando el representante/asesor del obligado tributario. A rellenar solo en caso de que los registros de facturación remitidos hayan sido generados por un representante/asesor del obligado tributario. Este flag solo se puede cumplimentar cuando esté informado el obligado tributario en la consulta + + + + + + + + Datos de una persona física o jurídica Española con un NIF asociado + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Impuesto sobre el Valor Añadido (IVA) + + + + + Impuesto sobre la Producción, los Servicios y la Importación (IPSI) de Ceuta y Melilla + + + + + Impuesto General Indirecto Canario (IGIC) + + + + + Otros + + + + + + + + + + + + + + + + + + La operación realizada ha sido un alta + + + + + La operación realizada ha sido una anulación + + + + + diff --git a/tests/Feature/EInvoice/Verifactu/schema/SuministroLR.xsd b/tests/Feature/EInvoice/Verifactu/schema/SuministroLR.xsd new file mode 100644 index 0000000000..bde17b7728 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/SuministroLR.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + Datos correspondientes a los registros de facturacion + + + + + + + + + \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/schema/example-alta.xml b/tests/Feature/EInvoice/Verifactu/schema/example-alta.xml new file mode 100644 index 0000000000..3fefd0c039 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/example-alta.xml @@ -0,0 +1,77 @@ + + + + + + + XXXXX + AAAA + + + + + 1.0 + + AAAA + 12345 + 13-09-2024 + + XXXXX + F1 + Descripc + + + YYYY + BBBB + + + + + 01 + S1 + 4 + 10 + 0.4 + + + 01 + S1 + 21 + 100 + 21 + + + 21.4 + 131.4 + + + AAAA + 44 + 13-09-2024 + HuellaRegistroAnterior + + + + SSSS + NNNN + NombreSistemaInformatico + 77 + 1.0.03 + 383 + N + S + S + + 2024-09-13T19:20:30+01:00 + 01 + Huella + + + + + \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/schema/example-anulacion.xml b/tests/Feature/EInvoice/Verifactu/schema/example-anulacion.xml new file mode 100644 index 0000000000..d0509800e8 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/example-anulacion.xml @@ -0,0 +1,50 @@ + + + + + + + XXXXX + AAAA + + + + + 1.0 + + AAAA + 12345 + 13-09-2024 + + + + AAAA + 44 + 13-09-2024 + HuellaRegistroAnterior + + + + SSSS + NNNN + NombreSistemaInformatico + 77 + 1.0.03 + 383 + N + S + S + + 2024-09-13T19:20:30+01:00 + 01 + Huella + + + + + \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/schema/example-subsanacion.xml b/tests/Feature/EInvoice/Verifactu/schema/example-subsanacion.xml new file mode 100644 index 0000000000..2ef1de97c1 --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/schema/example-subsanacion.xml @@ -0,0 +1,78 @@ + + + + + + + XXXXX + AAAA + + + + + 1.0 + + AAAA + 12345 + 13-09-2024 + + XXXXX + S + F1 + Descripc + + + YYYY + BBBB + + + + + 01 + S1 + 4 + 10 + 0.4 + + + 01 + S1 + 21 + 100 + 21 + + + 21.4 + 131.4 + + + AAAA + 44 + 13-09-2024 + HuellaRegistroAnterior + + + + SSSS + NNNN + NombreSistemaInformatico + 77 + 1.0.03 + 383 + N + S + S + + 2024-09-13T19:20:30+01:00 + 01 + Huella + + + + + \ No newline at end of file