Skip to main content
PHP FacturaE automatically validates invoices before generating XML to ensure they comply with the FacturaE standard and Spanish tax requirements. This prevents invalid invoices from being created and helps catch errors early.

Automatic Validation

Validation happens automatically when you call toXml() or export():
use PhpFacturae\Invoice;
use PhpFacturae\Exceptions\InvoiceValidationException;

try {
    $xml = Invoice::create('FAC-001')
        ->seller($seller)
        ->buyer($buyer)
        ->line('Service', price: 1000.00, vat: 21)
        ->toXml();
} catch (InvoiceValidationException $e) {
    // Validation failed
    echo "Validation errors:\n";
    foreach ($e->errors as $error) {
        echo "- $error\n";
    }
}
The invoice is validated automatically before XML generation. You don’t need to call validation manually.

InvoiceValidator Class

The InvoiceValidator class performs all validation checks. It’s called internally by Invoice::toXml() at Invoice.php:420:
private function validate(): void
{
    $errors = (new InvoiceValidator())->validate($this);

    if (!empty($errors)) {
        throw new InvoiceValidationException($errors);
    }
}

Validation Rules

The validator checks these requirements (from Validation/InvoiceValidator.php:12):
1
Seller is Required
2
Every invoice must have a seller party.
3
if ($invoice->getSeller() === null) {
    $errors[] = 'Seller is required.';
}
4
Seller Address is Required
5
The seller must have a complete address.
6
if ($invoice->getSeller()->getAddress() === null) {
    $errors[] = 'Seller address is required.';
}
7
Buyer is Required
8
Every invoice must have a buyer party.
9
if ($invoice->getBuyer() === null) {
    $errors[] = 'Buyer is required.';
}
10
Buyer Address is Required
11
The buyer must have a complete address.
12
if ($invoice->getBuyer()->getAddress() === null) {
    $errors[] = 'Buyer address is required.';
}
13
At Least One Line Required
14
Invoices must contain at least one invoice line.
15
if (empty($invoice->getLines())) {
    $errors[] = 'At least one invoice line is required.';
}
16
Invoice Number Required
17
The invoice must have a number.
18
if ($invoice->getNumber() === '') {
    $errors[] = 'Invoice number is required.';
}
19
Billing Period Validation
20
If a billing period is specified, both start and end dates must be provided.
21
if ($invoice->getBillingPeriodStart() !== null xor $invoice->getBillingPeriodEnd() !== null) {
    $errors[] = 'Billing period requires both start and end dates.';
}
22
Billing Period Date Order
23
The start date must be before the end date.
24
if ($invoice->getBillingPeriodStart() > $invoice->getBillingPeriodEnd()) {
    $errors[] = 'Billing period start date must be before end date.';
}

InvoiceValidationException

When validation fails, an InvoiceValidationException is thrown with all validation errors:
use PhpFacturae\Exceptions\InvoiceValidationException;

try {
    $invoice->toXml();
} catch (InvoiceValidationException $e) {
    // Access all errors
    $errors = $e->errors;
    
    // Get formatted message
    echo $e->getMessage();
    // Output: "Invoice validation failed: Seller is required, At least one invoice line is required"
}
The exception is defined at Exceptions/InvoiceValidationException.php:9:
final class InvoiceValidationException extends RuntimeException
{
    /** @param string[] $errors */
    public function __construct(
        public readonly array $errors,
    ) {
        parent::__construct(
            'Invoice validation failed: ' . implode(', ', $errors)
        );
    }
}
The errors property contains an array of all validation error messages, making it easy to display them to users or log them.

Common Validation Errors

Here are the most common validation errors and how to fix them:

Missing Seller

// ❌ Error: Seller is required
$invoice = Invoice::create('FAC-001')
    ->buyer($buyer)
    ->line('Service', price: 100, vat: 21);

// ✅ Fixed
$invoice = Invoice::create('FAC-001')
    ->seller($seller)  // Add seller
    ->buyer($buyer)
    ->line('Service', price: 100, vat: 21);

Missing Address

// ❌ Error: Seller address is required
$seller = Party::company('B12345678', 'Company SL');
// Missing address!

// ✅ Fixed
$seller = Party::company('B12345678', 'Company SL')
    ->address('Calle Mayor 1', '28001', 'Madrid', 'Madrid');

Missing Invoice Lines

// ❌ Error: At least one invoice line is required
$invoice = Invoice::create('FAC-001')
    ->seller($seller)
    ->buyer($buyer);
// No lines!

// ✅ Fixed
$invoice = Invoice::create('FAC-001')
    ->seller($seller)
    ->buyer($buyer)
    ->line('Service', price: 100, vat: 21);  // Add at least one line

Incomplete Billing Period

// ❌ Error: Billing period requires both start and end dates
$invoice = Invoice::create('FAC-001')
    ->seller($seller)
    ->buyer($buyer)
    ->line('Service', price: 100, vat: 21)
    ->billingPeriod('2024-01-01', null);  // Missing end date!

// ✅ Fixed
$invoice = Invoice::create('FAC-001')
    ->seller($seller)
    ->buyer($buyer)
    ->line('Service', price: 100, vat: 21)
    ->billingPeriod('2024-01-01', '2024-01-31');  // Both dates provided

Invalid Date Order

// ❌ Error: Billing period start date must be before end date
$invoice = Invoice::create('FAC-001')
    ->seller($seller)
    ->buyer($buyer)
    ->line('Service', price: 100, vat: 21)
    ->billingPeriod('2024-12-31', '2024-01-01');  // End before start!

// ✅ Fixed
$invoice = Invoice::create('FAC-001')
    ->seller($seller)
    ->buyer($buyer)
    ->line('Service', price: 100, vat: 21)
    ->billingPeriod('2024-01-01', '2024-12-31');  // Correct order

XSD Schema Validation

PHP FacturaE generates XML that complies with the official FacturaE 3.2.x XSD schema. The schema version is configurable:
use PhpFacturae\Enums\Schema;

$invoice = Invoice::create('FAC-001')
    ->schema(Schema::V3_2_2)  // Default
    ->seller($seller)
    ->buyer($buyer)
    ->line('Service', price: 100, vat: 21)
    ->toXml();
Available schemas:
  • Schema::V3_2 - FacturaE 3.2
  • Schema::V3_2_1 - FacturaE 3.2.1
  • Schema::V3_2_2 - FacturaE 3.2.2 (default)
Each schema has a corresponding XML namespace defined at Enums/Schema.php:13:
public function xmlNamespace(): string
{
    return match ($this) {
        self::V3_2   => 'http://www.facturae.gob.es/formato/Versiones/Facturaev3_2.xml',
        self::V3_2_1 => 'http://www.facturae.gob.es/formato/Versiones/Facturaev3_2_1.xml',
        self::V3_2_2 => 'http://www.facturae.gob.es/formato/Versiones/Facturaev3_2_2.xml',
    };
}
While PHP FacturaE validates the invoice structure, it does not perform full XSD schema validation against the official FacturaE XSD files. For complete XSD validation, use external tools or the FACe validator.

FACe Validator Integration

For invoices submitted to the Spanish public administration via FACe, you should validate against the FACe validator service:
// Generate the invoice
$xml = $invoice->toXml();

// Submit to FACe for validation
// (You'll need to implement FACe API integration separately)
$faceValidator = new FaceValidatorClient();
$validationResult = $faceValidator->validate($xml);

if (!$validationResult->isValid()) {
    foreach ($validationResult->getErrors() as $error) {
        echo "FACe validation error: $error\n";
    }
}
The FACe platform (Punto General de Entrada de Facturas Electrónicas) has additional validation requirements beyond the standard FacturaE format. Always test your invoices with the FACe validator before production use.

Complete Example

use PhpFacturae\Invoice;
use PhpFacturae\Party;
use PhpFacturae\Exceptions\InvoiceValidationException;

function createValidInvoice(): string
{
    try {
        $seller = Party::company('B12345678', 'Mi Empresa SL')
            ->address('Calle Mayor 1', '28001', 'Madrid', 'Madrid')
            ->email('[email protected]');

        $buyer = Party::company('A87654321', 'Cliente SA')
            ->address('Gran Via 50', '28013', 'Madrid', 'Madrid')
            ->email('[email protected]');

        $invoice = Invoice::create('FAC-2024-001')
            ->date('2024-03-15')
            ->seller($seller)
            ->buyer($buyer)
            ->line('Consulting services', price: 2000.00, vat: 21)
            ->line('Travel expenses', price: 150.00, vat: 21)
            ->transferPayment(
                iban: 'ES91 2100 0418 4502 0005 1332',
                dueDate: '2024-04-15'
            );

        // Validation happens automatically here
        return $invoice->toXml();

    } catch (InvoiceValidationException $e) {
        // Handle validation errors
        error_log('Invoice validation failed:');
        foreach ($e->errors as $error) {
            error_log("  - $error");
        }
        throw $e;
    }
}

Best Practices

1
Always Handle Validation Errors
2
Wrap toXml() and export() calls in try-catch blocks to handle validation failures gracefully.
3
Validate Early in Development
4
Test invoice generation early to catch validation issues before they reach production.
5
Provide Complete Data
6
Ensure all required fields (seller, buyer, addresses, lines) are populated before generating XML.
7
Test with Real Data
8
Use realistic invoice data during testing to catch edge cases and validation issues.
9
Log Validation Errors
10
Always log validation errors for debugging and monitoring purposes.

Testing Validation

From the test suite at tests/InvoiceTest.php:217:
public function test_fails_without_seller(): void
{
    $this->expectException(InvoiceValidationException::class);

    Invoice::create('FAC-001')
        ->buyer(Party::person('00000000A', 'J', 'G')
            ->address('C/', '28001', 'Madrid', 'Madrid'))
        ->line('Test', price: 100, vat: 21)
        ->toXml();
}

public function test_fails_without_lines(): void
{
    $this->expectException(InvoiceValidationException::class);

    Invoice::create('FAC-001')
        ->seller(Party::company('A00000000', 'T')
            ->address('C/', '28001', 'Madrid', 'Madrid'))
        ->buyer(Party::person('00000000A', 'J', 'G')
            ->address('C/', '28001', 'Madrid', 'Madrid'))
        ->toXml();
}

Build docs developers (and LLMs) love