cadrumo.application.ledger.identity_roles module¶
Which identifier on the page is the counterparty’s – decided, never guessed.
This module exists because of a measured defect, not a hypothetical one. Scanning a document for the first checksum-valid tax identifier produces a confident wrong answer whenever the real supplier’s identifier fails its own check character: validation correctly rejects the true one, the scan walks on, and the next valid identifier on the page – a bank’s, a gestoría’s, the taxpayer’s own – is returned as the supplier. Nothing fails. The record names a real, valid, entirely unrelated entity, and no downstream check can tell.
First-match is the defect. The fix is not a better scan order: any total order
over the candidates is still a guess wearing a ranking. When role evidence does
not pick exactly one candidate, the resolution is
AMBIGUOUS with every candidate surfaced, and
the operator – who has the document – decides.
Two exclusions are applied before candidacy, and both are load-bearing:
The taxpayer’s own identifier is never a counterparty candidate. It appears on
every invoice in the corpus, on both directions, so leaving it in the pool means
the most frequently occurring identifier on the page competes for the role it can
never hold. Comparison is on the normalised form, so a printed B-1234567-4
does not evade exclusion against a stored B12345674.
That exclusion asks an IDENTITY question, never a validity one, and routes
through same_tax_identifier() – the same predicate
counterparty_is_the_filer() uses, so a document
cannot evade one while being caught by the other. Routing it through
canonical_identity_token() instead makes it a validity question, and then
a filer whose stored identifier is foreign, or Spanish with a bad control
character, excludes nothing at all. The strict form remains correct for
verifying a candidate; the two are not interchangeable.
A checksum failure is recorded, not silently dropped. An identifier that fails
its control character is a real fact about the document – it is what makes the
true supplier invisible to a validating scan – so it surfaces as an
IDENTITY_UNVERIFIED finding rather than
vanishing. That is the difference between “we could not verify the supplier” and
“we found a supplier”, and the operator needs the first when the first is true.
An ABSENT counterparty identifier is not a failed role. A document that
prints no counterparty identifier at all, or prints only the filer’s own, states
no role for this resolver to get wrong: a factura simplificada may legitimately
omit the recipient’s NIF, and an ordinary domestic ticket identifies no customer.
Those raise no ROLE_UNRESOLVED. An
UNVERIFIABLE one still does, because that is the measured defect above. The
distinction is not cosmetic: every discrepancy kind blocks confirmation by
construction, so a blocker firing across the legitimate population is one an
operator learns to clear unread – and then clears on the checksum case too.
Withholding the finding never asserts that the role is fine. The resolution
carries resolved=None under an
UNANCHORED envelope whose note says the
document stated nothing, so the absence reads as “not asked” at every consumer.
EU identifiers count. A Spanish-only check silently discards every intra-EU
counterparty, which is precisely the Modelo 349 population – the filing that
exists to report them. Validation therefore routes through the repository’s
established EU IVA authority (core.identity) rather than a local
Spain-shaped test.
See also
FieldProvenanceThe envelope a resolution produces.
ground_ambiguous_candidates()The sanctioned constructor for the ambiguous outcome.
DraftDiscrepancyFindingCarrier for the unverified-identity and unresolved-role findings.
- class IdentityCandidate(**data)[source]¶
Bases:
BaseModelOne tax identifier observed on the document, with where it was read.
- Variables:
value – The identifier exactly as printed, separators and all.
anchor – The verbatim printed form, when it differs from the value (a label-prefixed occurrence, say). Defaults to the value itself.
country_code – ISO country the identifier is stated under, when the document says.
Nonemeans unstated, which is read as Spain only because that is what an unqualified identifier on a Spanish invoice means – never inferred from the identifier’s own shape.role_evidence – What on the page suggests this identifier belongs to the counterparty, if anything. Empty means no role evidence at all.
- Parameters:
data (Any)
- value: str¶
- anchor: str | None¶
- country_code: str | None¶
- role_evidence: str¶
- class IdentityRoleResolution(**data)[source]¶
Bases:
BaseModelThe outcome of resolving which candidate holds the counterparty role.
- Variables:
provenance – The envelope for the counterparty identifier field.
resolved – The canonical identifier when exactly one candidate survived,
Noneotherwise.Noneon an ambiguous resolution is the point: there is no value to carry forward, and a caller that wants one must take it from the operator.findings – Deterministic findings raised while resolving – unverified identifiers, and an unresolved role.
- Parameters:
data (Any)
- provenance: FieldProvenance¶
- resolved: str | None¶
- findings: tuple[DraftDiscrepancyFinding, ...]¶
- canonical_identity_token(value, *, country_code=None, operation)[source]¶
Return the canonical form of value, or
Nonewhen it does not verify.Routes Spanish identifiers through the AEAT control-character algorithm and non-Spanish ones through the EU IVA format authority. Returning
Nonerather than raising keeps this usable as a filter: a document carrying a malformed identifier is an ordinary case the caller reports, not an exception it must catch per candidate.An absent country asks the identifier’s own prefix before assuming Spain. An intra-community IVA number leads with its Member State’s prefix, so a document printing
SE556677889901states its country in the number itself – and that is exactly the document whose address block often states none. Defaulting the absence to Spain measured such a number against the AEAT control-character rule, which it cannot satisfy, so a perfectly well-formed foreign identifier yielded no verdict at all. The country the default assumed is the very thing the establishment ladder exists to determine, and the ladder’s own first rung already reads this prefix.The prefix is read through
country_code_for_printed_tax_identifier(), the single authority on which country a printed number names, rather than by matching two leading letters here: the prefix alone is not evidence, and that authority requires the body to match the structure its own prefix claims.A SUPPLIED country still decides, unchanged. A caller that names one is asserting it, and a supplied country disagreeing with the printed prefix is a disagreement to surface rather than to resolve silently in the prefix’s favour – so only the absence position moves.
- Parameters:
value (
str) – The identifier as printed.country_code (
str|None) – ISO country it is stated under. WhenNone, the identifier’s own prefix answers; an identifier carrying no recognisable prefix takes the Spanish path, which is right because a bareB-CIF is what an unqualified Spanish invoice prints.operation (
PinnedAuthorityOperation) – Caller-owned pinned authority operation for the country catalogue used to interpret a printed identifier.
- Return type:
- Returns:
The canonical identifier, or
Nonewhen it fails verification.
- resolve_counterparty_identity(*, field, candidates, taxpayer_tax_id, origin, operation)[source]¶
Resolve which observed identifier holds the counterparty role.
Never returns a first match. Exactly one surviving candidate resolves; two or more resolve to
AMBIGUOUScarrying every one of them; none resolves toUNANCHOREDwith an unresolved-role finding.- Parameters:
field (
str) – Name of the draft field being resolved.candidates (
tuple[IdentityCandidate,...]) – Every tax identifier observed on the document, in document order. Order is preserved for display only and is never used to break a tie – breaking a tie by position is first-match under another name.taxpayer_tax_id (
str|None) – This profile’s own identifier, excluded from candidacy. Never required to be checksum-valid or Spanish – exclusion is an identity comparison.Noneor blank means unknown, which weakens the resolution and is reported in the note rather than passed over.origin (
FieldOrigin) – How the candidates were obtained.operation (
PinnedAuthorityOperation) – Caller-owned pinned authority operation for identifier country interpretation.
- Return type:
- Returns:
The resolution, carrying the envelope, any resolved value, and every deterministic finding raised on the way.