Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/Models/Records/RegistrationRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
* @field RegistroAlta
*/
class RegistrationRecord extends Record {
private const REGIMES_WITHOUT_TOTAL_CHECK = [
RegimeType::C03,
RegimeType::C05,
RegimeType::C06,
RegimeType::C08,
RegimeType::C09,
];

/**
* Indicador de subsanación de un registro de facturación de alta previamente generado
*
Expand Down Expand Up @@ -208,6 +216,10 @@ final public function validateTotals(ExecutionContextInterface $context): void {
->addViolation();
}

if ($this->hasRegimeWithoutTotalCheck()) {
return;
}

$isValidTotalAmount = false;
$bestTotalAmount = number_format($expectedTotalBaseAmount + $expectedTotalTaxAmount, 2, '.', '');
foreach ([0, -0.01, 0.01, -0.02, 0.02] as $tolerance) {
Expand All @@ -224,6 +236,22 @@ final public function validateTotals(ExecutionContextInterface $context): void {
}
}

/**
* AEAT does not cross-check ImporteTotal against the breakdown when any
* line is under these keys: the total legitimately differs from base plus
* quota (the margin schemes tax only the margin, travel agencies and
* VAT groups declare amounts that are not the invoiced price).
* Source: AEAT "Validaciones y errores" for VERI*FACTU, ImporteTotal.
*/
private function hasRegimeWithoutTotalCheck(): bool {
foreach ($this->breakdown as $details) {
if (isset($details->regimeType) && in_array($details->regimeType, self::REGIMES_WITHOUT_TOTAL_CHECK, true)) {
return true;
}
}
return false;
}

#[Assert\Callback]
final public function validateRecipients(ExecutionContextInterface $context): void {
if (!isset($this->invoiceType)) {
Expand Down
48 changes: 48 additions & 0 deletions tests/Models/Records/RegistrationRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,54 @@ public function testValidatesTotalAmounts(): void {
}
}

public function testSkipsTotalAmountCheckForSpecialRegimes(): void {
$record = new RegistrationRecord();
$record->invoiceId = new InvoiceIdentifier();
$record->invoiceId->issuerId = 'A00000000';
$record->invoiceId->invoiceNumber = 'REBU-1';
$record->invoiceId->issueDate = new DateTimeImmutable('2025-06-01');
$record->issuerName = 'Perico de los Palotes, S.A.';
$record->invoiceType = InvoiceType::Simplificada;
$record->description = 'Venta de terminal usado en REBU';
$record->breakdown[0] = new BreakdownDetails();
$record->breakdown[0]->taxType = TaxType::IVA;
$record->breakdown[0]->regimeType = RegimeType::C03;
$record->breakdown[0]->operationType = OperationType::Subject;
$record->breakdown[0]->baseAmount = '82.64';
$record->breakdown[0]->taxRate = '21.00';
$record->breakdown[0]->taxAmount = '17.36';
$record->totalTaxAmount = '17.36';
$record->totalAmount = '300.00';
$record->previousInvoiceId = null;
$record->previousHash = null;
$record->hashedAt = new DateTimeImmutable('2025-06-01T20:30:40+02:00');
$record->hash = $record->calculateHash();

// The price is the full sale price, not base plus quota of the margin
$record->validate();

// The quota is still checked
$record->totalTaxAmount = '18.00';
$record->hash = $record->calculateHash();
try {
$record->validate();
$this->fail('Did not throw exception for total tax amount validation');
} catch (InvalidModelException $e) {
$this->assertStringContainsString('Expected total tax amount of 17.36, got 18.00', $e->getMessage());
}

// A general line keeps the check
$record->totalTaxAmount = '17.36';
$record->breakdown[0]->regimeType = RegimeType::C01;
$record->hash = $record->calculateHash();
try {
$record->validate();
$this->fail('Did not throw exception for total amount validation');
} catch (InvalidModelException $e) {
$this->assertStringContainsString('Expected total amount of 100.00, got 300.00', $e->getMessage());
}
}

public function testValidatesRecipients(): void {
$record = new RegistrationRecord();
$record->invoiceId = new InvoiceIdentifier();
Expand Down
Loading