aeat.application.modelo._reconcile module

Modelo reconciliation: compare work-unit state and computed result against evidence.

modelo_reconcile accepts a modelo work unit and either an AEAT justificante PDF or a filed declaración PDF, then produces a ModeloReconciliationReport.

For a justificante, the report records whether the work unit’s modelo, period, ejercicio, and active-profile tax id match the receipt, AND — where the revision declares reconciliation_total_casilla_ids and a persisted calculation revision exists — whether the receipt’s printed total equals the canonical computed result casilla (one-aggregation-path-pull-equals-calculate). A filed-amount divergence surfaces as a typed total diff carrying the reconciling expectation’s legal grounding; where the total could not be reconciled (no map, no revision, no printed total) a totals_not_reconciled advisory discloses it so an identity-only matches is never a silent false green.

For a filed declaración, the same header comparison runs, and — for the modelos enrolled in _DECLARATION_CASILLA_RECONCILE_MODELOS — every casilla the registry’s verification policy reconciles is compared, one by one, against the persisted revision’s casilla_values via application.modelo._reconcile_casilla.detect_casilla_divergences(). A divergence surfaces as a typed casilla diff (ModeloReconciliationDiffKind.CASILLA). A modelo not yet enrolled in casilla-level declaration reconcile is refused with ReconciliationDeclaracionSourceUnsupportedError rather than silently degrading to header-only comparison.

The path-based service is local-only: it never contacts AEAT and never invokes require_live_read — the computed result is read from the already-persisted CalculationRevision, never a fresh calculation. Authenticated live pulls use modelo_reconcile_bytes after storing captured justificante bytes in secure storage. Both paths append a MODELO_RECONCILED BucketEvent through BucketEventHistoryRepository, persisting the structured diffs so reconcile history reports which fields diverged.

class ModeloReconciliationEvidenceKind(*values)[source]

Bases: StrEnum

Closed external-evidence labels accepted by reconciliation commands.

DECLARATION performs casilla-level reconciliation for modelos in _DECLARATION_CASILLA_RECONCILE_MODELOS; other modelos raise ReconciliationDeclaracionSourceUnsupportedError.

JUSTIFICANTE
DECLARATION
class ModeloReconciliationVerdict(*values)[source]

Bases: StrEnum

Closed verdict catalogue for ModeloReconciliationReport.

Closed set: matches / mismatches. A reconcile that reaches a report has already parsed its evidence; an unparseable justificante is surfaced as the typed ReconciliationEvidenceInvalidError refusal (REFUSED_RECONCILIATION_EVIDENCE_INVALID) before any report is built, so there is no evidence_invalid verdict shell. Any expansion requires a design decision and must not add shells.

MATCHES
MISMATCHES
class ModeloReconciliationDiffKind(*values)[source]

Bases: StrEnum

Closed category for a ModeloReconciliationDiff.

header_field — a receipt-identity disagreement (modelo, ejercicio, period, tax id). total — a filed-amount disagreement between the receipt total and the canonical computed result casilla. casilla — a per-casilla value disagreement between the persisted computed revision and a filed declaración, emitted for modelos enrolled in _DECLARATION_CASILLA_RECONCILE_MODELOS (application.modelo._reconcile_casilla.detect_casilla_divergences()).

HEADER_FIELD
TOTAL
CASILLA
class ModeloReconciliationHistoryEntry(**data)[source]

Bases: BaseModel

One past reconciliation read back from the bucket event history.

modelo_reconcile persists no stored record: a reconciliation is repeatable on demand from the justificante, so the durable trace is the append-only MODELO_RECONCILED BucketEvent it emits. This typed entry projects one such event so the operator can enumerate past reconciliation verdicts without re-parsing any evidence. The read path is the same bucket-event catalogue the write path appends into — there is no parallel reconciliation store.

Parameters:

data (Any)

event_id: str
bucket_id: BucketId
work_unit_id: WorkUnitId
source_kind: ModeloReconciliationEvidenceKind
source_path: str
verdict: ModeloReconciliationVerdict
diff_count: int
diffs: tuple[ModeloReconciliationDiff, ...]
actor: str
reconciled_at: datetime
class ModeloReconciliationDiff(**data)[source]

Bases: BaseModel

One disagreement between work unit / profile / computed state and evidence.

diff_kind is the closed category (header field, filed total, or per-casilla). kind remains the specific mismatch token (modelo_mismatch, total_ingresar_mismatch, casilla_value_mismatch, casilla_missing_in_filed, casilla_extra_in_filed, …). A total or casilla diff carries the reconciling verification expectation’s / casilla’s legal_refs / source_refs so the divergence surfaces with its legal grounding (aeat-calculation-grounding); header diffs carry empty grounding. For a casilla diff, field_name is the casilla id and work_unit_value / evidence_value carry the computed / filed decimal strings (empty when the corresponding side carried no value, per CasillaDivergenceKind).

Parameters:
field_name: str
work_unit_value: str
evidence_value: str
kind: str
diff_kind: ModeloReconciliationDiffKind
legal_refs: tuple[str, ...]
source_refs: tuple[str, ...]
class ModeloReconciliationAdvisory(**data)[source]

Bases: BaseModel

One non-blocking reconciliation advisory (surfaced as a CLI Notice).

Carries a stable code (totals_not_reconciled / identity_anchor_unverified), an operator-facing message, and structured context (the reason, the anchor, the modelo). The CLI folds each advisory into a typed Notice on the envelope’s notices channel per cli-notices-are-the-only-diagnostic-channel — an advisory is never a bespoke result field. Advisories never flip the verdict: they disclose that a comparison could not be performed (so identity-only matches is never a silent false green), not that a value diverged.

Parameters:
code: str
message: str
context: Mapping[str, str]
class ModeloReconciliationCommand(**data)[source]

Bases: BaseModel

Strict input contract for modelo_reconcile.

source_path points to the operator-supplied evidence file and source_kind records how that file must be parsed. Justificante PDFs are supported for every modelo; declaration PDFs are supported (casilla-level) only for modelos in _DECLARATION_CASILLA_RECONCILE_MODELOS — an unenrolled modelo is refused before parsing.

Parameters:
  • work_unit_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=64, max_length=64, pattern=^[0-9a-f]{64}$, ascii_only=None)])

  • source_kind (ModeloReconciliationEvidenceKind)

  • source_path (Path)

  • actor (str)

work_unit_id: WorkUnitId
source_kind: ModeloReconciliationEvidenceKind
source_path: Path
actor: str
class ModeloReconciliationBytesCommand(**data)[source]

Bases: BaseModel

Strict input contract for reconciling secure-storage justificante bytes.

Used by authenticated live pulls after the captured justificante has already been persisted in secure storage. The raw bytes remain in memory; source_ref is the non-file secure-storage reference recorded in the reconciliation event.

Parameters:
  • work_unit_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=64, max_length=64, pattern=^[0-9a-f]{64}$, ascii_only=None)])

  • source_kind (ModeloReconciliationEvidenceKind)

  • source_bytes (bytes)

  • source_ref (str)

  • actor (str)

work_unit_id: WorkUnitId
source_kind: ModeloReconciliationEvidenceKind
source_bytes: bytes
source_ref: str
actor: str
class ModeloReconciliationReport(**data)[source]

Bases: BaseModel

Outcome of modelo_reconcile.

The verdict summarises the comparison at the work-unit level. The diff list enumerates the disagreements — header-field (modelo, period, ejercicio, tax id), the filed justificante total against the computed result casilla where reconciled, and (for a declaración source on an enrolled modelo) each per-casilla divergence; empty on matches. The advisory list carries non-blocking disclosures (a total or casilla set that could not be reconciled, an identity anchor that could not be verified); advisories never flip the verdict.

Parameters:
work_unit_id: WorkUnitId
bucket_id: BucketId
source_kind: ModeloReconciliationEvidenceKind
source_path: str
verdict: ModeloReconciliationVerdict
diffs: tuple[ModeloReconciliationDiff, ...]
advisories: tuple[ModeloReconciliationAdvisory, ...]
reconciled_at: datetime
narrative: str
exception ReconciliationEvidenceInvalidError(message=None, *, context=None, suggestion=None, translated_message=None)[source]

Bases: AeatError

Raised when the supplied external evidence cannot be parsed.

Raised for malformed justificantes. The CLI surfaces it as a refusal with the canonical recovery hint; downstream consumers branch on it without string-matching the message.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • suggestion (str | None)

  • translated_message (str | None)

Return type:

None

code: ClassVar[ErrorCode]
exception ReconciliationDeclaracionSourceUnsupportedError(message=None, *, context=None, suggestion=None, translated_message=None)[source]

Bases: AeatError

Raised when a declaración reconcile targets a modelo not yet enrolled.

Casilla-level declaración reconciliation is enrolled one modelo at a time in _DECLARATION_CASILLA_RECONCILE_MODELOS; a modelo outside that set refuses cleanly rather than silently degrading to a header-only compare.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • suggestion (str | None)

  • translated_message (str | None)

Return type:

None

code: ClassVar[ErrorCode]
exception ReconciliationCrossBucketRefusedError(message=None, *, context=None, suggestion=None, translated_message=None)[source]

Bases: AeatError

Raised when the addressed work unit belongs to a different bucket than the active profile bucket.

Every event is scoped to a bucket id. Allowing the service to emit into a non-active bucket would let any caller write into other operators’ history. The check is enforced at the application service so neither the CLI nor any future caller can bypass it.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • suggestion (str | None)

  • translated_message (str | None)

Return type:

None

code: ClassVar[ErrorCode]
modelo_reconcile(command)[source]

Reconcile a modelo work unit against a justificante or declaración PDF file.

Local-only: never contacts AEAT and never invokes require_live_read.

For a justificante, reimplements the metadata comparison inline against the justificante parser at adapters.inbound.justificante. The receipt totals ARE reconciled against the persisted revision’s computed result where the revision declares reconciliation_total_casilla_ids.

For a declaración, parses via adapters.inbound.declaracion.parse_declaracion() and — for modelos enrolled in _DECLARATION_CASILLA_RECONCILE_MODELOS — compares every registry-reconciled casilla against the persisted revision’s casilla_values, surfacing each divergence as a typed casilla diff. A modelo outside that set raises ReconciliationDeclaracionSourceUnsupportedError.

Emits MODELO_RECONCILED into the bucket-event-history catalogue. The verdict is included in the event payload so downstream auditors can replay the reconciliation timeline without re-parsing the evidence.

Return type:

ModeloReconciliationReport

Returns:

A ModeloReconciliationReport.

Parameters:

command (ModeloReconciliationCommand)

modelo_reconcile_bytes(command)[source]

Reconcile secure-storage evidence bytes without materialising a plaintext file.

Declaración reconciliation is not offered on the bytes path: the only authenticated live-capture flow today captures justificante snapshots (application.live.capture_justificante_snapshot()), never a filed declaración. Use modelo_reconcile() with a local declaración PDF file for casilla-level reconcile.

Return type:

ModeloReconciliationReport

Returns:

The ModeloReconciliationReport comparing the parsed justificante metadata to the work unit and active profile.

Parameters:

command (ModeloReconciliationBytesCommand)

list_modelo_reconciliations(*, bucket_id, work_unit_id=None)[source]

Return every recorded reconciliation in bucket_id as typed entries.

modelo_reconcile stores no record; its durable trace is the MODELO_RECONCILED BucketEvent it appends. This read-back enumerates those events from the same BucketEventHistoryRepository catalogue the write path appends into (no parallel read path), filtered to the active bucket_id and ordered oldest-first by occurred_at. Each event is projected onto a typed ModeloReconciliationHistoryEntry — the verdict, source kind, diff count, actor, and reconciliation instant are preserved, never collapsed to a flat dict[str, Any].

An optional work_unit_id narrows the result to one work unit’s reconciliation history. An empty result (no reconciliations recorded, or none for the requested work unit) returns an empty tuple — the clean “no reconciliations recorded yet” signal, not an error.

Return type:

tuple[ModeloReconciliationHistoryEntry, ...]

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • work_unit_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=64, max_length=64, pattern=^[0-9a-f]{64}$, ascii_only=None)] | None)