diff --git a/src/quartermaster/routes.py b/src/quartermaster/routes.py index eece90a..8642e5c 100644 --- a/src/quartermaster/routes.py +++ b/src/quartermaster/routes.py @@ -162,6 +162,34 @@ def edit_entry( return _render_section(request, db, entry.section, editing_id=entry.id) +@router.post("/entries/{entry_id}", response_class=HTMLResponse) +def save_entry( + entry_id: int, + request: Request, + name: str = Form(...), + amount: str = Form(...), + notes: str | None = Form(None), + db: Session = Depends(get_session), +) -> HTMLResponse: + clean_name = name.strip() + if not clean_name: + raise HTTPException(status_code=400, detail="name is required") + parsed = _parse_amount(amount) + updated = service.update_entry( + db, entry_id, name=clean_name, amount=parsed, notes=notes + ) + if updated is None: + raise HTTPException(status_code=404, detail="entry not found") + response = _render_section(request, db, updated.section) + extras: list[HTMLResponse] = [ + _render_zero(request, db), + _render_group_totals(request, db), + ] + if updated.section == Section.debt_minimum: + extras.append(_render_target(request, db)) + return _append_oob(response, *extras) + + @router.post("/entries/{entry_id}/notes", response_class=HTMLResponse) def update_entry_notes( entry_id: int, diff --git a/tests/test_routes.py b/tests/test_routes.py index 72767e8..ff301d2 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -114,3 +114,93 @@ def test_get_entry_edit_other_rows_stay_in_read_mode(client): assert response.status_code == 200 assert response.text.count('entry-row editing') == 1 assert response.text.count('entry-row reading') == 1 + + +def test_post_entry_updates_name_and_amount(client): + client.post( + "/sections/subscription/entries", + data={"name": "Twitch", "amount": "10.99"}, + ) + response = client.post( + "/entries/1", + data={"name": "Twitch Prime", "amount": "11.99", "notes": ""}, + ) + assert response.status_code == 200 + assert "Twitch Prime" in response.text + assert "$11.99" in response.text + # returns to read mode + assert 'class="entry-row reading"' in response.text + # OOB swaps for zero widget and group total + assert 'id="zero-widget"' in response.text + assert 'id="group-total-flexible"' in response.text + + +def test_post_entry_updates_notes_as_badge(client): + client.post( + "/sections/subscription/entries", + data={"name": "Spotify", "amount": "17.48"}, + ) + response = client.post( + "/entries/1", + data={"name": "Spotify", "amount": "17.48", "notes": "family plan"}, + ) + assert response.status_code == 200 + assert "note-badge" in response.text + assert "family plan" in response.text + + +def test_post_entry_debt_minimum_includes_target_oob(client): + client.post( + "/sections/debt_minimum/entries", + data={"name": "Card A", "amount": "50.00"}, + ) + response = client.post( + "/entries/1", + data={"name": "Card A", "amount": "60.00", "notes": ""}, + ) + assert response.status_code == 200 + assert 'id="section-debt_target"' in response.text + + +def test_post_entry_empty_name_returns_400(client): + client.post( + "/sections/subscription/entries", + data={"name": "Twitch", "amount": "10.99"}, + ) + response = client.post( + "/entries/1", + data={"name": " ", "amount": "11.99", "notes": ""}, + ) + assert response.status_code == 400 + + +def test_post_entry_negative_amount_returns_400(client): + client.post( + "/sections/subscription/entries", + data={"name": "Twitch", "amount": "10.99"}, + ) + response = client.post( + "/entries/1", + data={"name": "Twitch", "amount": "-1.00", "notes": ""}, + ) + assert response.status_code == 400 + + +def test_post_entry_non_numeric_amount_returns_400(client): + client.post( + "/sections/subscription/entries", + data={"name": "Twitch", "amount": "10.99"}, + ) + response = client.post( + "/entries/1", + data={"name": "Twitch", "amount": "eleven", "notes": ""}, + ) + assert response.status_code == 400 + + +def test_post_entry_missing_returns_404(client): + response = client.post( + "/entries/9999", + data={"name": "Whatever", "amount": "1.00", "notes": ""}, + ) + assert response.status_code == 404