Architecture overview¶
aeat is a local-first command-line application that prepares Spanish tax
filings. It models the tax authority’s regulatory registry, ingests and
classifies your financial records, and computes the numbered boxes of each tax
form. Then it checks the draft and exports a file you submit yourself. AEAT is the Agencia
Estatal de Administración Tributaria, Spain’s tax agency.
This page is the entry point for a developer reading the codebase for the first time. It’s a map, not a directory: it names the layers, the load-bearing concepts, and the canonical types you’ll navigate to, and it stops there. For exact signatures and commands, follow the links in Crossing into the code.
Why the system is shaped this way¶
Preparing a Spanish tax return by hand is error-prone and hard to audit. A figure on a form has to trace back to a bank movement, a regulation, and a published rule. A later annual form has to stay consistent with the quarterly ones that fed it. Three design choices answer that problem, and they shape everything else:
Local-first. Your financial records never leave your machine. The tool runs offline and stores everything in an encrypted database on disk.
The registry is the authority. Tax rules - rates, brackets, deadlines, and the legal basis of each box - aren’t hard-coded. They’re authored as data, compiled, validated, and read through a single authority. A figure can always trace back to the rule that produced it.
The human files, not the tool. The pipeline ends at an export file. Submitting it to the agency is a deliberate human step. The tool has no submission path at all, so live filing is absent rather than guarded.
The structure that follows is hexagonal: business rules sit at the center, and the parsers, browsers, and storage that touch the outside world sit at the edge. Dependencies point inward. That keeps tax logic testable on its own and lets an adapter change without touching a rule.
How to read this page¶
Four views cover the system at the highest level. Each pairs a diagram with a short explanation:
The layers - the five hexagonal layers and what each owns.
The registry authority pipeline - how tax-rule data becomes the snapshots runtime code reads.
The modelo lifecycle - the journey from records to an export file, and how each figure is grounded.
Persistence and the safety boundary - where state lives, and why nothing leaves.
The layers¶
aeat separates responsibilities into five layers under src/aeat/. Each layer
depends only on the layers inside it.
flowchart TD
EP["entrypoints — CLI (config · app)"]
AD["adapters — inbound · outbound · persistence"]
APP["application — use-case services"]
DOM["domain — tax rules and entities"]
CORE["core — enums · JSON contract · config · primitives"]
EP --> APP
EP --> CORE
APP --> DOM
APP --> AD
APP --> CORE
AD --> DOM
AD --> CORE
DOM --> CORE
DOM -.->|"secure-repo seam"| AD
coreis the innermost layer. It owns the cross-cutting primitives every other layer shares: the typed enums for closed value sets (period codes, tax domains, modelo identifiers, and binding source kinds), the JSON envelope contract, configuration, the money and time types, and the error taxonomy.domainholds pure tax rules and records: the modelo registry, casilla definitions (a casilla is one numbered box on a form), filing observations, and the calculations over them. It depends only oncore.applicationorchestrates use cases. It joins domain rules with adapters to build filing drafts, aggregate the ledger, run diagnostics, and project state. It performs no input or output of its own.adaptersconnects the application to the outside world in three parts.inboundparses incoming files (PDF, CSV, Open Financial Exchange (OFX), and XLSX statements).outboundreaches external services (AEAT browser sessions, calculation oracles, and authentication providers).persistencestores records in the local encrypted database.entrypointsexposes the application to operators. The command line lives here, and its root surface is limited to two command families,configandapp.
Two boundary rules keep the layers honest. Boundary data crosses as validated
pydantic v2 models, never loose dictionaries. Closed value sets are declared as
typed enums in core and flow as enum members, so an invalid value is rejected
at the boundary.
The dependency arrows point inward with one annotated exception. Three domain
repositories - filing, justificante, and submission - sit directly on the
encrypted-storage base class in adapters.persistence. The diagram draws that
seam as a dashed edge rather than hiding it.
Inside domain and application, the subpackages cluster into five conceptual
groups. Some subpackages are omitted for clarity; the API reference lists them
all.
Cluster |
|
|
|---|---|---|
Registry and calculations |
|
|
Ledger and transactions |
|
|
Filing and export |
|
|
Live, portals, and auth |
|
|
Profile and storage |
|
|
The modelo lifecycle¶
A modelo is a numbered AEAT tax form. Its data flows one way, from your records to a file you upload yourself.
flowchart LR
REC["Financial records — statements · invoices · evidence"]
ING["Ingest and classify — transactions · invoices · evidence"]
LED["Ledger / aggregation substrate — encrypted bucket · resolver mesh"]
CRE["work create"]
CAL["calculate — produces CalculationRevision (casillas)"]
VER["verify — completeness gate"]
FIL["file — local final marker"]
EXP["export — fichero-BOE file on disk"]
HUM{{"Human upload at AEAT sede — outside the tool"}}
AEAT["AEAT"]
REC --> ING --> LED
CRE --> CAL
LED --> CAL
CAL --> VER
VER --> FIL
VER --> EXP
FIL --> EXP
EXP --> HUM
HUM -.->|"manual submission"| AEAT
The tool ingests and classifies financial records - bank statements, invoices, and their evidence - into the encrypted ledger. From there the journey follows a fixed sequence of command-line verbs:
aeat app modelo work createpins a filing to a modelo, year, and period.calculateresolves every casilla and saves aCalculationRevision.verifyruns a completeness and consistency gate over the draft.filemarks a verified revision as internally filed.aeat app modelo exportwrites the official upload file (a fixed-layout fichero-BOE artifact) to disk.
The boundary is structural. file is a local marker, not a submission, and
export writes a file to your disk. No command transmits anything to the
agency: the upload is a human action at the AEAT portal, modeled as a step
beyond the tool’s boundary. Read-only live checks against the agency are gated
behind an explicit opt-in (AEAT_LIVE_TESTS_ENABLED) and never write.
How a figure is grounded¶
A casilla value isn’t a bare number. It carries its provenance - the legal references, source references, and formula identifier that produced it - from the registry definition through to the operator-facing output.
flowchart LR
DEF["Registry casilla definition — legal_refs · source_refs · formula_id"]
subgraph mesh["source-resolver mesh — merge_source_resolutions"]
direction TB
R1["Ledger aggregation resolvers"]
R2["Invoice catalogue resolver"]
R3["Previous-filing resolver"]
R4["Relation-prefill resolver"]
end
ENG["Registry formula engine — evaluates formula_id"]
OBS["CasillaObservation — value + legal_refs + source_refs + formula_id"]
REV["CalculationRevision.observations (+ flat casilla_values)"]
PAY["CLI payloads"]
OPS["Operator surface — JSON envelope + text"]
DEF --> ENG
DEF --> mesh
R1 --> ENG
R2 --> ENG
R3 --> ENG
R4 --> ENG
ENG --> OBS --> REV --> PAY --> OPS
Each binding on a casilla declares a typed source. A mesh of resolvers - ledger
aggregation, invoice catalogue, previous-filing carry, and cross-modelo relation
prefill - turns those sources into binding values, merged through
merge_source_resolutions. The registry formula engine evaluates the casilla’s
formula over the resolved inputs and stamps the result as a CasillaObservation
carrying its legal_refs, source_refs, and formula_id. That observation
rides inside the persisted CalculationRevision, then into the CLI payloads,
then to the operator. The provenance never drops on the way out.
Persistence and the safety boundary¶
All sensitive financial data lives in one place: an encrypted, per-profile store. The structure, not a convention, keeps it there.
flowchart TD
OP(["Operator"])
subgraph boundary["Encrypted bucket-scoped store — FINANCIAL data never leaves (no temp / scratch / plaintext)"]
direction TB
PROF["Active profile — resolve_active_bucket_id"]
SESS["BucketSession — KEK/DEK · idle-timed · one taxpayer at a time"]
FAC["Runtime factories — secure_object_repository_for_active_bucket"]
REPO["SecureObjectRepository — AEAD payloads · HMAC keys"]
MK["MasterKeyProvider — Keyring / FileFallback · AES-256"]
DB[("Encrypted SQLite")]
ATT["AttachmentStore — put_bytes / read_bytes · content-addressed"]
EV["Evidence bytes — invoices · statements (FINANCIAL)"]
PROF --> SESS --> FAC --> REPO
REPO --> MK
REPO --> DB
ATT --> REPO
EV --> ATT
end
SEDE["adapters/outbound/aeat — read-only live checks"]
AEAT["AEAT"]
HUM{{"Human filing — outside the app"}}
OP --> PROF
SEDE -.->|"read-only, gated by AEAT_LIVE_TESTS_ENABLED"| AEAT
OP --> HUM
HUM -.->|"manual submission"| AEAT
You work one taxpayer at a time. Selecting a profile opens a bucket session,
which scopes a SecureObjectRepository to that taxpayer’s encrypted store. The
repository encrypts every payload at the column boundary with a key from the
MasterKeyProvider - the operating system keychain, or a passphrase-derived
fallback. It persists the ciphertext to a local encrypted SQLite database.
Evidence bytes - invoices, statements, and decrypted documents - go through the
content-addressed AttachmentStore, which stores the bytes themselves, never a
link.
Nothing in this store crosses outward to the agency as a write. The read-only live checks reach the agency only to read, gated by the same opt-in. The single path that reaches the agency as a write is the human upload, which sits beyond the tool’s boundary. A structural test enforces the absence of any write verb in the AEAT sede adapter, so the safety posture can’t erode by accident.
How the documentation stays true to the code¶
The discipline that governs the layers governs the documentation too: nothing a
reader relies on is maintained by hand where the code can supply it. aeat keeps
three English documentation surfaces, and each is generated or verified from the
codebase.
The repository markdown - this guide and the others under docs/ - is
hand-written for people orienting to the project. Every technical claim in it is
checked against the code before it lands. In-source docstrings are the single
source for the API reference, so a signature is never copied into prose. The
generated reference, both the source-code interface and the command-line tree, is
scaffolded from the code itself.
Because these surfaces are generated, they can’t silently drift. A strict build, with warnings treated as errors, fails on a broken cross-reference, a missing stub, or a command reference that no longer matches the commands.
Crossing into the code¶
Use this overview to orient, then drop into the detail:
The layer tree - browse
src/aeat/core,src/aeat/domain,src/aeat/application,src/aeat/adapters, andsrc/aeat/entrypointsto see the subpackages each cluster names.The command-line reference - CLI reference for exact verbs, options, and output.
The API reference - aeat package for module signatures and the full subpackage list.
The registry authoring guide - Authoring and reviewing documentation for how to author the TOML that the pipeline compiles.
The pipeline explanation - Understanding the AEAT pipeline for the same journey told for the taxpayer.
The glossary - Glossary for any term (casilla, modelo, fichero-BOE, or justificante) you want defined.
Getting it running - Quickstart: produce a modelo file to install and prepare a first filing.
Contributing - the project source and issue tracker live on GitHub.