Backing transaction ledger: Postings replace the applied field #20
7 changed files with 291 additions and 37 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from quartermaster import month_service, service
|
from quartermaster import month_service, service
|
||||||
|
|
@ -65,10 +66,10 @@ def test_month_group_views_planned_and_applied(db):
|
||||||
service.add_entry(db, Section.fixed_bill, "Rent", Decimal("1200.00"))
|
service.add_entry(db, Section.fixed_bill, "Rent", Decimal("1200.00"))
|
||||||
service.add_entry(db, Section.sinking_fund, "Emergency", Decimal("300.00"))
|
service.add_entry(db, Section.sinking_fund, "Emergency", Decimal("300.00"))
|
||||||
month = month_service.create_month(db, "2026-04")
|
month = month_service.create_month(db, "2026-04")
|
||||||
# seed applied values
|
# seed applied values via a posting
|
||||||
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
month_service.update_month_entry(
|
month_service.add_posting(
|
||||||
db, month, rent.id, applied=Decimal("1200.00")
|
db, month, rent.id, date.today(), Decimal("1200.00"), description="test"
|
||||||
)
|
)
|
||||||
db.refresh(month)
|
db.refresh(month)
|
||||||
views = {v.group: v for v in month_service.month_group_views(month)}
|
views = {v.group: v for v in month_service.month_group_views(month)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -16,10 +17,10 @@ def _seed(db, balance_to_zero=False):
|
||||||
service.add_entry(db, Section.other, "Misc", Decimal("60.00"))
|
service.add_entry(db, Section.other, "Misc", Decimal("60.00"))
|
||||||
month = month_service.create_month(db, "2026-04")
|
month = month_service.create_month(db, "2026-04")
|
||||||
if balance_to_zero:
|
if balance_to_zero:
|
||||||
# Apply income fully and distribute applied to match planned exactly
|
# Post one transaction per entry matching the planned amount
|
||||||
for entry in month.entries:
|
for entry in month.entries:
|
||||||
month_service.update_month_entry(
|
month_service.add_posting(
|
||||||
db, month, entry.id, applied=entry.planned
|
db, month, entry.id, date.today(), entry.planned, description="seed"
|
||||||
)
|
)
|
||||||
db.refresh(month)
|
db.refresh(month)
|
||||||
return month
|
return month
|
||||||
|
|
@ -61,8 +62,8 @@ def test_close_requires_zero_balance(db):
|
||||||
# applied zero is 0 - 0 = 0, but income applied is 0 so
|
# applied zero is 0 - 0 = 0, but income applied is 0 so
|
||||||
# (0 income - 0 others) == 0 actually. Force a non-zero by applying to food.
|
# (0 income - 0 others) == 0 actually. Force a non-zero by applying to food.
|
||||||
food = next(e for e in month.entries if e.origin_name == "Groceries")
|
food = next(e for e in month.entries if e.origin_name == "Groceries")
|
||||||
month_service.update_month_entry(
|
month_service.add_posting(
|
||||||
db, month, food.id, applied=Decimal("50.00")
|
db, month, food.id, date.today(), Decimal("50.00"), description="seed"
|
||||||
)
|
)
|
||||||
db.refresh(month)
|
db.refresh(month)
|
||||||
with pytest.raises(month_service.MonthLifecycleError) as exc:
|
with pytest.raises(month_service.MonthLifecycleError) as exc:
|
||||||
|
|
@ -128,13 +129,16 @@ def _seed_via_api(client, balance_to_zero=False):
|
||||||
if balance_to_zero:
|
if balance_to_zero:
|
||||||
# entry ids 1,2,3 in that order
|
# entry ids 1,2,3 in that order
|
||||||
client.post(
|
client.post(
|
||||||
"/month/2026-04/entries/1", data={"applied": "1000.00"}
|
"/month/2026-04/entries/1/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "1000.00"},
|
||||||
)
|
)
|
||||||
client.post(
|
client.post(
|
||||||
"/month/2026-04/entries/2", data={"applied": "600.00"}
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "600.00"},
|
||||||
)
|
)
|
||||||
client.post(
|
client.post(
|
||||||
"/month/2026-04/entries/3", data={"applied": "400.00"}
|
"/month/2026-04/entries/3/postings",
|
||||||
|
data={"occurred_on": "2026-04-15", "amount": "400.00"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -148,8 +152,11 @@ def test_activate_route(client):
|
||||||
def test_close_route_rejects_unbalanced(client):
|
def test_close_route_rejects_unbalanced(client):
|
||||||
_seed_via_api(client)
|
_seed_via_api(client)
|
||||||
client.post("/month/2026-04/activate")
|
client.post("/month/2026-04/activate")
|
||||||
# apply some expense without income to produce an unbalanced state
|
# post an expense without income to produce an unbalanced state
|
||||||
client.post("/month/2026-04/entries/2", data={"applied": "100.00"})
|
client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "100.00"},
|
||||||
|
)
|
||||||
response = client.post("/month/2026-04/close")
|
response = client.post("/month/2026-04/close")
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "0.00" in response.json()["detail"]
|
assert "0.00" in response.json()["detail"]
|
||||||
|
|
@ -182,7 +189,8 @@ def test_mutations_rejected_on_closed_month(client):
|
||||||
)
|
)
|
||||||
assert add_response.status_code == 400
|
assert add_response.status_code == 400
|
||||||
update_response = client.post(
|
update_response = client.post(
|
||||||
"/month/2026-04/entries/1", data={"applied": "1200.00"}
|
"/month/2026-04/entries/1/postings",
|
||||||
|
data={"occurred_on": "2026-04-15", "amount": "50.00"},
|
||||||
)
|
)
|
||||||
assert update_response.status_code == 400
|
assert update_response.status_code == 400
|
||||||
delete_response = client.delete("/month/2026-04/entries/1")
|
delete_response = client.delete("/month/2026-04/entries/1")
|
||||||
|
|
@ -201,8 +209,11 @@ def test_month_page_shows_planning_badge_and_activate_button(client):
|
||||||
def test_month_page_shows_active_badge_with_disabled_close_when_unbalanced(client):
|
def test_month_page_shows_active_badge_with_disabled_close_when_unbalanced(client):
|
||||||
_seed_via_api(client)
|
_seed_via_api(client)
|
||||||
client.post("/month/2026-04/activate")
|
client.post("/month/2026-04/activate")
|
||||||
# force imbalance
|
# force imbalance via a posting
|
||||||
client.post("/month/2026-04/entries/2", data={"applied": "100.00"})
|
client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "100.00"},
|
||||||
|
)
|
||||||
response = client.get("/month/2026-04")
|
response = client.get("/month/2026-04")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "state-active" in response.text
|
assert "state-active" in response.text
|
||||||
|
|
@ -237,7 +248,10 @@ def test_closed_month_inputs_are_disabled(client):
|
||||||
client.post("/month/2026-04/activate")
|
client.post("/month/2026-04/activate")
|
||||||
client.post("/month/2026-04/close")
|
client.post("/month/2026-04/close")
|
||||||
response = client.get("/month/2026-04")
|
response = client.get("/month/2026-04")
|
||||||
# name inputs rendered with disabled
|
# Editable inputs are replaced by readonly spans on closed months,
|
||||||
assert "disabled" in response.text
|
# so the name/planned inputs should not appear.
|
||||||
|
assert 'name="planned"' not in response.text
|
||||||
|
assert 'name="name"' not in response.text
|
||||||
|
assert "class=\"readonly\"" in response.text
|
||||||
# delete buttons not rendered
|
# delete buttons not rendered
|
||||||
assert "Delete Paycheck" not in response.text
|
assert "Delete Paycheck" not in response.text
|
||||||
|
|
|
||||||
|
|
@ -38,23 +38,24 @@ def test_created_month_renders_snapshot(client):
|
||||||
assert "Paycheck" in response.text
|
assert "Paycheck" in response.text
|
||||||
assert "Rent" in response.text
|
assert "Rent" in response.text
|
||||||
assert "Card A" in response.text
|
assert "Card A" in response.text
|
||||||
# totals: applied / planned
|
# totals: applied / planned rendered with thousands separators
|
||||||
assert "$2500.00" in response.text
|
assert "$2,500.00" in response.text
|
||||||
assert "$1200.00" in response.text
|
assert "$1,200.00" in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_applied_update_returns_section_partial(client):
|
def test_applied_update_returns_section_partial(client):
|
||||||
_seed_budget_via_api(client)
|
_seed_budget_via_api(client)
|
||||||
client.post("/month/2026-04/create")
|
client.post("/month/2026-04/create")
|
||||||
# fetch month page, identify rent month_entry id = 2 (after income id 1)
|
# rent is month_entry id=2 (after income id=1). Post a transaction so
|
||||||
|
# applied accumulates to 1200.
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/month/2026-04/entries/2", data={"applied": "1200.00"}
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "1200.00"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
# rent is in fixed_bill section; total_applied should now include 1200
|
# rent is in fixed_bill section; total_applied should now include 1200
|
||||||
assert 'id="section-fixed_bill"' in response.text
|
assert 'id="section-fixed_bill"' in response.text
|
||||||
# two $1200.00 occurrences: total_applied and planned (same amount)
|
assert "$1,200.00" in response.text or "$1200.00" in response.text
|
||||||
assert response.text.count("$1200.00") >= 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_name_edit_flips_to_modified(client):
|
def test_name_edit_flips_to_modified(client):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -8,6 +9,13 @@ from quartermaster import month_service, service
|
||||||
from quartermaster.models import Section
|
from quartermaster.models import Section
|
||||||
|
|
||||||
|
|
||||||
|
def _apply(db, month, entry_id, amount):
|
||||||
|
"""Record an applied amount against a month entry by posting."""
|
||||||
|
return month_service.add_posting(
|
||||||
|
db, month, entry_id, date.today(), Decimal(str(amount)), description="test"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _seed_budget(db):
|
def _seed_budget(db):
|
||||||
income = service.add_entry(db, Section.income, "Paycheck", Decimal("2500.00"))
|
income = service.add_entry(db, Section.income, "Paycheck", Decimal("2500.00"))
|
||||||
rent = service.add_entry(
|
rent = service.add_entry(
|
||||||
|
|
@ -96,9 +104,8 @@ def test_applied_update_changes_totals(db):
|
||||||
_seed_budget(db)
|
_seed_budget(db)
|
||||||
month = month_service.create_month(db, "2026-04")
|
month = month_service.create_month(db, "2026-04")
|
||||||
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
month_service.update_month_entry(
|
_apply(db, month, rent.id, "1200.00")
|
||||||
db, month, rent.id, applied=Decimal("1200.00")
|
db.refresh(month)
|
||||||
)
|
|
||||||
view = month_service.section_view(month, Section.fixed_bill, "Fixed")
|
view = month_service.section_view(month, Section.fixed_bill, "Fixed")
|
||||||
assert view.total_applied == Decimal("1200.00")
|
assert view.total_applied == Decimal("1200.00")
|
||||||
assert view.total_planned == Decimal("1200.00")
|
assert view.total_planned == Decimal("1200.00")
|
||||||
|
|
|
||||||
|
|
@ -155,5 +155,6 @@ def test_month_page_renders_notes_inputs(client):
|
||||||
client.post("/month/2026-04/create")
|
client.post("/month/2026-04/create")
|
||||||
response = client.get("/month/2026-04")
|
response = client.get("/month/2026-04")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "entry-notes-row" in response.text
|
# notes input lives inside the expandable entry body now
|
||||||
|
assert 'name="notes"' in response.text
|
||||||
assert "auto-pay" in response.text
|
assert "auto-pay" in response.text
|
||||||
|
|
|
||||||
226
tests/test_postings.py
Normal file
226
tests/test_postings.py
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from quartermaster import month_service, service
|
||||||
|
from quartermaster.models import Posting, Section
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_month(db, section=Section.fixed_bill, name="Rent", planned="1200"):
|
||||||
|
service.add_entry(db, Section.income, "Paycheck", Decimal("2500"))
|
||||||
|
service.add_entry(db, section, name, Decimal(planned))
|
||||||
|
return month_service.create_month(db, "2026-04")
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_posting_sums_into_applied(db):
|
||||||
|
month = _seed_month(db)
|
||||||
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
|
month_service.add_posting(
|
||||||
|
db, month, rent.id, date(2026, 4, 1), Decimal("600"), description="first half"
|
||||||
|
)
|
||||||
|
month_service.add_posting(
|
||||||
|
db, month, rent.id, date(2026, 4, 15), Decimal("600"), description="second half"
|
||||||
|
)
|
||||||
|
db.refresh(rent)
|
||||||
|
assert rent.applied == Decimal("1200.00")
|
||||||
|
assert len(rent.postings) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_entry_applied_is_zero(db):
|
||||||
|
month = _seed_month(db)
|
||||||
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
|
assert rent.applied == Decimal("0.00")
|
||||||
|
assert rent.postings == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_posting_allowed(db):
|
||||||
|
month = _seed_month(db)
|
||||||
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
|
month_service.add_posting(
|
||||||
|
db, month, rent.id, date(2026, 4, 1), Decimal("1200")
|
||||||
|
)
|
||||||
|
month_service.add_posting(
|
||||||
|
db, month, rent.id, date(2026, 4, 3), Decimal("-50"), description="refund"
|
||||||
|
)
|
||||||
|
db.refresh(rent)
|
||||||
|
assert rent.applied == Decimal("1150.00")
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_posting(db):
|
||||||
|
month = _seed_month(db)
|
||||||
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
|
posting = month_service.add_posting(
|
||||||
|
db, month, rent.id, date(2026, 4, 1), Decimal("1100")
|
||||||
|
)
|
||||||
|
month_service.update_posting(
|
||||||
|
db, month, posting.id,
|
||||||
|
amount=Decimal("1200"),
|
||||||
|
description="corrected",
|
||||||
|
occurred_on=date(2026, 4, 3),
|
||||||
|
)
|
||||||
|
db.refresh(rent)
|
||||||
|
assert rent.applied == Decimal("1200.00")
|
||||||
|
updated = rent.postings[0]
|
||||||
|
assert updated.description == "corrected"
|
||||||
|
assert updated.occurred_on == date(2026, 4, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_posting(db):
|
||||||
|
month = _seed_month(db)
|
||||||
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
|
p1 = month_service.add_posting(db, month, rent.id, date.today(), Decimal("600"))
|
||||||
|
p2 = month_service.add_posting(db, month, rent.id, date.today(), Decimal("600"))
|
||||||
|
entry = month_service.delete_posting(db, month, p1.id)
|
||||||
|
db.refresh(rent)
|
||||||
|
assert entry.id == rent.id
|
||||||
|
assert rent.applied == Decimal("600.00")
|
||||||
|
assert len(rent.postings) == 1
|
||||||
|
assert rent.postings[0].id == p2.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_entry_cascades_postings(db):
|
||||||
|
month = _seed_month(db)
|
||||||
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
|
month_service.add_posting(db, month, rent.id, date.today(), Decimal("600"))
|
||||||
|
month_service.add_posting(db, month, rent.id, date.today(), Decimal("600"))
|
||||||
|
# Postings count before deletion
|
||||||
|
all_postings = db.query(Posting).filter(Posting.month_entry_id == rent.id).count()
|
||||||
|
assert all_postings == 2
|
||||||
|
month_service.delete_month_entry(db, month, rent.id)
|
||||||
|
db.expire_all()
|
||||||
|
remaining = db.query(Posting).filter(Posting.month_entry_id == rent.id).count()
|
||||||
|
assert remaining == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_postings_ordered_desc(db):
|
||||||
|
month = _seed_month(db)
|
||||||
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
|
month_service.add_posting(db, month, rent.id, date(2026, 4, 1), Decimal("100"))
|
||||||
|
month_service.add_posting(db, month, rent.id, date(2026, 4, 15), Decimal("200"))
|
||||||
|
month_service.add_posting(db, month, rent.id, date(2026, 4, 8), Decimal("150"))
|
||||||
|
db.refresh(rent)
|
||||||
|
dates = [p.occurred_on for p in rent.postings]
|
||||||
|
assert dates == [date(2026, 4, 15), date(2026, 4, 8), date(2026, 4, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_month_entry_rejects_applied_kwarg(db):
|
||||||
|
month = _seed_month(db)
|
||||||
|
rent = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
month_service.update_month_entry(
|
||||||
|
db, month, rent.id, applied=Decimal("500")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Route-level tests -----
|
||||||
|
|
||||||
|
def _seed_via_api(client):
|
||||||
|
client.post(
|
||||||
|
"/sections/income/entries",
|
||||||
|
data={"name": "Paycheck", "amount": "2500"},
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
"/sections/fixed_bill/entries",
|
||||||
|
data={"name": "Rent", "amount": "1200"},
|
||||||
|
)
|
||||||
|
client.post("/month/2026-04/create")
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_posting_route(client):
|
||||||
|
_seed_via_api(client)
|
||||||
|
response = client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={
|
||||||
|
"occurred_on": "2026-04-01",
|
||||||
|
"amount": "1200",
|
||||||
|
"description": "April rent",
|
||||||
|
"payee": "Landlord",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "April rent" in response.text
|
||||||
|
assert "Landlord" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_posting_route(client):
|
||||||
|
_seed_via_api(client)
|
||||||
|
client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "1100"},
|
||||||
|
)
|
||||||
|
# posting id is 1 (first created)
|
||||||
|
response = client.post(
|
||||||
|
"/month/2026-04/postings/1",
|
||||||
|
data={"amount": "1200", "description": "fixed amount"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "fixed amount" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_posting_route(client):
|
||||||
|
_seed_via_api(client)
|
||||||
|
client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "1200"},
|
||||||
|
)
|
||||||
|
response = client.delete("/month/2026-04/postings/1")
|
||||||
|
assert response.status_code == 200
|
||||||
|
# The section partial should render with no postings and applied back to $0
|
||||||
|
assert "$0.00" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_posting_routes_reject_invalid_date(client):
|
||||||
|
_seed_via_api(client)
|
||||||
|
response = client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "not-a-date", "amount": "100"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_posting_rejected_on_closed_month(client):
|
||||||
|
_seed_via_api(client)
|
||||||
|
# Post balancing transactions so the month can close
|
||||||
|
client.post(
|
||||||
|
"/month/2026-04/entries/1/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "1200"},
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "1200"},
|
||||||
|
)
|
||||||
|
client.post("/month/2026-04/activate")
|
||||||
|
close = client.post("/month/2026-04/close")
|
||||||
|
assert close.status_code == 204
|
||||||
|
# Now try to add a new posting on the closed month
|
||||||
|
response = client.post(
|
||||||
|
"/month/2026-04/entries/1/postings",
|
||||||
|
data={"occurred_on": "2026-04-20", "amount": "1"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_applied_update_via_posting_flips_zero_tone(client):
|
||||||
|
_seed_via_api(client)
|
||||||
|
response = client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "1200"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
# With only rent applied, applied zero = 0 - 1200 = -1200 -> negative
|
||||||
|
assert "tone-negative" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_posting_count_badge_renders(client):
|
||||||
|
_seed_via_api(client)
|
||||||
|
for i in range(3):
|
||||||
|
client.post(
|
||||||
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "400"},
|
||||||
|
)
|
||||||
|
page = client.get("/month/2026-04")
|
||||||
|
assert page.status_code == 200
|
||||||
|
assert "3 txns" in page.text
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from quartermaster import month_service, service
|
from quartermaster import month_service, service
|
||||||
from quartermaster.models import Section
|
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():
|
def test_zero_tone_classification():
|
||||||
assert service.zero_tone(Decimal("0")) == "zero"
|
assert service.zero_tone(Decimal("0")) == "zero"
|
||||||
assert service.zero_tone(Decimal("0.00")) == "zero"
|
assert service.zero_tone(Decimal("0.00")) == "zero"
|
||||||
|
|
@ -66,12 +73,8 @@ def test_month_zero_reflects_applied_updates(db):
|
||||||
month = month_service.create_month(db, "2026-04")
|
month = month_service.create_month(db, "2026-04")
|
||||||
income_entry = next(e for e in month.entries if e.origin_name == "Paycheck")
|
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")
|
rent_entry = next(e for e in month.entries if e.origin_name == "Rent")
|
||||||
month_service.update_month_entry(
|
_apply(db, month, income_entry.id, "2500.00")
|
||||||
db, month, income_entry.id, applied=Decimal("2500.00")
|
_apply(db, month, rent_entry.id, "1200.00")
|
||||||
)
|
|
||||||
month_service.update_month_entry(
|
|
||||||
db, month, rent_entry.id, applied=Decimal("1200.00")
|
|
||||||
)
|
|
||||||
db.refresh(month)
|
db.refresh(month)
|
||||||
z = month_service.month_zero(month)
|
z = month_service.month_zero(month)
|
||||||
# applied income 2500 - applied (1200 + 0) = 1300
|
# applied income 2500 - applied (1200 + 0) = 1300
|
||||||
|
|
@ -117,9 +120,10 @@ def test_month_entry_update_returns_zero_widget_oob(client):
|
||||||
data={"name": "Rent", "amount": "1200.00"},
|
data={"name": "Rent", "amount": "1200.00"},
|
||||||
)
|
)
|
||||||
client.post("/month/2026-04/create")
|
client.post("/month/2026-04/create")
|
||||||
# updating rent applied should bring the month zero widget back with new values
|
# posting rent applied via the ledger should bring the month zero widget back with new values
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/month/2026-04/entries/2", data={"applied": "1200.00"}
|
"/month/2026-04/entries/2/postings",
|
||||||
|
data={"occurred_on": "2026-04-01", "amount": "1200.00"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert 'id="zero-widget"' in response.text
|
assert 'id="zero-widget"' in response.text
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue