fix: don't enforce write-time transfer rules when reading transactions (#7)

TransactionRead inherited TransactionBase's model_validator, so the rules a
new row must satisfy were also applied on the way out, to rows already in
the database. A row can stop satisfying them through no fault of its own:
transactions.transfer_account_id is ON DELETE SET NULL, so deleting an
account leaves every transfer that pointed at it without a destination.

The result was that deleting an account could 500 the entire transactions
list — permanently, and with the list gone there was no way in the UI to
find and delete the offending row.

The fields move to TransactionFields, which reading uses; TransactionBase
keeps the invariants for the write path. The frontend already tolerates a
missing destination (TransactionsTable renders the transfer suffix only when
transfer_account_id is set, and the edit modal makes you pick a destination
before saving), so such a row is now visible, editable and deletable
instead of fatal.
This commit is contained in:
technosexual
2026-08-30 18:10:23 +03:00
committed by GitHub
parent 7deacd2a98
commit 20f41b444a
2 changed files with 38 additions and 2 deletions

View File

@@ -39,7 +39,15 @@ def transfer_rule_violation(
return None
class TransactionBase(BaseModel):
class TransactionFields(BaseModel):
"""A transaction's shape, without the rules that only make sense while
writing one. Reading goes through this: rows already in the database can
stop satisfying a write-time rule through no fault of their own — the
transfer_account_id FK is ON DELETE SET NULL, so deleting an account
leaves the transfers that pointed at it with no destination — and
refusing to serialize such a row would take the whole transactions list
down with it, leaving no way in the UI to find and delete the row."""
account_id: int
category_id: int | None = None
transfer_account_id: int | None = None
@@ -57,6 +65,11 @@ class TransactionBase(BaseModel):
def _capitalize_description(cls, value: str) -> str:
return capitalize_first_letter(value)
class TransactionBase(TransactionFields):
"""The write-side shape: the fields plus the invariants a new or edited
row has to satisfy."""
@model_validator(mode="after")
def _validate_type_specific_fields(self) -> "TransactionBase":
violation = transfer_rule_violation(
@@ -93,7 +106,7 @@ class TransactionUpdate(BaseModel):
return capitalize_first_letter(value) if value is not None else None
class TransactionRead(TransactionBase):
class TransactionRead(TransactionFields):
model_config = ConfigDict(from_attributes=True)
id: int