You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Post-#1404/#1405 cleanup arising from product-architect's reviews of PR #1407 (comment) and PR #1412 (comment). Bundles four medium-severity recommendations from the same review cycle:
Deduplicate the proportional deposit-split formula now inlined in four service files (refactor, no behaviour change).
Extract a shared OverflowMenu component from the 1363-line InvoiceDepositsSection.tsx (refactor, no behaviour change).
Replace an inline style={{}} JSX prop with a CSS-Module class (refactor, no behaviour change).
Surface API errors in the three deposit-revert handlers that currently swallow failures silently (bug fix, user-visible UX improvement).
Items 1-3 are behaviour-preserving; item 4 is a bug fix. Combined into one issue because all four are closely related cleanup from the same architect review cycle.
Findings
1. Extract splitByDeposits() helper to dedupe the proportional-split pattern
The proportional deposit-split formula is now inlined in four places:
server/src/services/budgetSourceService.ts -> inline in computeDiscretionaryInvoiceAmount()
All four share a common shape:
Group rows by invoice_id
For each invoice, compute residual = max(0, invoice.amount - sum(deposits.amount))
Bucket the residual under the parent invoice's status
Bucket each deposit under its own status
Extract a single splitByDeposits() helper that returns the per-invoice split as a structured value, then have the four call sites reduce over those splits to produce their respective aggregate shapes.
2. Extract shared OverflowMenu component from InvoiceDepositsSection.tsx
client/src/pages/InvoiceDepositPage/InvoiceDepositsSection.tsx is ~1363 lines, and the per-row overflow menu (trigger button + menu items + keyboard navigation + focus management) is duplicated almost verbatim between DepositRow (desktop table) and DepositCard (mobile cards) — ~330 lines duplicated.
Extract a single shared OverflowMenu component to client/src/components/OverflowMenu/ per the project's "Component Reuse Policy" (per CLAUDE.md: "every new component must be built as a reusable shared component — no one-off implementations"). The component should expose:
items: Array<{ label: string; onClick: () => void; ... }> (or similar) for the menu items
triggerAriaLabel: string for accessibility
Full WAI-ARIA Menu Button keyboard nav (ArrowUp/Down/Home/End/Escape, focus return on close) — same behaviour as the inline implementation today
Important
Per project policy, every new shared component requires a ux-designer visual spec before implementation begins. The orchestrator must launch the ux-designer agent for the visual spec phase prior to launching the frontend-developer.
3. Replace inline style={{}} on table row with CSS Module class
InvoiceDepositsSection.tsx contains an inline style={{ ... }} on a table row (architect flagged it as the same anti-pattern as the recent fd73bcad fix on the budget-line form). Move the inline declaration to a named class in InvoiceDepositsSection.module.css.
4. Surface API errors in revert handlers
In InvoiceDepositsSection.tsx, the handleRevertToPending, handleRevertToPaid, and handleStateConfirm functions catch API errors silently — the user gets no feedback when a transition fails (e.g., on INVALID_DEPOSIT_STATUS_TRANSITION or network errors). The PR-review by product-owner and architect both flagged this as a UX gap.
Surface the error to the user via the existing toast/banner pattern or via FormError in the relevant modal. For revert actions (which fire PATCH immediately without a modal), surface a transient error banner or toast at the section level. Use the existing translateApiError(code, tErrors) helper for code mapping.
Acceptance Criteria
AC-1 A shared splitByDeposits(rows) function exists in server/src/services/shared/depositAggregateUtils.ts and is reused by computeDepositAwareAggregates, computeStatusContribution, aggregateInvoiceStatusBreakdown, AND computeDiscretionaryInvoiceAmount in budgetSourceService.ts. Behaviour preserved exactly — existing tests still pass without modification.
AC-2 A reusable OverflowMenu shared component exists under client/src/components/OverflowMenu/. DepositRow and DepositCard in InvoiceDepositsSection.tsx both consume it instead of duplicating the menu code. Full WAI-ARIA Menu Button keyboard nav preserved (ArrowUp/Down/Home/End/Escape, focus return on close). Existing E2E tests for the deposit menu pass without modification.
AC-3 No style={{}} JSX prop remains in InvoiceDepositsSection.tsx — moved to InvoiceDepositsSection.module.css with named classes.
AC-4 The three revert handlers in InvoiceDepositsSection.tsx (handleRevertToPending, handleRevertToPaid, handleStateConfirm) surface API errors to the user. Server error codes are translated via translateApiError(code, tErrors). Network errors show a generic translated message. Use the existing FormError shared component for modal-based actions and an inline transient banner for the menu-driven Revert actions (which don't open a modal). Add at least one E2E test that verifies the error is visible after a forced failure (or, if E2E setup is too heavy, an integration/component test that simulates the API rejection).
AC-5 All existing tests pass without modification (refactor is behaviour-preserving for AC-1 through AC-3). New tests added for AC-4 (error visibility).
AC-6 No new dependencies, no schema changes, no API contract changes.
AC-7 UX-designer approves the visual spec for the new OverflowMenu shared component before implementation begins.
Non-Goals
No UX behaviour changes for items 1-3 (pure refactors).
No new deposit-related features.
The mobile bottom-sheet modal (separate UX issue, file separately if needed).
Other UX-designer nits (focus ring tokens, count chip tokens, statusQuotation dark mode, tablet column hiding) — file as separate refinement issues if desired; these are not part of "architect's recommendations."
Summary
Post-#1404/#1405 cleanup arising from product-architect's reviews of PR #1407 (comment) and PR #1412 (comment). Bundles four medium-severity recommendations from the same review cycle:
OverflowMenucomponent from the 1363-lineInvoiceDepositsSection.tsx(refactor, no behaviour change).style={{}}JSX prop with a CSS-Module class (refactor, no behaviour change).Items 1-3 are behaviour-preserving; item 4 is a bug fix. Combined into one issue because all four are closely related cleanup from the same architect review cycle.
Findings
1. Extract
splitByDeposits()helper to dedupe the proportional-split patternThe proportional deposit-split formula is now inlined in four places:
server/src/services/shared/depositAggregateUtils.ts->computeDepositAwareAggregates()server/src/services/shared/depositAggregateUtils.ts->computeStatusContribution()server/src/services/shared/depositAggregateUtils.ts->aggregateInvoiceStatusBreakdown()(added in Invoice status summary breakdown is not deposit-aware (InvoicesPage header + Dashboard pipeline card) #1411)server/src/services/budgetSourceService.ts-> inline incomputeDiscretionaryInvoiceAmount()All four share a common shape:
invoice_idresidual = max(0, invoice.amount - sum(deposits.amount))Extract a single
splitByDeposits()helper that returns the per-invoice split as a structured value, then have the four call sites reduce over those splits to produce their respective aggregate shapes.2. Extract shared
OverflowMenucomponent fromInvoiceDepositsSection.tsxclient/src/pages/InvoiceDepositPage/InvoiceDepositsSection.tsxis ~1363 lines, and the per-row overflow menu (trigger button + menu items + keyboard navigation + focus management) is duplicated almost verbatim betweenDepositRow(desktop table) andDepositCard(mobile cards) — ~330 lines duplicated.Extract a single shared
OverflowMenucomponent toclient/src/components/OverflowMenu/per the project's "Component Reuse Policy" (perCLAUDE.md: "every new component must be built as a reusable shared component — no one-off implementations"). The component should expose:items: Array<{ label: string; onClick: () => void; ... }>(or similar) for the menu itemstriggerAriaLabel: stringfor accessibilityImportant
Per project policy, every new shared component requires a ux-designer visual spec before implementation begins. The orchestrator must launch the
ux-designeragent for the visual spec phase prior to launching thefrontend-developer.3. Replace inline
style={{}}on table row with CSS Module classInvoiceDepositsSection.tsxcontains an inlinestyle={{ ... }}on a table row (architect flagged it as the same anti-pattern as the recentfd73bcadfix on the budget-line form). Move the inline declaration to a named class inInvoiceDepositsSection.module.css.4. Surface API errors in revert handlers
In
InvoiceDepositsSection.tsx, thehandleRevertToPending,handleRevertToPaid, andhandleStateConfirmfunctions catch API errors silently — the user gets no feedback when a transition fails (e.g., onINVALID_DEPOSIT_STATUS_TRANSITIONor network errors). The PR-review by product-owner and architect both flagged this as a UX gap.Surface the error to the user via the existing toast/banner pattern or via
FormErrorin the relevant modal. For revert actions (which fire PATCH immediately without a modal), surface a transient error banner or toast at the section level. Use the existingtranslateApiError(code, tErrors)helper for code mapping.Acceptance Criteria
splitByDeposits(rows)function exists inserver/src/services/shared/depositAggregateUtils.tsand is reused bycomputeDepositAwareAggregates,computeStatusContribution,aggregateInvoiceStatusBreakdown, ANDcomputeDiscretionaryInvoiceAmountinbudgetSourceService.ts. Behaviour preserved exactly — existing tests still pass without modification.OverflowMenushared component exists underclient/src/components/OverflowMenu/.DepositRowandDepositCardinInvoiceDepositsSection.tsxboth consume it instead of duplicating the menu code. Full WAI-ARIA Menu Button keyboard nav preserved (ArrowUp/Down/Home/End/Escape, focus return on close). Existing E2E tests for the deposit menu pass without modification.style={{}}JSX prop remains inInvoiceDepositsSection.tsx— moved toInvoiceDepositsSection.module.csswith named classes.InvoiceDepositsSection.tsx(handleRevertToPending,handleRevertToPaid,handleStateConfirm) surface API errors to the user. Server error codes are translated viatranslateApiError(code, tErrors). Network errors show a generic translated message. Use the existingFormErrorshared component for modal-based actions and an inline transient banner for the menu-driven Revert actions (which don't open a modal). Add at least one E2E test that verifies the error is visible after a forced failure (or, if E2E setup is too heavy, an integration/component test that simulates the API rejection).OverflowMenushared component before implementation begins.Non-Goals
References
fd73bcad