diff --git a/src/quartermaster/month_service.py b/src/quartermaster/month_service.py index 9498345..60e7e6d 100644 --- a/src/quartermaster/month_service.py +++ b/src/quartermaster/month_service.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import re from dataclasses import dataclass from datetime import date, datetime, timezone @@ -28,6 +29,8 @@ from quartermaster.models import ( Section, ) +logger = logging.getLogger("quartermaster.month_service") + YEAR_MONTH_RE = re.compile(r"^\d{4}-(0[1-9]|1[0-2])$") @@ -148,6 +151,10 @@ def create_month(db: Session, year_month: str) -> Month: ) db.commit() db.refresh(month) + logger.info( + "created month snapshot", + extra={"event": "month_created", "year_month": month.year_month}, + ) return month @@ -314,6 +321,15 @@ def add_posting( db.add(posting) db.commit() db.refresh(posting) + logger.info( + "added posting", + extra={ + "event": "posting_added", + "posting_id": posting.id, + "month_entry_id": posting.month_entry_id, + "amount": str(posting.amount), + }, + ) return posting @@ -368,6 +384,10 @@ def delete_posting( entry = posting.entry db.delete(posting) db.commit() + logger.info( + "deleted posting", + extra={"event": "posting_deleted", "posting_id": posting_id}, + ) return entry @@ -428,6 +448,10 @@ def close_month(db: Session, month: Month) -> Month: month.closed_at = datetime.now(timezone.utc) db.commit() db.refresh(month) + logger.info( + "closed month", + extra={"event": "month_closed", "year_month": month.year_month}, + ) return month diff --git a/tests/test_logging.py b/tests/test_logging.py index e6a1086..dacbb50 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -3,8 +3,14 @@ from __future__ import annotations import io import json import logging +from datetime import date +from decimal import Decimal +import pytest + +from quartermaster import month_service, service from quartermaster.logging_config import LOG_CONFIG +from quartermaster.models import Section def _build_formatter(): @@ -89,3 +95,85 @@ def test_log_config_loads_via_dictconfig(): from quartermaster.logging_config import LOG_CONFIG logging.config.dictConfig(LOG_CONFIG) + + +@pytest.fixture(autouse=True) +def _reset_quartermaster_logger(): + """Ensure the quartermaster logger propagates to root so caplog can capture records. + + logging.config.dictConfig (called in test_log_config_loads_via_dictconfig) sets + propagate=False on the quartermaster logger. caplog injects its handler into the + root logger, so records only reach it when propagation is enabled. This fixture + temporarily re-enables propagation for every test in this module and restores the + original value on teardown. + """ + qm_logger = logging.getLogger("quartermaster") + original_propagate = qm_logger.propagate + qm_logger.propagate = True + yield + qm_logger.propagate = original_propagate + + +def _make_debt_minimum(db, name="Loan", amount=Decimal("100.00")): + from quartermaster.models import Entry + entry = Entry(section=Section.debt_minimum, name=name, amount=amount) + db.add(entry) + db.commit() + db.refresh(entry) + return entry + + +def test_create_month_logs_month_created_event(db, caplog): + caplog.set_level(logging.INFO, logger="quartermaster") + month_service.create_month(db, "2026-05") + + events = [r for r in caplog.records if getattr(r, "event", None) == "month_created"] + assert len(events) == 1 + assert events[0].levelname == "INFO" + + +def test_close_month_logs_month_closed_event(db, caplog): + month = month_service.create_month(db, "2026-06") + # move through active → closed with a zero-applied budget (no entries yet) + month_service.activate_month(db, month) + caplog.clear() + caplog.set_level(logging.INFO, logger="quartermaster") + + month_service.close_month(db, month) + + events = [r for r in caplog.records if getattr(r, "event", None) == "month_closed"] + assert len(events) == 1 + + +def test_add_posting_logs_posting_added_event(db, caplog): + _make_debt_minimum(db) + month = month_service.create_month(db, "2026-07") + entry = month.entries[0] + caplog.set_level(logging.INFO, logger="quartermaster") + + month_service.add_posting( + db, month, entry.id, + occurred_on=date(2026, 7, 15), + amount=Decimal("25.00"), + ) + + events = [r for r in caplog.records if getattr(r, "event", None) == "posting_added"] + assert len(events) == 1 + + +def test_delete_posting_logs_posting_deleted_event(db, caplog): + _make_debt_minimum(db) + month = month_service.create_month(db, "2026-08") + entry = month.entries[0] + posting = month_service.add_posting( + db, month, entry.id, + occurred_on=date(2026, 8, 15), + amount=Decimal("10.00"), + ) + caplog.clear() + caplog.set_level(logging.INFO, logger="quartermaster") + + month_service.delete_posting(db, month, posting.id) + + events = [r for r in caplog.records if getattr(r, "event", None) == "posting_deleted"] + assert len(events) == 1