2026-04-17 11:04:18 -06:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from quartermaster import service
|
|
|
|
|
from quartermaster.models import Section
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_and_total(db):
|
|
|
|
|
service.add_entry(db, Section.income, "Paycheck", Decimal("2500.00"))
|
|
|
|
|
service.add_entry(db, Section.income, "Side gig", Decimal("250.50"))
|
|
|
|
|
entries = service.list_entries(db, Section.income)
|
|
|
|
|
assert [e.name for e in entries] == ["Paycheck", "Side gig"]
|
|
|
|
|
assert service.section_total(entries) == Decimal("2750.50")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_entry(db):
|
|
|
|
|
entry = service.add_entry(db, Section.other, "One-off", Decimal("10.00"))
|
|
|
|
|
service.delete_entry(db, entry.id)
|
|
|
|
|
assert service.list_entries(db, Section.other) == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_set_debt_target(db):
|
|
|
|
|
dm = service.add_entry(db, Section.debt_minimum, "Card A", Decimal("50.00"))
|
|
|
|
|
target = service.set_debt_target(db, dm.id)
|
|
|
|
|
assert target.debt_minimum_id == dm.id
|
|
|
|
|
assert target.entry is not None and target.entry.name == "Card A"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_debt_target_rejects_non_debt_minimum(db):
|
|
|
|
|
income = service.add_entry(db, Section.income, "Paycheck", Decimal("1.00"))
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
service.set_debt_target(db, income.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_debt_target_cleared_on_delete(db):
|
|
|
|
|
dm = service.add_entry(db, Section.debt_minimum, "Card B", Decimal("75.00"))
|
|
|
|
|
service.set_debt_target(db, dm.id)
|
|
|
|
|
service.delete_entry(db, dm.id)
|
|
|
|
|
target = service.get_debt_target(db)
|
|
|
|
|
assert target.debt_minimum_id is None
|
2026-04-17 18:45:12 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_entry_name_only(db):
|
|
|
|
|
entry = service.add_entry(db, Section.subscription, "Twitch", Decimal("10.99"))
|
|
|
|
|
updated = service.update_entry(db, entry.id, name="Twitch Prime")
|
|
|
|
|
assert updated is not None
|
|
|
|
|
assert updated.name == "Twitch Prime"
|
|
|
|
|
assert updated.amount == Decimal("10.99")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_entry_amount_only(db):
|
|
|
|
|
entry = service.add_entry(db, Section.subscription, "Twitch", Decimal("10.99"))
|
|
|
|
|
updated = service.update_entry(db, entry.id, amount=Decimal("11.99"))
|
|
|
|
|
assert updated is not None
|
|
|
|
|
assert updated.name == "Twitch"
|
|
|
|
|
assert updated.amount == Decimal("11.99")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_entry_notes_set_and_clear(db):
|
|
|
|
|
entry = service.add_entry(db, Section.other, "Parking", Decimal("25.00"))
|
|
|
|
|
updated = service.update_entry(db, entry.id, notes="work")
|
|
|
|
|
assert updated is not None
|
|
|
|
|
assert updated.notes == "work"
|
|
|
|
|
updated = service.update_entry(db, entry.id, notes="")
|
|
|
|
|
assert updated is not None
|
|
|
|
|
assert updated.notes is None
|
|
|
|
|
service.update_entry(db, entry.id, notes="work again")
|
|
|
|
|
updated = service.update_entry(db, entry.id, notes=None)
|
|
|
|
|
assert updated is not None
|
|
|
|
|
assert updated.notes is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_entry_all_three_atomic(db):
|
|
|
|
|
entry = service.add_entry(db, Section.food, "Groceries", Decimal("400.00"))
|
|
|
|
|
updated = service.update_entry(
|
|
|
|
|
db,
|
|
|
|
|
entry.id,
|
|
|
|
|
name="Groceries (Costco)",
|
|
|
|
|
amount=Decimal("450.00"),
|
|
|
|
|
notes="weekly run",
|
|
|
|
|
)
|
|
|
|
|
assert updated is not None
|
|
|
|
|
assert updated.name == "Groceries (Costco)"
|
|
|
|
|
assert updated.amount == Decimal("450.00")
|
|
|
|
|
assert updated.notes == "weekly run"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_entry_notes_untouched_when_sentinel(db):
|
|
|
|
|
entry = service.add_entry(
|
|
|
|
|
db, Section.other, "Gift", Decimal("25.00"), notes="birthday"
|
|
|
|
|
)
|
|
|
|
|
updated = service.update_entry(db, entry.id, amount=Decimal("30.00"))
|
|
|
|
|
assert updated is not None
|
|
|
|
|
assert updated.notes == "birthday"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_entry_missing_returns_none(db):
|
|
|
|
|
assert service.update_entry(db, 9999, name="Whatever") is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_entry_does_not_mutate_existing_month_snapshot(db):
|
|
|
|
|
from quartermaster import month_service
|
|
|
|
|
entry = service.add_entry(
|
|
|
|
|
db, Section.subscription, "Twitch", Decimal("10.99")
|
|
|
|
|
)
|
|
|
|
|
month = month_service.create_month(db, "2026-04")
|
|
|
|
|
me = next(e for e in month.entries if e.source_entry_id == entry.id)
|
|
|
|
|
assert me.planned == Decimal("10.99")
|
|
|
|
|
assert me.origin_planned == Decimal("10.99")
|
|
|
|
|
assert me.name == "Twitch"
|
|
|
|
|
assert me.origin_name == "Twitch"
|
|
|
|
|
|
|
|
|
|
service.update_entry(
|
|
|
|
|
db, entry.id, name="Twitch Prime", amount=Decimal("11.99")
|
|
|
|
|
)
|
|
|
|
|
db.refresh(me)
|
|
|
|
|
assert me.planned == Decimal("10.99")
|
|
|
|
|
assert me.origin_planned == Decimal("10.99")
|
|
|
|
|
assert me.name == "Twitch"
|
|
|
|
|
assert me.origin_name == "Twitch"
|