quartermaster/tests/test_month_service.py
archeious 9da7205f77 test: cover month snapshot, deviation states, and per-month target
Service tests assert that create_month produces origin fields matching
the budget, that edits flip deviation_state to edited, that added rows
are new_in_month, and that a budget entry deleted after snapshot leaves
the month entry unchanged. Route tests exercise the create flow,
applied updates, name edits producing the modified tag, per-month
target isolation, and the malformed-year-month 404.

Refs #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:40:05 -06:00

148 lines
5.4 KiB
Python

from __future__ import annotations
from decimal import Decimal
import pytest
from quartermaster import month_service, service
from quartermaster.models import Section
def _seed_budget(db):
income = service.add_entry(db, Section.income, "Paycheck", Decimal("2500.00"))
rent = service.add_entry(
db, Section.fixed_bill, "Rent", Decimal("1200.00")
)
card_a = service.add_entry(
db, Section.debt_minimum, "Card A", Decimal("40.00")
)
card_b = service.add_entry(
db, Section.debt_minimum, "Card B", Decimal("60.00")
)
service.set_debt_target(db, card_a.id)
return income, rent, card_a, card_b
def test_valid_year_month():
assert month_service.valid_year_month("2026-04")
assert month_service.valid_year_month("2026-12")
assert not month_service.valid_year_month("2026-00")
assert not month_service.valid_year_month("2026-13")
assert not month_service.valid_year_month("26-04")
assert not month_service.valid_year_month("2026/04")
def test_shift_year_month_rolls_years():
assert month_service.shift_year_month("2026-12", 1) == "2027-01"
assert month_service.shift_year_month("2026-01", -1) == "2025-12"
assert month_service.shift_year_month("2026-04", 0) == "2026-04"
assert month_service.shift_year_month("2026-04", 12) == "2027-04"
def test_create_month_snapshots_budget(db):
income, rent, card_a, card_b = _seed_budget(db)
month = month_service.create_month(db, "2026-04")
assert month.year_month == "2026-04"
assert len(month.entries) == 4
by_source = {e.source_entry_id: e for e in month.entries}
assert by_source[income.id].planned == Decimal("2500.00")
assert by_source[income.id].origin_name == "Paycheck"
assert by_source[income.id].origin_planned == Decimal("2500.00")
assert by_source[income.id].applied == Decimal("0.00")
assert by_source[rent.id].section == Section.fixed_bill
assert month.target is not None
assert month.target.entry is not None
assert month.target.entry.source_entry_id == card_a.id
def test_create_month_idempotent(db):
_seed_budget(db)
first = month_service.create_month(db, "2026-04")
second = month_service.create_month(db, "2026-04")
assert first.id == second.id
def test_create_month_rejects_bad_year_month(db):
with pytest.raises(ValueError):
month_service.create_month(db, "2026-00")
def test_deviation_state_reflects_edits(db):
_seed_budget(db)
month = month_service.create_month(db, "2026-04")
rent = next(e for e in month.entries if e.origin_name == "Rent")
assert month_service.deviation_state(rent) == month_service.DeviationState.unchanged
month_service.update_month_entry(
db, month, rent.id, planned=Decimal("1300.00")
)
rent = next(e for e in month.entries if e.id == rent.id)
assert month_service.deviation_state(rent) == month_service.DeviationState.edited
def test_added_row_is_new_in_month(db):
_seed_budget(db)
month = month_service.create_month(db, "2026-04")
extra = month_service.add_month_entry(
db, month, Section.other, "Surprise", Decimal("20.00")
)
assert extra.origin_name is None
assert month_service.deviation_state(extra) == (
month_service.DeviationState.new_in_month
)
def test_applied_update_changes_totals(db):
_seed_budget(db)
month = month_service.create_month(db, "2026-04")
rent = next(e for e in month.entries if e.origin_name == "Rent")
month_service.update_month_entry(
db, month, rent.id, applied=Decimal("1200.00")
)
view = month_service.section_view(month, Section.fixed_bill, "Fixed")
assert view.total_applied == Decimal("1200.00")
assert view.total_planned == Decimal("1200.00")
def test_per_month_target_is_isolated(db):
_seed_budget(db)
april = month_service.create_month(db, "2026-04")
may = month_service.create_month(db, "2026-05")
april_b = next(
e for e in april.entries
if e.section == Section.debt_minimum and e.origin_name == "Card B"
)
month_service.set_month_target(db, april, april_b.id)
april_target = month_service.get_month_target(db, april)
may_target = month_service.get_month_target(db, may)
assert april_target.entry is not None
assert april_target.entry.origin_name == "Card B"
assert may_target.entry is not None
assert may_target.entry.origin_name == "Card A"
def test_delete_month_entry_returns_section(db):
_seed_budget(db)
month = month_service.create_month(db, "2026-04")
rent = next(e for e in month.entries if e.origin_name == "Rent")
result = month_service.delete_month_entry(db, month, rent.id)
assert result == Section.fixed_bill
db.refresh(month)
remaining = [
e for e in month.entries if e.section == Section.fixed_bill
]
assert remaining == []
def test_budget_entry_deleted_after_snapshot_leaves_month_intact(db):
_seed_budget(db)
month = month_service.create_month(db, "2026-04")
# remove budget-side rent; month-side rent should still render unchanged
budget_rent = service.list_entries(db, Section.fixed_bill)[0]
service.delete_entry(db, budget_rent.id)
db.expire_all()
month_rent = next(e for e in month.entries if e.origin_name == "Rent")
assert month_rent.source_entry_id is None
assert month_service.deviation_state(month_rent) == (
month_service.DeviationState.unchanged
)