From 034a8d65f58e731ac922d466bf72236203e0f40e Mon Sep 17 00:00:00 2001 From: archeious Date: Fri, 17 Apr 2026 12:51:25 -0600 Subject: [PATCH] feat(notes): service-layer support for notes on entry and month_entry add_entry and add_month_entry accept an optional notes keyword. A new set_entry_notes function updates a single budget entry's notes. update_month_entry gains a notes parameter guarded by a sentinel so callers can distinguish "do not touch notes" from "clear to NULL". create_month copies entry.notes into each freshly snapshotted month_entry. Blank / whitespace notes normalise to NULL. Refs #13 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/quartermaster/month_service.py | 16 ++++++++++++++ src/quartermaster/service.py | 34 ++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/quartermaster/month_service.py b/src/quartermaster/month_service.py index 92e7b9f..8928aad 100644 --- a/src/quartermaster/month_service.py +++ b/src/quartermaster/month_service.py @@ -112,6 +112,7 @@ def create_month(db: Session, year_month: str) -> Month: name=e.name, planned=e.amount, applied=Decimal("0.00"), + notes=e.notes, origin_name=e.name, origin_planned=e.amount, source_entry_id=e.id, @@ -203,12 +204,20 @@ def section_view(month: Month, section: Section, label: str) -> MonthSectionView ) +def _clean_notes(raw: str | None) -> str | None: + if raw is None: + return None + stripped = raw.strip() + return stripped if stripped else None + + def add_month_entry( db: Session, month: Month, section: Section, name: str, planned: Decimal, + notes: str | None = None, ) -> MonthEntry: entry = MonthEntry( month_id=month.id, @@ -216,6 +225,7 @@ def add_month_entry( name=name.strip(), planned=planned, applied=Decimal("0.00"), + notes=_clean_notes(notes), origin_name=None, origin_planned=None, source_entry_id=None, @@ -243,6 +253,9 @@ def delete_month_entry(db: Session, month: Month, entry_id: int) -> Section | No return section +_NOTES_SENTINEL = object() + + def update_month_entry( db: Session, month: Month, @@ -251,6 +264,7 @@ def update_month_entry( name: str | None = None, planned: Decimal | None = None, applied: Decimal | None = None, + notes: str | None | object = _NOTES_SENTINEL, ) -> MonthEntry | None: entry = get_month_entry(db, month, entry_id) if entry is None: @@ -261,6 +275,8 @@ def update_month_entry( entry.planned = planned if applied is not None: entry.applied = applied + if notes is not _NOTES_SENTINEL: + entry.notes = _clean_notes(notes) # type: ignore[arg-type] db.commit() db.refresh(entry) return entry diff --git a/src/quartermaster/service.py b/src/quartermaster/service.py index 7ed9fb7..5a9cc95 100644 --- a/src/quartermaster/service.py +++ b/src/quartermaster/service.py @@ -87,14 +87,44 @@ def budget_zero(db: Session) -> Decimal: return (total_income - total_non_income).quantize(Decimal("0.01")) -def add_entry(db: Session, section: Section, name: str, amount: Decimal) -> Entry: - entry = Entry(section=section, name=name.strip(), amount=amount) +def add_entry( + db: Session, + section: Section, + name: str, + amount: Decimal, + notes: str | None = None, +) -> Entry: + entry = Entry( + section=section, + name=name.strip(), + amount=amount, + notes=_clean_notes(notes), + ) db.add(entry) db.commit() db.refresh(entry) return entry +def _clean_notes(raw: str | None) -> str | None: + if raw is None: + return None + stripped = raw.strip() + return stripped if stripped else None + + +def set_entry_notes( + db: Session, entry_id: int, notes: str | None +) -> Entry | None: + entry = db.get(Entry, entry_id) + if entry is None: + return None + entry.notes = _clean_notes(notes) + db.commit() + db.refresh(entry) + return entry + + def delete_entry(db: Session, entry_id: int) -> Entry | None: entry = db.get(Entry, entry_id) if entry is None: