diff --git a/src/quartermaster/month_service.py b/src/quartermaster/month_service.py index 8928aad..9ecc0b9 100644 --- a/src/quartermaster/month_service.py +++ b/src/quartermaster/month_service.py @@ -2,7 +2,7 @@ from __future__ import annotations import re from dataclasses import dataclass -from datetime import date +from datetime import date, datetime, timezone from decimal import Decimal from enum import Enum @@ -23,6 +23,7 @@ from quartermaster.models import ( Month, MonthDebtTarget, MonthEntry, + MonthState, Section, ) @@ -306,3 +307,54 @@ def set_month_target( db.commit() db.refresh(target) return target + + +class MonthLifecycleError(Exception): + """Raised when a state transition or edit is rejected.""" + + +def activate_month(db: Session, month: Month) -> Month: + if month.state != MonthState.planning: + raise MonthLifecycleError( + f"cannot activate a {month.state.value} month" + ) + month.state = MonthState.active + month.activated_at = datetime.now(timezone.utc) + db.commit() + db.refresh(month) + return month + + +def close_month(db: Session, month: Month) -> Month: + if month.state != MonthState.active: + raise MonthLifecycleError( + f"cannot close a {month.state.value} month" + ) + zero = month_zero(month) + if zero.applied != Decimal("0.00"): + raise MonthLifecycleError( + "applied balance must equal $0.00 before closing; " + f"currently ${zero.applied}" + ) + month.state = MonthState.closed + month.closed_at = datetime.now(timezone.utc) + db.commit() + db.refresh(month) + return month + + +def reopen_month(db: Session, month: Month) -> Month: + if month.state != MonthState.closed: + raise MonthLifecycleError( + f"cannot reopen a {month.state.value} month" + ) + month.state = MonthState.active + month.closed_at = None + db.commit() + db.refresh(month) + return month + + +def ensure_editable(month: Month) -> None: + if month.state == MonthState.closed: + raise MonthLifecycleError("month is closed; reopen to edit")