New test_postings.py walks service and route layers: add sums into applied, negatives are allowed, update and delete round-trip, entry deletion cascades postings, order is desc by date, update_month_entry rejects the removed applied kwarg. Route tests assert HTTP behaviour, invalid-date rejection, closed-month lock, tone flip after a posting, and the "N txns" count badge renders. Existing tests that previously set applied via update_month_entry or the entries route now use add_posting or POST to /postings. Format assertions updated to match the new thousands-separator number rendering and the replaced entry-notes-row markup. Refs #19 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
5.1 KiB
Python
144 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
|
|
from quartermaster import month_service, service
|
|
from quartermaster.models import Section
|
|
|
|
|
|
def _apply(db, month, entry_id, amount):
|
|
return month_service.add_posting(
|
|
db, month, entry_id, date.today(), Decimal(str(amount)), description="test"
|
|
)
|
|
|
|
|
|
def test_zero_tone_classification():
|
|
assert service.zero_tone(Decimal("0")) == "zero"
|
|
assert service.zero_tone(Decimal("0.00")) == "zero"
|
|
assert service.zero_tone(Decimal("1.00")) == "positive"
|
|
assert service.zero_tone(Decimal("-0.01")) == "negative"
|
|
|
|
|
|
def test_budget_zero_empty_is_zero(db):
|
|
assert service.budget_zero(db) == Decimal("0.00")
|
|
|
|
|
|
def test_budget_zero_positive_when_unassigned(db):
|
|
service.add_entry(db, Section.income, "Paycheck", Decimal("2500.00"))
|
|
service.add_entry(db, Section.fixed_bill, "Rent", Decimal("1200.00"))
|
|
assert service.budget_zero(db) == Decimal("1300.00")
|
|
|
|
|
|
def test_budget_zero_negative_when_overbudget(db):
|
|
service.add_entry(db, Section.income, "Paycheck", Decimal("1000.00"))
|
|
service.add_entry(db, Section.fixed_bill, "Rent", Decimal("1200.00"))
|
|
assert service.budget_zero(db) == Decimal("-200.00")
|
|
|
|
|
|
def test_budget_zero_exactly_zero(db):
|
|
service.add_entry(db, Section.income, "Paycheck", Decimal("1500.00"))
|
|
service.add_entry(db, Section.fixed_bill, "Rent", Decimal("1000.00"))
|
|
service.add_entry(db, Section.food, "Groceries", Decimal("500.00"))
|
|
assert service.budget_zero(db) == Decimal("0.00")
|
|
|
|
|
|
def test_budget_zero_ignores_debt_target(db):
|
|
# target is a pointer, not a totalable section; the math does not touch it
|
|
dm = service.add_entry(db, Section.debt_minimum, "Card A", Decimal("40.00"))
|
|
service.add_entry(db, Section.income, "Paycheck", Decimal("100.00"))
|
|
service.set_debt_target(db, dm.id)
|
|
assert service.budget_zero(db) == Decimal("60.00")
|
|
|
|
|
|
def _seed_budget(db):
|
|
service.add_entry(db, Section.income, "Paycheck", Decimal("2500.00"))
|
|
service.add_entry(db, Section.fixed_bill, "Rent", Decimal("1200.00"))
|
|
service.add_entry(db, Section.debt_minimum, "Card A", Decimal("40.00"))
|
|
|
|
|
|
def test_month_zero_starts_planned_nonzero_applied_zero(db):
|
|
_seed_budget(db)
|
|
month = month_service.create_month(db, "2026-04")
|
|
z = month_service.month_zero(month)
|
|
# income 2500 - (1200 + 40) = 1260
|
|
assert z.planned == Decimal("1260.00")
|
|
# applied all 0 on fresh snapshot
|
|
assert z.applied == Decimal("2500.00") * 0 - Decimal("0")
|
|
assert z.applied == Decimal("0.00")
|
|
|
|
|
|
def test_month_zero_reflects_applied_updates(db):
|
|
_seed_budget(db)
|
|
month = month_service.create_month(db, "2026-04")
|
|
income_entry = next(e for e in month.entries if e.origin_name == "Paycheck")
|
|
rent_entry = next(e for e in month.entries if e.origin_name == "Rent")
|
|
_apply(db, month, income_entry.id, "2500.00")
|
|
_apply(db, month, rent_entry.id, "1200.00")
|
|
db.refresh(month)
|
|
z = month_service.month_zero(month)
|
|
# applied income 2500 - applied (1200 + 0) = 1300
|
|
assert z.applied == Decimal("1300.00")
|
|
|
|
|
|
def test_add_entry_returns_zero_widget_oob(client):
|
|
response = client.post(
|
|
"/sections/income/entries",
|
|
data={"name": "Paycheck", "amount": "2500.00"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert 'id="zero-widget"' in response.text
|
|
assert 'hx-swap-oob="outerHTML"' in response.text
|
|
assert "tone-positive" in response.text
|
|
|
|
|
|
def test_delete_entry_returns_zero_widget_oob(client):
|
|
client.post(
|
|
"/sections/income/entries",
|
|
data={"name": "Paycheck", "amount": "2500.00"},
|
|
)
|
|
response = client.delete("/entries/1")
|
|
assert response.status_code == 200
|
|
assert 'id="zero-widget"' in response.text
|
|
assert "tone-zero" in response.text
|
|
|
|
|
|
def test_budget_page_renders_zero_widget(client):
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert 'id="zero-widget"' in response.text
|
|
assert "Zero Amount" in response.text
|
|
|
|
|
|
def test_month_entry_update_returns_zero_widget_oob(client):
|
|
client.post(
|
|
"/sections/income/entries",
|
|
data={"name": "Paycheck", "amount": "2500.00"},
|
|
)
|
|
client.post(
|
|
"/sections/fixed_bill/entries",
|
|
data={"name": "Rent", "amount": "1200.00"},
|
|
)
|
|
client.post("/month/2026-04/create")
|
|
# posting rent applied via the ledger should bring the month zero widget back with new values
|
|
response = client.post(
|
|
"/month/2026-04/entries/2/postings",
|
|
data={"occurred_on": "2026-04-01", "amount": "1200.00"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert 'id="zero-widget"' in response.text
|
|
# applied: 0 - 1200 = -1200, negative tone
|
|
assert "tone-negative" in response.text
|
|
|
|
|
|
def test_month_page_renders_paired_zero_widget(client):
|
|
client.post(
|
|
"/sections/income/entries",
|
|
data={"name": "Paycheck", "amount": "2500.00"},
|
|
)
|
|
client.post("/month/2026-04/create")
|
|
response = client.get("/month/2026-04")
|
|
assert response.status_code == 200
|
|
assert 'id="zero-widget"' in response.text
|
|
assert response.text.count("Planned") >= 1
|
|
assert response.text.count("Applied") >= 1
|