aeat.application.aggregation._retencion_observations_repository module

Encrypted persistence for per-perceptor retención records (Modelo 180/193).

The DEDICATED store the retenciones-summary family reads to count perceptors DISTINCTLY. Modelo 180 casilla decl.total-perceptores (“Número total de perceptores … Número de registros de tipo 2”, AEAT Diseño de Registro) is the count of distinct perceptor NIFs on the annual declaration, NOT the sum of the quarterly Modelo 115 aggregate counts. The validated distinct-count primitive aggregate_retenciones_180 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 RetencionObservation (perceptor NIF + scheme + taxable base + retención) keyed by (modelo, filing_year, period) plus the per-perceptor identity, so the pull and calculate surfaces read ONE store.

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.RETENCION_OBSERVATIONS_NAMESPACE.

ADR 2026-06-24-retenciones-perceptor-count-adr. Producers (the pull/aggregate entrypoints) write here through one shared helper; the P02 calc-mesh resolver reads here and calls the distinct-count primitive.

retencion_observation_key(modelo, filing_year, period, perceptor_nif, scheme)[source]

Opaque per-perceptor 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, scheme) pairs persist as distinct rows so a perceptor paid under more than one scheme is preserved while the distinct-NIF count stays correct.

Return type:

str

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

Bases: SecureBoundRepository[_RetencionObservationEnvelopePayload]

Encrypted repository for per-perceptor RetencionObservation payloads.

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

See also

adapters.persistence.storage.RETENCION_OBSERVATIONS_NAMESPACE

Secure-object namespace and hashed object-key contract.

retencion_observation_key()

Deterministic key builder that keeps the plaintext NIF out of storage metadata.

persist_retencion_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 _RetencionObservationEnvelopePayload

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 (_RetencionObservationEnvelopePayload)

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

Persist one per-perceptor retención row keyed by (modelo, filing_year, period, NIF, scheme).

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 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 perceptor must not leave the stale row behind — otherwise the next calculate’s distinct count is inflated by a perceptor no longer declared (a silent over-count, the inverse of the bug RET-1 fixes). An empty observations clears the window (the operator declared none); the P02 resolver raises a no-silent AggregationValidationError when a declaring revision then reads an empty store, before a zero perceptor count can be filed.

Return type:

None

Parameters:
load_observations(modelo, period)[source]

Return every persisted per-perceptor observation for one (modelo, filing_year, period).

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

Return type:

tuple[RetencionObservation, ...]

Returns:

Persisted RetencionObservation records for the requested window.

Parameters:
iter_modelo(modelo)[source]

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

Return type:

Iterator[_RetencionObservationEnvelopePayload]

Parameters:

modelo (str)

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

The ONE shared write path every per-perceptor 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 RET-1 fixes). Writes to the active bucket’s encrypted store with SET-REPLACE semantics so pull and calculate read one source. aggregate_per_modelo stays pure — persistence is the entrypoint’s job, not the aggregator’s (aeat-architecture-boundaries).

Return type:

None

Parameters: