2026-04-17 11:40:05 -06:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_budget_via_api(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(
|
|
|
|
|
"/sections/debt_minimum/entries",
|
|
|
|
|
data={"name": "Card A", "amount": "40.00"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_missing_month_renders_create_flow(client):
|
|
|
|
|
response = client.get("/month/2026-04")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "No snapshot yet" in response.text
|
|
|
|
|
assert 'hx-post="/month/2026-04/create"' in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_month_redirects_via_htmx(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
response = client.post("/month/2026-04/create")
|
|
|
|
|
assert response.status_code == 204
|
|
|
|
|
assert response.headers.get("hx-redirect") == "/month/2026-04"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_created_month_renders_snapshot(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
client.post("/month/2026-04/create")
|
|
|
|
|
response = client.get("/month/2026-04")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "Paycheck" in response.text
|
|
|
|
|
assert "Rent" in response.text
|
|
|
|
|
assert "Card A" in response.text
|
2026-04-17 17:34:53 -06:00
|
|
|
# totals: applied / planned rendered with thousands separators
|
|
|
|
|
assert "$2,500.00" in response.text
|
|
|
|
|
assert "$1,200.00" in response.text
|
2026-04-17 11:40:05 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_applied_update_returns_section_partial(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
client.post("/month/2026-04/create")
|
2026-04-17 17:34:53 -06:00
|
|
|
# rent is month_entry id=2 (after income id=1). Post a transaction so
|
|
|
|
|
# applied accumulates to 1200.
|
2026-04-17 11:40:05 -06:00
|
|
|
response = client.post(
|
2026-04-17 17:34:53 -06:00
|
|
|
"/month/2026-04/entries/2/postings",
|
|
|
|
|
data={"occurred_on": "2026-04-01", "amount": "1200.00"},
|
2026-04-17 11:40:05 -06:00
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
# rent is in fixed_bill section; total_applied should now include 1200
|
|
|
|
|
assert 'id="section-fixed_bill"' in response.text
|
2026-04-17 17:34:53 -06:00
|
|
|
assert "$1,200.00" in response.text or "$1200.00" in response.text
|
2026-04-17 11:40:05 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_name_edit_flips_to_modified(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
client.post("/month/2026-04/create")
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/month/2026-04/entries/2", data={"name": "Rent (April)"}
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "state-edited" in response.text
|
|
|
|
|
assert 'class="tag tag-edited">modified' in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_new_entry_within_month_is_marked_new(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
client.post("/month/2026-04/create")
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/month/2026-04/sections/other/entries",
|
|
|
|
|
data={"name": "Gift", "planned": "50.00"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "state-new_in_month" in response.text
|
|
|
|
|
assert "new this month" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_month_entry(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
client.post("/month/2026-04/create")
|
|
|
|
|
response = client.delete("/month/2026-04/entries/2")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "Rent" not in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_month_debt_minimum_updates_target(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
client.post("/month/2026-04/create")
|
|
|
|
|
# Card A is entry id 3 in the month snapshot
|
|
|
|
|
response = client.delete("/month/2026-04/entries/3")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "section-debt_target" in response.text
|
|
|
|
|
assert "No target selected" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_set_month_target(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/debt_minimum/entries",
|
|
|
|
|
data={"name": "Card B", "amount": "60.00"},
|
|
|
|
|
)
|
|
|
|
|
client.post("/month/2026-04/create")
|
|
|
|
|
# Card B is month_entry id 4
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/month/2026-04/target", data={"month_entry_id": "4"}
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "Card B" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_month_target_isolated_between_months(client):
|
|
|
|
|
_seed_budget_via_api(client)
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/debt_minimum/entries",
|
|
|
|
|
data={"name": "Card B", "amount": "60.00"},
|
|
|
|
|
)
|
|
|
|
|
client.post("/month/2026-04/create")
|
|
|
|
|
client.post("/month/2026-05/create")
|
|
|
|
|
# Change April target to Card B (month_entry id 4 within April)
|
|
|
|
|
client.post("/month/2026-04/target", data={"month_entry_id": "4"})
|
|
|
|
|
may_page = client.get("/month/2026-05")
|
|
|
|
|
# May still points at Card A (copied from budget)
|
|
|
|
|
assert "Card A" in may_page.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_malformed_year_month_returns_404(client):
|
|
|
|
|
response = client.get("/month/2026-13")
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_budget_page_shows_month_nav(client):
|
|
|
|
|
response = client.get("/")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "This month" in response.text
|