invoiceninja/app/Services/EDocument/Standards/Verifactu.php

233 lines
8.4 KiB
PHP

<?php
namespace App\Services\EDocument\Standards;
use Carbon\Carbon;
use App\Models\Invoice;
use BaconQrCode\Writer;
use App\Services\AbstractService;
use BaconQrCode\Renderer\ImageRenderer;
use Symfony\Component\Serializer\Serializer;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use App\Services\EDocument\Standards\Verifactu\Types\Cabecera;
use App\Services\EDocument\Standards\Verifactu\Types\SoapBody;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use App\Services\EDocument\Standards\Verifactu\VerifactuClient;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use App\Services\EDocument\Standards\Verifactu\Types\RegistroAlta;
use App\Services\EDocument\Standards\Verifactu\Types\SoapEnvelope;
use App\Services\EDocument\Standards\Verifactu\Types\ObligadoEmision;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use App\Services\EDocument\Standards\Verifactu\InvoiceninjaToVerifactuMapper;
use App\Services\EDocument\Standards\Verifactu\Types\RegFactuSistemaFacturacion;
class Verifactu
{
public function __construct(private Invoice $invoice)
{
}
public function send(mixed $registro)
{
$client = new VerifactuClient(VerifactuClient::MODE_PROD);
$response = $client->sendRegistroAlta($this->getRegistroAlta());
var_dump($response);
}
public function getRegistroAlta(): string
{
$mapper = new InvoiceninjaToVerifactuMapper();
$regAlta = $mapper->mapRegistroAlta($this->invoice);
$soapEnvelope = new SoapEnvelope();
$soapBody = new SoapBody();
$RegFactuSistemaFacturacion = new RegFactuSistemaFacturacion();
//The User or Corp that generated AND sends the invoice (ie Invoice Ninja!)
$cabecera = new Cabecera();
$obligadoEmision = new ObligadoEmision();
$obligadoEmision->setNombreRazon($this->invoice->company->present()->name());
$obligadoEmision->setNIF($this->invoice->company->settings->vat_number);
$cabecera->setObligadoEmision($obligadoEmision);
$RegFactuSistemaFacturacion->setCabecera($cabecera);
//The invoice itself
$RegFactuSistemaFacturacion->setRegistroAlta($regAlta);
$soapBody->setRegFactuSistemaFacturacion($RegFactuSistemaFacturacion);
$soapEnvelope->setBody($soapBody);
return $this->serializeXml($soapEnvelope);
}
public function serializeXml(SoapEnvelope $registro): string
{
$serializer = $this->getSerializer();
$context = [
\Symfony\Component\Serializer\Normalizer\DateTimeNormalizer::FORMAT_KEY => 'd-m-Y',
\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true
];
$object = $serializer->normalize($registro, null, [\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
$object = $this->removeEmptyValues($object);
$data = $serializer->encode($object, 'xml', $context);
$data = str_replace(['<response>','</response>'], '', $data);
$data = preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $data);
return $data;
}
/**
* getQrCode
*
* @return string
*/
public function getQrCode(): string
{
//testmode
$base_url = 'https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR';
//production
// $base_url = https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR
// Format date to dd-mm-yyyy
$fecha = \Carbon\Carbon::parse($this->invoice->date)->format('d-m-Y');
// Format amount to 2 decimal places without thousands separator
$importe = number_format($this->invoice->amount, 2, '.', '');
// NIF is required for the QR code, if no NIF is present we use 00000000H
if(strlen($this->invoice->client->vat_number) > 2 && $this->invoice->client->country->iso_3166_2 === 'ES') {
$nif = $this->invoice->client->vat_number;
} else {
$nif = '00000000H'; //unknown / foreign client
}
$params = [
'nif' => $nif,
'numserie' => $this->invoice->number,
'fecha' => $fecha,
'importe' => $importe,
];
// Build URL with properly encoded parameters
$query = http_build_query($params);
try {
$renderer = new ImageRenderer(
new RendererStyle(200),
new SvgImageBackEnd()
);
$writer = new Writer($renderer);
$qr = $writer->writeString($base_url . '?' . $query, 'utf-8');
return htmlspecialchars("<svg viewBox='0 0 200 200' width='200' height='200' x='0' y='0' xmlns='http://www.w3.org/2000/svg'>
<rect x='0' y='0' width='100%'' height='100%' />{$qr}</svg>");
} catch (\Throwable $e) {
nlog(" QR failure => ".$e->getMessage());
return '';
}
}
/**
* getSerializer
*
* Returns the Symfony Serializer
*
* @return Serializer
*/
public function getSerializer(): Serializer
{
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
$typeExtractors = [$reflectionExtractor, $phpDocExtractor];
$descriptionExtractors = [$phpDocExtractor];
$propertyInitializableExtractors = [$reflectionExtractor];
$propertyInfo = new PropertyInfoExtractor(
$propertyInitializableExtractors,
$descriptionExtractors,
$typeExtractors,
);
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
$normalizer = new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter, null, $propertyInfo);
$normalizers = [new DateTimeNormalizer(), $normalizer, new ArrayDenormalizer()];
$namespaces = [
'soapenv' => 'http://schemas.xmlsoap.org/soap/envelope/',
'sum' => 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd',
'sum1' => 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd',
'xd' => 'http://www.w3.org/2000/09/xmldsig#'
];
$encoders = [
new XmlEncoder([
'xml_root_node_name' => 'soapenv:Envelope',
'xml_format_output' => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
'xml_encoding' => 'UTF-8',
'xml_version' => '1.0',
'namespace_prefix_map' => $namespaces,
'default_root_ns' => 'soapenv',
'xml_namespaces' => array_combine(
array_map(fn($prefix) => "xmlns:$prefix", array_keys($namespaces)),
array_values($namespaces)
)
]),
new JsonEncoder()
];
return new Serializer($normalizers, $encoders);
}
/**
* removeEmptyValues
*
* Removes empty values from the array
*
* @param array $array
* @return array
*/
private function removeEmptyValues(array $array): array
{
foreach ($array as $key => $value) {
if (is_array($value)) {
$array[$key] = $this->removeEmptyValues($value);
if (empty($array[$key])) {
unset($array[$key]);
}
} elseif ($value === null || $value === '') {
unset($array[$key]);
}
}
return $array;
}
}