diff --git a/src/Models/Records/IssuedBy.php b/src/Models/Records/IssuedBy.php
new file mode 100644
index 0000000..ac69723
--- /dev/null
+++ b/src/Models/Records/IssuedBy.php
@@ -0,0 +1,15 @@
+issuedBy === IssuedBy::ThirdParty && $this->thirdParty === null) {
+ $context->buildViolation('An invoice issued by a third party requires the third party')
+ ->atPath('thirdParty')
+ ->addViolation();
+ }
+ if ($this->issuedBy !== IssuedBy::ThirdParty && $this->thirdParty !== null) {
+ $context->buildViolation('Only an invoice issued by a third party can carry the third party')
+ ->atPath('thirdParty')
+ ->addViolation();
+ }
+ if ($this->issuedBy === IssuedBy::Recipient && count($this->recipients) === 0) {
+ $context->buildViolation('A self-billed invoice requires its recipient, who issues it')
+ ->atPath('recipients')
+ ->addViolation();
+ }
+ }
+
#[Assert\Callback]
final public function validateRecipients(ExecutionContextInterface $context): void {
if (!isset($this->invoiceType)) {
@@ -418,6 +452,19 @@ protected function importCustomProperties(UXML $recordElement): void {
}
$this->description = $description;
+ // Issued by a third party or by the recipient
+ $rawIssuedBy = $recordElement->get('sum1:EmitidaPorTerceroODestinatario')?->asText();
+ if ($rawIssuedBy !== null) {
+ $this->issuedBy = IssuedBy::tryFrom($rawIssuedBy) ?? throw new ImportException('Invalid value');
+ }
+ $terceroElement = $recordElement->get('sum1:Tercero');
+ if ($terceroElement !== null) {
+ $this->thirdParty = new FiscalIdentifier(
+ $terceroElement->get('sum1:NombreRazon')?->asText() ?? throw new ImportException('Missing from element'),
+ $terceroElement->get('sum1:NIF')?->asText() ?? throw new ImportException('Missing from element'),
+ );
+ }
+
// Recipients
foreach ($recordElement->getAll('sum1:Destinatarios/sum1:IDDestinatario') as $destinatarioElement) {
$recipientName = $destinatarioElement->get('sum1:NombreRazon')?->asText();
@@ -526,6 +573,16 @@ protected function exportCustomProperties(UXML $recordElement): void {
// Description
$recordElement->add('sum1:DescripcionOperacion', $this->description);
+ // Issued by a third party or by the recipient
+ if ($this->issuedBy !== null) {
+ $recordElement->add('sum1:EmitidaPorTerceroODestinatario', $this->issuedBy->value);
+ }
+ if ($this->thirdParty !== null) {
+ $terceroElement = $recordElement->add('sum1:Tercero');
+ $terceroElement->add('sum1:NombreRazon', $this->thirdParty->name);
+ $terceroElement->add('sum1:NIF', $this->thirdParty->nif);
+ }
+
// Recipients
if (count($this->recipients) > 0) {
$destinatariosElement = $recordElement->add('sum1:Destinatarios');
diff --git a/tests/Models/Records/RegistrationRecordTest.php b/tests/Models/Records/RegistrationRecordTest.php
index 1e789e9..164e9d3 100644
--- a/tests/Models/Records/RegistrationRecordTest.php
+++ b/tests/Models/Records/RegistrationRecordTest.php
@@ -11,6 +11,7 @@
use Komma\Verifactu\Models\Records\ForeignIdType;
use Komma\Verifactu\Models\Records\InvoiceIdentifier;
use Komma\Verifactu\Models\Records\InvoiceType;
+use Komma\Verifactu\Models\Records\IssuedBy;
use Komma\Verifactu\Models\Records\OperationType;
use Komma\Verifactu\Models\Records\Record;
use Komma\Verifactu\Models\Records\RegimeType;
@@ -410,6 +411,54 @@ public function testValidatesReplacedInvoices(): void {
$record->validate();
}
+ public function testValidatesIssuedBy(): void {
+ $record = Record::fromXml(TestUtils::getXmlFile(__DIR__ . '/registration-record-f1.xml'));
+ $this->assertInstanceOf(RegistrationRecord::class, $record);
+
+ $record->issuedBy = IssuedBy::ThirdParty;
+ try {
+ $record->validate();
+ $this->fail('An invoice issued by a third party without the third party must not validate');
+ } catch (InvalidModelException $e) {
+ $this->assertStringContainsString('requires the third party', $e->getMessage());
+ }
+
+ $record->thirdParty = new FiscalIdentifier('Gestoría Rías SL', 'B70000021');
+ $record->validate();
+
+ $record->issuedBy = IssuedBy::Recipient;
+ try {
+ $record->validate();
+ $this->fail('A self-billed invoice must not carry a third party');
+ } catch (InvalidModelException $e) {
+ $this->assertStringContainsString('Only an invoice issued by a third party', $e->getMessage());
+ }
+
+ $record->thirdParty = null;
+ $record->validate();
+ $this->assertSame(IssuedBy::Recipient, $record->issuedBy);
+ }
+
+ public function testExportsAndImportsIssuedByRecipient(): void {
+ $modelXml = TestUtils::getXmlFile(__DIR__ . '/registration-record-f1.xml');
+ $record = Record::fromXml($modelXml);
+ $this->assertInstanceOf(RegistrationRecord::class, $record);
+ $record->issuedBy = IssuedBy::Recipient;
+
+ $computerSystemXml = $modelXml->get('sum1:SistemaInformatico');
+ $this->assertNotNull($computerSystemXml);
+ $exportedXml = UXML::newInstance('container', null, ['xmlns:sum1' => Record::NS]);
+ $record->export($exportedXml, ComputerSystem::fromXml($computerSystemXml));
+
+ $xml = $exportedXml->get('sum1:RegistroAlta')?->asXML() ?? '';
+ $this->assertStringContainsString('D', $xml);
+ $this->assertLessThan(strpos($xml, ''), strpos($xml, ''));
+
+ $reimported = Record::fromXml($exportedXml->get('sum1:RegistroAlta'));
+ $this->assertInstanceOf(RegistrationRecord::class, $reimported);
+ $this->assertSame(IssuedBy::Recipient, $reimported->issuedBy);
+ }
+
#[DataProvider('xmlPathsProvider')]
public function testImportsAndExportsXmlElement(string $xmlPath): void {
// Import model