diff --git a/tests/test_zero_amount.py b/tests/test_zero_amount.py new file mode 100644 index 0000000..8fe7d49 --- /dev/null +++ b/tests/test_zero_amount.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from decimal import Decimal + +from quartermaster import month_service, service +from quartermaster.models import Section + + +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") + month_service.update_month_entry( + db, month, income_entry.id, applied=Decimal("2500.00") + ) + month_service.update_month_entry( + db, month, rent_entry.id, applied=Decimal("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") + # updating rent applied should bring the month zero widget back with new values + response = client.post( + "/month/2026-04/entries/2", data={"applied": "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 "zero-widget-pair" in response.text + assert response.text.count("Planned") >= 1 + assert response.text.count("Applied") >= 1