"""Pydantic v2 schema for the :mod:`domain.filing` subpackage.
Every type in this module is a strict, frozen pydantic v2 model
or a closed :class:`enum.StrEnum`. These are the boundary-crossing
records the rest of the project pins against — keep them stable.
"""
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
from enum import StrEnum
from pydantic import BaseModel, Field
from ...core import STRICT_FROZEN_CONFIG, BindingSourceKind, Period
from ...core.errors import BaseSeverity
from ...core.hashing import content_hash_hex
from ...core.i18n import Translatable as tr
from ...core.identity import SubjectTaxId
from ..calculations.registry import BindingId, CasillaId, FormulaId, LegalRefId, RegistrySnapshotRef, SourceRefId
from ..submission import ModeloDraftStatus
APPROVAL_BASIS_VERSION = "review-basis-v4"
[docs]
class ModeloValueKind(StrEnum):
"""Provenance kind of a :class:`ModeloValue`."""
LITERAL = "LITERAL"
COMPUTED = "COMPUTED"
INHERITED = "INHERITED"
DEFAULT = "DEFAULT"
EMPTY = "EMPTY"
# A type alias for the small set of primitive value types a casilla
# can carry. Pydantic will parse JSON values back into the right
# Python type via this union (Decimal is preferred over float for
# any monetary value).
ModeloScalar = Decimal | int | str | bool | date | None
[docs]
class ModeloValue(BaseModel):
"""The typed value of one casilla on a :class:`ModeloDraft`.
Attributes:
casilla_id: Stable casilla ID (e.g. ``"03"``).
value: The scalar value carried by this casilla. ``None``
iff ``kind`` is :attr:`ModeloValueKind.EMPTY`.
kind: Provenance kind — literal user input, computed,
inherited from a previous draft, default from the
casilla schema, or empty.
source: Free-text provenance string — e.g.
``"user-supplied"``, ``"computed from 01,02"``,
``"default per modelo schema"``.
formula_trace_casilla_ids: For ``COMPUTED`` values, the casilla IDs
that fed the computation. ``None`` for non-computed
kinds.
"""
model_config = STRICT_FROZEN_CONFIG
casilla_id: CasillaId
value: ModeloScalar
kind: ModeloValueKind
source: str
formula_trace_casilla_ids: tuple[CasillaId, ...] | None = None
[docs]
class ModeloBindingValue(BaseModel):
"""The typed value of one registry binding on a :class:`ModeloDraft`.
Carries the same regulatory grounding the casilla half exposes via
:class:`ModeloCasillaProvenance`: ``legal_refs`` and ``source_refs``
populated from the binding definition, plus a typed
:class:`~core.BindingSourceKind` ``source`` (replacing the former
free-text provenance string) so a bound value is operator-traceable at
parity with a computed casilla.
Attributes:
binding_id: Stable registry binding id this value materialises.
value: The scalar value carried for this binding.
kind: Provenance kind — literal input, computed, inherited, etc.
source: Typed registry binding source kind (e.g.
:attr:`~core.BindingSourceKind.MANUAL_INPUT`,
:attr:`~core.BindingSourceKind.LEDGER_IVA_AGGREGATION`).
legal_refs: Legal references carried from the binding definition.
source_refs: Source references carried from the binding definition.
row_index: 1-based row index for multi-row (detail-record) bindings.
"""
model_config = STRICT_FROZEN_CONFIG
binding_id: BindingId
value: ModeloScalar
kind: ModeloValueKind
source: BindingSourceKind
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
source_refs: tuple[SourceRefId, ...] = Field(min_length=1)
row_index: int | None = Field(default=None, ge=1)
[docs]
class ModeloCasillaProvenance(BaseModel):
"""Regulatory grounding for one casilla carried on a filing draft.
``formula_id`` is set for computed casillas (those whose value is
produced by a registry formula) and ``None`` for manual-input or
bound casillas. ``legal_refs`` and ``source_refs`` are always
populated from the registry casilla definition when the draft is
created.
"""
model_config = STRICT_FROZEN_CONFIG
casilla_id: CasillaId
formula_id: FormulaId | None = None
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
source_refs: tuple[SourceRefId, ...] = Field(min_length=1)
[docs]
class ModeloValidationFinding(BaseModel):
"""One finding produced by the validator.
Attributes:
casilla_id: The casilla the finding is about, or ``None``
for cross-cutting findings such as deadline checks.
severity: ERROR / WARNING / INFO.
code: A stable machine-readable code (e.g.
``"casilla-required-missing"``).
message: A strictly-typed :class:`Translatable` key.
references_rules: Tuple of Manual práctico Rule IDs that
justify the finding (see :class:`domain.manuals.Rule`).
"""
model_config = STRICT_FROZEN_CONFIG
casilla_id: CasillaId | None
severity: BaseSeverity
code: str
message: tr
references_rules: tuple[str, ...] = Field(default_factory=tuple)
[docs]
class ModeloApprovalBasis(BaseModel):
"""Persisted approval-basis digests for deterministic stale detection."""
model_config = STRICT_FROZEN_CONFIG
version: str = APPROVAL_BASIS_VERSION
draft_payload_fingerprint: str
draft_review_fingerprint: str
transaction_catalogue_fingerprint: str
invoice_catalogue_fingerprint: str
prior_filing_observations_fingerprint: str
profile_activity_fingerprint: str
category_profiles_fingerprint: str
schema_formula_fingerprint: str
[docs]
class ModeloDraft(BaseModel):
"""A typed, validated draft of one filing.
The ``draft_id`` is a content-addressed hash of
``(modelo, period, profile_tax_id, snapshot_ref, values)``.
Re-validating a draft preserves its identity because findings,
status, ``updated_at`` and ``notes`` are deliberately excluded
from the hash.
"""
model_config = STRICT_FROZEN_CONFIG
draft_id: str
modelo: str
period: Period
profile_tax_id: SubjectTaxId
# Typed Spanish NIF/NIE/CIF of the filing subject. New drafts
# populate this from the validated profile substrate so the
# identity is re-checkable at persistence time.
subject_tax_id: SubjectTaxId
# Four-axis coordinates identifying the registry snapshot this
# draft was built against. Replaces the role of the opaque
# ``schema_version`` string for re-resolution against the live
# registry catalogue. Newly built drafts populate this from the
# snapshot used to produce the casilla values.
snapshot_ref: RegistrySnapshotRef
status: ModeloDraftStatus
values: tuple[ModeloValue, ...]
binding_values: tuple[ModeloBindingValue, ...] = Field(default_factory=tuple)
casilla_provenance: tuple[ModeloCasillaProvenance, ...] = Field(default_factory=tuple)
findings: tuple[ModeloValidationFinding, ...] = Field(default_factory=tuple)
created_at: datetime
updated_at: datetime
schema_version: str
notes: str = ""
approved_at: datetime | None = None
approved_by: str | None = None
review_checksum: str | None = None
approval_basis: ModeloApprovalBasis | None = None
[docs]
def compute_modelo_draft_id(
*,
modelo: str,
period: Period,
profile_tax_id: SubjectTaxId,
snapshot_ref: RegistrySnapshotRef,
values: tuple[ModeloValue, ...],
binding_values: tuple[ModeloBindingValue, ...] = (),
) -> str:
"""Compute the stable, content-addressed ``draft_id``.
The period is serialised as ``{"filing_year": <int>, "code": "<token>"}``
so the hash is deterministic and self-consistent regardless of the
human-readable ``str(period)`` form.
Args:
modelo: Modelo string ID.
period: Typed :class:`~core.Period` for the filing period.
profile_tax_id: Validated taxpayer tax ID.
snapshot_ref: Typed registry snapshot coordinate this draft was
built against.
values: The tuple of :class:`ModeloValue` records to hash.
binding_values: Optional tuple of :class:`ModeloBindingValue` records
included in the hash; defaults to an empty tuple.
Returns:
A 16-character lowercase hex SHA-256 prefix.
"""
sorted_values = sorted(values, key=lambda v: v.casilla_id)
sorted_binding_values = sorted(binding_values, key=lambda v: (v.binding_id, v.row_index or 0))
payload = {
"modelo": modelo,
"period": {"filing_year": period.filing_year, "code": period.registry_token},
"profile_tax_id": profile_tax_id,
"snapshot_ref": snapshot_ref.model_dump(mode="json"),
"values": [v.model_dump(mode="json") for v in sorted_values],
"binding_values": [v.model_dump(mode="json") for v in sorted_binding_values],
}
return content_hash_hex(payload)[:16]