Files
rmcampos 65f6985278
Frontend CI / build (push) Successful in 2m9s
Backend CI / build-and-push (push) Successful in 7m22s
feat: add usd brl exchange rate request
2026-07-12 18:09:38 -03:00

1036 lines
61 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ledger — Outstanding Work
## Status as of end of session (2026-07-11, latest session, continued)
**Done, not yet committed: bill-offset support for repeat/installment
series on Card Bills.** Bug found via user testing right after the §8 work
above: on `CardBills.jsx`'s add-transaction form, picking any `Repeat`
value other than "Doesn't repeat" made the `Bill` dropdown disappear
entirely — pre-existing, deliberate restriction from the original
credit-card work (`billDueDate` was only ever applied when
`repeat == NONE`, both frontend and backend), but it blocked a real use
case: a purchase made before the due day but after the bank's statement
already closed needs to land on *next* month's bill instead of the natural
one — same logic that already worked for one-off transactions, just never
extended to series.
Fixed by having the override apply as an offset: the user's chosen bill
for the *first* occurrence is compared to that occurrence's natural bill
to derive a whole-month offset (e.g. picking "next bill" = +1), then every
generated occurrence's own natural bill is shifted by that same offset —
so a MONTHLY/INSTALLMENTS/etc. series starting on an overridden bill keeps
each subsequent charge one bill cycle ahead of where it'd naturally land,
rather than only the first occurrence being correct.
- Backend: `CreditCardBillSyncService#nextDueDate` (the Java port of the
due-day roll-forward math) changed from `private` to package-private so
`TransactionResource` can reuse it. New `TransactionResource#billOffsetMonths`
(natural-vs-chosen bill → whole-month offset, null when no genuine
override) and `#shiftedBillDueDate` (an occurrence's own natural bill,
shifted by that offset, clamped for short months). Wired into both
`create()`'s batch-generation loop (replacing the old
`repeat == NONE ? request.billDueDate : null` line) and `applyToSeries()`
(replacing a flat `t.billDueDate = request.billDueDate` that would have
pinned every future-scoped row to one identical date instead of shifting
each by the same offset — found and fixed in the same pass, confirmed
with user it should cover both create and edit).
- Frontend (`CardBills.jsx`): the `Bill` dropdown (relabeled "First bill"
when a repeat is selected) now always renders regardless of `repeat`,
laid out on its own row when `Occurrences` is also showing; `isOverride`
in `handleSubmit` no longer gates on `repeat === 'NONE'`. The edit form's
bill override was already ungated (single-row edits never had this
restriction) — no change needed there.
Verified via curl (fresh signup, CREDIT_CARD account with `dueDayOfMonth`
10): a MONTHLY series of 3 starting 2026-01-05 (natural bill 01-10) with
an explicit `billDueDate=2026-02-10` override correctly produced bills
02-10, 03-10, 04-10 across the three occurrences (each exactly one cycle
ahead of its own natural bill); an INSTALLMENTS series with the same
inputs matched identically; a 4-occurrence MONTHLY series created with no
override (all `billDueDate` null, natural fallback) then edited at
occurrence 2 with `scope=FUTURE` and a one-bill-ahead override correctly
left occurrence 1 untouched (`null`) and shifted occurrences 2-4 to
03-10/04-10/05-10 respectively — each by the same offset, not one flat
date. Also verified via Playwright: the Bill dropdown (as "First bill")
and Occurrences field render together for a MONTHLY selection, with the
new explanatory hint text visible, no console errors.
## Status as of end of session (2026-07-11, earlier in the session)
**Done, not yet committed: §8 series tracking + scope-aware edit/delete**
(requested 2026-07-11, planned via plan mode, plan saved at
`/home/ricardo/.claude/plans/idempotent-hugging-cupcake.md`). Resolves §8's
long-standing gap (repeat/installment rows only shared a cosmetic
`seriesInfo` string, no real grouping key) and adds the "apply to future
occurrences?" prompt on edit/delete the user asked for.
- Backend: two new nullable columns on `Transaction``seriesId` (Java
`UUID.randomUUID().toString()`, `VARCHAR(36)`, `V8__transaction_series_id.sql`,
indexed) and `seriesRepeat` (the `RepeatFrequency` the batch was created
with, `V9__transaction_series_repeat.sql`) — both set once per batch in
`TransactionResource#create`, identically across every generated row, both
null for one-off transactions (no backfill for pre-existing rows — they
keep their `seriesInfo` display string and fall back to plain single-row
edit/delete, per §8's already-accepted tradeoff).
- New `EditScope` enum (`THIS`/`FUTURE`). `PUT /transactions/{id}` gained an
optional `scope` body field (default `THIS`, backward compatible);
`DELETE /transactions/{id}` gained an optional `?scope=` query param
(default `THIS`). `FUTURE` is only honored when the target row has a
non-null `seriesId`.
- Edit "this and future": `description`/`category`/`billDueDate` flat-set
across every row from the edited occurrence onward (via new
`seriesFrom(anchor)`, `occurredOn`-then-`id` ordered). `occurredOn` is
**never** propagated — only the anchor row's own date changes.
`amount` on an `INSTALLMENTS` series re-splits the entered total evenly
across the remaining rows (reuses the existing `splitAmount` helper,
same remainder-on-last-row rule as creation); non-INSTALLMENTS series
flat-set every affected row to the entered amount.
- Delete "this and future": deletes every row from the target occurrence
onward in the same series.
- Every delete (single or batch) calls new `renumberSeries()`, which
relabels remaining rows' `seriesInfo` contiguously (closing gaps like
`1/12,2/12,4/12,5/12``1/11,2/11,3/11...`), and clears
`seriesId`/`seriesInfo`/`seriesRepeat` entirely if a series collapses to
one remaining row (it silently reverts to a plain non-series
transaction — no scope UI shown for it anymore).
- Frontend (`Transactions.jsx` and its line-for-line duplicate block in
`CardBills.jsx`, both updated identically): the in-row edit form shows a
"This occurrence only" / "This and future" radio pair — and, when the
latter is picked on an `INSTALLMENTS` row, a one-line re-split hint —
only when `t.seriesId` is set; the delete confirm panel swaps its single
"Delete" button for "Delete this only" / "Delete this & future" under the
same condition. Non-series transactions render zero new UI, byte-for-byte
unchanged from before. `TransactionsApi.remove` gained a defaulted
`scope` param (`'THIS'`); `update`'s payload just gained one more
optional key.
Verified via curl (fresh signup, INSTALLMENTS batch creation confirms
shared `seriesId`; `PUT .../2?scope=FUTURE` on occurrence 2 of 4 re-split a
new $90 total across occurrences 2-4 leaving occurrence 1 untouched, and
only occurrence 2's own date changed; `DELETE .../1?scope=THIS` renumbered
the remaining three from `2/4,3/4,4/4` to `1/3,2/3,3/3`; a further
`DELETE ...?scope=FUTURE` collapsing a series to one row correctly nulled
out its `seriesId`/`seriesInfo`/`seriesRepeat`; a separate uneven-total
re-split ($100 over 3) landed `33.33/33.33/33.34`, remainder on the last
row as designed; a plain non-series transaction created/deleted with no
`scope` param at all, confirming backward compatibility) and via a fresh
Playwright pass (`playwright-core` + cached Chromium installed ad hoc into
scratchpad, no OS-level deps available so `--with-deps` wasn't used) on
both `Transactions.jsx` and `CardBills.jsx`: scope radios and the
INSTALLMENTS hint appear only on series rows, the re-split renders
correctly in the row list after saving, deleting "this only" renumbers the
badge in place, and a non-series row (`Coffee`) shows the exact pre-change
single-button edit/delete UI with no radios. No console errors in any
pass.
## Status as of end of session (2026-07-09, previous session)
**Product.md audit → 5 fix chunks, all done, each reviewed/committed
individually by the user as we went.** User wrote `Product.md` (repo root,
new file) spelling out expected behavior for Overview/Accounts/Credit
Cards. Three subagents audited the actual code against it; findings below,
in the order fixed:
1. Overview: "Checking & Savings accounts" list was `kind !== 'CREDIT_CARD'`
(leaked INVESTMENT accounts in) instead of reusing `liquidAccounts`. Also
added a new "Investments" section and a third hero-panel column ("Total
investments") per user follow-up request — same today-cutoff balance
logic as the other two columns.
2. Backend: added `Transaction.createdAt` (`V5` migration, `Instant`,
`@PrePersist`-guaranteed on every insert path incl. repeat/installment
batches and `CreditCardBillSyncService`'s system rows) — needed for #3.
`columnDefinition` carries the `DEFAULT now()` into Hibernate's
dev-mode DDL too, not just the Flyway migration, so `seed-data.sql`
(which doesn't set the column) keeps working in dev.
3. Overview's "Recent activity" was scoped to the page's date-range
dropdown and sorted by `occurredOn` — spec wants "latest 5 added,"
global, regardless of account. Now sorts by `createdAt` desc (id as
tiebreaker), ignores range entirely, excludes `linkedCard` bill-sync
rows. This made the date-range dropdown itself dead (it had no other
consumer) — removed from Overview's header along with `rangeOptions`/
`rangeBounds`/`startOfWeek`.
4. Accounts.jsx rendered raw `a.balance` (all-time, future-inclusive) for
every account kind, and credit cards showed that same raw balance
instead of a bill total. Now fetches each account's transactions
(shares the `['transactions', id]` query key with Overview) and shows
a proper today-cutoff balance / current-bill amount.
5. CreditCards.jsx "Available credit" (`totalLimit - totalOwed`) mixed
mismatched sets: `totalLimit` only summed cards *with* a limit, but
`totalOwed` summed *all* cards — a no-limit card's debt silently ate
into another card's available credit. Fixed by summing owed only over
`cardsWithLimit` for this one figure (the "Total owed across all
cards" stat itself is intentionally unchanged, still all cards). Also
"View transactions" now passes an explicit `&bill=<dueDate>` param
(via `nextBillFor`), matching the pattern Transactions.jsx's "View
bill" link already used, instead of relying on `CardBills.jsx`'s
implicit default-bill fallback.
Refactor along the way (chunk 4 + a follow-up ask): extracted the
"balance as of a cutoff date" walk — previously duplicated near-verbatim
in Overview.jsx and Transactions.jsx — into `frontend/src/utils/balance.js`
as two pieces: `sortChronologically` (sort once) and `balanceAsOf(account,
sortedTransactions, cutoffDate)` (walk many times against the same sorted
array) — preserves Transactions.jsx's existing sort-once/walk-per-day-group
performance shape instead of resorting on every call. All three pages
(Overview, Accounts, Transactions) now call the same implementation.
Also promoted a small `isoDate` formatter (pre-existing local copy in
`CardBills.jsx`) into `utils/date.js` so CreditCards.jsx's new bill-param
link could reuse it instead of a third copy-paste.
Verified via ad hoc Playwright (scratchpad-installed `playwright-core`,
cached Chromium) driving a throwaway signup user (`qa-test2@ledger.local`)
through `/auth/signup` + direct API calls to seed specific test data
(future-dated txn, backdated txn, a limited + a no-limit credit card),
then screenshotting/reading Overview, Accounts, Credit Cards, Card Bills,
and Transactions. All five fixes confirmed with concrete numbers; no
console errors in any pass.
## Status as of end of session (2026-07-08, previous session)
**Done, not yet committed: split credit cards out of Transactions.jsx onto
a new "Card Bills" page.** User found the Transactions page overwhelming —
a large fraction of it was credit-card-only machinery (bill prev/next
navigator, "Projected" section, "Amount owed" sign-flip, the Bill dropdown
on add/edit). Landed as two chunks:
1. New `frontend/src/pages/CardBills.jsx` (route `/card-bills`, its own
sidebar entry after "Credit Cards"). Auto-selects the user's card (or
shows a plain picker if they have more than one), navigates by actual
bill due dates via chevrons (reusing `groupTransactionsByBill`/
`nextBillFor` from `utils/creditCard.js`), shows that bill's total +
due date, and lists just that bill's transactions with full add/edit/
delete parity including the Bill-period override — no future-grey
styling, no other filters. The Credit Cards dashboard's "View
transactions" link now points here instead of into Transactions.
2. Stripped `Transactions.jsx` from 1244 → 888 lines: credit card accounts
filtered out of `accounts` right after the query (cascades to the
account dropdown, `txnQueries`, every downstream calc), then deleted
`billOptionsFor`/`billOptionsWithCurrent`, `filterBillDueDate`,
`billBalanceAsOf`, `isCreditCardFilter` sign-flip, `projectedCcDeduction`,
`aggregateCreditCards`/`isCreditCardTxn`, `projectedBills`,
`cardBillDates`/`defaultBillDueDate`/`stepBill`,
`viewAccountTransactions`, `resetToDefaultView`, and both Bill
dropdowns. Small added robustness: a stale `ledger:lastAccountId`
pointing at a now-filtered-out credit card clears itself back to "All
accounts" instead of silently showing an empty list.
Also fixed along the way: `utils/date.js#dueLabel` read "in -3 days" for
an overdue bill (never triggered before, since its only callers passed
future dates) — now "3 days ago"/"yesterday". Verified via a scratch
Playwright driver (`playwright` installed ad hoc into the scratchpad dir,
reusing the cached Chromium binary) — full add/edit/delete/navigate flow
on Card Bills, the dashboard deep link, and a regression pass confirming
normal (non-card) Transactions behavior and Overview were unaffected. No
console errors in any pass.
**Follow-up in the same session: credit card bills sync onto their linked
payment account as real transactions.** Splitting credit cards out of
Transactions.jsx also removed the old client-side "Projected" row, which
was the only way a linked checking account showed its upcoming card bill.
Rather than rebuild that (a side-channel projection that TODO.md's prior
entries show caused repeated double-counting/month-leakage bugs), the
user proposed materializing it: whenever a credit card's transactions
change, maintain one real `Transaction` per open bill on the card's
`paymentAccount`, dated on the bill's due date, holding that bill's total.
- Backend: new nullable `Transaction.linkedCard` FK
(`V4__transaction_linked_card.sql`) marks a row as system-generated. New
`CreditCardBillSyncService` (`com.ledger.anthropic`,
`@ApplicationScoped`) owns `recomputeAccountBalance` (moved out of
`TransactionResource`, same algorithm, now has two callers) and
`sync(Account card)` — the single entry point, safe to call
unconditionally on any account. Groups the card's own transactions by
effective due date (a Java port of `utils/date.js#nextDueDate` +
`utils/creditCard.js#billDueDateFor`/`groupTransactionsByBill`), upserts
one row per non-zero bill on the linked account, deletes any synced row
whose date no longer matches or that's sitting on the wrong account
(this is what makes a payment-account change self-correct with no
special-casing). Called from `TransactionResource`'s create/update/
delete and unconditionally at the end of `AccountResource#update`.
`TransactionResource` rejects direct `PUT`/`DELETE` on a transaction
with `linkedCard != null` (400).
- Frontend: `Transactions.jsx` renders a `linkedCard` row dashed, with a
"Card bill" tag and a "View bill" link to Card Bills instead of edit/
clone/delete icons. Everything else — amount, running balance, day
grouping, today/future styling, CSV export, search/category filtering —
needed zero new code, since these are ordinary rows returned by the
same `GET /transactions/account/{accountId}` the page already calls.
`CardBills.jsx` needed no changes (synced rows live on the payment
account, never on the card itself).
- One bug found and fixed during verification: `CreditCardBillSyncService
#sync` set `row.amount` *after* `row.persist()` for a newly-created row
— IDENTITY-strategy entities insert immediately on `persist()` (can't
defer for a generated id), so `amount` (NOT NULL) has to be set first.
Fixed by assigning every field before `persist()`.
Verified via the same Playwright driver, fresh throwaway users each time:
linked card + checking, add a charge → dashed synced row with correct
amount/date and no edit/delete UI; a second charge updates the same row
(no duplicate); overriding the Bill dropdown to a different due date moves
the amount off the old date onto a new one; deleting the card transaction
removes the synced row and recomputes the checking balance; editing the
card to a different payment account moves the row (old account ends up
empty, new one gets it); direct `curl`-equivalent `PUT`/`DELETE` against a
synced transaction's id both return 400. No console or backend errors in
any pass.
`CLAUDE.md` updated: `Account.paymentAccount`'s Javadoc, the "Balance
math" bullet's file reference, and a new bullet documenting the sync
mechanism and why it replaced the client-side projection.
**Follow-up in the same session: "View bill" deep link + a StrictMode race
it exposed.** The "View bill" link on a synced row only ever opened Card
Bills on whatever bill it defaults to (next upcoming, or most recent past)
— clicking it from a future-dated bill row still required an extra manual
chevron click to actually reach that bill. Fixed by passing the row's own
`t.occurredOn` (already *is* the bill's due date — no separate bill-id
needed) as a new `?bill=` param, read by `CardBills.jsx` alongside the
existing `?account=`.
First attempt raced: two separate effects both decided what
`selectedBillDueDate` should be, and when the accounts/transactions
queries were already cached (e.g. bouncing back from Transactions), both
ran in the same pass reading the same stale empty value — whichever ran
second (the pre-existing billDates-reconciliation effect) won, silently
dropping the URL's bill back to the default. Merging them into one effect
"fixed" that pass but broke under `React.StrictMode` (active in
`main.jsx`), which double-invokes an effect in dev using the *same* stale
closure — a ref mutated in the first invocation made the second invocation
skip the "apply the URL bill" branch but still read the stale (pre-update)
`selectedBillDueDate`, falling through to the same reset. Only surfaced
with two or more real bills on the card, since with just one, "fallback"
and "correct" happened to be the same value — not caught by initial
Playwright verification, which always used fresh signups (nothing cached)
and single-bill cards.
Root-caused with temporary debug logging read back through the same
Playwright driver (`page.on('console')`), reproducing the exact two-bill
scenario. Real fix: decoupled "should the URL's bill be applied" from
`selectedBillDueDate` entirely (removed from the effect's dependency array
and its condition) — gated instead on a `userSteppedAway` ref that
`stepBill()` flips, so the URL-application logic is idempotent no matter
how many times React invokes it against stale data. Debug logging removed
before commit.
Verified: the two-real-bills repro now lands on the correct bill
immediately; the original warm-cache scenario still does too; chevron
stepping afterward is unaffected (doesn't fight the URL); no console
errors in any pass.
## Status as of end of session (2026-07-08, new session)
**Done, not yet committed: bill navigation in Transactions.jsx.** Follow-up
to the credit-card overhaul below, requested in a fresh session. The
"Showing {bill} only · View full history" chip was a one-way clear button
— once you switched to full history there was no way back to a specific
bill, and no way to browse *other* bills (e.g. the previous month's) at
all.
Made it a real toggle, plus prev/next navigation:
- New `groupTransactionsByBill`-derived `cardBillDates` memo — every bill
(ascending due date) the currently-filtered credit card has ever had a
transaction assigned to, drawn from its complete history (not whatever
date range happens to be active), so navigation can reach bills outside
the visible window.
- `stepBill('prev' | 'next')` moves `filterBillDueDate` to the adjacent
entry in `cardBillDates`, always switching into an unbounded custom range
(same as "View all" already did) so the date filter never hides the
target bill. Chevrons disable at either end.
- The chip itself now toggles both ways: bill-scoped shows "Showing {bill}
only · View full history" (click clears to full history); full-history
shows "Viewing full history · Show bill" (click jumps to
`defaultBillDueDate()` — the next upcoming bill if it's a real one,
otherwise the most recent past bill).
- `billBalanceAsOf` (added in the prior session's 8th fix) already made
"Amount owed" and day totals follow the bill filter — this now also
updates correctly as you step between bills via the chevrons, not just
via the initial "View all" click.
Verified via Playwright: a card with 3 bills (July/August/September, one
transaction each) — chevrons correctly disable at both ends, "Amount
owed" and the day total update at each step, full-history shows the real
$90 total across all three, and "Show bill" correctly returns to July (the
next upcoming bill relative to today).
**Follow-up in the same session: "Back to all transactions" button.** The
only way out of a bill/account drill-down was clearing the bill filter,
which just leaves you on that one card's full history — still not the
page's normal default view. Clicking "View full history" required a page
reload to get back to browsing normally. Added `resetToDefaultView()` —
clears every filter (account, category, search, range→'this-month',
custom dates, bill, month offset) and the remembered-account localStorage
entry — wired to a new "← Back to all transactions" button shown whenever
the bill navigator is visible (i.e. whenever `cardBillDates.length > 0`),
in both the bill-scoped and full-history states.
Verified via Playwright: from a bill-scoped view, clicking it returns to
"All accounts" / "This month" with the normal "Projected" section back,
no reload needed.
**Follow-up in the same session: flickering $0 "Projected" row.** User
noticed the projected credit-card bill sometimes showed $0 and sometimes
didn't appear at all. Root cause: `nextBillFor` (in `creditCard.js`)
returns a synthetic $0 placeholder — dated via `nextDueDate(dueDayOfMonth,
today)`, not tied to any real transaction — whenever a card has no
activity assigned to an upcoming bill yet. `projectedBills` then filtered
that placeholder by whether its date fell in the *currently viewed* month,
so it flickered in and out as `today` or the viewed month changed, with no
visible reason. Fixed by excluding non-positive amounts
(`p.amountOwed > 0`) from `projectedBills` — the placeholder never had
anything to actually project, so it's just dropped rather than shown as a
misleading $0 row. `nextBillFor` itself is untouched (Overview's per-card
tile intentionally still shows "$0, due in N days" for a card with no
activity — that's a fixed, always-one-row-per-card display with no
date-range filtering, so it doesn't have the same flicker problem).
Verified via Playwright: a linked card with zero transactions shows no
"Projected" row in June, July, or August (previously flickered depending
on the viewed month); adding a real $25 charge makes it appear correctly
with the real amount.
**Follow-up in the same session: Overview's "Credit card debt" didn't
match the per-card "next bill" tiles right below it.** User asked why the
top total showed $0 while a card's tile showed a real upcoming amount.
Root cause: the two figures used genuinely different logic —
`totalCardDebt` summed real debt *as of today* while excluding anything
dated in the future or explicitly tagged outside the current calendar
month (from the earlier "should consider only current month" fix), while
the per-card tiles use `nextBillFor`, which shows whichever bill is
chronologically next regardless of month or the transaction's date.
Confirmed with the user (their call, given the tension with the earlier
fix): the total should just be the sum of the tiles, always consistent by
construction. `totalCardDebt` now sums `nextBillFor(...).amountOwed` per
card directly — same function, same result as what's rendered below.
This does mean a transaction explicitly deferred to a future bill counts
toward the total again when it's that card's only/next open bill (no
longer zeroed out for not being "this month's" bill) — that's the
accepted tradeoff for the total always matching what's on screen.
Verified via Playwright: a card whose only transaction is a $80 charge
explicitly tagged to next month's bill now shows "$80.00" in both the top
total and its own tile (previously $0 vs $80 — the reported mismatch).
**Follow-up in the same session: Transactions page filter-card redesign.**
User found the filters card (search, account, category, range, Export all
crammed into one panel plus a second row of month/date controls)
cluttered and confusing. Restructured in `Transactions.jsx`:
- **New toolbar row** directly below the page header, above the balance
panel: Account select + range mode select ("This month"/"Custom range")
+ either month chevrons/label or FromTo date inputs inline, depending
on mode — the two most important filters, always visible, no longer
buried in a bordered card.
- **Search + Category** moved into a collapsible "More filters" panel
(new `showMoreFilters` state), collapsed by default; shows a small
"Active" tag next to the toggle when collapsed with a search term or
category selected, so an active filter is never silently hidden.
- **Export** demoted to a small ghost button on the same low-emphasis row
as the "More filters" toggle, right-aligned — no longer competing
visually with the primary filters.
- All existing behavior preserved as-is: `filterBillDueDate` still clears
on every account/range/month/date-range change, the credit-card bill
navigator (chevrons + toggle + "Back to all transactions") still lives
in the balance panel and works unchanged, and the "your real balance —
not limited to..." disclaimer still appears correctly.
Verified via Playwright: default view shows the compact toolbar with
"More filters" collapsed and Export tucked to the side; toggling shows
search+category and the "Active" chip appears correctly when collapsed
with a filter set; switching to "Custom range" swaps in date inputs
inline; the credit-card bill navigator and new toolbar coexist correctly
(tested filtering to a card, stepping through "View all").
**Follow-up polish in the same session:** the range select still showed
the static label "This month" next to a separate "Jul 2026" span flanked
by chevrons — two elements doing one job. Merged them: the select's
"this-month" option now renders the live month label itself (e.g. "Jul
2026"), and the prev/next chevrons wrap the select directly instead of a
separate label — so it reads as one control, `< [Jul 2026 ▾] >`, that's
also still a real dropdown (selecting "Custom range" from it works exactly
as before). No behavior change, purely visual consolidation.
## Status as of end of session (2026-07-08)
**Done, not yet committed: Credit card overhaul** (requested 2026-07-08 via
`Credit-Card-Issues.md`, planned via plan mode, plan saved at
`/home/ricardo/.claude/plans/woolly-skipping-quasar.md`). Landed as 4 chunks,
each independently reviewable/committable:
1. **`Transaction.billDueDate`** (backend) — new nullable `LocalDate` column
(`V2__transaction_bill_due_date.sql`), settable/clearable via
`CreateTransactionRequest`/`UpdateTransactionRequest`. Display/grouping tag
only, same spirit as `seriesInfo` — never read by
`recomputeAccountBalance`. For repeat/installment transactions the
override is only applied to the batch's semantics correctly: every
generated row gets `billDueDate = null` regardless of what was sent
(a fixed date copied onto every installment would be wrong), verified via
curl.
2. **Bill dropdown + shared util** (frontend) — new
`frontend/src/utils/creditCard.js` (`billDueDateFor`,
`groupTransactionsByBill`, `nextBillFor`), the single source of truth for
"what bill is this transaction in" / "what's the next bill", replacing
the old computed-only grouping in `Transactions.jsx`. Add/edit transaction
forms show a "Bill" dropdown (3 upcoming due dates) when the account is a
credit card with `dueDayOfMonth` set — solves "a purchase made after the
statement effectively closes should land on next month's bill, not this
month's." Only shown for non-repeating transactions (matches the backend
scope limit). Verified end-to-end in browser: overriding to "Aug 10 bill"
correctly moved the "Credit card bills" aggregate from Jul 10 to Aug 10.
3. **Overview redesign** — Total Balance now sums only CHECKING/SAVINGS
(excludes INVESTMENT and CREDIT_CARD, confirmed with user); a second
figure shows total credit-card debt. Credit card accounts get their own
"Credit cards" section below the regular accounts grid, showing each
card's **next bill** (amount + due date), not just current balance.
4. **`Account.paymentAccount` + projected bill row** — CREDIT_CARD accounts
can link a CHECKING/SAVINGS "payment account"
(`V3__account_payment_account.sql`, validated in
`AccountResource#validatePaymentAccount`: must be CREDIT_CARD-only,
can't self-reference, target must be CHECKING/SAVINGS owned by the same
user). In Transactions, viewing that linked checking account (or "All
accounts") now shows a dashed, read-only "Projected" row — "Credit card
bill — {card name}" — for the card's next bill. It's not a real
`Transaction` (no id, no edit/delete), and only affects the *projected/
future* balance stat (via a new `projectedCcDeduction` applied solely to
`futureBalanceRaw`), never the real `currentBalanceTotal`/
`balanceAsOfDay`. Verified in browser: Checking's real "Current balance"
stayed $0 while its July-end projected balance correctly showed -$75.50
matching the pending card bill.
Verified via curl (backend validation: 400s for non-CC `paymentAccountId`,
CREDIT_CARD/INVESTMENT target, self-reference) and via a full Playwright
browser smoke test (signup → create linked checking+card → add transaction
with bill override → check Overview/Transactions render correctly), no
console errors. No automated test suite exists in this repo, so this was the
verification path (same as prior chunks).
**Follow-up fixes found via user testing (2026-07-08, same session), both in
`Transactions.jsx`:**
1. **Bill leakage across months.** `creditCardBillGroups` ("Credit card
bills" aggregate) grouped by each transaction's assigned bill due date,
but never checked that due date actually fell within the currently
viewed month — so a transaction dated *this* month but tagged (via the
new Bill dropdown, or a natural due-day rollover) to *next* month's bill
would "leak" that future due date into the current month's view,
confusingly. Pre-existing behavior before this session's work, but the
new per-transaction Bill override made it much easier to trigger. Fixed
by having `isAggregatedCreditCardTxn` also check the assigned bill's due
date against the active date-range filter — when a transaction's bill
falls outside the viewed range, it now falls through to the normal
per-day list on its real `occurredOn` date instead of either leaking or
disappearing.
2. **Double-counted projected balance in "All accounts."** The projected
future-balance deduction (added for the checking↔card link) was applied
per-account inside the "All accounts" reduce, but summing it there
double-counts the same debt: once as the checking account's projected
payment, again as the card's own real (already-negative) balance, which
never actually moves since no real payment transaction is recorded.
Fixed by only applying the deduction when a single linked checking/
savings account is the active filter — the "All accounts" net total no
longer applies it at all (a card's real balance already reflects the
debt; there's nothing to project there).
Re-verified all scenarios via Playwright after both fixes: month-scoped
"Credit card bills"/"Projected" sections, month navigation to the bill's due
month, and the "All accounts" future-balance figure no longer doubling.
**Third + fourth follow-up fixes (2026-07-08, same session): Overview's
"Credit card debt" total, in two passes — the first pass introduced a
regression the second pass had to correct.**
*Pass 1:* `totalCardDebt` summed each card's real, all-time balance
(`accountBalances`), which includes every charge ever made regardless of
which bill it's tagged to — so a transaction dated this month but assigned
(via the Bill dropdown) to *next* month's bill was still inflating this
month's total. First fix: sum only the bill group (via
`groupTransactionsByBill`) whose due date falls in the current calendar
month.
*Regression this introduced:* the "Bill" dropdown always shows *some*
selected value (defaulting to the natural next due date) and the create/
edit forms were submitting that value unconditionally — so even a normal
transaction the user never touched the dropdown for got a non-null
`billDueDate` persisted. Once the due day for a cycle has already passed
(e.g. due day 3, transaction dated the 8th), the *natural* rollover lands
next month — meaning ordinary, just-today expenses were silently excluded
from "Credit card debt" too, which is what surfaced as "not considering an
expense in today's date."
*Root-cause fix (pass 2):* `Transactions.jsx`'s `handleSubmit` and
`handleEditSubmit` now only send `billDueDate` when it actually differs
from the freshly-computed natural default for that `occurredOn` — i.e. only
when the user *genuinely* overrode it. Left at the default, `billDueDate`
stays `null`, exactly like before the Bill dropdown existed.
`Overview.jsx`'s `totalCardDebt` was correspondingly simplified: walk each
card's transactions from `openingBalance`, excluding only (a) future-dated
ones (`occurredOn > today`, same as the existing real-balance convention)
and (b) ones with an *explicit* `billDueDate` outside the current month —
natural rollover alone no longer excludes anything. The per-card figure in
"Credit cards" is unchanged (still shows the next bill regardless of
month).
Verified via Playwright, three scenarios: (1) explicit override to next
month → correctly excluded from the total; (2) ordinary today-dated charge
on a card whose due day already passed this cycle (natural rollover, no
override) → correctly included; (3) a same-month natural bill → still
counts as before. All three consistent with no regressions.
**Fifth follow-up fix (2026-07-08, same session): `Transactions.jsx`'s "All
accounts" balance totals and per-row visibility for credit cards.** Two
related gaps, both in the "All accounts" view:
1. `currentBalanceTotal`/`balanceAsOfDay`/`futureBalanceRaw` summed *every*
account including credit cards, so a card's debt was netted directly
into "Current balance" — inconsistent with Overview's Total Balance,
which had already been scoped to checking/savings only. Fixed by
introducing the same `liquidAccounts` (CHECKING + SAVINGS) restriction
for all three "All accounts" aggregates; filtering to one specific
account (including a credit card directly) is unaffected.
2. A credit card transaction whose bill fell *outside* the viewed range
was falling through to the normal per-day list (from the earlier
"bill leakage" fix) — but credit card transactions should never appear
as individual rows in "All accounts," only via "Credit card bills."
Split the old combined `isAggregatedCreditCardTxn` into `isCreditCardTxn`
(a plain kind check, used to unconditionally exclude CC transactions
from the per-day list) and kept the due-date-range check scoped to just
`creditCardBillGroups` — so a transaction whose bill is out of range now
simply doesn't appear anywhere in that view (not the day list, not the
bill aggregate) rather than leaking into the day list.
Verified via Playwright with checking + linked credit card: "All accounts"
Current balance now shows the checking-only total (matches Overview's Total
Balance), the card's transaction appears only under "Credit card bills" (not
as a day-list row), and filtering directly to the card still shows its own
rows normally (unaffected).
**Sixth follow-up fix (2026-07-08, same session): duplicate bill sections
in `Transactions.jsx`.** The fifth fix's `creditCardBillGroups` (real, old
"Credit card bills" section, transaction-derived) and `projectedBills` (new
"Projected" section, `nextBillFor`-derived) had converged to show the same
thing for any *linked* card — both scoped to bills due within the viewed
range — so a linked card's bill rendered twice. Merged into one section:
`creditCardBillGroups` is gone; `projectedBills` now covers every credit
card with a due day set when viewing "All accounts" (previously only
cards with a linked payment account), and just the card(s) linked to the
active account when filtering to one specific checking/savings account
(unchanged from before). Added the "View all" button (previously only on
the old section) to the merged "Projected" rows.
Verified via Playwright with one linked and one unlinked credit card: "All
accounts" shows a single row per card (both linked and unlinked covered,
each with "View all"), and filtering to the linked card's checking account
narrows to just that card's bill, dropping the unrelated unlinked one.
**Seventh follow-up fix (2026-07-08, same session, pre-existing bug):
"View all" on a projected bill loaded the account's full history instead
of just that bill.** `viewAccountTransactions` only ever set an unbounded
custom date range — fine for the original "View transactions" link (full
history by design), but wrong for a specific bill row, since a bill is
defined by `billDueDateFor`, not by an `occurredOn` range (an explicitly
overridden transaction can be dated in a totally different month than its
assigned bill, so no date-range filter could isolate it correctly anyway).
Added a new `filterBillDueDate` state that filters `filteredFlat` by
`billDueDateFor(account, t)` equality rather than by date range.
`viewAccountTransactions` now takes an optional `billDueDate` param (passed
from the "View all" button as `p.dueDate`) that sets this alongside the
account filter. Cleared automatically whenever account, range, month, or
custom-date-range controls are touched directly (search/category are
left alone — narrowing within a bill still makes sense); a small chip
("Showing {date} bill only · View full history") shows near the balance
stat when active, doubling as the way to clear it manually.
Verified via Playwright: two charges on the same card, same date, one
tagged to July's bill and one to August's — "View all" on the (single,
soonest) projected row now shows only the July-tagged charge, and "View
full history" restores both.
**Eighth follow-up fix (2026-07-08, same session): the "Amount owed" stat
(and day-heading totals) still showed the card's full real balance while
a specific bill was selected, instead of just that bill's total.** Unlike
the category/search filters — which deliberately leave "Current balance"/
"Amount owed" as the real, unfiltered figure (a longstanding, confirmed
design decision) — selecting a specific bill via "View all" is a genuine
scope change, so the balance stat needs to follow it. Added
`billBalanceAsOf(account, dueDateIso, cutoffDate)`, summing just that
bill's transactions (via `billDueDateFor`) up to a cutoff date, and used it
in place of `accountBalanceAsOf` for both `currentBalanceTotal` and
`balanceAsOfDay` whenever `filterBillDueDate` is set. The "your real
balance — not limited to..." disclaimer is now shown only for category/
search (not for the bill filter, since the balance genuinely is limited to
the bill in that case — showing the disclaimer would be misleading).
Verified via Playwright: with a $20 July-bill charge and a $35
August-tagged charge on the same card (same day, $55 real balance),
"View all" on the July bill now shows "Amount owed: $20.00" (matching the
filtered list and day total) instead of the card's full $55.
## Status as of end of session (2026-07-07, later)
**Done, not yet committed: Credit cards feature** (requested 2026-07-07,
planned via plan mode, plan saved at
`/home/ricardo/.claude/plans/luminous-painting-pillow.md`).
- Backend: `Account` gained `creditLimit` (nullable BigDecimal) and
`dueDayOfMonth` (nullable Integer, 1-31, a simple recurring day — no
statement-cycle/billing-period model, decided explicitly). Both editable
via `CreateAccountRequest`/`UpdateAccountRequest`, validated in
`AccountResource#validateCreditCardFields` (400 on negative limit or
out-of-range day). Both optional — a credit card account can exist
without them.
- Frontend: `Accounts.jsx`'s add/edit forms conditionally show "Credit
limit"/"Due day of month" fields when Kind = Credit Card (gated on kind
*at submit time*, so switching kind away before saving discards stray
values). New page `frontend/src/pages/CreditCards.jsx` (route
`/credit-cards`, sidebar link after Accounts) — a glance dashboard: total
owed/limit/available-credit summary panel, per-card utilization progress
bar + status word ("Good standing"/"Near limit"/"Over limit", same
85%/100% thresholds as Budgets.jsx) reusing existing `.track`/
`.budget-status` CSS (no new styles needed), and a due-date line
("Due {date} · in N days") computed by rolling `dueDayOfMonth` forward to
its next occurrence (clamped for short months). Gracefully omits the bar/
due-date line when those fields are null.
- Also fixed: on `Transactions.jsx`, filtering to a single credit-card
account now shows its balance as a positive "Amount owed" (label + sign +
color all flip), consistently across the headline stat, each day-group
total, and each row's running-balance figure — the underlying balance
math (`recomputeAccountBalance`, `openingBalance`) is untouched, this is
display-only. The "All accounts" net total (assets minus card debt) is
intentionally left as a real negative-inclusive sum, unaffected.
- Not done / explicitly deferred: no bulk/statement-history tracking, no
"pay down" flow — this is glance/visibility only, same spirit as §8's
deferred recurring-series tracking.
## Status as of end of session (2026-07-07)
**Done and committed:** §1–§6 UI-wiring chunks (Categories page,
Add-transaction form, Add/edit-budget form + Overview budget panel,
Transactions filters + Export, Overview date-range dropdown) — 5 commits,
`319c023` through `c7af925`. §7 **Account edit/delete** is also done and
committed (`d5aacaa`).
**Also done and committed:** §7 **Category edit/delete** (backend
`PUT`/`DELETE /categories/{id}` + `GET /categories/{id}/usage`, frontend
edit/delete rows in `Categories.jsx`, plus a follow-up: category color
picker now auto-picks a random non-repeating color from a 10-color palette
on "Add category" instead of showing 5 fixed presets, with a "Choose color"
toggle to override manually) and §7 **Budget delete** (backend
`DELETE /budgets/{id}` — no usage-check needed, nothing references a Budget
row; frontend edit/delete icons on `budget-card`, pencil reopens the
existing "Set budget" form pre-filled).
**Done, not yet committed:** §7 **User profile** page — see §7 item 5 below
for full detail — plus two bug fixes found along the way:
1. `client.js`'s global 401 interceptor was catching failed-login 401s (bad
credentials) as if they were expired-session 401s, forcing a hard
`window.location.href = '/login'` reload instead of letting `Login.jsx`
show its inline error. Fixed by excluding `/auth/*` requests from that
handler.
2. The new `PUT /users/me/password`'s "wrong current password" case
originally returned 401, which would have tripped that same interceptor
for an already-logged-in user. Changed to 400 (the user IS
authenticated; this is a request-validation failure, not a session
issue).
Also added an `account_history` table + "Account history" card on the
Profile page: every display-name/email change is logged old→new, password
changes log only the date (never a value). See §7 item 5.
**Also done and committed:** §7 **Transaction edit/delete** (the last item
in §7 — all of §7 is now complete) plus three follow-up fixes/features on
the Transactions page found via user testing. See §7 item 1 for full detail
on all of these:
1. Added `Account.openingBalance` (immutable, set once at creation) so
`account.balance` and every transaction's `runningBalance` are fully
re-derived in true chronological (`occurredOn`, then `id`) order on every
create/edit/delete, via a new `TransactionResource#recomputeAccountBalance`
helper — replacing the old insertion-order-based math, which was subtly
wrong for backdated transactions.
2. Fixed a client-side timezone bug: `new Date("yyyy-MM-dd")` parses as UTC
midnight, which renders as the previous calendar day in timezones behind
UTC — a transaction dated "today" was showing under "Yesterday." Fixed
with a shared `frontend/src/utils/date.js#parseLocalDate` used everywhere
`occurredOn` strings are turned into `Date` objects (`Transactions.jsx`
and `Overview.jsx` both had the bug).
3. Added a "Current balance" stat to the Transactions page (real account
balance, respects the account filter, unaffected by category/search —
by design, confirmed with user) and changed each day-group's figure from
"net sum for that day" to "balance as of that day." When a category
filter is active, a second "`<category>` total" stat now also appears
(net sum of the currently filtered rows) — this one *does* reflect
search/account/range filters too, since it's explicitly a filtered
subtotal rather than a real balance.
**Reference pattern for delete UIs** (see `[[project-accounts-pattern]]` in
memory): in-card/in-row swap to a "Delete X? This cannot be undone."
confirm panel (no `window.confirm`, no app-wide modal) using the `.btn-red`
style, and a pre-validation check before showing the confirm step at all
(e.g. `GET /categories/{id}/usage`) rather than attempting the delete and
showing an error after — skip the pre-check only when nothing could
possibly reference the row (Budget's and Transaction's case).
**Not started:** §7 is fully complete, and so is the recurrence/installments
feature below — nothing outstanding right now beyond §5's demo-login bug,
§6's backend gaps, and the new §8 item below (neither urgent).
## 8. Recurring-series tracking at the DB level — [x] done (raised 2026-07-07, resolved 2026-07-11)
Was: a repeat/installment purchase generated N independent `Transaction`
rows linked only by a cosmetic `seriesInfo` string (e.g. `"3/12"`), no real
`series_id`/group column anywhere — editing/deleting one occurrence only
ever touched that single row, deleting one left a gap in the numbering, no
"edit/delete this and all future occurrences" option.
Now fixed — see the 2026-07-11 session entry above for full detail:
- [x] Real `series_id` (Java-generated UUID, `V8__transaction_series_id.sql`)
set at generation time in `TransactionResource#create`.
- [x] `seriesInfo` renumbers contiguously after any delete; a series
collapsed to one row clears its series fields entirely.
- [x] "This occurrence only" / "This and future" scope choice on both edit
and delete, frontend + backend, including INSTALLMENTS-aware amount
re-splitting.
**Done, not yet committed: Recurrence/installments** (requested 2026-07-07).
- Backend: `Transaction` gained `seriesInfo` (nullable String, e.g. `"3/12"`,
display-only — no series/group table). `POST /transactions` now returns
`List<Transaction>` instead of a single `Transaction` (frontend never
consumed the return value beyond cache invalidation, so this was safe).
`CreateTransactionRequest` gained `repeat` (`RepeatFrequency` enum: NONE/
WEEKLY/MONTHLY/YEARLY/INSTALLMENTS) and `occurrences` (2-60, validated).
Confirmed approach: pre-generate all occurrences upfront as real rows
(no background `@Scheduled` job, no new dependency — `quarkus-scheduler`
isn't in this project and adding it was explicitly deferred). WEEKLY/
MONTHLY/YEARLY repeat the same amount each occurrence; INSTALLMENTS
splits the total evenly (remainder cent(s) land on the last installment
so the parts always sum back exactly). Each generated row is a normal
transaction, editable/deletable with the §7 item 1 machinery already
built — no bulk/series management exists, by design (kept simple).
- Frontend: "Repeat" select + conditional "Occurrences" input on the
add-transaction form, with inline help text explaining what will be
created. Each `txn-row` shows a `seriesInfo` badge (reusing the existing
`.tag` class) when present, e.g. "3/12".
**Environment reminders for next session** (see [[project-dev-environment]]
in memory for more):
- Local dev runs via `docker compose` — containers `ledger-backend` (8080),
`ledger-frontend` (5173), `ledger-db` (5432) — check `docker ps` before
assuming anything needs starting.
- The real logged-in user is `ricardompcampos@hotmail.com`, not the seeded
`demo@ledger.app` account.
- **Demo login is still broken** (`seed-data.sql`'s bcrypt hash uses an
incompatible `$2b$` prefix) — not fixed, see §5.
- **Editing any file under `backend/src/main/java` while `quarkus:dev` is
running wipes the local database** (Hibernate `drop-and-create` on live
reload) — expect this, not a bug. User has said to just proceed without
pausing for confirmation each time, since local data is disposable.
Snapshot from a full frontend + backend audit (2026-07-06). The backend is
further along than it looks from the UI — Transactions, Budgets, and
Categories all have working create endpoints that are simply never called.
The gap is almost entirely in the frontend; a couple of backend gaps are
called out separately at the end.
Reference pattern for "how a working button looks in this codebase":
`frontend/src/pages/Accounts.jsx` — `useState` toggle for a panel +
controlled form inputs + `useMutation` (react-query) + `queryClient
.invalidateQueries` on success. There is no shared `<Modal>` or `<Dropdown>`
component yet, so the first one built should probably be extracted for reuse
rather than copy-pasted three more times.
## 1. Categories — no UI at all (highest priority, blocks the rest)
Backend is fully ready: `Category` entity, `GET /categories`, `POST
/categories` (`backend/src/main/java/com/ledger/category/CategoryResource.java`).
Frontend `CategoriesApi.list()` / `.create()` already exist in
`frontend/src/api/ledger.js:14-17` but are never imported anywhere.
- [x] Add a Categories page (`frontend/src/pages/Categories.jsx`) with list +
create form (name, color), using the Accounts.jsx pattern.
- [x] Add `/categories` route in `App.jsx`.
- [x] Add "Categories" link to `Sidebar.jsx` (`links` array, currently only
Overview/Transactions/Budgets/Accounts).
- [ ] Backend: add `PUT /categories/{id}` and `DELETE /categories/{id}` —
currently create/list only, no edit or delete.
- [ ] Once this exists, wire the Transactions category filter/picker to
`CategoriesApi.list()` instead of the hardcoded `categoryColors` object
(`Transactions.jsx:112-117`).
## 2. Transactions page — "Add transaction" is a dead button
`Transactions.jsx:86-89` — no `onClick`, no form/modal exists anywhere.
Backend `POST /transactions` already works
(`backend/src/main/java/com/ledger/transaction/TransactionResource.java:36`),
required fields: `accountId`, `description`, `amount`; optional:
`categoryId`, `occurredOn` (defaults to today). Note it also updates the
account's running balance server-side as a side effect.
Frontend `TransactionsApi.create` already exists in `ledger.js:22`.
- [x] Build the add-transaction form/panel (account select, category select
populated from `CategoriesApi.list()`, description, amount, date).
- [x] Wire it to `TransactionsApi.create` + invalidate the relevant queries
(account list for balance, transactions list).
- [x] Wire the account filter select (`Transactions.jsx:104-109`) — currently
renders options but has no `onChange`/state.
- [x] Wire the category filter select (`Transactions.jsx:112-117`) — same
issue, also depends on item 1. Now populated from `CategoriesApi.list()`.
- [x] Wire the date-range select ("Last 30 days" / "Last 90 days" / "This
year", `Transactions.jsx:120-124`) — filtering is still done client-side
over all fetched transactions (no backend date-range params yet, see
§6), but the control now actually filters the visible list.
- [x] Wire or remove the "Export" button (`Transactions.jsx:127-130`) —
exports the currently filtered transactions as a CSV download.
## 3. Budgets page — no way to create or edit a budget
`Budgets.jsx:51-54` — "Edit budgets" button has no `onClick`, and there's no
"Add budget" affordance either. Backend `POST /budgets` is a working
upsert-by-category-and-month
(`backend/src/main/java/com/ledger/budget/BudgetResource.java:67`), needs
`categoryId`, `month`, `limitAmount`. Frontend `BudgetsApi.upsert` already
exists in `ledger.js:30`.
- [x] Build a set/edit-limit form (category select, month, limit amount),
reusing the Accounts.jsx pattern, calling `BudgetsApi.upsert`.
- [x] `Budgets.jsx:24` hardcodes `yearMonth` to the current month
(`new Date().toISOString().slice(0, 7)`) — no way to view or set a
budget for any other month. Add month navigation.
- [x] `Overview.jsx:118-124` has a placeholder panel ("Category breakdowns
will live here once budgets have real spend data behind them") even
though `GET /budgets/month/{yearMonth}/spend` already returns exactly
that data — just needs to be fetched and rendered (as cards/progress
bars per CLAUDE.md, not charts).
## 4. Overview page — "This month" button does nothing
`Overview.jsx:51-54` — static button, no `onClick`, no dropdown markup, no
date-range state anywhere on the page.
- [x] Build the dropdown (This week / Last week / This month / Last 30 days /
This year) — added as a reusable `frontend/src/components/Dropdown.jsx`
(generic trigger + menu, click-outside-to-close), styled to match
`.btn-ghost` and the hairline-border/no-shadow language.
- [x] Wire selection to the "Recent activity" list — filtered client-side
over the already-fetched per-account transactions (same approach as
the Transactions date-range filter in §2/§4; no backend date-range
endpoint needed at current data volumes, see §6).
## 5. Security / data fixes found and applied during implementation
- [x] `User.passwordHash` had no `@JsonIgnore` — every endpoint that embeds a
`User` relation (Category, Account, Transaction, Budget) was leaking
the bcrypt hash in plain JSON. Fixed in
`backend/src/main/java/com/ledger/user/User.java`.
- [x] `seed-data.sql`'s transaction INSERTs referenced the `t` (VALUES)
alias inside the `categories` JOIN condition before `t` was introduced
in the FROM clause — fails on Postgres ("missing FROM-clause entry for
table t"). Fixed by reordering the JOINs so `t` is introduced before
it's referenced.
- [ ] `seed-data.sql`'s demo password hash uses a `$2b$` bcrypt prefix that
Quarkus's Elytron `BcryptUtil` doesn't recognize (`ELY08003: Unknown
crypt string algorithm`) — demo login (`demo@ledger.app`/`demo12345`)
is currently broken. Needs a hash regenerated via `BcryptUtil.bcryptHash`
(Elytron-compatible, likely `$2a$` prefix) rather than a hash lifted
from another bcrypt implementation.
**Note:** editing any file under `backend/src/main/java` while `quarkus:dev`
is running triggers a live-reload schema regeneration (`drop-and-create`,
per the Known Gaps section above) — it silently wipes all local data. Not a
bug to fix, just something to expect until real migrations exist.
## 6. Backend gaps
- [ ] No date-range query params on `GET /transactions/account/{accountId}`
— returns everything, unbounded, no pagination. Needed for both the
Overview dropdown and the Transactions date-range select to be
meaningful server-side rather than client-side filtering over the full
history.
- [ ] No dashboard/stats aggregation endpoint — Overview currently computes
totals client-side from full per-account transaction lists fetched one
account at a time. Fine for now at small data volumes, but worth an
endpoint if this needs to scale.
- [ ] No `DELETE`/`PUT` for Transaction or Budget (Budget's create doubles as
update via upsert, so this is lower priority than Category's missing
edit/delete).
## Suggested order (all done as of 2026-07-06)
1. [x] Categories page (unblocks category pickers everywhere else).
2. [x] Add-transaction form.
3. [x] Add/edit-budget form + wire the Overview budget-spend panel (data
already exists server-side, purely a frontend job).
4. [x] Transactions filters (account/category/date) + Export.
5. [x] Overview "This month" dropdown (client-side range filter).
All five UI-wiring chunks from this audit are implemented. What's left is
§6's backend gaps (date-range/pagination endpoint, Category edit/delete,
Transaction/Budget delete) and the outstanding bcrypt-prefix bug in
§5 (demo login) — none of these block normal use of the app with a real
account, they're follow-ups for later.
## 7. Edit/delete + profile management (requested 2026-07-06, not yet built)
Every entity currently only supports create + list. There is no edit or
delete UI anywhere, and the only delete endpoint that exists server-side
(`DELETE /accounts/{id}`) isn't even called from the frontend. None of this
is built yet — tracked here for a future round of chunks.
1. **Transactions** — [x] done, committed.
- Backend: `Account` gained an immutable `openingBalance` field (set once
at creation). `PUT`/`DELETE /transactions/{id}` added, both calling a
new `recomputeAccountBalance()` helper that re-derives `account.balance`
and every transaction's `runningBalance` from `openingBalance`, walking
transactions in chronological (`occurredOn`, then `id`) order — not
insertion order like the old `create()` did. `seed-data.sql` updated
with `opening_balance` values for the demo accounts.
- Frontend: pencil/trash icons on each `txn-row`, same in-row edit-form /
confirm-panel pattern as the rest of the app. No pre-check before
delete (nothing references a transaction, same as Budget's case).
- Follow-ups from user testing: fixed a UTC/local timezone bug in day
grouping (`frontend/src/utils/date.js#parseLocalDate`), added a
"Current balance" stat + changed day-group totals to "balance as of
that day" instead of a same-day net sum, and added a category-filtered
total stat. See the note above for full detail.
2. **Budgets** — [x] done, awaiting user review/commit.
- Backend: added `DELETE /budgets/{id}`.
- Frontend: edit/delete icons on `budget-card` — pencil reopens the top
"Set budget" form pre-filled with that category+limit (still upserts
via the same endpoint), trash swaps to the in-card confirm panel.
3. **Accounts** — [x] done, committed (`d5aacaa`).
- Backend: added `PUT /accounts/{id}` (name/institution/kind only —
balance stays server-derived from transactions). `DELETE` now catches
the FK-constraint case and returns 409 instead of a raw 500.
- Frontend: edit/delete icon buttons on `account-card-lg`, in-card
confirm panel, pre-validates via `TransactionsApi.listByAccount` before
showing the confirm step (see delete-pattern note above).
4. **Categories** — [x] implemented, awaiting user review/commit.
- Backend: added `PUT /categories/{id}`, `GET /categories/{id}/usage`
(transaction + budget counts), `DELETE /categories/{id}` — blocks
(409) if used by any transaction or budget rather than nulling out
`category_id` on existing rows (budgets require a category —
`Budget.category` is `optional = false` — so nulling wasn't an option
there anyway, and blocking keeps both entities' delete behavior
consistent).
- Frontend: edit/delete rows in `Categories.jsx`, same in-row swap
pattern as Accounts, pre-validates via the new `/usage` endpoint.
5. **User profile** (email, password, display name) — [x] done, awaiting
user review/commit.
- Backend: new `UserResource` — `PUT /users/me` (display name + email,
uniqueness-checked like signup, re-issues the JWT since the token's
`upn` is the email), `PUT /users/me/password` (requires current
password via `BcryptUtil.matches`, 400 not 401 on mismatch). Extracted
the duplicated JWT-issuing code out of `AuthResource` into
`security/TokenService` since a second call site needed it.
- Frontend: new `/profile` page + sidebar link, two panels (profile
info, change password), same form pattern as everywhere else.
`AuthContext` got `applySession()` to refresh the cached token/user
after a profile save.
- Bonus: `account_history` table + "Account history" card logging
display-name/email changes (old→new) and password changes (date
only) — see note above.
**Suggested build order:** Account edit/delete, Category edit/delete,
Budget delete, and User profile — [x] all four done, see above — leaving
only Transaction edit/delete, the trickiest one (running-balance
recomputation on delete/edit affects every later transaction on that
account).
</content>