Month lifecycle: Planning, Active, Closed states with reconciliation gate #15

Closed
opened 2026-04-17 12:59:11 -06:00 by claude-code · 0 comments
Collaborator

Goal

Each month moves through three explicit states. Closing requires the applied balance to be exactly $0.00. The Primary Debt Target is a hint about where leftover money should go, but the allocation is manual: the user edits the target's applied amount until the month balances. Nothing is swept automatically.

States

State Meaning Editable Transitions
Planning Snapshotted from budget; month has not started yet Yes → Active
Active Month is happening; applied amounts accumulating Yes → Closed
Closed Month is finalised, balance at $0.00 applied No → Active (reopen)

Creating a month via POST /month/YYYY-MM/create lands it in Planning.

Activate

POST /month/YYYY-MM/activate moves Planning → Active and stamps activated_at. No other validation.

Close

POST /month/YYYY-MM/close moves Active → Closed and stamps closed_at. Rejected with 400 if:

  • State is not Active
  • Applied zero (income.applied - non_income.applied) is not exactly $0.00

The UI shows the Close button disabled when the balance is not zero, with hover text explaining why. Server-side validation is authoritative.

Reopen

POST /month/YYYY-MM/reopen moves Closed → Active and nulls closed_at. No sweep to reverse (nothing was swept). User can edit, then re-close when balanced again.

Schema additions

month
  ...existing columns...
  state          TEXT NOT NULL DEFAULT 'planning'   # 'planning' | 'active' | 'closed'
  activated_at   TIMESTAMP NULL
  closed_at      TIMESTAMP NULL

Edit locking on closed months

When state = closed, all mutation routes for that month reject with 400:

  • Add entry
  • Delete entry
  • Update entry (name, planned, applied, notes)
  • Update debt target
  • Delete via DELETE endpoint

UI reflects this: inputs get the disabled attribute, delete buttons are hidden, add forms are hidden, the target selector is hidden. A banner replaces them with "Closed on " and a Reopen button.

UI

  • Month nav grows a state badge: "Planning" / "Active" / "Closed on YYYY-MM-DD"
  • Planning state: primary button "Activate"
  • Active state: primary button "Close". Disabled when balance != $0.00. Tooltip explains.
  • Closed state: secondary button "Reopen"

The Primary Debt Target card keeps its current shape. Its presence signals where leftover dollars "belong," but filling them in is the user's job via editing applied on the target row (or any other row).

Acceptance criteria

  • Alembic migration adds state, activated_at, closed_at with defaults
  • create_month lands in Planning state
  • POST /month/YYYY-MM/activate moves Planning → Active and stamps activated_at
  • POST /month/YYYY-MM/close rejects unless state is Active AND applied zero == 0
  • POST /month/YYYY-MM/reopen moves Closed → Active
  • All mutation routes return 400 when the month is closed
  • Edit inputs, delete buttons, add forms, target selector are hidden / disabled in closed state
  • Nav shows the current state and correct transition button
  • Pytest covers all transitions, validation failures, and edit-locking
  • No regression on budget config page, zero amount widget, group totals, or deviation flags

Out of scope

  • Active → Planning reverse transition (edge case; defer)
  • Automatic sweep on close
  • Auto-activation based on calendar date
  • Month close-out audit log beyond the activated_at / closed_at timestamps
## Goal Each month moves through three explicit states. Closing requires the applied balance to be exactly `$0.00`. The Primary Debt Target is a hint about where leftover money should go, but the allocation is manual: the user edits the target's applied amount until the month balances. Nothing is swept automatically. ## States | State | Meaning | Editable | Transitions | |---|---|---|---| | Planning | Snapshotted from budget; month has not started yet | Yes | → Active | | Active | Month is happening; applied amounts accumulating | Yes | → Closed | | Closed | Month is finalised, balance at `$0.00` applied | No | → Active (reopen) | Creating a month via `POST /month/YYYY-MM/create` lands it in Planning. ### Activate `POST /month/YYYY-MM/activate` moves Planning → Active and stamps `activated_at`. No other validation. ### Close `POST /month/YYYY-MM/close` moves Active → Closed and stamps `closed_at`. Rejected with 400 if: * State is not Active * Applied zero (`income.applied - non_income.applied`) is not exactly `$0.00` The UI shows the Close button disabled when the balance is not zero, with hover text explaining why. Server-side validation is authoritative. ### Reopen `POST /month/YYYY-MM/reopen` moves Closed → Active and nulls `closed_at`. No sweep to reverse (nothing was swept). User can edit, then re-close when balanced again. ## Schema additions ``` month ...existing columns... state TEXT NOT NULL DEFAULT 'planning' # 'planning' | 'active' | 'closed' activated_at TIMESTAMP NULL closed_at TIMESTAMP NULL ``` ## Edit locking on closed months When `state = closed`, all mutation routes for that month reject with 400: * Add entry * Delete entry * Update entry (name, planned, applied, notes) * Update debt target * Delete via DELETE endpoint UI reflects this: inputs get the `disabled` attribute, delete buttons are hidden, add forms are hidden, the target selector is hidden. A banner replaces them with "Closed on <date>" and a Reopen button. ## UI * Month nav grows a state badge: "Planning" / "Active" / "Closed on YYYY-MM-DD" * Planning state: primary button "Activate" * Active state: primary button "Close". Disabled when balance != $0.00. Tooltip explains. * Closed state: secondary button "Reopen" The Primary Debt Target card keeps its current shape. Its presence signals where leftover dollars "belong," but filling them in is the user's job via editing `applied` on the target row (or any other row). ## Acceptance criteria * [ ] Alembic migration adds `state`, `activated_at`, `closed_at` with defaults * [ ] `create_month` lands in Planning state * [ ] `POST /month/YYYY-MM/activate` moves Planning → Active and stamps `activated_at` * [ ] `POST /month/YYYY-MM/close` rejects unless state is Active AND applied zero == 0 * [ ] `POST /month/YYYY-MM/reopen` moves Closed → Active * [ ] All mutation routes return 400 when the month is closed * [ ] Edit inputs, delete buttons, add forms, target selector are hidden / disabled in closed state * [ ] Nav shows the current state and correct transition button * [ ] Pytest covers all transitions, validation failures, and edit-locking * [ ] No regression on budget config page, zero amount widget, group totals, or deviation flags ## Out of scope * Active → Planning reverse transition (edge case; defer) * Automatic sweep on close * Auto-activation based on calendar date * Month close-out audit log beyond the `activated_at` / `closed_at` timestamps
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: archeious/quartermaster#15
No description provided.