Source code for aeat.adapters.persistence.profile.modelos_filing

"""Encrypted SQL repository for the modelo filing-record catalogue.

:class:`ModeloRecordCatalogueRepository` persists and loads
:class:`ModeloRecord` entries in a :class:`ModeloRecordCatalogue` via
:class:`~adapters.persistence.storage.SecureObjectRepository` at
``FINANCIAL`` :class:`~adapters.persistence.storage.SensitivityClass`
using an :class:`~adapters.persistence.storage.Envelope` wrapper. The
catalogue is stored as a single encrypted BLOB per profile bucket.

This concrete repository is the persistence adapter behind the read-side
:class:`~domain.modelos.ModeloRecordCatalogueRepositoryProtocol`. It lives
in the persistence adapter (not in :mod:`~domain.modelos`) because its
secure-object coupling is SQL/crypto-bound; the domain package owns only the
typed :class:`ModeloRecordCatalogue` model and its pure mutators.

See Also:
    :mod:`~adapters.persistence.profile._modelo_runtime`
        Bucket-id resolution and runtime secure-object factory shared by modelo
        persistence adapters.
    :class:`~domain.modelos.ModeloRecordCatalogue`
        Domain catalogue payload encrypted by this repository.
    :class:`~domain.modelos.ModeloRecordCatalogueRepositoryProtocol`
        Domain port this concrete persistence adapter implements.
    :data:`~adapters.persistence.storage.MODELO_FILING_RECORD_CATALOGUE_NAMESPACE`
        Central namespace, sensitivity, schema-version, and singleton-key
        contract for these secure objects.
    :mod:`~adapters.persistence.profile.modelos_work_units`
        Sibling work-unit catalogue repository whose current/filed pointers
        reference filing records stored here.
    :func:`~application.modelo.file_modelo_revision`
        Application service that writes local/internal filing state through the
        modelo catalogue repositories.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from ....core.external_constants import UTF_8_ENCODING
from ....core.logging import get_logger
from ....core.time import now
from ....domain.modelos import (
    ModeloRecordCatalogue,
    ModeloRecordPersistenceError,
    raise_catalogue_integrity_error,
)
from ._modelo_runtime import resolve_modelo_repository_bucket_id, secure_objects_for_modelo_bucket

if TYPE_CHECKING:  # pragma: no cover — import-cycle guard
    from ..storage import SecureObjectRepository, SecureObjectWrite

_LOGGER = get_logger(__name__)
# namespace string preserved across rename to avoid orphaning persisted envelopes
_FILING_NAMESPACE = "aeat.domain.modelos.filing_records"
_FILING_OBJECT_KEY = "catalogue"
_FILING_CATALOGUE_VERSION = 1
_FILING_PERSISTENCE_MESSAGE = "errors.fail.fail_modelo_filing_record_persistence"


[docs] class ModeloRecordCatalogueRepository: """Repository over encrypted SQL-backed filing-record catalogue storage. :data:`~adapters.persistence.storage.MODELO_FILING_RECORD_CATALOGUE_NAMESPACE` is the central namespace, schema-version, sensitivity, and singleton-key contract for the encrypted :class:`ModeloRecordCatalogue` row. The catalogue payload keeps member-scoped current/history lookups in the domain type, while this repository wraps it in :class:`~adapters.persistence.storage.Envelope` and writes it through :class:`~adapters.persistence.storage.SecureObjectRepository`. It exposes the concrete load/save implementation behind :class:`~domain.modelos.ModeloRecordCatalogueRepositoryProtocol`. """ def __init__(self, *, bucket_id: str | None = None, objects: SecureObjectRepository | None = None) -> None: """Bind to a profile bucket's secure-object store, or an injected one. Args: bucket_id: Profile bucket whose encrypted store backs this repository; resolved from the active session when ``None``. objects: Optional injected secure-object repository (testing seam). """ self._bucket_id = bucket_id.strip() if bucket_id is not None else None if objects is not None: self._objects = objects return self._bucket_id = resolve_modelo_repository_bucket_id(bucket_id, error_type=ModeloRecordPersistenceError) self._objects = secure_objects_for_modelo_bucket(self._bucket_id) @property def bucket_id(self) -> str | None: """Profile bucket this repository reads from and writes to. A bucket is the per-profile partition that isolates one taxpayer's encrypted records from another's. Returns the resolved bucket identifier, or ``None`` when the repository was constructed against an injected :class:`~adapters.persistence.storage.SecureObjectRepository` and no bucket id was supplied. """ return self._bucket_id
[docs] def exists(self) -> bool: """Report whether a persisted filing-record catalogue exists. Returns ``True`` when an encrypted catalogue BLOB has already been written for this bucket, ``False`` otherwise. This is a presence check only; it neither decrypts nor validates the stored payload. Call ``load`` to retrieve and verify the catalogue contents. """ return self._objects.exists(_FILING_NAMESPACE, _FILING_OBJECT_KEY)
[docs] def load(self) -> ModeloRecordCatalogue: """Load and decrypt the filing-record catalogue from storage. A modelo is an AEAT tax form, and each filing record is the durable receipt that a calculation revision (a dated, immutable result for that form) was filed for a given form, year, and period. The whole catalogue is persisted as one encrypted FINANCIAL-class BLOB and returned as a :class:`ModeloRecordCatalogue`. Returns an empty catalogue when nothing has been persisted yet. Returns: The decrypted :class:`ModeloRecordCatalogue` for this bucket. Raises: :class:`ModeloRecordPersistenceError`: If the stored envelope fails its sensitivity-class or schema-version integrity checks, or if its on-disk classification is not FINANCIAL, or if it was written at a schema version newer than this consumer supports. """ from ..storage import ( ClassificationError, Envelope, EnvelopeVersionError, SensitivityClass, ) try: record = self._objects.load( _FILING_NAMESPACE, _FILING_OBJECT_KEY, expected_class=SensitivityClass.FINANCIAL, max_supported_version=_FILING_CATALOGUE_VERSION, ) except (ClassificationError, EnvelopeVersionError) as exc: raise_catalogue_integrity_error( exc, error_cls=ModeloRecordPersistenceError, label="filing-record", translated_message=_FILING_PERSISTENCE_MESSAGE, logger=_LOGGER, ) if record is None: return ModeloRecordCatalogue() envelope = Envelope[ModeloRecordCatalogue].model_validate_json(record.payload.decode(UTF_8_ENCODING)) if envelope.classification is not SensitivityClass.FINANCIAL: _LOGGER.error( "filing-record catalogue classification mismatch", extra={ "expected_classification": SensitivityClass.FINANCIAL.value, "actual_classification": envelope.classification.value, }, ) raise ModeloRecordPersistenceError( "filing-record catalogue classification mismatch", translated_message=_FILING_PERSISTENCE_MESSAGE, context={ "reason": "classification_mismatch", "expected_classification": SensitivityClass.FINANCIAL.value, "actual_classification": envelope.classification.value, }, ) if envelope.schema_version > _FILING_CATALOGUE_VERSION: _LOGGER.error( "filing-record catalogue envelope version unsupported", extra={ "stored_schema_version": envelope.schema_version, "max_supported_version": _FILING_CATALOGUE_VERSION, }, ) raise ModeloRecordPersistenceError( "filing-record catalogue envelope version unsupported", translated_message=_FILING_PERSISTENCE_MESSAGE, context={ "reason": "unsupported_envelope_version", "stored_schema_version": envelope.schema_version, "max_supported_version": _FILING_CATALOGUE_VERSION, }, ) return envelope.payload
[docs] def save(self, catalogue: ModeloRecordCatalogue) -> None: """Persist the filing-record catalogue as a single encrypted BLOB. Wraps ``catalogue`` in a ``FINANCIAL``-class :class:`~adapters.persistence.storage.Envelope` stamped with the current schema version and write timestamp, then writes it through the secure object store. The entire catalogue is rewritten as one encrypted object per bucket, replacing any prior catalogue for this bucket. Args: catalogue: The :class:`ModeloRecordCatalogue` to encrypt and store. """ self._objects.save_many((self.to_secure_object_write(catalogue),))
[docs] def to_secure_object_write(self, catalogue: ModeloRecordCatalogue) -> SecureObjectWrite: """Return the secure-object upsert for ``catalogue`` without committing it. The returned :class:`~adapters.persistence.storage.SecureObjectWrite` carries the same :class:`~adapters.persistence.storage.Envelope` and :class:`~adapters.persistence.storage.SensitivityClass` classification that :meth:`save` would persist directly. """ from ..storage import Envelope, SecureObjectWrite, SensitivityClass envelope = Envelope[ModeloRecordCatalogue]( schema_version=_FILING_CATALOGUE_VERSION, written_at=now(), classification=SensitivityClass.FINANCIAL, payload=catalogue, ) return SecureObjectWrite( namespace=_FILING_NAMESPACE, object_key=_FILING_OBJECT_KEY, classification=SensitivityClass.FINANCIAL, schema_version=_FILING_CATALOGUE_VERSION, written_at=envelope.written_at, payload=envelope.model_dump_json().encode(UTF_8_ENCODING), )
[docs] def save_with_secure_object_writes( self, catalogue: ModeloRecordCatalogue, extra_writes: tuple[SecureObjectWrite, ...], ) -> None: """Persist ``catalogue`` plus related secure objects in one unit of work. Args: catalogue: The :class:`ModeloRecordCatalogue` to persist. extra_writes: Additional :class:`~adapters.persistence.storage.SecureObjectWrite` objects to commit atomically with the catalogue. """ self._objects.save_many((self.to_secure_object_write(catalogue), *extra_writes))
__all__ = [ "ModeloRecordCatalogueRepository", ]