feat: tax categories and the tax summary report

wanderbooks is a general MCP-first accounting application: every
business needs its accounts mapped to where they land on its income tax
return, and its CPA needs book-to-tax differences surfaced, not hidden.

- ledger_core::tax: a catalog of stable categories (gross_receipts,
  commissions_fees, meals, guaranteed_payments, officer_compensation,
  nondeductible, ...) with kind (income, cost of goods, deduction), the
  returns each appears on (Schedule C, 1065, 1120-S, 1120), a default
  deductible percent (meals 50, nondeductible 0), and guidance. Keys,
  not line numbers: form lines move from year to year.
- Accounts carry tax_category and an optional deductible_percent
  override, validated against the account type (and the return, at the
  MCP layer), journaled as account.tax_set, replayed, projected (schema
  v6).
- tax_summary(start, end, form): the P&L grouped by category, book and
  deductible amounts (half away from zero to the cent), contra income
  (refunds) reducing receipts, and every account still uncategorized.
- MCP: books.tax_categories (filtered to the configured return),
  books.set_tax_category, a tax_summary report in every format and in
  the CPA package, chart output with categories, and a fifth
  tax_category column in chart CSV imports, validated before anything is
  created. [company] tax_form, or derived from tax_classification.

Scope stays book basis: no tax depreciation, basis, or filing.

Tests: a hand-computed quarter (50% meals 333.33 -> 166.67, penalties at
0%, refunds as negative receipts, guaranteed payments, one uncategorized
account; book net 5,276.67 vs tax 5,483.33); overrides; category/account
/return validation; form parsing and derivation; CSV parsing; stdio:
filtered catalog, mapping, refusal, summary, all-or-nothing import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-11 20:53:56 -07:00
1 parent 8164abb2b8
commit f858fb4cfe
18 files changed
+1352 -14

No files matched your search

+25
View File
@@ -6,6 +6,7 @@
//! grouping on type, so getting this right is the whole game.
use crate::{Currency, LedgerError};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -72,6 +73,14 @@ pub struct Account {
pub placeholder: bool,
/// Day one: always the ledger's base currency (see R-5).
pub currency: Currency,
/// Tax category key (see [`crate::tax`]), for revenue and expense
/// accounts.
#[serde(default)]
pub tax_category: Option<String>,
/// Percent deductible, overriding the category's default (e.g. 50 for
/// meals, 0 for penalties).
#[serde(default)]
pub deductible_percent: Option<Decimal>,
}
/// The registry of accounts. Owns id allocation for accounts it holds.
@@ -116,6 +125,22 @@ impl ChartOfAccounts {
self.accounts.values()
}
/// Set (or clear) an account's tax category and deductible percent.
pub fn set_tax(
&mut self,
id: AccountId,
category: Option<String>,
deductible_percent: Option<Decimal>,
) -> Result<(), LedgerError> {
let a = self
.accounts
.get_mut(&id)
.ok_or(LedgerError::UnknownAccount(id))?;
a.tax_category = category;
a.deductible_percent = deductible_percent;
Ok(())
}
/// Number of accounts in the chart.
pub fn len(&self) -> usize {
self.accounts.len()