Skip to content

feat: Schedule E line summary + classification quality gating#91

Merged
chitcommit merged 1 commit intomainfrom
feat/schedule-e-tax-export
Apr 11, 2026
Merged

feat: Schedule E line summary + classification quality gating#91
chitcommit merged 1 commit intomainfrom
feat/schedule-e-tax-export

Conversation

@chitcommit
Copy link
Copy Markdown
Contributor

@chitcommit chitcommit commented Apr 11, 2026

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 ScheduleEReport

1. 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: ClassificationQuality

Counts L2-classified vs L1-only vs unclassified rows among the report's contributing transactions. readyToFile is true iff confirmedPct >= 95.

Trust level Meaning for filing
L2 classified (coa_code set) Human-approved — safe to file
L1 suggested only (suggested_coa_code set, coa_code null) AI/keyword guess not yet confirmed — review before trusting
Unclassified (neither set) Fell back to keyword matcher at render time — likely 9010 Suspense

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. Surfaces l1SuggestedOnlyAmount so 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.
  • Dynamic tax year picker — was hard-coded to 2024/2025; now shows current year ± 3.

Backend plumbing

  • ReportingTransactionRow.suggestedCoaCode? added
  • getTransactionsForTenantScope now selects suggestedCoaCode
  • buildScheduleEReport computes 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-e automatically emits the new fields (additive, no client break)

Test plan

  • TypeScript check passes (0 errors)
  • 253 tests pass (+7 new: 3 lineSummary, 4 classificationQuality)
  • Vite build succeeds
  • Manual: visit /reports → Schedule E tab → verify line summary renders with drill-down
  • Manual: run /classification batch-suggest on a tenant with unclassified txns, return to Schedule E → verify banner shows "Not ready" with L1 count
  • Manual: classify every suggested row via /classification → verify banner flips to "Ready to file" at ≥95%

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
  • lineSummary preserves Schedule E line order
  • classificationQuality L2/L1/unclassified counting
  • readyToFile=true at 100% L2
  • readyToFile=true on empty report (zero-division guard)
  • readyToFile=true at exactly 95% threshold, false at 90%

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Schedule E classification quality indicator showing readiness-to-file status and transaction classification counts (L2, L1-suggested, unclassified).
    • Added aggregated Schedule E line summary table with expandable breakdowns by chart-of-accounts code and computed net totals.
    • Updated Tax Year selector to dynamically show a 5-year range instead of fixed years.

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>
Copilot AI review requested due to automatic review settings April 11, 2026 10:08
@chitcommit chitcommit enabled auto-merge (squash) April 11, 2026 10:08
@github-actions
Copy link
Copy Markdown
Contributor

@coderabbitai review

Please evaluate:

  • Security implications
  • Credential exposure risk
  • Dependency supply chain concerns
  • Breaking API changes

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Type Definitions
client/src/hooks/use-reports.ts, server/lib/tax-reporting.ts
Added new interfaces ScheduleELineSummaryItem (with per-COA-code breakdown) and ClassificationQuality (with L2/L1/unclassified counts and readyToFile indicator). Updated ScheduleEReport to export both new fields.
UI Components & Reports Page
client/src/pages/Reports.tsx
Integrated ClassificationQualityBanner and LineSummarySection components into ScheduleETab, updated Tax Year selector to dynamic 5-year range, and extended imports for new types.
Server Reporting Logic
server/lib/tax-reporting.ts
Implemented lineSummaryAgg accumulation by Schedule E line and COA code, computed classificationQuality metrics tracking L2/L1-suggested/unclassified transactions, and built lineSummary output with sorted COA breakdowns and readyToFile logic (95% threshold).
Storage & Database
server/lib/consolidated-reporting.ts, server/storage/system.ts
Added optional suggestedCoaCode field to ReportingTransactionRow interface and updated getTransactionsForTenantScope query to include suggestedCoaCode projection.
Test Coverage
server/__tests__/tax-reporting.test.ts
Added comprehensive test suites for line summary aggregation (multi-property, multi-COA grouping, deterministic ordering) and classification quality metrics (count separation, percentage computation, readyToFile edge cases).

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Lines aggregate with gentle care,
COA codes grouped beyond compare,
Classification metrics bloom so bright,
Ready-to-file at ninety-five—
The Bunny Report comes alive!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding Schedule E line summary aggregation and classification quality metrics with a readiness gating mechanism.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/schedule-e-tax-export

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
chittyfinance 713d135 Apr 11 2026, 10:09 AM

@chitcommit chitcommit merged commit 0940ee0 into main Apr 11, 2026
12 of 13 checks passed
@chitcommit chitcommit deleted the feat/schedule-e-tax-export branch April 11, 2026 10:09
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +349 to +353
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lineSummary aggregation (Schedule E Lines 3–19) with per-COA drill-down breakdowns.
  • Add classificationQuality stats (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). */
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/** 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%). */

Copilot uses AI. Check for mistakes.
Comment on lines +480 to +490
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,
};
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +569 to +571
{Array.from({ length: 5 }, (_, i) => currentYear - 3 + i).map((y) => (
<option key={y} value={y}>{y}</option>
))}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
expect(report.classificationQuality.readyToFile).toBe(true);
});

it('marks readyToFile=false just below the 95% threshold', () => {
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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").

Suggested change
it('marks readyToFile=false just below the 95% threshold', () => {
it('gates readyToFile at the 95% threshold', () => {

Copilot uses AI. Check for mistakes.
Comment on lines +347 to +361
// 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);
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants