skills/franciscosanchezn/easyfactu-es/spanish-tax-compliance

spanish-tax-compliance

SKILL.md

Spanish Tax Compliance (VeriFactu)

Implement Spanish electronic invoicing compliance with VeriFactu regulations, AEAT integration, NIF validation, and tax calculation.

When to Use This Skill

  • Implementing VeriFactu electronic invoicing
  • Validating Spanish tax identifiers (NIF, DNI, NIE, CIF)
  • Calculating IVA (VAT) rates and IRPF retention
  • Building the RegistroFactura XML structure for AEAT submission
  • Implementing invoice hash chaining
  • Understanding invoice types and numbering rules

VeriFactu Regulation Overview

What is VeriFactu?

VeriFactu (Verificación de Facturas) is Spain's anti-fraud electronic invoicing system mandated by the AEAT (Agencia Estatal de Administración Tributaria). It requires:

  1. Compliant invoicing software that signs and chains invoices
  2. Real-time or near-real-time submission to AEAT
  3. Immutable audit trail via cryptographic hash chaining
  4. Software identification in each submission

Key Requirements

  • Each invoice must be signed with a hash linked to the previous invoice
  • Invoice numbers must be sequential with no gaps
  • The software system must identify itself in submissions
  • Invoices must be submitted electronically to AEAT
  • Credit/correction invoices must reference the original

Timeline

  • Mandatory for all businesses and freelancers (autónomos)
  • Implementation dates vary by business size
  • Testing available via AEAT staging environment

Invoice Types

Factura Ordinaria (Standard Invoice)

Full invoice with all required fields. Used for most B2B transactions.

from pydantic import BaseModel, Field
from decimal import Decimal
from datetime import date
from enum import Enum


class TipoFactura(str, Enum):
    """VeriFactu invoice types."""
    F1 = "F1"  # Factura ordinaria
    F2 = "F2"  # Factura simplificada (ticket)
    R1 = "R1"  # Factura rectificativa (error en datos)
    R2 = "R2"  # Factura rectificativa (concurso acreedores)
    R3 = "R3"  # Factura rectificativa (deuda incobrable)
    R4 = "R4"  # Factura rectificativa (resto)
    R5 = "R5"  # Factura rectificativa (simplificada)


class LineaDetalle(BaseModel):
    """Invoice line item."""
    descripcion: str = Field(..., max_length=500)
    cantidad: Decimal = Field(..., gt=0)
    precio_unitario: Decimal = Field(..., ge=0)
    tipo_impositivo: Decimal = Field(..., ge=0, le=100)  # IVA rate
    base_imponible: Decimal  # = cantidad * precio_unitario
    cuota_repercutida: Decimal  # = base_imponible * tipo_impositivo / 100


class FacturaOrdinaria(BaseModel):
    """Full VeriFactu-compliant invoice."""
    # Identification
    numero_factura: str = Field(..., pattern=r"^[A-Z0-9/-]+$")
    serie: str | None = None
    fecha_expedicion: date
    tipo_factura: TipoFactura = TipoFactura.F1

    # Issuer (Emisor)
    nif_emisor: str = Field(..., description="NIF of the invoice issuer")
    nombre_emisor: str = Field(..., max_length=120)

    # Recipient (Destinatario)
    nif_destinatario: str = Field(..., description="NIF of the recipient")
    nombre_destinatario: str = Field(..., max_length=120)

    # Line items
    lineas: list[LineaDetalle] = Field(..., min_length=1)

    # Totals
    base_imponible_total: Decimal
    cuota_total: Decimal  # Total IVA
    importe_total: Decimal  # base + cuota

    # VeriFactu fields
    huella: str | None = None  # Hash of this invoice
    huella_anterior: str | None = None  # Hash of previous invoice (chain)

    # Software identification
    id_sistema: str = Field(default="FSN-VERIFACTU-001")
    nombre_sistema: str = Field(default="VeriFactu by FSN")
    version_sistema: str = Field(default="1.0.0")

Factura Simplificada (Simplified Invoice)

For transactions under €400 (retail, hospitality). Simplified data requirements.

class FacturaSimplificada(BaseModel):
    """Simplified invoice (ticket) for amounts < 400€."""
    numero_factura: str
    fecha_expedicion: date
    tipo_factura: TipoFactura = TipoFactura.F2

    # Issuer only (no recipient required for simplified)
    nif_emisor: str
    nombre_emisor: str

    # Simplified totals
    base_imponible: Decimal
    tipo_impositivo: Decimal
    cuota: Decimal
    importe_total: Decimal = Field(..., le=Decimal("400.00"))

Factura Rectificativa (Correction Invoice)

References and corrects a previous invoice.

class FacturaRectificativa(BaseModel):
    """Correction invoice referencing the original."""
    numero_factura: str
    fecha_expedicion: date
    tipo_factura: TipoFactura  # R1-R5

    # Reference to original invoice
    numero_factura_original: str
    serie_original: str | None = None
    fecha_original: date
    motivo_rectificacion: str = Field(..., max_length=500)

    # Correction amount (can be negative)
    base_imponible: Decimal
    cuota: Decimal
    importe_total: Decimal

Invoice Numbering Rules

  • Sequential: No gaps allowed within a series
  • Unique: Each number + series combination must be unique per fiscal year
  • Format: Alphanumeric, typically YYYY-NNNNNN or with series prefix
  • Series (optional): Group invoices by type or branch
from datetime import date


def generate_invoice_number(
    year: int,
    sequence: int,
    series: str = "",
) -> str:
    """Generate a sequential invoice number.

    Examples:
        generate_invoice_number(2026, 1) → "2026-000001"
        generate_invoice_number(2026, 42, "A") → "A/2026-000042"
    """
    number = f"{year}-{sequence:06d}"
    if series:
        return f"{series}/{number}"
    return number


async def get_next_invoice_number(
    session: AsyncSession,
    tenant_id: str,
    year: int,
    series: str = "",
) -> str:
    """Get the next sequential invoice number for a tenant.

    IMPORTANT: Must be called within a transaction with row-level locking
    to prevent gaps.
    """
    result = await session.execute(
        text("""
            SELECT COALESCE(MAX(sequence_number), 0) + 1
            FROM invoices
            WHERE tenant_id = :tenant_id
              AND fiscal_year = :year
              AND series = :series
            FOR UPDATE
        """),
        {"tenant_id": tenant_id, "year": year, "series": series},
    )
    next_seq = result.scalar_one()
    return generate_invoice_number(year, next_seq, series)

Hash Chaining (Encadenamiento)

Each invoice's hash includes the hash of the previous invoice, creating an immutable chain.

import hashlib
from datetime import date
from decimal import Decimal


def compute_invoice_hash(
    nif_emisor: str,
    numero_factura: str,
    fecha_expedicion: date,
    importe_total: Decimal,
    huella_anterior: str = "",
) -> str:
    """Compute the SHA-256 hash for a VeriFactu invoice.

    The hash chain links each invoice to its predecessor,
    preventing retroactive modification.

    Based on mdiago/VeriFactu reference implementation.
    """
    # Concatenate fields in the specified order
    data = (
        f"{nif_emisor}"
        f"{numero_factura}"
        f"{fecha_expedicion.strftime('%d-%m-%Y')}"
        f"{importe_total:.2f}"
        f"{huella_anterior}"
    )

    # SHA-256 hash
    return hashlib.sha256(data.encode("utf-8")).hexdigest()


# Usage:
# First invoice in chain
hash_1 = compute_invoice_hash(
    nif_emisor="B12345678",
    numero_factura="2026-000001",
    fecha_expedicion=date(2026, 1, 15),
    importe_total=Decimal("1210.00"),
    huella_anterior="",  # No previous for first invoice
)

# Second invoice references the first
hash_2 = compute_invoice_hash(
    nif_emisor="B12345678",
    numero_factura="2026-000002",
    fecha_expedicion=date(2026, 1, 20),
    importe_total=Decimal("484.00"),
    huella_anterior=hash_1,
)

Software System Identification (SistemaInformatico)

class SistemaInformatico(BaseModel):
    """Software system identification required by VeriFactu."""
    nombre: str = "VeriFactu by FSN"
    id_sistema: str = "FSN-VERIFACTU-001"
    version: str = "1.0.0"
    nif_desarrollador: str  # NIF of the software developer
    nombre_desarrollador: str  # Name of the developer/company

AEAT Integration

Endpoints

Environment URL
Staging https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RegistroFacturacion
Production https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RegistroFacturacion

RegistroFactura XML Structure

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SistemaFacturacion.xsd">
  <soapenv:Header/>
  <soapenv:Body>
    <sf:RegistroFacturacion>
      <sf:Cabecera>
        <sf:ObligadoEmision>
          <sf:NombreRazon>Empresa SL</sf:NombreRazon>
          <sf:NIF>B12345678</sf:NIF>
        </sf:ObligadoEmision>
      </sf:Cabecera>
      <sf:RegistroFactura>
        <sf:RegistroAlta>
          <sf:IDFactura>
            <sf:IDEmisorFactura>
              <sf:NIF>B12345678</sf:NIF>
            </sf:IDEmisorFactura>
            <sf:NumSerieFactura>2026-000001</sf:NumSerieFactura>
            <sf:FechaExpedicionFactura>15-01-2026</sf:FechaExpedicionFactura>
          </sf:IDFactura>
          <sf:TipoFactura>F1</sf:TipoFactura>
          <sf:Destinatarios>
            <sf:IDDestinatario>
              <sf:NombreRazon>Cliente SA</sf:NombreRazon>
              <sf:NIF>A87654321</sf:NIF>
            </sf:IDDestinatario>
          </sf:Destinatarios>
          <sf:Desglose>
            <sf:DetalleDesglose>
              <sf:ClaveRegimen>01</sf:ClaveRegimen>
              <sf:TipoImpositivo>21.00</sf:TipoImpositivo>
              <sf:BaseImponible>1000.00</sf:BaseImponible>
              <sf:CuotaRepercutida>210.00</sf:CuotaRepercutida>
            </sf:DetalleDesglose>
          </sf:Desglose>
          <sf:ImporteTotal>1210.00</sf:ImporteTotal>
          <sf:Encadenamiento>
            <sf:PrimerRegistro>S</sf:PrimerRegistro>
          </sf:Encadenamiento>
          <sf:SistemaInformatico>
            <sf:NombreRazon>FSN Development</sf:NombreRazon>
            <sf:NIF>12345678Z</sf:NIF>
            <sf:NombreSistemaInformatico>VeriFactu by FSN</sf:NombreSistemaInformatico>
            <sf:IdSistemaInformatico>FSN-VERIFACTU-001</sf:IdSistemaInformatico>
            <sf:Version>1.0.0</sf:Version>
          </sf:SistemaInformatico>
          <sf:Huella>a1b2c3d4e5f6...</sf:Huella>
        </sf:RegistroAlta>
      </sf:RegistroFactura>
    </sf:RegistroFacturacion>
  </soapenv:Body>
</soapenv:Envelope>

Python Submission Client

import httpx
from lxml import etree


class AEATClient:
    """Client for submitting invoices to the AEAT VeriFactu system."""

    STAGING_URL = "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RegistroFacturacion"
    PRODUCTION_URL = "https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RegistroFacturacion"

    def __init__(
        self,
        certificate_path: str,
        certificate_password: str,
        environment: str = "staging",
    ) -> None:
        self.url = self.STAGING_URL if environment == "staging" else self.PRODUCTION_URL
        self.cert = (certificate_path, certificate_password)

    async def submit_invoice(self, invoice: FacturaOrdinaria) -> AEATResponse:
        """Submit an invoice to AEAT."""
        xml = self._build_xml(invoice)

        async with httpx.AsyncClient(cert=self.cert) as client:
            response = await client.post(
                self.url,
                content=xml,
                headers={"Content-Type": "text/xml; charset=utf-8"},
            )
            response.raise_for_status()

        return self._parse_response(response.content)

    def _build_xml(self, invoice: FacturaOrdinaria) -> bytes:
        """Build the SOAP XML envelope for the invoice."""
        # XML construction logic
        ...

    def _parse_response(self, content: bytes) -> AEATResponse:
        """Parse the AEAT response."""
        ...


class AEATResponse(BaseModel):
    """Response from AEAT after invoice submission."""
    estado: str  # "Correcto" or "Incorrecto"
    csv: str | None = None  # Código Seguro de Verificación
    errores: list[str] = []

Certificate Requirements

  • AEAT requires a digital certificate for signing submissions
  • Certificates can be obtained from FNMT (Fábrica Nacional de Moneda y Timbre)
  • Use .p12 or .pem format for the certificate
  • Store certificate securely (Kubernetes Secret or environment variable)

Tax Rules

IVA (VAT) Rates

Rate Percentage Applies To
General 21% Most goods and services
Reduced 10% Food, water, transport, hospitality
Super-reduced 4% Bread, milk, medicine, books, first home
Exempt 0% Healthcare, education, financial services
from decimal import Decimal
from enum import Enum


class TipoIVA(Enum):
    """Spanish IVA (VAT) rates."""
    GENERAL = Decimal("21.00")
    REDUCIDO = Decimal("10.00")
    SUPERREDUCIDO = Decimal("4.00")
    EXENTO = Decimal("0.00")


def calculate_iva(
    base_imponible: Decimal,
    tipo: TipoIVA = TipoIVA.GENERAL,
) -> tuple[Decimal, Decimal]:
    """Calculate IVA amount and total.

    Returns:
        Tuple of (cuota_iva, importe_total).
    """
    cuota = (base_imponible * tipo.value / Decimal("100")).quantize(Decimal("0.01"))
    total = base_imponible + cuota
    return cuota, total

IRPF Retention (Freelancers)

class TipoRetencion(Enum):
    """IRPF retention rates for freelancers."""
    ESTANDAR = Decimal("15.00")  # Standard rate
    NUEVO_AUTONOMO = Decimal("7.00")  # First 3 years

def calculate_irpf_retention(
    base_imponible: Decimal,
    tipo: TipoRetencion = TipoRetencion.ESTANDAR,
) -> Decimal:
    """Calculate IRPF retention for freelancer invoices."""
    return (base_imponible * tipo.value / Decimal("100")).quantize(Decimal("0.01"))

Intra-Community Operations

# Intra-community (EU B2B) invoices have IVA at 0%
# but require the recipient's EU VAT number
class OperacionIntracomunitaria(BaseModel):
    """Intra-community operation (EU VAT exempt)."""
    nif_destinatario_ue: str = Field(
        ...,
        pattern=r"^[A-Z]{2}\d+$",
        description="EU VAT number (e.g., DE123456789)",
    )
    tipo_impositivo: Decimal = Decimal("0.00")
    clave_regimen: str = "02"  # Intra-community

NIF Validation

DNI Validation

def validate_dni(dni: str) -> bool:
    """Validate Spanish DNI (8 digits + letter).

    The check letter is calculated from the number modulo 23.
    """
    LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE"

    if len(dni) != 9:
        return False

    number_part = dni[:8]
    letter = dni[8].upper()

    if not number_part.isdigit():
        return False

    expected_letter = LETTERS[int(number_part) % 23]
    return letter == expected_letter

NIE Validation

def validate_nie(nie: str) -> bool:
    """Validate Spanish NIE (X/Y/Z + 7 digits + letter).

    NIE replaces the first letter with a number for check digit calculation:
    X → 0, Y → 1, Z → 2
    """
    LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE"
    NIE_PREFIX = {"X": "0", "Y": "1", "Z": "2"}

    if len(nie) != 9:
        return False

    prefix = nie[0].upper()
    if prefix not in NIE_PREFIX:
        return False

    number_str = NIE_PREFIX[prefix] + nie[1:8]
    letter = nie[8].upper()

    if not number_str.isdigit():
        return False

    expected_letter = LETTERS[int(number_str) % 23]
    return letter == expected_letter

CIF Validation

def validate_cif(cif: str) -> bool:
    """Validate Spanish CIF (company tax ID).

    Format: Letter + 7 digits + control (digit or letter).
    """
    if len(cif) != 9:
        return False

    letter = cif[0].upper()
    digits = cif[1:8]
    control = cif[8]

    if not digits.isdigit():
        return False

    # Calculate control digit
    even_sum = sum(int(d) for d in digits[1::2])
    odd_sum = 0
    for d in digits[::2]:
        doubled = int(d) * 2
        odd_sum += doubled // 10 + doubled % 10

    total = even_sum + odd_sum
    control_digit = (10 - total % 10) % 10

    # Control can be letter or digit depending on first letter
    CONTROL_LETTERS = "JABCDEFGHI"
    letter_types = "KPQS"  # Always letter control
    digit_types = "ABEH"   # Always digit control

    if letter in letter_types:
        return control.upper() == CONTROL_LETTERS[control_digit]
    elif letter in digit_types:
        return control == str(control_digit)
    else:
        # Either is valid
        return control == str(control_digit) or control.upper() == CONTROL_LETTERS[control_digit]

Unified NIF Validator

def validate_nif(nif: str) -> bool:
    """Validate any Spanish tax identifier (DNI, NIE, or CIF).

    Args:
        nif: The tax identifier to validate.

    Returns:
        True if the NIF is valid.
    """
    if not nif or len(nif) != 9:
        return False

    first_char = nif[0].upper()

    if first_char.isdigit():
        return validate_dni(nif)
    elif first_char in "XYZ":
        return validate_nie(nif)
    else:
        return validate_cif(nif)

Data Retention

Legal Requirements

  • Invoices: Must be retained for 4 years (tax inspection period)
  • Accounting records: 6 years (commercial code)
  • Electronic records: Same as paper, including digital signatures and hashes
  • Hash chain integrity: Must be verifiable for the entire retention period

GDPR Considerations

  • Invoice data contains personal data (name, NIF, address)
  • Retention is justified under legal obligation (Art. 6.1.c GDPR)
  • After retention period, data should be anonymized or deleted
  • Subject access requests must be fulfilled within 1 month
  • Data minimization: only store what's legally required

Reference Implementation

mdiago/VeriFactu (C# / .NET)

GitHub: https://github.com/mdiago/VeriFactu

Use this repository as a protocol and specification reference for:

  • RegistroFactura XML structure and field mappings
  • Encadenamiento (chain linking) implementation details
  • SistemaInformatico identification requirements
  • AEAT endpoint URLs and SOAP envelope format

Important: Reference for protocol understanding only. Do not copy code — implement in Python following the monorepo's patterns.

AEAT Official Resources

Guidelines

  • Sequential invoice numbers with no gaps — use database locks
  • Hash chain every invoice to the previous one (SHA-256)
  • Validate NIF/CIF/NIE before accepting invoices
  • Use staging environment for AEAT integration testing
  • Store digital certificates securely (K8s Secrets)
  • Retain invoices 4+ years as required by law
  • Reference mdiago/VeriFactu for protocol details, not code
  • Use Pydantic models for all invoice data structures
  • Apply Decimal type for all monetary amounts (never float)
  • Test with real AEAT staging before going to production
Weekly Installs
1
First Seen
12 days ago
Installed on
mcpjam1
claude-code1
junie1
windsurf1
zencoder1
crush1