fix(reports): source-scoped claim close-out, quotation deposit-only claims, hide non-applicable claim lines (#1895, #1896, #1918) - #1922
Conversation
…laims, hide non-applicable claim lines - Mark-claimed is now source-scoped: sourceId + explicit depositIds gate the deposit sweep to (untagged OR tagged-to-sourceId) deposits, and an invoice only flips to claimed when no other budget source still has line-level interest in it. - Quotation invoices with a sweepable deposit no longer 409 the whole batch; they get a deposit-only close-out instead, and the wizard surfaces offending invoice numbers when a genuine INVOICES_NOT_CLAIMABLE error occurs. - Claim reports (getSourceReport) drop budget lines that contribute nothing to the claim status slice, so quotation invoices funded only by an unclaimed tagged deposit no longer show non-applicable line items in the wizard or the AI report content input. Fixes #1895 Fixes #1896 Fixes #1918 Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com> Co-Authored-By: Claude backend-developer <noreply@anthropic.com> Co-Authored-By: Claude frontend-developer <noreply@anthropic.com> Co-Authored-By: Claude translator <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com> Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
|
[ux-designer] UX review — scope: copy quality (2 new/reworded i18n strings) and error-banner pattern consistency. No CSS/component files touched by this PR, confirmed via diff. Verdict: APPROVED
No token/visual regressions possible here since no CSS or layout files are in this diff. Co-Authored-By: Claude ux-designer (Sonnet 4.6) noreply@anthropic.com |
steilerDev
left a comment
There was a problem hiding this comment.
[security-engineer]
Verdict: APPROVE (no Critical/High findings)
Scope: server/src/routes/sourceReports.ts and server/src/services/sourceReportService.ts (source-scoped claim close-out, #1895/#1896/#1918). Reviewed against the requested focus areas.
1. New batched SQL in markInvoicesClaimed — parameterization: OK
The otherSourceInvoiceIds query (sourceReportService.ts step d) uses the same safe pattern already established elsewhere in this file (splitRows, unallocRows): sql.join(invoiceIds.map((id) => sql\${id}`), sql`, `)for theIN (...)list and${sourceId}for the scalar comparison. Drizzle'ssql` tagged template parameterizes every interpolation — no string concatenation, no raw user input reaches the query text. No injection surface.
2. sourceId / depositIds request fields
- Schema:
sourceId(string, minLength 1),invoiceIds(array, minItems 1),depositIds(array) are allrequiredwithadditionalProperties: false— consistent with the route's existing convention. Minor informational note: unlike the sibling/generate-contentroute (added in #1901) which capsmaxLength/maxItemson its array/string fields,mark-claimed'ssourceId/invoiceIds/depositIdshave no upper bounds. Not a regression introduced by this PR (invoiceIdsalready lacked a cap pre-PR) and bounded in practice by Fastify's default body size limit, but worth aligning for consistency in a future pass. - Authorization model: Route only checks
request.useris present (both Admin/Member allowed), matching the existing convention for this endpoint and the single-household trust model (no per-source ownership/ACL exists anywhere in the app — any authenticated user can already read/write all budget data). No cross-tenant concern. depositIdssweep scope — Low finding: Server-side,sweepEligibleDepositsis derived purely by looking up the client-supplieddepositIdsand filtering tobudgetSourceId === null || budgetSourceId === sourceId(sourceReportService.ts:554-563). There is no check that a swept deposit's parent invoice is actually related to theinvoiceIdsbeing claimed (or tosourceIdvia a budget line) — Step f sweeps every sweep-eligible deposit regardless of whether its invoice appears anywhere ininvoiceIds/allInvoiceIds's claim intent. A caller bypassing the UI could submitdepositIdsfor deposits belonging to a completely unrelated invoice (as long as untagged or already tagged to the givensourceId) and have them flipped toclaimed.- In practice this isn't reachable through the shipped UI:
ReportWizardPage.tsx'shandleMarkClaimedderivesdepositIdsexclusively fromreport.invoices(already scoped tosourceIdbygetSourceReport), so the frontend never sends deposits outside the report's scope. - Given the app's trust model (Admin and Member both already have unrestricted read/write over all budget data — there's no per-user or per-source ownership boundary to escalate across) and that the transition is reversible (
claimed → paidis an allowed transition, so a wrongly-swept deposit can be corrected via the existing deposit-status endpoint), I'm treating this as a data-integrity / business-logic gap rather than a privilege-escalation vulnerability. Recommend (non-blocking): additionally constrainsweepEligibleDepositsto deposits whoseinvoiceIdis drawn from a server-derived relevant-invoice set (e.g. intersect with invoices that have a budget line or tagged deposit undersourceId) rather than trusting the client-submitteddepositIdslinkage as-is.
- In practice this isn't reachable through the shipped UI:
3. InvoicesNotClaimableError detail exposure
Returns { invoiceIds: offendingIds } — a subset of the same invoice IDs the caller submitted in the request body (or their own report data). No new sensitive-data disclosure; consistent with the existing StaleOwnershipError convention of returning offending IDs in details.
Summary
No injection, auth-bypass, or sensitive-data-leak findings. One Low finding (depositIds sweep scope) and one Informational note (missing size caps on mark-claimed body fields) — neither blocks merge. CI's two failing E2E shards (2/16, 5/16) are outside security review scope.
|
[product-architect] Architecture review — PR #1922 (#1895 / #1896 / #1918)Verified against Verdict: 🔴 High —
|
… counts, sourceId validation Fix-loop round 1 for PR #1922, addressing product-architect REQUEST CHANGES findings plus the shard-2 E2E regression: - mark-claimed now accepts invoiceIds:[] as long as depositIds is non-empty (anyOf schema rule), enabling a pure deposit-only sweep with no invoice in scope. The 400 ValidationError now only fires when both arrays are empty. - sourceId is validated to exist (404 NotFoundError) before any read/write in the transaction. - Success counts (claimedInvoiceCount/claimedDepositCount) are now derived from the server's response (claimedInvoiceIds/claimedDepositIds), not the client's request arrays, since the server can decline to flip a requested invoice (deposit-only close-out). - Added a client-side "nothing claimable" guard (sourceReports.claimNothingClaimable) for the case where both computed arrays are empty, closing the confirm modal without calling the API. - wiki/API-Contract.md updated with the new validation rules, the 404 row, and NULL-attribution wording for the other-source-interest check (submodule commit d6e13dc). - E2E: reportWizardEditableContent.spec.ts Scenario 10 rewritten for the both-empty guard path (was asserting stale claim-success copy, causing the shard 2/16 failure); reportWizardExpansion Scenario 6/10 copy and count fixes plus new Scenario 11 (deposit-only close-out reached via a manual UI line exclusion); reportWizard.spec.ts Scenario 1 count fix; POM docstring updated. Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com> Co-Authored-By: Claude backend-developer <noreply@anthropic.com> Co-Authored-By: Claude frontend-developer <noreply@anthropic.com> Co-Authored-By: Claude translator <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com> Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Fix-loop round 2 for PR #1922: Step5Actions.tsx's claim error banner referenced a non-existent CSS Modules key (sharedStyles.formErrorBanner), a pre-existing bug on beta that silently rendered the error text with no class and no role="alert" for every claim-error path. It was only surfaced now because round-1 added the first E2E assertion that positively checks this banner's visibility (E2E Tests shard 2/16 on the round-1 CI run). Switched to sharedStyles.bannerError (the established error-banner class used by ChunkLoadErrorBoundary, UserManagementPage, BackupsPage) plus role="alert", matching the pattern used everywhere else in the codebase. Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com> Co-Authored-By: Claude frontend-developer <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Fix-loop round 3 for PR #1922: round-2's production fix (Step5Actions.tsx now renders sharedStyles.bannerError, not the never-existent formErrorBanner) left the E2E page object's claimErrorBanner locator pointed at the old, now-doubly- stale "formErrorBanner" substring, so E2E Tests shard 2/16 kept failing on the same assertion for a new reason. Repointed the locator at [class*="bannerError"] and corrected the neighboring comment describing the two banner variants. Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com> Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
|
[product-architect] Re-review — PR #1922 @
|
…ntract review smell Co-Authored-By: Claude product-architect <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🎉 This PR is included in version 2.13.0-beta.36 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
POST /api/source-reports/mark-claimedis now source-scoped: it requiressourceIdand an explicitdepositIdslist. The deposit sweep is restricted to deposits that are untagged or tagged tosourceId, and an invoice only flipspending/paid→claimedwhen no other budget source still funds a budget line on it — otherwise a deposit-only close-out runs and the invoice keeps its current status. Excluding a line in the wizard now keeps that invoice's status untouched (its excluded portion stays claimable later) while still sweeping its own deposits.quotation), so it no longer 409s and rolls back an otherwise-valid mark-claimed batch. WhenINVOICES_NOT_CLAIMABLEis still returned, the wizard now names the offending invoice number(s) instead of showing a generic error.getSourceReport) now drop budget lines whose contribution to the claim status slice rounds to zero — e.g. a quotation invoice funded only by an unclaimed deposit tagged to the source. This hides non-applicable line items from the wizard's expandable row and from the AI report-content generation input, while leavingallocatedAmount,totalAmount, and the deposits sub-table unaffected.budget-overviewandproof-of-fundsreports are unchanged.Fixes #1895
Fixes #1896
Fixes #1918
Test plan
sourceReportService.test.ts,sourceReports.test.ts,reportContentGenerationService.test.ts; client:sourceReportsApi.test.ts,ReportWizardPage.test.tsx) — 92 server + 108 client tests green, coverage 91.6–100% across touched filesreportWizardExpansion.spec.tsextended with new scenarios for the cross-source sweep (Bug: mark-as-claimed is invoice-scoped, not source-scoped — claims other budget sources' money #1895), the quotation + pending-deposit mixed batch (Bug: quotation-status invoice with a pending deposit enters the claim report and 409s the entire mark-claimed batch #1896), and the hidden-items quotation invoice (Bug: claim report shows non-applicable budget lines for quotation invoices entering via an unclaimed deposit #1918); existing warning-copy regex updated to match the corrected claim-modal copyCo-Authored-By: Claude dev-team-lead noreply@anthropic.com
Co-Authored-By: Claude backend-developer noreply@anthropic.com
Co-Authored-By: Claude frontend-developer noreply@anthropic.com
Co-Authored-By: Claude translator noreply@anthropic.com
Co-Authored-By: Claude qa-integration-tester noreply@anthropic.com
Co-Authored-By: Claude e2e-test-engineer noreply@anthropic.com