feat: Schedule E line summary + classification quality gating#91
feat: Schedule E line summary + classification quality gating#91chitcommit merged 1 commit intomainfrom
Conversation
Extends buildScheduleEReport with two trust-path integrations that turn
the existing per-property view into a filer-ready summary.
### 1. Schedule E line summary (cross-property aggregation)
The existing report showed per-property cards but not the aggregated totals
that go on the actual IRS form. Added `lineSummary: ScheduleELineSummaryItem[]`
which rolls every transaction up by Schedule E line (Line 3 through Line 19)
with a per-COA-code breakdown for drill-down:
lineSummary: [
{
lineNumber: 'Line 14',
lineLabel: 'Repairs',
amount: 500,
transactionCount: 3,
coaBreakdown: [
{ coaCode: '5070', coaName: 'Repairs', amount: 400, transactionCount: 2 },
{ coaCode: '5020', coaName: 'Cleaning & Maintenance', amount: 100, ... },
],
},
...
]
Breakdown sorted by amount descending so tax preparers see the biggest
contributors first. Order of lineSummary matches SCHEDULE_E_LINE_ORDER.
### 2. Classification quality gating
Tax reports should prefer L2-classified rows (human-approved coa_code).
L1-only rows (suggested_coa_code set, coa_code null) are AI/keyword guesses
that haven't been confirmed — the filer should review them before trusting
the totals.
New field: classificationQuality: ClassificationQuality — counts L2
vs L1-only vs unclassified rows among the report's contributing
transactions. readyToFile is true iff confirmedPct >= 95 (chosen
because tax filings tolerate small residual uncertainty but more than 5%
unconfirmed means the filer hasn't actually reviewed the AI suggestions).
classificationQuality: {
totalTransactions: 20,
l2ClassifiedCount: 19,
l1SuggestedOnlyCount: 1,
unclassifiedCount: 0,
l1SuggestedOnlyAmount: 10,
confirmedPct: 95,
readyToFile: true,
}
### Supporting changes
- `ReportingTransactionRow.suggestedCoaCode?` added; storage query now
returns it
- `/api/reports/tax/schedule-e` automatically emits both fields (no new
endpoint needed — same response shape, additive fields)
- Reports.tsx: new ClassificationQualityBanner component shows
ready-to-file status with confirmed/L1/unclassified counts. Surfaces
l1SuggestedOnlyAmount so filers know how much is at risk
- Reports.tsx: new LineSummarySection renders the aggregated Line 3-19
table with a per-row "N codes" button that expands to show the COA
breakdown
- Reports.tsx: tax year dropdown is now dynamic (current year ± 3) instead
of hard-coded 2024/2025
### Tests (+7, 253 total)
- lineSummary aggregation across properties with single-COA line
- lineSummary grouping multiple COA codes into Line 17 (Utilities)
sorted by amount descending
- Schedule E line order preserved in lineSummary
- classificationQuality: L2/L1/unclassified counting
- readyToFile=true at 100% L2
- readyToFile=true on empty report
- readyToFile=true at exactly 95% threshold, false at 90%
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@coderabbitai review Please evaluate:
|
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis PR introduces Schedule E line-level summaries with chart-of-accounts (COA) breakdowns and classification quality metrics (L2-classified, L1-suggested-only, and unclassified transaction counts). Changes span type definitions, backend aggregation logic, database query updates, new UI components, and comprehensive test coverage. Changes
Sequence DiagramsequenceDiagram
participant Client as Client (Reports Page)
participant API as API / buildScheduleEReport()
participant DB as Database
participant Agg as Aggregation Logic
participant Metrics as Metrics Computation
Client->>API: Request Schedule E Report
API->>DB: getTransactionsForTenantScope()
DB-->>API: Transactions (with coaCode, suggestedCoaCode)
API->>Agg: Process transactions by lineNumber
Agg->>Agg: Build lineSummaryAgg map<br/>(line → {amount, coaBreakdown[]})
Agg->>Agg: Track contributed flag<br/>for each transaction
API->>Metrics: Count L2/L1/unclassified
Metrics->>Metrics: Calculate confirmedPct<br/>= l2Count / totalContributed
Metrics->>Metrics: Determine readyToFile<br/>(95% threshold)
API-->>Client: ScheduleEReport<br/>(lineSummary[], classificationQuality)
Client->>Client: Render ClassificationQualityBanner<br/>& LineSummarySection
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
chittyfinance | 713d135 | Apr 11 2026, 10:09 AM |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 713d135694
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| unclassifiedCount, | ||
| l1SuggestedOnlyAmount: round2(l1SuggestedOnlyAmount), | ||
| confirmedPct, | ||
| readyToFile: contributingTxCount === 0 || confirmedPct >= 95, |
There was a problem hiding this comment.
Base ready-to-file gate on unrounded confirmation ratio
readyToFile is decided from confirmedPct, but confirmedPct is rounded to two decimals first. That can incorrectly flip the gate to ready for borderline cases (e.g., a true 94.995% confirmation rate rounds to 95.00 and passes), which is risky because this banner is meant to block filing until quality is high enough.
Useful? React with 👍 / 👎.
| const summaryKey = tx.type === 'income' ? 'Line 3' : lineNumber; | ||
| let summaryBucket = lineSummaryAgg.get(summaryKey); | ||
| if (!summaryBucket) { | ||
| summaryBucket = { amount: 0, count: 0, coaBreakdown: new Map() }; | ||
| lineSummaryAgg.set(summaryKey, summaryBucket); |
There was a problem hiding this comment.
Exclude entity-level rows from cross-property line summary
This rollup runs for every contributed transaction, including the entity-level branch (propertyId missing) that is stored separately in entityLines. As a result, lineSummary includes non-property rows even though the feature/UI describes it as an across-properties filing summary, so tenants with entity-level items will see line-summary totals that don’t match the property matrix.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR upgrades the Schedule E report into a filing-oriented view by adding (1) an aggregated “what you type into the IRS form” line summary across properties and (2) a classification-quality gate that surfaces whether enough transactions are human-confirmed (L2) to be considered safe to file.
Changes:
- Add
lineSummaryaggregation (Schedule E Lines 3–19) with per-COA drill-down breakdowns. - Add
classificationQualitystats (L2 vs L1-suggested vs unclassified) and a 95% “ready to file” threshold. - Update client Schedule E UI to show a readiness banner, a line summary section, and a dynamic tax year picker.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| server/storage/system.ts | Extends tenant-scope transaction selection to include suggestedCoaCode for reporting/quality calculations. |
| server/lib/tax-reporting.ts | Implements cross-property line summary aggregation and classification-quality computation on Schedule E reports. |
| server/lib/consolidated-reporting.ts | Extends the reporting row shape with suggestedCoaCode for downstream consumers. |
| server/tests/tax-reporting.test.ts | Adds test coverage for lineSummary and classificationQuality behaviors/thresholds. |
| client/src/pages/Reports.tsx | Adds readiness banner + line summary UI and replaces hard-coded year options with generated options. |
| client/src/hooks/use-reports.ts | Extends client-side Schedule E report types to include lineSummary and classificationQuality. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| l1SuggestedOnlyAmount: number; | ||
| /** 0-100. Share of transactions that are L2 (safe to file). */ | ||
| confirmedPct: number; | ||
| /** True when confirmedPct is at or above the safe-to-file threshold (default 0.95). */ |
There was a problem hiding this comment.
The readyToFile docstring says the threshold is "default 0.95", but confirmedPct is documented as 0–100 and the implementation gates at >= 95. Update the comment to avoid implying a 0–1 fractional threshold.
| /** True when confirmedPct is at or above the safe-to-file threshold (default 0.95). */ | |
| /** True when confirmedPct is at or above the safe-to-file threshold (default 95%). */ |
| const confirmedPct = | ||
| contributingTxCount === 0 ? 100 : round2((l2ClassifiedCount / contributingTxCount) * 100); | ||
| const classificationQuality: ClassificationQuality = { | ||
| totalTransactions: contributingTxCount, | ||
| l2ClassifiedCount, | ||
| l1SuggestedOnlyCount, | ||
| unclassifiedCount, | ||
| l1SuggestedOnlyAmount: round2(l1SuggestedOnlyAmount), | ||
| confirmedPct, | ||
| readyToFile: contributingTxCount === 0 || confirmedPct >= 95, | ||
| }; |
There was a problem hiding this comment.
readyToFile is computed from confirmedPct, which is already rounded to 2 decimals. This can allow a value slightly below 95% (e.g. 94.996%) to round up and incorrectly pass the gate. Compare the unrounded ratio against the 95% threshold, and keep the rounded confirmedPct only for display.
| {Array.from({ length: 5 }, (_, i) => currentYear - 3 + i).map((y) => ( | ||
| <option key={y} value={y}>{y}</option> | ||
| ))} |
There was a problem hiding this comment.
The PR description says the tax year picker should show current year ± 3, but this generates only 5 years (currentYear-3 through currentYear+1). If ±3 is intended, expand the range accordingly (e.g. 7 options through currentYear+3).
| expect(report.classificationQuality.readyToFile).toBe(true); | ||
| }); | ||
|
|
||
| it('marks readyToFile=false just below the 95% threshold', () => { |
There was a problem hiding this comment.
This test name is misleading: the assertions cover both the exactly-95% case (ready=true) and a 90% case (ready=false). Rename the test to reflect the scenarios being validated (e.g., "gates at the 95% threshold").
| it('marks readyToFile=false just below the 95% threshold', () => { | |
| it('gates readyToFile at the 95% threshold', () => { |
| // Roll up into cross-property line summary (amount sign-normalized: | ||
| // income positive, expenses positive magnitudes) | ||
| const summaryKey = tx.type === 'income' ? 'Line 3' : lineNumber; | ||
| let summaryBucket = lineSummaryAgg.get(summaryKey); | ||
| if (!summaryBucket) { | ||
| summaryBucket = { amount: 0, count: 0, coaBreakdown: new Map() }; | ||
| lineSummaryAgg.set(summaryKey, summaryBucket); | ||
| } | ||
| const normalized = tx.type === 'income' ? rawAmount : absAmount; | ||
| summaryBucket.amount += normalized; | ||
| summaryBucket.count += 1; | ||
| const coaBucket = summaryBucket.coaBreakdown.get(coaCode) || { amount: 0, count: 0 }; | ||
| coaBucket.amount += normalized; | ||
| coaBucket.count += 1; | ||
| summaryBucket.coaBreakdown.set(coaCode, coaBucket); |
There was a problem hiding this comment.
The lineSummary rollup keys the COA breakdown off coaCode returned by resolveScheduleELine(...), which only treats tx.coaCode (L2) as pre-classified and otherwise re-runs findAccountCode(). For L1-only rows (suggestedCoaCode set, coaCode null), this means the summary can drift from the stored suggestion. Consider updating the resolver call to prefer tx.coaCode ?? tx.suggestedCoaCode before falling back to keyword matching.
|
Code Review - PR 91: Schedule E Line Summary + Classification Quality Gating. Overall this is solid, well-structured work. Zero-extra-queries constraint is respected, trust-path semantics align with CLAUDE.md COA level definitions, and test coverage for new paths is good. A few items worth addressing before merge. BUGS: (1) currentYear not shown in diff - the year picker uses currentYear but its declaration is not in the diff. If not already defined in the component, this will be a runtime ReferenceError. (2) Income hardcoded to Line 3 regardless of COA code - all income is forced into Line 3 (Rents received) but Schedule E also has Line 4 (Royalties received). Income with a COA code mapping to Line 4 will be silently bucketed under Line 3 with the wrong lineLabel. For a tax-filing report this could be material. Consider using lineNumber for income too, falling back to Line 3 only when unmapped. (3) l1SuggestedOnlyAmount accumulates income L1 transactions - for L1-suggested income, the income amount is added to what the banner labels as 'suggested-only transactions have not been reviewed.' The wording implies expense deductions. Guard with tx.type === expense if only expenses are intended, or update the banner copy to be explicit. DESIGN: (4) Year picker range is 5 values (currentYear-3 to currentYear+1), not the 7 implied by 'current year +/- 3' in the PR description. (5) readyToFile has a redundant guard - confirmedPct is already 100 when contributingTxCount is 0, so the first condition is always subsumed by confirmedPct >= 95. (6) Test name mislabels the boundary - 'marks readyToFile=false just below the 95% threshold' first asserts true at exactly 95%, which the name does not convey. MINOR: (7) ScheduleELineSummaryItem and ClassificationQuality are duplicated between server and client - silent drift risk, worth tracking as a shared/ candidate. (8) Floating-point at the boundary - round2 on the percentage could cause edge cases at exactly 95.00; integer arithmetic (l2ClassifiedCount * 100) / contributingTxCount >= 95 avoids this. WELL DONE: zero extra queries, coaBreakdown sorted by amount, banner hides on empty reports, 95% boundary tested on both sides, suggestedCoaCode comment is exactly the right guard-rail. |
Summary
Turns the existing per-property Schedule E view into a filer-ready report. Two trust-path integrations that make concrete use of the COA work shipped in #86-#90.
What's new on
ScheduleEReport1.
lineSummary: ScheduleELineSummaryItem[]Cross-property aggregation of Schedule E Line 3-19 — this is what you type into the actual IRS form. Each line carries a per-COA-code breakdown sorted by contribution amount, for drill-down UX.
{ "lineNumber": "Line 14", "lineLabel": "Repairs", "amount": 500, "transactionCount": 3, "coaBreakdown": [ { "coaCode": "5070", "coaName": "Repairs", "amount": 400, "transactionCount": 2 }, { "coaCode": "5020", "coaName": "Cleaning & Maintenance", "amount": 100, "transactionCount": 1 } ] }2.
classificationQuality: ClassificationQualityCounts L2-classified vs L1-only vs unclassified rows among the report's contributing transactions.
readyToFileis true iffconfirmedPct >= 95.coa_codeset)suggested_coa_codeset,coa_codenull)The 95% threshold is chosen because tax filings tolerate small residual uncertainty (tenant rounding, late-posting charges) but more than 5% unconfirmed means the filer hasn't actually reviewed the AI suggestions.
Frontend changes
ClassificationQualityBanner— green "Ready to file" or red "Not ready" with confirmed/L1/unclassified counts. Surfacesl1SuggestedOnlyAmountso filers know how much is at risk.LineSummarySection— aggregated Line 3-19 table. Each row has a "N codes" button that expands to show the COA breakdown inline (amount, count, per-code). Income/expense color-coded; net at the top-right.Backend plumbing
ReportingTransactionRow.suggestedCoaCode?addedgetTransactionsForTenantScopenow selectssuggestedCoaCodebuildScheduleEReportcomputes both new fields in the same loop as the existing property totals — zero extra queries, zero extra passes over the data/api/reports/tax/schedule-eautomatically emits the new fields (additive, no client break)Test plan
/reports→ Schedule E tab → verify line summary renders with drill-down/classificationbatch-suggest on a tenant with unclassified txns, return to Schedule E → verify banner shows "Not ready" with L1 count/classification→ verify banner flips to "Ready to file" at ≥95%Tests (+7, 253 total)
lineSummaryaggregation across properties with single-COA linelineSummarygrouping multiple COA codes into Line 17 (Utilities) sorted by amount descendinglineSummarypreserves Schedule E line orderclassificationQualityL2/L1/unclassified countingreadyToFile=trueat 100% L2readyToFile=trueon empty report (zero-division guard)readyToFile=trueat exactly 95% threshold,falseat 90%🤖 Generated with Claude Code
Summary by CodeRabbit