check / check (push) Canceled after 0s
- Starter charts put "Platform and Payment Processing Fees" at 6100, the
code payroll posts wages to, so a template company that ran payroll
would book its wages as processing fees; their own payroll accounts
(7000, 7050, 2300) were never used. Now 6100 Salary & Wage Expense and
6110 Payroll Tax Expense (the engine's control accounts, with tax
categories), fees at 6060, and the unused accounts gone (the
withholding payables 2110-2150 are created when payroll first runs).
New tests: every template imports with categories valid for its
return; no template puts another account at a control code; a
partnership names every partner and its shares sum to 100%.
- format_amount printed a negative zero ("(0.00)" for -0.004); it rounds
half away from zero first now, like amount_parts.
- Specs (GOAL/SOURCE/INVARIANTS) for statement finishing, P&L, balance
sheet, cash flow, trial balance, members' equity (each must foot as
printed), format_amount, and the templates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
109 lines
4.0 KiB
Rust
109 lines
4.0 KiB
Rust
//! The starter charts, checked against the engine they'll run in
|
|
//! (templates.rs INVARIANTS 1-3).
|
|
|
|
use ledger_core::tax::{TaxForm, check_category};
|
|
use ledger_core::{
|
|
AccountType, ControlAccount, Currency, GENERAL_EXPENSES, Ledger, PAYABLES, PAYROLL,
|
|
RECEIVABLES, RETAINED_EARNINGS, REVENUE,
|
|
};
|
|
use ledger_import::{EntityType, chart_template};
|
|
|
|
fn ty(s: &str) -> AccountType {
|
|
match s {
|
|
"asset" => AccountType::Asset,
|
|
"liability" => AccountType::Liability,
|
|
"equity" => AccountType::Equity,
|
|
"revenue" => AccountType::Revenue,
|
|
"expense" => AccountType::Expense,
|
|
other => panic!("bad type {other}"),
|
|
}
|
|
}
|
|
|
|
const ENTITIES: [(EntityType, TaxForm); 4] = [
|
|
(EntityType::SoleProprietorship, TaxForm::ScheduleC),
|
|
(EntityType::Partnership, TaxForm::Form1065),
|
|
(EntityType::SCorporation, TaxForm::Form1120S),
|
|
(EntityType::CCorporation, TaxForm::Form1120),
|
|
];
|
|
|
|
/// INVARIANT 1: every template imports into fresh books, every tax
|
|
/// category valid for the entity's return.
|
|
#[test]
|
|
fn every_template_imports_with_categories_valid_for_its_return() {
|
|
let owners = ["Ada".to_string(), "Grace".to_string(), "Linus".to_string()];
|
|
for (entity, form) in ENTITIES {
|
|
let t = chart_template(entity, &owners).unwrap();
|
|
let mut l = Ledger::new(Currency::USD);
|
|
for r in &t.rows {
|
|
let parent = r
|
|
.parent_code
|
|
.as_ref()
|
|
.map(|p| l.chart().find_by_code(p).expect("parent first").id);
|
|
let id = l
|
|
.add_account(&r.code, &r.name, ty(&r.account_type), parent, false)
|
|
.unwrap_or_else(|e| panic!("{entity:?} {}: {e}", r.code));
|
|
if let Some(cat) = &r.tax_category {
|
|
check_category(cat, ty(&r.account_type), Some(form))
|
|
.unwrap_or_else(|e| panic!("{entity:?} {} {cat}: {e}", r.code));
|
|
l.set_account_tax(id, Some(cat), None).unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// INVARIANT 2: an account at a control account's code is that control
|
|
/// account: same type and name (the engine posts by code).
|
|
#[test]
|
|
fn no_template_puts_another_account_at_a_control_code() {
|
|
let controls: Vec<&ControlAccount> = [
|
|
&RECEIVABLES,
|
|
&REVENUE,
|
|
&PAYABLES,
|
|
&GENERAL_EXPENSES,
|
|
&RETAINED_EARNINGS,
|
|
]
|
|
.into_iter()
|
|
.chain(PAYROLL.iter())
|
|
.collect();
|
|
let owners = ["Ada".to_string(), "Grace".to_string()];
|
|
for (entity, _) in ENTITIES {
|
|
let t = chart_template(entity, &owners).unwrap();
|
|
for c in &controls {
|
|
if let Some(r) = t.rows.iter().find(|r| r.code == c.code) {
|
|
assert_eq!(ty(&r.account_type), c.account_type, "{entity:?} {}", c.code);
|
|
// Revenue's control is named generically; a template may
|
|
// call its 4000 "Sales". Every other control keeps its name.
|
|
if c.code != REVENUE.code {
|
|
assert_eq!(r.name, c.name, "{entity:?} {}", c.code);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// INVARIANT 3: a partnership has a capital and a distributions account
|
|
/// per partner, and shares summing to exactly 100%.
|
|
#[test]
|
|
fn a_partnership_template_names_every_partner_and_shares_sum_to_100() {
|
|
let owners: Vec<String> = ["A", "B", "C"].iter().map(|s| s.to_string()).collect();
|
|
let t = chart_template(EntityType::Partnership, &owners).unwrap();
|
|
for name in &owners {
|
|
assert!(t.rows.iter().any(|r| r.name == format!("Capital — {name}")));
|
|
assert!(
|
|
t.rows
|
|
.iter()
|
|
.any(|r| r.name == format!("Distributions — {name}"))
|
|
);
|
|
}
|
|
let total: rust_decimal::Decimal = t
|
|
.settings_toml
|
|
.lines()
|
|
.filter_map(|l| {
|
|
l.strip_prefix("share = \"")
|
|
.and_then(|s| s.strip_suffix("%\""))
|
|
})
|
|
.map(|s| s.parse::<rust_decimal::Decimal>().unwrap())
|
|
.sum();
|
|
assert_eq!(total, rust_decimal::Decimal::from(100));
|
|
}
|