cadrumo.application.ledger.counterparty_establishment module

Where a counterparty is established, asked once and remembered against the entity.

Establishment is a property of the ENTITY, not of the invoice. A supplier in Las Palmas is in Las Palmas on every document they ever send, so a pipeline that carries the answer per invoice asks the same question every month and stores four hundred copies of one fact.

That matters here because of what the paper does not say. A domestic Spanish invoice routinely prints a bare B-CIF, no country line, and a five-digit postal code that France, Germany and Italy also use — so the printed evidence establishes neither party’s IVA territory, and the three Spanish territories (peninsula and Balearics under LIVA, Canarias under IGIC, Ceuta and Melilla under IPSI) are treated differently by law. The honest answer on that population is to ask. The reason asking is viable rather than exhausting is this module: one question per counterparty whose paper is non-decisive, never one per invoice.

An absent fact is never a territory. There is no branch here that produces the registry-projected mainland token — or any other projected scope — from the absence of a record. The peninsula is the majority population, so a default there would pass every test written by someone with mainland fixtures while silently placing Canarian and Ceutan parties inside a territory their operations are not subject to. Absence returns nothing and the caller asks.

Identity is the canonical tax identifier, and nothing else. The key is a digest of canonical_identity_token() — the same authority the counterparty-role resolver already verifies candidates through, so a printed ESB-1234567-4, ES B12345674 and B12345674 all address one record rather than three. A name is deliberately not identity: two unrelated companies share one, and keying on it would hand one entity’s territory to another. An unverifiable identifier yields no key at all, which is the conservative direction — no record is written and no record is found, so the operator is asked again rather than answered wrongly.

A stored fact never silently overrides the document, and the document never silently overrides a stored fact. Where a later document’s printed evidence resolves a territory and it disagrees with what the operator confirmed, that is a real signal — a confirmed-Canarian party printing a French country code is either a different entity, a changed establishment, or a mistaken assertion — and none of the three is settled by preferring one side. The resolution carries a CounterpartyEstablishmentContradiction and NO fact, so a caller cannot proceed on either value by accident.

See also

DeclaredFacts

The one channel a supplied fact reaches the criteria assembly through. This module produces a member of it and opens no second route.

canonical_identity_token()

The identifier authority the counterparty key is derived from.

IvaTerritorialScope

The territory a confirmed fact carries.

exception ConfirmedCounterpartyFactsInputError(message=None, *, context=None, translated_message=None, precondition_verdict=None)[source]

Bases: LedgerPreconditionErrorMixin, CadrumoError

Raised when a counterparty establishment assertion is not storable.

Parameters:
Return type:

None

code
exception CounterpartyEstablishmentConflictError(message=None, *, context=None, translated_message=None)[source]

Bases: CadrumoError

Raised when an assertion contradicts the one already confirmed for a counterparty.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
confirmed_counterparty_facts_key(tax_identifier, *, country_code=None)[source]

Return the record key for a counterparty, or None when it has no identity.

The key is the SHA-256 of the canonical identifier rather than the identifier itself, because it becomes the storage object key and an object key sits outside the encrypted payload: addressing a record by a real counterparty NIF would put a taxpayer’s trading partner in the clear.

Returning None for an identifier that does not verify is the whole treatment of that case. There is no fallback key derived from the raw string: two different unverifiable readings of one page would collide under it, and the collision would silently share one entity’s territory with another.

Parameters:
  • tax_identifier (str) – The counterparty’s identifier as printed.

  • country_code (str | None) –

    The country the identifier is stated under, when the document says. None asks the identifier’s own prefix, matching the identifier authority — an unqualified identifier on a Spanish invoice is Spanish, a prefixed intra-community one names its own Member State, and a foreign one printed without its prefix simply does not verify.

    That distinction is load-bearing here rather than cosmetic. While the absence defaulted to Spain, a foreign counterparty whose document printed a prefixed IVA number and no address country got no key at all — so no confirmed establishment fact could be stored for them and none could be retrieved, disabling the ladder’s remembered-fact rung for exactly the population the intra-community and export treatment exists for.

Return type:

str | None

Returns:

The 64-character key, or None when the identifier does not verify.

class ConfirmedCounterpartyFacts(**data)[source]

Bases: BaseModel

One confirmed statement of where a counterparty is established.

Variables:
  • counterparty_key – The record key, derived from the canonical identifier. Clock-free: the same counterparty always addresses the same record, so a retry finds the existing one rather than minting a second.

  • canonical_tax_identifier – The identifier in its canonical form, held inside the encrypted payload. Kept so an operator reviewing the store sees whom each record is about — the key alone is a digest.

  • territorial_scope – The territory the operator confirmed.

  • source – Who established it. Constrained to OPERATOR_ASSERTION: the ladder’s earlier rungs read printed evidence per document and are re-read per document, so a document-sourced value cached here would answer as an assertion and take the contradiction channel offline.

  • asserted_by – The operator identity that confirmed it.

  • asserted_at – When it was confirmed. A last-seen body field, never folded into the key.

  • identification_state –

    The Member State that IVA-identifies this counterparty, when the operator has confirmed one. A DIFFERENT fact from territorial_scope, which is where the party is: Ley 37/1992 art. 25 exempts on the registration, arts. 69-70 govern the place, and the two diverge in real trade. Neither is ever read for the other.

    None means unanswered, never “identified nowhere” and above all never “identified in Spain”. A decision needing it refuses with a review item rather than reading the territory beside it.

  • note – What the operator relied on, in their own words. Empty when they said nothing.

Parameters:

data (Any)

counterparty_key: ContentDigest
canonical_tax_identifier: str
territorial_scope: IvaTerritorialScope | None
identification_state: EUMemberState | None
source: ClassifierInputSource
asserted_by: str
asserted_at: datetime
note: str
classmethod create(*, tax_identifier, asserted_by, territorial_scope=None, identification_state=None, country_code=None, note='', asserted_at=None)[source]

Build a fact for one counterparty, refusing an identifier with no identity.

Raises:

ConfirmedCounterpartyFactsInputError – When the identifier does not verify, so there is nothing stable to key the fact to.

Return type:

Self

Parameters:
property declared_fact: DeclaredFact[IvaTerritorialScope] | None

Return this fact in the form the criteria assembly consumes, or None.

The value and its attribution travel together, so a classification standing on a remembered assertion records that an operator said so — not that a document did.

None when the operator answered the identification and left the territory open. That is a question NOT ASKED, and it must reach the assembly as a missing input rather than as any territory: the mainland is the majority answer, so a default here would be invisible in testing while placing Canarian and Ceutan counterparties inside a territory their operations are not subject to.

property declared_identification: DeclaredFact[EUMemberState] | None

Return the confirmed identification in the assembly’s channel, or None.

None when the operator has not answered it. A caller must not read that as an answer – it is the absence the review item exists for.

class CounterpartyEstablishmentContradiction(**data)[source]

Bases: BaseModel

A later document’s territory evidence disagreeing with the confirmed fact.

Carried rather than resolved. The stored value is an operator’s claim about an entity and the printed value is an issuer’s claim about one document, and a disagreement is one of three things — a different entity behind a reused identifier, an establishment that moved, or an assertion made in error. None is settled by preferring a side, so both are shown and a human decides.

Variables:
  • counterparty_key – The record the disagreement is about.

  • canonical_tax_identifier – Whom, in readable form.

  • confirmed_scope – What the operator confirmed for this counterparty.

  • evidenced_scope – What this document’s printed evidence resolved to.

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

Parameters:

data (Any)

counterparty_key: ContentDigest
canonical_tax_identifier: str
confirmed_scope: IvaTerritorialScope
evidenced_scope: IvaTerritorialScope
detail: str
class ConfirmedCounterpartyResolution(**data)[source]

Bases: BaseModel

What the store had to say about one counterparty on one document.

Three states, and “probably” is not one of them: a usable fact, a contradiction, or neither. The two populated fields are mutually exclusive by construction — a contradicted resolution carries no fact, so a caller reading fact cannot proceed on a value the evidence disputes.

Variables:
  • fact – The remembered assertion, in the channel the assembly consumes. None when nothing was confirmed for this counterparty, when the identifier had no identity, or when the evidence contradicts it.

  • identification – The confirmed Member State of IVA identification, in the same channel. None when the operator has not answered it, which is independent of whether the territory was answered – the two are different questions about the same entity and either may stand alone. A caller must not read this None as “identified nowhere”.

  • contradiction – The disagreement, when there is one.

Parameters:

data (Any)

fact: DeclaredFact[IvaTerritorialScope] | None
identification: DeclaredFact[EUMemberState] | None
contradiction: CounterpartyEstablishmentContradiction | None
property contradicted: bool

Return whether the evidence disputes the confirmed fact.

record_confirmed_counterparty_facts(*, bucket_id, tax_identifier, asserted_by, territorial_scope=None, identification_state=None, country_code=None, note='', asserted_at=None, repository)[source]

Confirm where a counterparty is established, once, for every later document.

Idempotent on the whole record rather than on the key alone. A retry carrying the same territory returns the STORED fact unchanged — the original asserted_at and asserted_by survive, because re-stamping them would make a repeated call look like a fresh confirmation. A call carrying a DIFFERENT territory refuses: overwriting would silently discard an answer the operator gave, and quietly reclassify every invoice already derived under it.

The identification axis takes the same conflict rule, with one asymmetry that is not a softening. Supplying an identification where NONE is stored is an ADDITION – the operator is answering a second question about the same entity, not replacing an answer – so it is written through. Supplying a DIFFERENT one refuses exactly as a territory would. Supplying none leaves a stored answer standing, because None is an unasked question and reading it as “identified nowhere” would let a partial retry silently withdraw a fact art. 25 turns on.

A same-key call differing only in note is a genuine correction of free prose and is written through, since no classification stands on it.

Parameters:
  • bucket_id (str) – Active profile bucket.

  • tax_identifier (str) – The counterparty’s identifier as printed.

  • territorial_scope (IvaTerritorialScope | None) – The territory the operator confirms.

  • asserted_by (str) – Operator identity making the claim.

  • identification_state (EUMemberState | None) – The Member State that IVA-identifies the counterparty, when the operator answers it. None does not answer the question and never erases a stored answer.

  • country_code (str | None) – The country the identifier is stated under, if any.

  • note (str) – What the operator relied on. Free prose, never consulted.

  • asserted_at (datetime | None) – Override clock, for deterministic tests.

  • repository (CounterpartyEstablishmentRepositoryProtocol) – Injected store.

Return type:

ConfirmedCounterpartyFacts

Returns:

The persisted fact, or the pre-existing one on an idempotent retry.

Raises:
forget_confirmed_counterparty_facts(*, bucket_id, tax_identifier, country_code=None, repository)[source]

Withdraw a confirmed establishment fact, returning whether one was held.

The sanctioned way to correct an assertion, and deliberately a separate act from making one: withdrawing states that the earlier answer was wrong, which an overwrite would have performed silently.

Return type:

bool

Parameters:
resolve_confirmed_counterparty_facts(*, bucket_id, tax_identifier, country_code=None, evidenced_scope=None, repository)[source]

Ask the store what is confirmed about this counterparty, and never guess.

The last rung of the establishment ladder. It is consulted after the printed evidence has had its turn, so evidenced_scope is what those rungs produced: None when the paper settled nothing, which is the case this rung exists for.

Every path that does not find a usable fact returns an EMPTY resolution. There is no territory-shaped default anywhere in this function, and the empty resolution is what makes the caller ask.

Parameters:
  • bucket_id (str) – Active profile bucket.

  • tax_identifier (str | None) – The counterparty’s identifier as printed, or None when the document printed none. None resolves to nothing: a document with no counterparty identifier has no entity to remember anything about.

  • country_code (str | None) – The country the identifier is stated under, if any.

  • evidenced_scope (IvaTerritorialScope | None) – What the printed evidence resolved to, when it resolved. Supplied so agreement can be recorded and disagreement surfaced; never used to write.

  • repository (CounterpartyEstablishmentRepositoryProtocol) – Injected store.

Return type:

ConfirmedCounterpartyResolution

Returns:

The fact, a contradiction, or neither.

class CounterpartyConfirmationOutcomeV1(**data)[source]

Bases: BaseModel

A confirmation and whether THIS call is the one that recorded it.

The distinction is the whole point of an idempotent writer: a repeat must be able to say “your answer was already stored, by whom, and when” rather than reporting a fresh confirmation. Callers used to derive this by handing in their own timestamp and comparing it against the returned record, which made every surface reconstruct knowledge the writer already had.

Parameters:

data (Any)

facts: ConfirmedCounterpartyFacts
recorded: bool
confirm_counterparty_establishment(*, bucket_id, tax_identifier, asserted_by, territorial_scope=None, identification_state=None, country_code=None, note='', repository)[source]

Confirm a counterparty’s establishment and say whether this call wrote it.

Refuses a call answering neither axis, then records through record_confirmed_counterparty_facts() and reports whether the stored record is the one this call supplied.

The stamp is generated here rather than by the writer’s own clock so the comparison has no check-then-act window: a pre-read would let a concurrent writer land between the read and the write, whereas comparing the returned stamp against the one handed in cannot, because the writer preserves the ORIGINAL stamp on a retry precisely so a repeat cannot look fresh.

Parameters:
  • bucket_id (str) – The owning profile bucket.

  • tax_identifier (str) – The counterparty being confirmed.

  • asserted_by (str) – Who is answering.

  • territorial_scope (IvaTerritorialScope | None) – Where the party is established, when answered.

  • identification_state (EUMemberState | None) – Where it is IVA-identified, when answered.

  • country_code (str | None) – Country context for canonicalising the identifier.

  • note (str) – Free prose; carries no classification weight.

  • repository (CounterpartyEstablishmentRepositoryProtocol) – Required remembered-counterparty store capability.

Return type:

CounterpartyConfirmationOutcomeV1

Returns:

The stored facts and whether this call recorded them.

Raises:

ConfirmedCounterpartyFactsInputError – When neither axis was answered.