# Financial domain developer guide Use `finance.services` for application writes. The ORM remains useful for reads; `Model.save`, `QuerySet.update`, bulk creation, and direct SQL are not supported application mutation APIs. Database checks enforce row-local rules, while the services enforce cross-record currency, account, archive, and relationship rules. There are no finance HTTP endpoints or editable admin models yet. ## Recording activity ```python from datetime import date from decimal import Decimal from finance.services import Actor, create_account, record_transaction actor = Actor("HUMAN", "local-owner") account = create_account(name="Example bank", kind="BANK", actor=actor) row = record_transaction( account_id=account.pk, amount=Decimal("-12.50"), kind="EXPENSE", posting_date=date(2026, 9, 6), description="Synthetic example", actor=actor, ) ``` Examples are synthetic and are not automatically loaded. Service operations must be called outside an existing `transaction.atomic` block. Each owns its outermost transaction, commits its success audit with the mutation, and writes a sanitized failure audit after rollback. Composite operations use private helpers within one transaction. Invalid actor identities or caller-owned transactions are rejected before mutation and cannot produce a trusted, independently committed audit. Database failures are allowed to propagate. Actor identifiers are caller-supplied accountability labels, not authentication. A future authenticated interface must derive them from trusted identity rather than accept arbitrary client input. Never use raw source payloads as identifiers. `DomainError.code` is a safe machine-readable code; consumers must not expose underlying database exceptions. Failure events have no target for creations that rolled back. Audits contain field names, not transaction values or source text. ## Public operations - `create_account`, `create_payee`, `create_category`: create reference data. - `archive(entity, actor=...)`: archive an Account, Payee, or Category. - `create_statement`: group observed card activity with optional quoted balance. - `create_plan`: describe installment count without generating future charges. - `record_transaction`: post one observed movement. - `create_transfer`: atomically create outgoing/incoming legs and their link. - `match_transfer`: link two existing unmatched legs without posting more money. - `attach_source`: attach unique document/item provenance to a transaction. - `annotate_transaction`: update only description, payee, and category. Each create/match/attach operation returns its saved model instance. All related inputs are UUIDs named `account_id`, `category_id`, etc. All public writes require `actor=Actor(kind, identifier)` with HUMAN, APPLICATION, or AGENT kind. ## Financial conventions Use Decimal, decimal strings, or integers for money; floats, booleans, non-finite values, zero movement, more than two fractional digits, and out-of-range amounts are rejected before database rounding. Amounts have 18 total digits and two fractional digits. Statement metadata may be zero. Positive amounts improve the owner's position; negative amounts reduce it. Purchases on a credit card are negative. Paying a card is a negative bank leg and a positive card leg, both TRANSFER. Refunds are positive separate records. Opening entries are unique per account and excluded from income/spending. Balances are `Sum("amount")` per account. Never aggregate balances across currency codes. Net spending is the negated sum of EXPENSE and REFUND amounts within one currency and the desired date range. Transfers and opening entries are excluded. No FX conversions or combined-currency summaries are implemented. Unmatched TRANSFER entries are expected when only one account has been imported. Matching validates currency, distinct accounts, opposite equal amounts, and unique use of each leg. Account and transaction locks are acquired in UUID order; reference locks precede transaction locks to avoid opposing lock order. Linked refunds target an expense in the same currency, possibly on another account; partial and over-refunds are allowed. Installment membership requires the same account, a one-based number within the plan total, and a unique (plan, number). Posting installment 3/10 does not create any other installment. Statement balances are quoted metadata and need not equal partially imported rows. Accounts' currencies, posted financial fields, and plan definitions are immutable through the supported API. Only transaction annotations can change. Existing archived assignments remain on history; they cannot be newly assigned. Referenced records are protected from deletion. Financial correction/reversal workflows are not exposed in this milestone. Source identity is (document SHA-256, item locator). Changing a parser version does not allow the same physical item to be attached twice. Multiple distinct source items can explicitly describe the same transaction. Semantic duplicate detection and file parsing belong to the next milestone. ## Verification Follow [local setup](development.md), then run: ```powershell docker compose build docker compose run --rm manage migrate docker compose run --rm test docker compose run --rm permissions docker compose run --rm manage makemigrations --check --dry-run docker compose run --rm manage migrate --check ``` The full PostgreSQL suite includes direct constraint violations, protected history, precision rejection, audit rollback, relationships, and concurrent transfer matching with separate database sessions. The permissions command checks grants on all finance tables and rolls back synthetic runtime account DML.