Skip to content

feat(close): settle changed transactions in the Closing Book - #444

Merged
jfrench9 merged 1 commit into
mainfrom
feature/changed-transactions-panel
Oct 4, 2026
Merged

jfrench9 merged 1 commit into
mainfrom
feature/changed-transactions-panel

Conversation

@jfrench9

@jfrench9 jfrench9 commented Oct 3, 2026

Copy link
Copy Markdown
Member

Summary

A transaction that changed at the source after RoboLedger posted it holds the period close. Until now the app could only say so: settling one meant asking an assistant. This adds a Changed transactions panel to the Closing Book where each change is reviewed and settled.

Changes

Ledger → Closing Book

  • New sidebar entry, Changed transactions, under Reconciliations. It appears wherever the Reconciliations entry does (a ledger with something posted). The entry is added in the app, so nothing changes in what the API lists.
  • The panel lists the flagged transactions with date, description, where the change came from (QuickBooks or a bank feed), a link to the transaction at the source when there is one, and the amount.
  • Review opens one change: what RoboLedger posted, what the source says now, and the difference by account (debit or credit).
  • Settle offers the three treatments in the product docs' words: restate, catch up, mark as handled.
    • The treatment the API recommends is selected to start.
    • One the ledger would refuse is disabled with the API's reason, for example a restate into a closed period.
    • Marking a change as handled requires a note.
    • A catch-up uses the API's default date and is drafted for the close, and the option says which date that is.
  • A change settled elsewhere since the list loaded drops off with a notice. One that changed again while open is previewed again with its newer figures, and nothing is settled.
  • While one change is being settled the other rows are held, so the wrong row cannot be closed under it.

Period close hub

  • The "edited in QuickBooks after they were synced" blocker now says how many changes hold this close and links to the panel.

No figure the app renders is computed differently. Restate and catch-up change ledger entries, through the same operation the assistant already calls.

Not included: choosing a catch-up date other than the default, posting a catch-up immediately instead of as a draft, and naming the entry that already handled a change. Those remain available through an assistant.

Deploy Notes

None. The panel uses @robosystems/client 2.5.1, already on main, and every operation and field it calls is in the API release that is deployed.

Testing

  • npm run test:all: 100 files, 783 tests passed; format, lint, typecheck and cf-lint clean. 18 of the tests are new for the panel, with additions to the sidebar, page and period-close tests.
  • npm run build: compiled.
  • Against a local API: no local ledger had a changed transaction, so two posted events on a demo ledger were flagged by hand, one in an open month and one in a closed month. The app was driven in a headless browser in light and dark mode: the blocker link, the list, both reviews (restate offered for the open month, disabled with its reason for the closed one), and one real mark-as-handled, after which the row left the list. The API's refusals for a restate into a closed period and for a mark-as-handled without a note were read through the client.
  • Not run against the API from the panel: a restate and a catch-up. Their requests are covered by the unit tests only.

🤖 Generated with Claude Code

A transaction that changed at the source after it was posted holds the
close, and until now the app could only say so. The Closing Book gains a
Changed transactions panel beside Reconciliations: each change shows what
was posted against what the source says now, with the difference by
account, and is settled as a restate, a catch-up or marked as handled.

- A treatment the ledger would refuse is disabled with the reason, and
  marking a change as handled requires a note.
- A change settled elsewhere drops off the list; one that changed again
  mid-review is shown again with its newer figures.
- The close blocker on the period hub links to the panel and says how
  many changes hold this close.
@github-actions github-actions Bot added the change:normal Normal change: automated review documented on the pull request before merge label Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Change classification: normal — platform or application code changed.

A normal change carries a documented automated review on this pull request before merge, in place of a second human approver. The change-classification job requests that review and passes once it has been posted. Recorded by the change-classification job.

@jfrench9

jfrench9 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

@claude please review this PR

@claude

claude Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Claude finished @jfrench9's task in 28s —— View job


Review of PR #444: Changed transactions panel

I read the diff and the full ChangedTransactionsPanel.tsx. I did not run the test suite or the app. The panel is well structured and follows the repo conventions: colocated components, useEffect fetching, optimistic removal on settle, and FilterBar not needed. The race handling is careful (planSeq and graphSeq), and the stale-row and re-flagged paths are thought through. I found no blocking bugs. Details below, roughly by importance.

Worth addressing

  1. The truncation message is misleading (ChangedTransactionsPanel.tsx:387-391). It says "Settle these to see the rest", but the list is fetched once per graphId and never refetched after a settle. The hidden items will not appear until the panel remounts. Either refetch when items drops below the limit, or reword the message ("Showing the 200 most recent; reopen this panel after settling to load more").

  2. Error handling does not use friendlyError. CLAUDE.md says new write surfaces should import friendlyError from @/lib/ledger/errors and extend it for new error classes. This panel uses extractDetail and shows the API's detail text directly (describeError, line 46). It also detects two outcomes by substring: 'not a reconciling item' and 're-flagged' (lines 211 and 218). Both are brittle, because a wording change on the API silently turns them into a generic error. Consider adding these classes to errors.ts with tests, and ideally matching on a structured code if the API provides one. Raw detail is also what the guidance says not to show to users.

  3. State is not fully reset on graph switch (effect at lines 119-152). It clears items, the selection, plan and outcome. It does not clear settleError, notice, note or isSettling. If a settle is in flight during a switch, the graphSeq guard returns early, and finally still clears isSettling, so that case is fine. A stale settleError or notice can still reappear on the next opened row until openItem clears them. This is minor, but resetting them in the effect is cheap.

  4. outcome is not cleared when another row is opened. The "Restated…" banner stays above a newly opened review, which can read as if it applies to that row. Clear it in openItem.

Smaller notes

  • externalUrl as href (line 321). The value comes from the API, so it is probably safe. If the field can ever carry a source-supplied URL, restrict it to https: before rendering. Otherwise a javascript: URL is possible, even with rel="noopener".
  • Label-matched sidebar injection (content.tsx, withChangedTransactions). It adds the entry by matching category.label === 'Reconciliations'. That works, and the PR says it is intentional. If the server relabels the category, the entry vanishes with no error. A comment or a test asserting the match would make that failure visible. A test may already exist in StructureSidebar.test.tsx; I did not check.
  • Hub blocker text (PeriodClosePanel.tsx). It renders {calendar.reconcilingItemCount} holding this close. If the count is ever undefined or 0 while the blocker code is present (for example an older API), it prints "undefined holding…" or "0 holding…". Guard it, or fall back to wording without a number.
  • detail row key. delta and EntryList fall back to index as a key. That is fine for static lists.
  • Default treatment may be blocked. setTreatment(result.default_disposition) assumes the API's recommendation is not blocked by the client-side blockedReason. If it is, the selected radio is disabled and Settle is disabled. The user can still pick another option, so this is only a UX edge.

Tests. The PR adds 18 panel tests and extends the sidebar, page and close-panel tests. The PR description says restate and catch-up were not exercised against a live API. I would add unit tests for the re-flagged path (the notice must survive openItem, which calls setNotice(null) first) and for the "already settled" path, if they are not already covered in the 491-line test file.

Verdict: Approve with the small follow-ups above. Items 1 and 2 are the ones I would fix before merge.

@jfrench9
jfrench9 merged commit 5100fb6 into main Oct 4, 2026
7 checks passed
@jfrench9
jfrench9 deleted the feature/changed-transactions-panel branch October 4, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:normal Normal change: automated review documented on the pull request before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant