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
DeclaredFactsThe 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.
IvaTerritorialScopeThe territory a confirmed fact carries.
- exception ConfirmedCounterpartyFactsInputError(message=None, *, context=None, translated_message=None, precondition_verdict=None)[source]¶
Bases:
LedgerPreconditionErrorMixin,CadrumoErrorRaised when a counterparty establishment assertion is not storable.
- Parameters:
message (str | None)
translated_message (str | None)
precondition_verdict (PreconditionVerdict | None)
- Return type:
None
- code¶
- exception CounterpartyEstablishmentConflictError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
CadrumoErrorRaised when an assertion contradicts the one already confirmed for a counterparty.
- Parameters:
- Return type:
None
- code¶
- confirmed_counterparty_facts_key(tax_identifier, *, country_code=None)[source]¶
Return the record key for a counterparty, or
Nonewhen 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
Nonefor 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.The country the identifier is stated under, when the document says.
Noneasks 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:
- Returns:
The 64-character key, or
Nonewhen the identifier does not verify.
- class ConfirmedCounterpartyFacts(**data)[source]¶
Bases:
BaseModelOne 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.Nonemeans 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:
tax_identifier (str)
asserted_by (str)
territorial_scope (IvaTerritorialScope | None)
identification_state (EUMemberState | None)
country_code (str | None)
note (str)
asserted_at (datetime | None)
- 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.
Nonewhen 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.Nonewhen 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:
BaseModelA 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:
BaseModelWhat 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
factcannot proceed on a value the evidence disputes.- Variables:
fact – The remembered assertion, in the channel the assembly consumes.
Nonewhen 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.
Nonewhen 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 thisNoneas “identified nowhere”.contradiction – The disagreement, when there is one.
- Parameters:
data (Any)
- fact: DeclaredFact[IvaTerritorialScope] | None¶
- identification: DeclaredFact[EUMemberState] | None¶
- contradiction: CounterpartyEstablishmentContradiction | None¶
- 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_atandasserted_bysurvive, 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
Noneis 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
noteis 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.Nonedoes 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:
- Returns:
The persisted fact, or the pre-existing one on an idempotent retry.
- Raises:
ConfirmedCounterpartyFactsInputError – When the identifier does not verify.
CounterpartyEstablishmentConflictError – When a different territory, or a different identification, is already confirmed for this counterparty.
- 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:
- Parameters:
bucket_id (str)
tax_identifier (str)
country_code (str | None)
repository (CounterpartyEstablishmentRepositoryProtocol)
- 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_scopeis what those rungs produced:Nonewhen 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, orNonewhen the document printed none.Noneresolves 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:
- Returns:
The fact, a contradiction, or neither.
- class CounterpartyConfirmationOutcomeV1(**data)[source]¶
Bases:
BaseModelA 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:
- Returns:
The stored facts and whether this call recorded them.
- Raises:
ConfirmedCounterpartyFactsInputError – When neither axis was answered.