cadrumo.application.ledger.establishment_ladder module

Ask the strongest available evidence where a counterparty is established, in order.

Four rungs, each already an authority in its own right, composed here into the ordered ladder the classification criteria need. The composition is the whole deliverable: every rung answers honestly on its own, and the DAMAGE comes from consulting them in the wrong order.

First decisive rung wins, and the order is not a preference. Spain, France, Germany and Italy all print five-digit postal codes, so a French 75001 handed to the Spanish province lookup returns the peninsula and a 51001 returns Ceuta – placing a French party inside a Spanish territory, or outside LIVA entirely. Neither is caught by a check further down, because both are perfectly well-formed answers to the wrong question. What prevents it is that the country rung is consulted first and, having answered, stops the ladder.

A printed IVA number is NOT a rung here, and that is the correction this ladder exists in its current form to carry. It once was the first and strongest one: a DE prefix resolved decisively to EU_MEMBER and stopped the walk. Every Member State registers non-residents on exactly the terms Spain does, so that read the party’s REGISTRATION as its PLACE — and it did so on one side only, because a Spanish registration was already, correctly, refused. The foreign direction was the dangerous one: a German-registered entity actually established in Spain resolved silently and confidently, where its Spanish mirror failed loud to the operator.

The prefix is now terminal for the OTHER fact, the party’s IVA identification state, which it settles decisively because registration is exactly what that fact asserts. What it never settles alone is where the party IS.

The rungs, strongest first:

  1. The printed address country, as a name matched against the bounded registry vocabulary or as an already-printed alpha-2 code. It is now first, and it needs no help from the registration: an address stating France places a French-established party whatever State registered it.

  2. The Spanish postal code, consulted ONLY where the country evidence positively named Spain. A postal pattern alone presupposes exactly what it would have to prove.

  3. A foreign registration CONCORDANT with an independent rung, where the address rungs settled nothing. The registration alone is never enough; what makes it usable is a second, independent signal agreeing with it — a printed treatment consistent with the party not being established here, such as a reverse-charge mention with no Spanish IVA charged. Concordant papers resolve silently, which is what keeps the foreign population from being asked.

  4. A previously confirmed counterparty-level fact, because establishment is a property of the entity and the operator should be asked at most once.

And any Spain-indicating rung beside a foreign registration is a CONTRADICTION, never a resolution either way. A Spanish address, country-gated Spanish postal evidence, or Spanish IVA charged at a registry rate, printed on a document whose counterparty carries another State’s IVA number, is the characteristic face of a foreign-registered entity operating through an establecimiento permanente in Spain. It fails loud, and it is checked BEFORE the ordinary rungs so the postal rung cannot quietly answer it.

Spain is a trigger, not an exhausted rung. The country rung deliberately returns no SCOPE for Spain – ES names the Member State while the territory inside it stays undetermined between the TAI, Canarias and Ceuta y Melilla – so the ladder reads the country CODE rather than the scope to decide whether the postal rung applies. Treating that None as “no evidence” would skip the one rung that can answer, and would do so for the majority population.

An exhausted ladder yields nothing, and nothing never becomes a territory. There is no branch here that produces a scope from the absence of evidence. The peninsula is the commonest answer, so a default there would pass every test written from mainland fixtures while silently placing Canarian and Ceutan parties inside a territory their operations are not subject to.

A corrupt registry is not an unestablished party. The country vocabulary and the territory table REFUSE a malformed or fold-colliding table rather than degrading, and that refusal travels out of here untouched. No rung is wrapped in a bare except: converting it to a quiet “not established” would send an operator to confirm a counterparty’s territory in answer to a broken bundled data file, which is a defect with no operator remedy.

The taxpayer’s own side never enters this ladder. One party on every ingested invoice is the operator, whose territory is a profile fact resolved by resolve_filer_territorial_scope(). Only the counterparty’s scope is ever sought on the paper.

See also

resolve_confirmed_counterparty_facts()

The fourth rung, and the store an operator’s answer persists into.

DeclaredFacts

The one channel a resolved scope reaches the criteria assembly through.

IvaTerritorialScope

The closed target every rung resolves into.

class EstablishmentRung(*values)[source]

Bases: StrEnum

Which rung of the ladder settled a counterparty’s territory.

Recorded rather than discarded because the rungs differ in what an auditor can be shown. The first three are anchorable to a printed form on the page; the fourth is a person’s assertion, and pointing an auditor at a page for it would be a claim the document never made.

There is no tax-identifier rung, and its absence is the point. One was here, first and strongest, until a printed foreign prefix was recognised as evidence of REGISTRATION rather than of place. The member is not retained unused: an unreachable rung in a closed set is a value every consumer would still have to handle, and the honest record of what settled such a document is that nothing did.

Variables:
  • CONCORDANT_REGISTRATION – A foreign registration corroborated by an independent signal agreeing with it, where the address rungs settled nothing and no rung indicated Spain. Weaker than the printed rungs above it, which is why it is consulted after them.

  • ADDRESS_COUNTRY –

    The country stated in the party’s address block named a country whose territory is settled by the country alone.

    Named for WHERE the evidence sits, not for how it was rendered. It was PRINTED_COUNTRY while a printed document was the only thing that could feed it; a structured record states the same country in an element, and an operator reading “printed” against a machine-readable invoice is told the value came from somewhere it did not. The rung is one rung either way – the same authority decides what a country establishes – so the label names the address rather than the medium.

  • SPANISH_POSTAL_CODE – The country evidence named Spain and the postal code in the address separated the three Spanish IVA territories.

  • CONFIRMED_COUNTERPARTY_FACT – An operator had already confirmed this counterparty’s establishment, and the paper settled nothing that disagreed.

CONCORDANT_REGISTRATION
ADDRESS_COUNTRY
SPANISH_POSTAL_CODE
CONFIRMED_COUNTERPARTY_FACT
class RegistrationEstablishmentConflict(**data)[source]

Bases: BaseModel

A foreign registration printed beside evidence placing the party in Spain.

Carried rather than resolved, on the same terms as the confirmed-fact disagreement and for a sharper reason. This is the shape the dangerous population characteristically presents. An entity registered in another Member State but operating through an establecimiento permanente in Spain charges Spanish IVA on its domestic supplies and prints a Spanish address to do it — so a foreign IVA number beside a Spanish address, Spanish postal evidence or a Spanish registry rate is not noise, it is the signature of the exact case the split exists to catch.

Preferring either side would be a guess with a filing behind it. Preferring the registration reinstates the silent EU_MEMBER this whole change removes; preferring the Spanish evidence would assert an establishment the document does not state. So no scope is returned and a human decides.

Variables:
  • identification_state – The Member State whose IVA number was printed.

  • spain_indicating – What placed the party in Spain, in operator-facing words. Never empty — a conflict nobody can see the other half of is not actionable.

  • detail – The disagreement in words, for the operator-facing finding.

Parameters:

data (Any)

identification_state: EUMemberState
spain_indicating: tuple[str, ...]
detail: str
class CounterpartyEstablishment(**data)[source]

Bases: BaseModel

What the ladder settled about one counterparty on one document.

Variables:
  • scope – The territory, or None when the ladder exhausted without a decisive rung. None is this codebase’s spelling of the ruling’s UNKNOWN: a missing scope is not a kind of scope, and adding an unknown member to the closed set would put a value into it that every rule table downstream would then have to special-case.

  • rung – Which rung answered. None exactly when scope is.

  • source – How the answer is backed – a printed form for the evidence rungs, a person for the remembered assertion. None exactly when scope is, because an unsettled question has nobody vouching for it.

  • contradiction – A confirmed fact the document’s printed evidence disputes. Carried WITH no scope: the stored value is an operator’s claim about an entity and the printed value is an issuer’s claim about one document, and neither may be preferred without a decision.

  • identification_state – The Member State whose IVA number the counterparty printed. Settled TERMINALLY by the prefix and carried independently of scope, because they are two facts: this one is decided by registration evidence, and the other is decided by no registration at all. It is populated on a conflict and on an unestablished result alike – the paper can name the registering State perfectly well while saying nothing about where the party operates.

  • registration_conflict – A foreign registration printed beside evidence placing the party in Spain. Carried WITH no scope, on the same reasoning as contradiction.

Parameters:

data (Any)

scope: IvaTerritorialScope | None
rung: EstablishmentRung | None
source: ClassifierInputSource | None
contradiction: CounterpartyEstablishmentContradiction | None
identification_state: EUMemberState | None
registration_conflict: RegistrationEstablishmentConflict | None
property established: bool

Return whether the ladder settled a territory.

property contradicted: bool

Return whether the evidence disputes a confirmed fact.

Reports the stored-fact disagreement only. The registration conflict is a different disagreement between different parties – an issuer’s own two statements rather than an issuer against an operator – and folding them into one flag would leave a caller unable to say which it is looking at, or to route them to the finding each deserves.

property declared_fact: DeclaredFact[IvaTerritorialScope] | None

Return this scope in the form the criteria assembly consumes.

The one channel a resolved scope reaches the assembly through, so the value and who established it travel together. None where the ladder settled nothing, which the assembly then reports as a missing input.

scope_printed_evidence_would_establish(*, legends, operation, tax_identifier=None, stated_country_name=None, resolved_country_code=None, postal_code=None)[source]

Return the territory this printed evidence alone would settle, or None.

The document rungs only, with no store consulted and nothing persisted. It exists so a DIAGNOSTIC can name a territory without re-deriving the boundary that produces it: an advisory warning that a party’s address values are of unverified attribution is far more actionable when it also says where those values would place the party, and the operator is then contesting a concrete claim rather than an abstraction.

Quoting rather than copying is the whole point. A second walk of the rungs written for the advisory would be a second copy of a regulatory boundary, free to drift from this one – and the ordering it would have to reproduce is precisely the safety property this module exists to hold.

Not a resolution, and never a substitute for one. No confirmed counterparty fact is consulted, so a contradiction cannot surface here and an answer from this function must never reach the criteria. Callers wanting the settled territory call resolve_counterparty_establishment_scope().

A registration conflict reads here as no territory, which is the honest projection for a diagnostic: the advisory this feeds says where the printed values would place the party, and a document whose registration and address disagree has no single such place to name. The conflict itself is an operator-facing finding rather than a hint, and it surfaces through the resolution path that can carry it.

Parameters:
  • tax_identifier (str | None) – The party’s identifier as printed, if any.

  • stated_country_name (str | None) – The country the document states for this party.

  • resolved_country_code (str | None) – The party’s country as an alpha-2 code, already RESOLVED through the bounded vocabulary – never the token a record stated. A structured document states an alpha-3 as readily as an alpha-2, and both are str | None, so feeding the stated token here type-checks and silently places nobody.

  • postal_code (str | None) – The party’s printed postal code.

  • legends (tuple[RegimeLegend, ...]) – The dated registry declarations selected by the enclosing pinned authority operation.

  • operation (PinnedAuthorityOperation) – The caller-owned pinned authority operation used for every country, territory, identification, and rate lookup.

Return type:

IvaTerritorialScope | None

Returns:

The territory the printed rungs settle, or None where they exhaust.

resolve_counterparty_establishment_scope(*, bucket_id, legends, operation, tax_identifier=None, stated_country_name=None, resolved_country_code=None, postal_code=None, regime_legend=None, charged_iva_rates=(), on_date=None, repository)[source]

Resolve where a counterparty is established, or settle nothing and say so.

The confirmed-fact rung is consulted even when the paper was decisive, and that is not a departure from first-decisive-rung: the store is asked to COMPARE, not to answer. A confirmed-Canarian counterparty printing a French country code is a real signal – a different entity behind a reused identifier, an establishment that moved, or an assertion made in error – and none of the three is settled by preferring a side, so the disagreement is carried and NO scope is returned. Skipping the store on a decisive page would take that channel offline for exactly the population it protects.

Parameters:
  • bucket_id (str) – Active profile bucket, for the confirmed-fact rung.

  • legends (tuple[RegimeLegend, ...]) – The dated registry declarations selected by the enclosing pinned authority operation.

  • operation (PinnedAuthorityOperation) – The caller-owned pinned authority operation used for every country, territory, identification, and rate lookup.

  • tax_identifier (str | None) – The counterparty’s identifier as printed, if any.

  • stated_country_name (str | None) – The country the document states for this party, in whatever language the issuer set it. Printed in an address block on a rendered document, carried in a country element on a structured one; the rung treats the two alike, so the parameter claims only that the document stated it.

  • resolved_country_code (str | None) – The party’s country as an already-resolved alpha-2 code, never the stated token, for surfaces that carry one instead of a name.

  • postal_code (str | None) – The counterparty’s printed postal code. Consulted only where the country evidence positively named Spain.

  • regime_legend (str | None) – The statutory mention the document prints, if any. Read for one thing only: whether the treatment independently agrees the counterparty is not established here.

  • charged_iva_rates (tuple[Decimal, ...]) – Every IVA percentage the document charges. Spanish registry rates among them place the party here; foreign ones say nothing, which is why the rates are passed rather than a flag.

  • on_date (date | None) – The invoice date, for asking the rate schedule what a rate meant when it was charged. None makes the rate check inconclusive rather than negative.

  • repository (CounterpartyEstablishmentRepositoryProtocol) – Injected store.

Returns:

the territory and the rung that settled it, a conflict, a contradiction, or none of them.

Return type:

CounterpartyEstablishment

Raises:
  • IvaCatalogueError – When the bundled country vocabulary or territory registry cannot be read, is malformed, or carries a name two countries claim. Deliberately propagated: a broken bundled data file is a defect, and quietly reporting it as an unestablished party would send an operator to answer a question that is not theirs.

  • Exception – Whatever the confirmed-fact store raises, on the same terms and for a sharper reason. A store that cannot be read is not a counterparty nobody has confirmed – and the operator may have confirmed this one already, so swallowing the failure would retract their own earlier answer and ask them for it again, against a store that would refuse to record it. The secure repository takes the same position on the tier below, raising rather than returning None when a row exists but is inconsistent, precisely so an inconsistency cannot hide behind an ordinary miss. No error type is named here because none is caught: the store owns its own vocabulary and this function is transparent to it.

resolve_draft_counterparty_establishment(*, bucket_id, legends, operation, draft, kind, repository)[source]

Route a read document’s COUNTERPARTY into the ladder, by direction.

A draft is pre-direction data: it records what each party’s block said without deciding which of them the filer is. This is where that decision reaches the establishment question, and it makes it through the one authority that already makes it for the confirm path, so a document cannot be read as having one counterparty when its identity is resolved and another when its territory is.

The filer’s own side is never asked here. One party on every ingested invoice is the operator, whose territory is a profile fact; feeding their printed block to a ladder built for the counterparty would answer a question the profile already answers, and would answer it from weaker evidence.

The document’s TREATMENT is read here, not just its address block. The concordance rung and the Spain-indicating check both need what the invoice did about tax, so the regime mention and every charged IVA rate travel in beside the address values. They are drawn from the whole document rather than from the counterparty’s side of it, and that is correct rather than sloppy: an invoice states one treatment for the operation, not one per party, and the charged rate is the issuer’s claim about the supply.

Every rung is reachable from a read document, and only from a read one. The reading path recovers each party’s printed country name, so the country rung has a source and the postal rung – gated on country evidence positively naming Spain – can be triggered by it. The STRUCTURED path is the remaining gap and it is upstream of this function: the e-invoice parsers read a postal element and no country element, so a Facturae, UBL or CII document reaches only the concordance and confirmed-fact rungs. A Spanish counterparty on one of those exhausts to nothing and is asked once, which is the honest outcome rather than a defective one; what would be defective is reading this function’s correct answers as evidence that every path reaches every rung.

Parameters:
  • bucket_id (str) – Active profile bucket, for the confirmed-fact rung.

  • legends (tuple[RegimeLegend, ...]) – The dated registry declarations selected by the enclosing pinned authority operation.

  • operation (PinnedAuthorityOperation) – The caller-owned pinned authority operation used for the establishment ladder’s registry lookups.

  • draft (InvoiceDraft) – The pre-direction reading of the document.

  • kind (InvoiceKind) – Which side of the invoice the filer is on, as the operator settled it at confirm. Never the reader’s suggestion.

  • repository (CounterpartyEstablishmentRepositoryProtocol) – Injected store.

Returns:

the territory and the rung that settled it, a conflict, a contradiction, or none of them.

Return type:

CounterpartyEstablishment