"""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 helpers:func:`is_eu_member_state_code` and :func:`assert_eu_member_state_code`anchor the EU axis to the substrate's :class:`aeat.domain.iva.EUMemberState`enum. 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.identityimportnif_iva_format_for_country,normalise_nif_ivafrom..ivaimportEUMemberStatefrom._errorsimportInvoiceValidationError__all__=["EU_MEMBER_STATE_CODES","assert_eu_member_state_code","is_eu_member_state_code","validate_country_code","validate_iva_number",]_IVA_BODY_RE=re.compile(r"^[a-zA-Z0-9]{4,20}$")_ISO_2_RE=re.compile(r"^[A-Z]{2}$")EU_MEMBER_STATE_CODES:frozenset[str]=frozenset(member.value.upper()formemberinEUMemberStateifmemberisnotEUMemberState.XI)"""Closed set of ISO-3166 alpha-2 codes (uppercase) for the 27 EUMember States, sourced from :class:`aeat.domain.iva.EUMemberState` whileexcluding the Northern Ireland IVA prefix ``XI``."""
[docs]defvalidate_country_code(value:str)->str:"""Normalise and validate an ISO-3166 alpha-2 country code. 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:`aeat.domain.iva.EUMemberState`; if the substrate's enum changes (Brexit-style additions or withdrawals) the helper picks up the new shape 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:returnFalsereturnnormalizedinEU_MEMBER_STATE_CODES
[docs]defassert_eu_member_state_code(value:str)->str:"""Validate ``value`` and assert it names an EU Member State. Args: value: Raw country code to validate. Returns: The uppercased two-letter EU Member State code. Raises: InvoiceValidationError: If the input is malformed or names a non-EU country. """normalized=validate_country_code(value)ifnormalizednotinEU_MEMBER_STATE_CODES:raiseInvoiceValidationError(f"country code {normalized!r} is not one of the 27 EU Member States; ""use validate_country_code if a non-EU counterparty is acceptable",)returnnormalized
[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 :data:`aeat.core.identity.NIF_IVA_FORMATS` authority: a malformed intra-community VAT 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