quartermaster/tests/test_postings.py

227 lines
7.4 KiB
Python
Raw Normal View History

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