Backing transaction ledger: Postings replace the applied field #20

Merged
claude-code merged 7 commits from feat/19-posting-ledger into main 2026-04-17 17:54:16 -06:00
Collaborator

Closes #19 (will be manually closed on merge).

Summary

The applied column on month_entry is gone. Each entry now owns a ledger of Postings (transactions). Applied is a derived sum. Existing applied values migrate into one opening-balance posting per entry.

  • Schema: new posting table (id, month_entry_id FK CASCADE, occurred_on, amount, description NULL, payee NULL, timestamps). month_entry.applied dropped.
  • Migration cc60e7f73a1c: creates the posting table, seeds opening-balance rows from every non-zero applied, then drops the column. Downgrade re-adds and populates from SUM(posting.amount).
  • Service CRUD: add_posting, update_posting, delete_posting, all behind ensure_editable. update_month_entry loses the applied kwarg (removed; callers should post).
  • Routes:
    • POST /month/{ym}/entries/{entry_id}/postings add
    • POST /month/{ym}/postings/{posting_id} edit (date / amount / description / payee, any subset)
    • DELETE /month/{ym}/postings/{posting_id} delete
    • Each returns the updated section partial plus OOB swaps for zero widget and all four group totals.
  • UI: every entry row becomes a <details> block with a leading caret. Collapsed view adds no horizontal space; applied cell shows $412.33 · 7 txns when postings exist. Expanded body holds the notes input, a transactions table (date / description / payee / amount / delete, ordered newest-first), and an inline add-posting form. Closed months render read-only spans instead of inputs and omit delete buttons and forms.
  • Negative amounts allowed (refunds, corrections). Dates not constrained to the month for now.
  • Deviation state: unaffected by postings. Posting activity is normal month flow, not plan drift.
  • Close gate: still works; reads the derived applied sum.

Test plan

  • uv run pytest passes (117/117, +15 new in test_postings.py)
  • uv run alembic upgrade head applies cleanly; alembic downgrade -1 reverses correctly
  • Verified on a seeded pre-migration DB: three month_entries with applied $2000, $1200, $0 become two opening-balance postings (nothing for the $0 entry); applied column successfully dropped
  • Live smoke on throwaway DB:
    • Month page renders <details> entry blocks with caret, applied cell, transactions table, add form
    • POST .../postings adds a transaction; applied cell and zero widget update via OOB
    • Multiple postings aggregate: three grocery postings ($84.32 + $127.45 + $50.00 = $261.77 applied)
    • DELETE .../postings/{id} removes a posting; applied recomputes
    • Invalid date returns 400
    • Negative posting supported
    • Closing a balanced month still works with derived applied
    • Closed month rejects new postings with 400
    • Posting count badge renders ("3 txns")

Out of scope

  • Constraining posting dates to the month
  • Tags or richer posting categorisation
  • CSV import / bulk entry
  • Recurring posting schedules
  • Bank-statement reconciliation
  • Closed-month archived visual treatment (separate follow-up)
Closes #19 (will be manually closed on merge). ## Summary The `applied` column on `month_entry` is gone. Each entry now owns a ledger of Postings (transactions). Applied is a derived sum. Existing applied values migrate into one opening-balance posting per entry. * **Schema**: new `posting` table (id, month_entry_id FK CASCADE, occurred_on, amount, description NULL, payee NULL, timestamps). `month_entry.applied` dropped. * **Migration `cc60e7f73a1c`**: creates the posting table, seeds opening-balance rows from every non-zero applied, then drops the column. Downgrade re-adds and populates from `SUM(posting.amount)`. * **Service CRUD**: `add_posting`, `update_posting`, `delete_posting`, all behind `ensure_editable`. `update_month_entry` loses the `applied` kwarg (removed; callers should post). * **Routes**: * `POST /month/{ym}/entries/{entry_id}/postings` add * `POST /month/{ym}/postings/{posting_id}` edit (date / amount / description / payee, any subset) * `DELETE /month/{ym}/postings/{posting_id}` delete * Each returns the updated section partial plus OOB swaps for zero widget and all four group totals. * **UI**: every entry row becomes a `<details>` block with a leading caret. Collapsed view adds no horizontal space; applied cell shows `$412.33 · 7 txns` when postings exist. Expanded body holds the notes input, a transactions table (date / description / payee / amount / delete, ordered newest-first), and an inline add-posting form. Closed months render read-only spans instead of inputs and omit delete buttons and forms. * **Negative amounts allowed** (refunds, corrections). **Dates not constrained** to the month for now. * **Deviation state**: unaffected by postings. Posting activity is normal month flow, not plan drift. * **Close gate**: still works; reads the derived applied sum. ## Test plan * [x] `uv run pytest` passes (117/117, +15 new in `test_postings.py`) * [x] `uv run alembic upgrade head` applies cleanly; `alembic downgrade -1` reverses correctly * [x] Verified on a seeded pre-migration DB: three month_entries with applied $2000, $1200, $0 become two opening-balance postings (nothing for the $0 entry); applied column successfully dropped * [x] Live smoke on throwaway DB: * [x] Month page renders `<details>` entry blocks with caret, applied cell, transactions table, add form * [x] `POST .../postings` adds a transaction; applied cell and zero widget update via OOB * [x] Multiple postings aggregate: three grocery postings ($84.32 + $127.45 + $50.00 = $261.77 applied) * [x] `DELETE .../postings/{id}` removes a posting; applied recomputes * [x] Invalid date returns 400 * [x] Negative posting supported * [x] Closing a balanced month still works with derived applied * [x] Closed month rejects new postings with 400 * [x] Posting count badge renders ("3 txns") ## Out of scope * Constraining posting dates to the month * Tags or richer posting categorisation * CSV import / bulk entry * Recurring posting schedules * Bank-statement reconciliation * Closed-month archived visual treatment (separate follow-up)
claude-code added 4 commits 2026-04-17 17:35:22 -06:00
Posting is a child of MonthEntry with occurred_on, amount, optional
description and payee. Cascade delete so removing an entry wipes its
ledger. Ordered on load by occurred_on DESC for readable UIs.

MonthEntry.applied becomes a @property summing posting amounts. The
stored applied column is dropped in the same migration.

The migration walks existing month_entry rows: for every non-zero
applied value, it inserts one opening-balance posting on the month's
activated_at (or created_at) date with description "opening balance"
and amount equal to the existing applied. Empty applied values get
no opening posting. Closed months go through the same path; their
totals stay intact via that single seeded row.

Downgrade is symmetric: re-adds the column and populates from
SUM(postings).

Refs #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
add_posting / update_posting / delete_posting all go through
ensure_editable(month), so closed months reject posting mutations
with the same lifecycle guard as every other mutation. Negative
amounts are allowed for refunds / corrections. Dates are parsed as
ISO (YYYY-MM-DD) but not constrained to the month for now.

update_month_entry loses the applied keyword; the route no longer
accepts an applied form field. applied is derived only from now
on. Three new routes wire the ledger:

  POST   /month/{ym}/entries/{entry_id}/postings
  POST   /month/{ym}/postings/{posting_id}
  DELETE /month/{ym}/postings/{posting_id}

Each returns the updated section partial plus OOB swaps for the
zero widget and all four group totals, same pattern the existing
mutations use.

Refs #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each month entry becomes a <details> block. The summary is the same
dense row (name, planned, applied, delete) plus a leading caret and
an applied cell that shows the transaction count ("$412.33 · 7 txns")
when postings exist. Expansion adds no horizontal space.

Expanded body holds: the entry's notes input, a transactions table
with date / description / payee / amount / delete per posting, and
an inline add-transaction form (date, description, payee, amount,
submit). Every field is HTMX-wired so editing any cell triggers the
section partial re-render with fresh derived totals.

Closed month: name / planned / notes / posting fields all collapse
to read-only spans, delete buttons and add forms are omitted. The
existing editable flag controls the branching.

Refs #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New test_postings.py walks service and route layers: add sums into
applied, negatives are allowed, update and delete round-trip, entry
deletion cascades postings, order is desc by date, update_month_entry
rejects the removed applied kwarg. Route tests assert HTTP behaviour,
invalid-date rejection, closed-month lock, tone flip after a posting,
and the "N txns" count badge renders.

Existing tests that previously set applied via update_month_entry or
the entries route now use add_posting or POST to /postings. Format
assertions updated to match the new thousands-separator number
rendering and the replaced entry-notes-row markup.

Refs #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
archeious added 1 commit 2026-04-17 17:40:04 -06:00
Widen the applied column from 5.5rem to 9rem so "\$134.32 · 7 txns"
fits on one line. Add white-space: nowrap to the cell and its
children as belt-and-braces. Mobile breakpoint gets 7rem with the
count text shrunk, still single-line.

Refs #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
archeious added 1 commit 2026-04-17 17:42:37 -06:00
The notes row was display:none when empty and revealed on entry-row
hover. Moving the cursor down to click the input left the entry row
and immediately hid the notes again, a classic hover-gap. Fix by
always rendering the row with a subtle 0.55 opacity when empty and
bumping it to 1.0 on its own hover or focus. Now the input is
always reachable without a hover dance.

Refs #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
archeious added 1 commit 2026-04-17 17:46:42 -06:00
Every section used to end with a wide horizontal add form (name
input, amount input, button, notes input spanning the row). It took
up more horizontal real estate than the entries themselves. Now the
form is wrapped in a <details class="add-entry"> whose summary is a
small tracked-caps link ("+ add fixed amount bills"). Click to
reveal the same form below; HTMX submission still resets and hides
on success.

Same treatment on the budget page sections and the month page
sections.

Refs #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-code merged commit 19cac8f08b into main 2026-04-17 17:54:16 -06:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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#20
No description provided.