Month lifecycle: Planning, Active, Closed with reconciliation gate #16

Merged
claude-code merged 4 commits from feat/15-month-lifecycle into main 2026-04-17 13:05:00 -06:00
Showing only changes of commit fa9a397d83 - Show all commits

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date from datetime import date, datetime, timezone
from decimal import Decimal from decimal import Decimal
from enum import Enum from enum import Enum
@ -23,6 +23,7 @@ from quartermaster.models import (
Month, Month,
MonthDebtTarget, MonthDebtTarget,
MonthEntry, MonthEntry,
MonthState,
Section, Section,
) )
@ -306,3 +307,54 @@ def set_month_target(
db.commit() db.commit()
db.refresh(target) db.refresh(target)
return 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")