Source code for cadrumo.domain.invoices.validators
"""Counterparty identity validators for invoice records.The EU-IVA prefix check and ISO-3166 alpha-2 country-code normaliserremain in this module because they are invoice-domain concerns.Each helper raises :class:`ValueError` on failure so pydanticsurfaces the error as a validation error in the enclosing``Invoice`` model.The registry-grounded :func:`is_eu_member_state_code` helper anchors the EUaxis to the substrate's registry-projected :class:`cadrumo.domain.iva.schema.EUMemberState`token. Modelo 369 binding selectors and the OSS / IOSS classifierboundary checks consume these helpers so the EU membership decisionflows from the substrate, not from a hand-maintained list."""from__future__importannotationsimportrefrom...core.country_codeimportCOUNTRY_CODE_ALPHA2_PATTERNfrom...core.identity.nif_ivaimportnormalise_nif_ivafrom..calculations.registry.eu_member_state_catalogueimportresolve_eu_member_state_cataloguefrom..calculations.registry.nif_iva_catalogueimportnif_iva_format_for_countryfrom..calculations.registry.tax_id_runtimeimportvalidate_runtime_spanish_tax_idfrom.errorsimportInvoiceValidationError__all__=["is_eu_member_state_code","validate_counterparty_tax_id","validate_country_code","validate_iva_number",]_IVA_BODY_RE=re.compile(r"^[a-zA-Z0-9]{4,20}$")#: The one country whose identifiers take the AEAT checksum regime._SPAIN_COUNTRY_CODE="ES"_ISO_2_RE=re.compile(rf"^{COUNTRY_CODE_ALPHA2_PATTERN}$")def_eu_member_state_codes()->frozenset[str]:"""Return the 27 strict EU members, excluding the fact-declared ``XI`` prefix."""catalogue=resolve_eu_member_state_catalogue()northern_ireland=catalogue.require("XI")returnfrozenset(str(member).upper()formemberincatalogue.all_statesifmember!=northern_ireland)
[docs]defvalidate_country_code(value:str)->str:"""Normalise and validate an ISO-3166 alpha-2 country code. Folds case, unlike :func:`~cadrumo.core.parsing.codes.normalise_iso_3166_alpha2_jurisdiction`, which refuses a lowercase token so the regulatory treatment of a ledger row is never guessed. A counterparty's country is a label on an invoice rather than a treatment selector, so folding an operator's ``"es"`` here costs nothing. The two share :data:`~cadrumo.core.country_code.COUNTRY_CODE_ALPHA2_PATTERN` and nothing else. Args: value: Raw country code to validate. Returns: The uppercased two-letter country code. Raises: InvoiceValidationError: If the input is not exactly two alphabetic characters. """normalized=value.strip().upper()ifnot_ISO_2_RE.match(normalized):raiseInvoiceValidationError("country code must be an ISO-3166 alpha-2 value")returnnormalized
[docs]defis_eu_member_state_code(value:str)->bool:"""Return ``True`` when ``value`` matches one of the 27 EU Member State codes. The membership check is anchored to :class:`cadrumo.domain.iva.schema.EUMemberState`; if fact 0131 changes (Brexit-style additions or withdrawals) the helper picks up the selected membership automatically. Args: value: Raw country code to check. Returns: ``True`` when ``value`` normalises to one of the 27 EU codes. """try:normalized=validate_country_code(value)exceptInvoiceValidationError:returnFalsereturnnormalizedin_eu_member_state_codes()
[docs]defvalidate_iva_number(value:str,country:str)->str:"""Validate a non-ES IVA number shape against its country format. For an EU Member State (and Northern Ireland ``XI``) the number is matched against the country's published NIF-IVA structural pattern, sourced from the central ``cadrumo.core.identity.nif_iva.NIF_IVA_FORMATS`` authority: a malformed intra-community IVA number is bounced by AEAT's Modelo 349 validator, so the refusal names the country and the expected format. Live VIES existence is not checked — only the structure. For a non-EU counterparty (no published pattern) the helper falls back to a generic leading-prefix plus 4-20 character alphanumeric body check, so non-EU counterparties remain acceptable. Args: value: Raw IVA identifier to validate. country: ISO-3166 alpha-2 country code already validated. Returns: The uppercased, whitespace-trimmed IVA identifier. Raises: InvoiceValidationError: If the value is malformed, the prefix does not match ``country``, or the EU NIF-IVA format does not match. """normalized=normalise_nif_iva(value)ifnotnormalized:raiseInvoiceValidationError("IVA number must not be blank")spec=nif_iva_format_for_country(country)ifspecisnotNone:ifnotspec.pattern.match(normalized):raiseInvoiceValidationError(f"IVA number {normalized!r} is not a valid {spec.country_name} NIF-IVA: "f"expected {spec.description} (e.g. {spec.example})",)returnnormalizedcountry_upper=country.strip().upper()ifnotnormalized.startswith(country_upper):raiseInvoiceValidationError("IVA number must start with the counterparty country ISO-2 prefix")body=normalized[len(country_upper):]ifnot_IVA_BODY_RE.match(body):raiseInvoiceValidationError("IVA number body must be 4-20 alphanumeric characters")returnnormalized
[docs]defvalidate_counterparty_tax_id(tax_id:str,*,country:str|None)->str:"""Validate a counterparty's tax identifier under its country's regime. Which regime applies is a domain rule, not a detail of whichever surface happens to hold the values: a Spanish counterparty is checked against the AEAT checksum, an EU one against its Member State's NIF-IVA structure. That choice was written twice -- once as a mapping with a default in the invoice normaliser, once as an ``if country == "ES"`` in the CLI wire projection -- and a third regime, or a change to which countries take which, would have had to reach both. An ABSENT country returns the identifier unvalidated, and that is the deliberate half. A factura simplificada may legitimately carry no counterparty country (RD 1619/2012 art. 6.1.d), and with no country there is no regime to check against; guessing one from the identifier's own shape is the substitution the identification-state fact exists to prevent. Args: tax_id: The counterparty identifier as declared. country: The counterparty's ISO alpha-2 country, or ``None`` when the document states none. Returns: The identifier in the canonical form its regime's validator returns, unchanged when no country was declared. Raises: InvoiceValidationError: The identifier does not satisfy its regime. """ifcountryisNone:returntax_idifcountry==_SPAIN_COUNTRY_CODE:returnvalidate_runtime_spanish_tax_id(tax_id)returnvalidate_iva_number(tax_id,country)