aeat.application.aggregation._percepciones_observations_repository module

Encrypted persistence for per-perceptor-clave percepciones records (Modelo 190).

The DEDICATED store the percepciones-count resolver reads to count percepciones DISTINCTLY. Modelo 190 casilla decl.total-percepciones (“Número total de percepciones … Número de registros de tipo 2”, AEAT Diseño de Registros) is the count of DISTINCT (perceptor NIF, clave, subclave) type-2 records on the annual declaration — a perceptor paid under two claves files two percepciones — NOT the distinct-NIF perceptor count (that is RET-1’s perceptor_count for Modelo 180/193) and NOT the sum of the quarterly Modelo 111 perceptor counts. The validated distinct-count primitive (the percepcion_count withholding fact) already exists; what it lacked was a persisted, calc-mesh-readable per-perceptor source so the calculate path could compute the distinct count instead of falling back to the wrong quarterly sum. This module is that source: it persists each clave-bearing WithholdingObservation keyed by (modelo, filing_year, period) plus the per-perceptor-clave identity, so the pull and calculate surfaces read ONE store (one-aggregation-path-pull-equals-calculate).

Sensitivity is SensitivityClass FINANCIAL — perceptor NIFs are identity-bearing financial data, stored encrypted at rest through a SecureBoundRepository that writes Envelope records. The plaintext NIF lives only inside the encrypted payload; the object key carries the sha256 of the NIF (the iva-wallet-decision key convention), never the cleartext value (sensitive-financial-data-secure-storage-only). The namespace, schema version, object-key grammar, and custody disposition are declared by adapters.persistence.storage.WITHHOLDING_OBSERVATIONS_NAMESPACE.

ADR 2026-06-25-modelo-190-percepciones-count-adr. Producers (the pull/aggregate entrypoints) write here through one shared helper; the P03 calc-mesh resolver reads here and calls the distinct-count primitive. This is the percepciones counterpart of _retencion_observations_repository (the perceptores store); the two stores are intentionally distinct — different distinct keys (NIF+clave+subclave vs NIF), different models, different modelos.

This module was renamed from _withholding_observations_repository.py per the import-centralization ADR ruling 4: the “withholding” stem collided with the project’s Spanish-stem naming convention (retencion already names the sibling Modelo 180/193 store), so this module — and the repository symbols it owns locally — follow the percepciones stem instead. The WithholdingObservation domain type it wraps is an unrelated, widely shared registry taxonomy type and is out of scope for this rename.

percepcion_observation_key(modelo, filing_year, period, perceptor_tax_id, clave, subclave)[source]

Opaque per-perceptor-clave object key — the NIF is hashed, never cleartext.

Secure-object payloads are encrypted, but object keys are storage metadata, so the perceptor NIF is sha256-hashed (the iva-wallet-decision key convention). Distinct (perceptor NIF, clave, subclave) triples persist as distinct rows so a perceptor paid under more than one clave is preserved as the distinct percepciones the Diseño counts (registros de tipo 2).

Return type:

str

Parameters:
class PercepcionObservationRepository(*, bucket_id=None, objects=None, settings=None)[source]

Bases: SecureBoundRepository[_PercepcionObservationEnvelopePayload]

Encrypted repository for per-perceptor-clave WithholdingObservation payloads.

The SecureBoundRepository base wraps each payload in a Envelope under adapters.persistence.storage.WITHHOLDING_OBSERVATIONS_NAMESPACE and enforces the namespace’s FINANCIAL SensitivityClass.

See also

adapters.persistence.storage.WITHHOLDING_OBSERVATIONS_NAMESPACE

Secure-object namespace and hashed object-key contract.

percepcion_observation_key()

Deterministic key builder that hashes the NIF and preserves clave/subclave identity.

persist_percepcion_observations()

Shared producer write path for pull and calculate parity.

Parameters:
namespace: ClassVar[str]
sensitivity: ClassVar[SensitivityClass]
schema_version: ClassVar[int]
payload_type

alias of _PercepcionObservationEnvelopePayload

extract_identifier(payload)[source]

Return the natural id for payload (used as the SQL object key).

Subclasses MUST override. The base implementation raises NotImplementedError.

Return type:

str

Parameters:

payload (_PercepcionObservationEnvelopePayload)

save_observation(*, modelo, filing_year, period, observation, source_kind, captured_at=None, source_metadata=None)[source]

Persist one per-perceptor-clave row keyed by (modelo, year, period, NIF, clave, subclave).

Return type:

None

Parameters:
replace_observations(*, modelo, filing_year, period, observations, source_kind, captured_at=None, source_metadata=None)[source]

Replace the FULL per-perceptor-clave set for one (modelo, filing_year, period).

SET-REPLACE, not additive upsert: clears any prior rows for the exact key-tuple, then writes the supplied set. A re-pull where the operator DROPPED a percepción must not leave the stale row behind — otherwise the next calculate’s distinct count is inflated by a percepción no longer declared (a silent over-count). An empty observations clears the window (the operator declared none); the P03 resolver surfaces a no-silent advisory when it then reads empty.

Return type:

None

Parameters:
load_observations(modelo, period)[source]

Return persisted per-perceptor-clave WithholdingObservation records.

The result covers one (modelo, filing_year, period) window.

The calc-mesh percepciones-count resolver (P03) folds these through the validated distinct-count primitive. An empty tuple means no per-perceptor records were persisted for the window — the resolver MUST surface a no-silent advisory rather than materialising a silent zero.

Return type:

tuple[WithholdingObservation, ...]

Parameters:
iter_modelo(modelo)[source]

Yield every persisted per-perceptor-clave payload for modelo in unspecified order.

Return type:

Iterator[_PercepcionObservationEnvelopePayload]

Parameters:

modelo (str)

persist_percepcion_observations(*, modelo, filing_year, period, observations, source_kind='aggregate_pull')[source]

The ONE shared write path every per-perceptor-clave producer calls.

Factoring the persist behind a single application helper makes store completeness STRUCTURAL rather than per-entrypoint discipline a future producer could forget (an unwritten producer -> an incomplete store -> the exact pull!=calculate divergence #28 fixes). Writes to the active bucket’s encrypted store with SET-REPLACE semantics so pull and calculate read one source.

Return type:

None

Parameters: