You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

5.6 KiB

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

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, then run:

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.