"""Canonical normalisers for the inbound ISO code tokens ledger records carry.Currency (ISO 4217) and source jurisdiction (ISO 3166-1 alpha-2) both arriveas free text from bank exports, operator input, and CLI options, and both arepersisted on ledger records. Each previously carried a per-boundary copy ofits shape check, so the same token could be accepted at one boundary andrefused at the next.These are pure functions at the ``core`` layer. They raise the registered:class:`~core.errors.hierarchy.CoreValidationError` ancestry, and a Pydantic validatordelegating here translates that failure to ``ValueError`` at its narrowboundary. Each caller re-raises its own domain error to keep operatordiagnostics boundary-specific."""from__future__importannotationsfromtypingimportAnnotatedfrompydanticimportBeforeValidatorfrom..errors.hierarchyimportCoreValidationError,pydantic_validation_boundary_ISO_4217_LENGTH:int=3_ISO_3166_ALPHA2_LENGTH:int=2
[docs]defnormalise_iso_4217_currency(value:object)->str:"""Normalise a raw currency token to its uppercase ISO 4217 code. Trims surrounding whitespace and uppercases before the shape check, so a padded or lowercase source cell (``" usd "``) normalises to ``"USD"`` rather than being refused for its padding. Callers that validate through Pydantic must run this as a ``mode="before"`` validator: a bare ``min_length`` / ``max_length`` field constraint fires on the padding first and never reaches the normaliser. Args: value: Raw currency token from a bank export, OFX statement header, operator input, or persisted payload. Returns: The three-letter uppercase ISO 4217 code. Raises: CoreValidationError: The token is not exactly three alphabetic characters once trimmed. """normalised=str(value).strip().upper()iflen(normalised)!=_ISO_4217_LENGTHornotnormalised.isalpha():raiseCoreValidationError("currency must be a three-letter ISO 4217 code")returnnormalised
[docs]defnormalise_iso_3166_alpha2_jurisdiction(value:str|None)->str|None:"""Validate a source-jurisdiction token as an ISO 3166-1 alpha-2 code. Trims surrounding whitespace, then requires exactly two alphabetic characters that are **already uppercase**. Unlike :func:`normalise_iso_4217_currency` this deliberately refuses a lowercase token rather than folding it: the jurisdiction axis selects the regulatory-source treatment of a ledger row (Spanish-source versus foreign-source), so a caller that supplies ``"es"`` is told to declare the canonical code rather than having one guessed for it. Args: value: Raw jurisdiction token, or ``None`` when the record explicitly declares the jurisdiction unknown. Returns: The trimmed uppercase alpha-2 code, or ``None`` when ``value`` is ``None``. Raises: CoreValidationError: The token is not exactly two uppercase alphabetic characters once trimmed. """ifvalueisNone:returnNonenormalised=value.strip()iflen(normalised)!=_ISO_3166_ALPHA2_LENGTHornotnormalised.isalpha()ornormalised!=normalised.upper():raiseCoreValidationError("source_jurisdiction must be a two-letter ISO 3166-1 alpha-2 uppercase code",)returnnormalised
IsoCurrencyCode=Annotated[str,BeforeValidator(pydantic_validation_boundary(normalise_iso_4217_currency))]"""A three-letter uppercase ISO 4217 code, normalised before it is checked.The normalisation has to happen BEFORE the shape check, which is why this is a``BeforeValidator`` and not a pattern. :func:`normalise_iso_4217_currency` saysso in its own docstring: a bare ``min_length`` / ``max_length`` field constraintfires on the padding first and never reaches the normaliser, so `` usd `` isrefused for its spaces rather than accepted as ``USD``.Three sites had written the shape out instead, in three different strengths --a length-only bound that accepted ``eur`` and ``$$$``, and two hand-rolled``^[A-Z]{3}$`` patterns that refused padding the normaliser exists to absorb."""__all__=["IsoCurrencyCode","normalise_iso_3166_alpha2_jurisdiction","normalise_iso_4217_currency",]