Instructions for agents working on this brick
Built from the tax module spec (erp-method modules/tax, v2, namespace tax, prefix tax_). The customer signed the PROCESS
(registration, tax_code, document_tax; 2026-10-22); tests/test_process.py fails if application/process.yaml drifts from it. A
process change goes through erp-compose change propose, the customer's new signature, then the code. Read erp-platform's
BRICKS.md before changing anything.
Where a change goes
| You are changing… | It goes in |
|---|---|
| a business rule | domain/ — and its test, named for its story (tests/test_stories.py, tests/test_documents.py) |
| how VAT is rounded | domain/rounding.py — tests/test_rounding.py (net + VAT = gross under every method) |
| the "starts after today" rule | domain/schedule.py (one place for rates, rules, registrations, treatments, settings) |
| who may do something, or which rows they see | application/use_cases.py — the permission, or _sees_*; never a role's name |
| a new action for people and agents | a new use case in application/use_cases.py — a route AND a tool |
| how another module is reached | infrastructure/seams.py only (a port with a fake in tests) |
| a setting the customer edits | domain/values.py (SETTING_DEFS) |
| an AI step | infrastructure/ai_steps.py and the use case that calls ask() |
| the Odoo migration | application/odoo_migration.py (pure mapping) behind migrate_from_odoo; reader port in infrastructure/odoo.py |
| a column or table | infrastructure/tables.py AND schema.sql (append; never drop or rename), regenerate the migration |
Never import SQLAlchemy, FastAPI or MCP in domain/. Never write another brick's tables — call its port. Do not hand-write an
upsert — use erp_platform.repository.
The rules that must not be undone
- A dated row in effect is never edited or deleted. A rate, rule, registration, customer treatment or setting change starts
AFTER today in
business_timezone; a correction is a new row. Only a key's FIRST row may carry an earlier date (a new installation, the Odoo migration; a rule for a pair nothing covers may start today). - A pair no rule covers is refused, never taxed at 0%. A business not VAT registered charges no VAT (code NV) and its document is not titled "VAT invoice"; one that recorded nothing is refused until it says.
- Tax is frozen at issue (
freeze_document_tax) and never recomputed ("issued: credit it instead"). Freezing twice is a no-op. - net + VAT = gross and the per-rate summary sums to the total under every rounding method. Methods are code; a row chooses one.
- Reverse charge charges 0 and records the VAT due, with the legend.
- An override that lowers tax needs a second person unless the owner's control
override_approval = self_with_reason. - No AI suggestion reaches a document without a person's acceptance. Tests never call a model.
- The controls (
tax_changes_need_second_person,override_approval,business_timezone) needtax:settings:controls, refused totax:settings:write. - Machine permissions (
tax:code:seed,tax:document_tax:compute,tax:document_tax:suggest,tax:history:import) are exact grants; no wildcard reaches them. - A refused transition returns its unmet gates as
{gate, met, reason, fix}(in the 409 detail after— unmet gates:). - No read-your-writes inside an open transaction (DataK3): a use case that writes then reports works the answer out in memory.
- The migration never recomputes history: it writes first rows, keyed in
tax_migration_map; a second run writes nothing.
Before you commit
pytest -q --tb=short
ruff check src tests
erp-compose check tests/tier1.compose.yaml --strict --roles src/brick_tax/roles.yaml
Live check
BUCKET=l2ctaxlab DODIL_ORG=dodil python tests/live_check.py runs the real use cases on a scratch DataK3 bucket (route and MCP),
creates the schema and drops it again. Do it once per change that touches storage, not per edit.