Fixed assets carry federal tax basis beside book value; tests sweep stale temp dirs
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
- core: TaxBasisRule (cost IRC 1012, carryover IRC 723, personal-use conversion Treas. Reg. 1.168(i)-4(b)), TaxBasis with the contributor's adjusted basis, Asset::tax_basis and ::built_in_gain (IRC 704(c)); the register gains tax rule, contributor basis, tax basis, built-in gain, total tax basis, missing count (rules.md B-25; sources vendored in docs/spec/sources/irs-partnership-basis.md) - store: asset.tax_basis_set journaled, replayed, projected (schema v10) - mcp: books.set_asset_tax_basis; register_asset takes tax_basis_rule and contributor_basis, checked before anything registers (found by the spec-blind suite); list_assets and the register export (json, csv) carry tax basis; instructions say to record it for every asset - import: the register's XLSX and PDF gain the federal tax basis table with citations - viewer: tax basis on the Assets page, the asset page (a Tax basis card), the report, the journal, and Needs attention for assets without one - just: check and the mutation recipes sweep this user's test book dirs older than an hour (killed runs had left 23 GB in RAM-backed /tmp) - docs: the parties, attachments, TIN vault, completeness engine, and updates designs (docs/parties-and-attachments.md) - tests: spec-blind tax basis suites in core, store, MCP; register PDF/XLSX and viewer tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
49c49aef33
commit
6e6f9d65b2
30 files changed
+3240
-93
No files matched your search
@@ -80,6 +80,118 @@ pub struct Asset {
|
||||
/// ([`crate::Ledger::set_asset_acquisition`]).
|
||||
#[serde(default)]
|
||||
pub acquired_by: Option<crate::EntryId>,
|
||||
/// Its federal tax basis, if recorded ([`crate::Ledger::set_asset_tax_basis`]).
|
||||
/// Book cost is [`Asset::cost`]; tax basis can differ (a member's
|
||||
/// contribution carries the member's basis, not its market value).
|
||||
#[serde(default)]
|
||||
pub tax: Option<TaxBasis>,
|
||||
}
|
||||
|
||||
/// Which rule sets an asset's federal tax basis.
|
||||
///
|
||||
/// ```text
|
||||
/// SOURCE: docs/spec/sources/irs-partnership-basis.md: IRC §1012(a) (cost),
|
||||
/// IRC §723 (property a partner contributes keeps the partner's
|
||||
/// adjusted basis), Treas. Reg. §1.168(i)-4(b) (property converted
|
||||
/// from personal use depreciates from the lesser of its fair market
|
||||
/// value and its adjusted basis), IRC §704(c)(1)(C) (built-in loss).
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaxBasisRule {
|
||||
/// Bought by the company: its cost (IRC §1012(a)), the book cost.
|
||||
Cost,
|
||||
/// Contributed by a member from business use: the member's adjusted
|
||||
/// basis carries over (IRC §723).
|
||||
Carryover,
|
||||
/// Contributed by a member from personal use: depreciable basis is the
|
||||
/// lesser of fair market value (the book cost) and the member's
|
||||
/// adjusted basis (Treas. Reg. §1.168(i)-4(b), with IRC §723).
|
||||
PersonalUseConversion,
|
||||
}
|
||||
|
||||
impl TaxBasisRule {
|
||||
/// Its wire name: `cost`, `carryover`, `personal_use_conversion`.
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
TaxBasisRule::Cost => "cost",
|
||||
TaxBasisRule::Carryover => "carryover",
|
||||
TaxBasisRule::PersonalUseConversion => "personal_use_conversion",
|
||||
}
|
||||
}
|
||||
|
||||
/// The rule named `key`.
|
||||
pub fn parse(key: &str) -> Option<Self> {
|
||||
[Self::Cost, Self::Carryover, Self::PersonalUseConversion]
|
||||
.into_iter()
|
||||
.find(|r| r.key() == key)
|
||||
}
|
||||
}
|
||||
|
||||
/// An asset's federal tax basis: the rule and, for a member's
|
||||
/// contribution, the member's adjusted basis at the time.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TaxBasis {
|
||||
/// The rule.
|
||||
pub rule: TaxBasisRule,
|
||||
/// The contributing member's adjusted basis at contribution (cost less
|
||||
/// any depreciation they took). `None` for [`TaxBasisRule::Cost`].
|
||||
pub contributor_basis: Option<Decimal>,
|
||||
}
|
||||
|
||||
impl TaxBasis {
|
||||
/// Whether the record fits its rule: Cost takes no contributor basis;
|
||||
/// Carryover and PersonalUseConversion require a non-negative one in
|
||||
/// cents at most. Returns why not. Needs no asset, so a caller can check
|
||||
/// before registering one.
|
||||
pub fn check(&self) -> Result<(), String> {
|
||||
use TaxBasisRule::*;
|
||||
match (self.rule, self.contributor_basis) {
|
||||
(Cost, Some(_)) => {
|
||||
Err("a purchase's tax basis is its cost; give no contributor basis".into())
|
||||
}
|
||||
(Carryover | PersonalUseConversion, None) => {
|
||||
Err("a contribution needs the contributing member's adjusted basis".into())
|
||||
}
|
||||
(_, Some(b)) if b.is_sign_negative() && !b.is_zero() => {
|
||||
Err("a basis can't be negative".into())
|
||||
}
|
||||
(_, Some(b)) if b.normalize().scale() > 2 => Err("a basis is in cents at most".into()),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
/// Its depreciable federal tax basis, by its rule, or `None` if not
|
||||
/// recorded.
|
||||
///
|
||||
/// ```text
|
||||
/// INVARIANTS:
|
||||
/// 1. Cost: the book cost.
|
||||
/// 2. Carryover: the contributor's adjusted basis.
|
||||
/// 3. PersonalUseConversion: min(book cost, contributor's basis); the
|
||||
/// book cost of a contribution is its fair market value.
|
||||
/// ```
|
||||
pub fn tax_basis(&self) -> Option<Decimal> {
|
||||
let t = self.tax?;
|
||||
Some(match (t.rule, t.contributor_basis) {
|
||||
(TaxBasisRule::Cost, _) => self.cost.amount,
|
||||
(TaxBasisRule::Carryover, Some(b)) => b,
|
||||
(TaxBasisRule::PersonalUseConversion, Some(b)) => b.min(self.cost.amount),
|
||||
(_, None) => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Built-in gain (positive) or loss (negative) at contribution, for
|
||||
/// IRC §704(c): fair market value (the book cost) less the contributor's
|
||||
/// adjusted basis. `None` for a purchase or when not recorded. A
|
||||
/// built-in loss counts only for the contributing member
|
||||
/// (§704(c)(1)(C)).
|
||||
pub fn built_in_gain(&self) -> Option<Decimal> {
|
||||
let b = self.tax?.contributor_basis?;
|
||||
Some(self.cost.amount - b)
|
||||
}
|
||||
}
|
||||
|
||||
/// The longest asset tag.
|
||||
|
||||
@@ -147,6 +147,11 @@ pub enum LedgerError {
|
||||
why: String,
|
||||
},
|
||||
|
||||
/// An asset's tax basis record doesn't fit its rule (see
|
||||
/// `Ledger::set_asset_tax_basis`).
|
||||
#[error("bad tax basis: {0}")]
|
||||
InvalidTaxBasis(String),
|
||||
|
||||
/// A serial number of the wrong form (see `assets::check_serial`).
|
||||
#[error("bad serial number: {0}")]
|
||||
InvalidSerial(String),
|
||||
|
||||
@@ -1459,6 +1459,7 @@ impl Ledger {
|
||||
tag: None,
|
||||
serial: None,
|
||||
acquired_by: None,
|
||||
tax: None,
|
||||
},
|
||||
);
|
||||
Ok(id)
|
||||
@@ -1656,6 +1657,47 @@ impl Ledger {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record an asset's federal tax basis: the rule, and for a member's
|
||||
/// contribution the member's adjusted basis. Book figures don't change.
|
||||
///
|
||||
/// ```text
|
||||
/// GOAL: the register carries tax basis beside book value, so book and
|
||||
/// tax never get confused and the CPA sees both.
|
||||
/// SOURCE: docs/spec/rules.md B-25; docs/spec/sources/
|
||||
/// irs-partnership-basis.md.
|
||||
/// INVARIANTS:
|
||||
/// 1. Cost takes no contributor basis; Carryover and
|
||||
/// PersonalUseConversion require one.
|
||||
/// 2. A contributor basis is non-negative, in cents at most.
|
||||
/// 3. A refusal MUST leave the books unchanged.
|
||||
/// SCENARIOS:
|
||||
/// 1. IF the asset is unknown, or 1 or 2 fails: SHOULD refuse with
|
||||
/// UnknownAsset or InvalidTaxBasis.
|
||||
/// 2. IF set again: the new record replaces the old (the journal
|
||||
/// keeps both).
|
||||
/// ```
|
||||
pub fn set_asset_tax_basis(
|
||||
&mut self,
|
||||
id: AssetId,
|
||||
tax: assets::TaxBasis,
|
||||
) -> Result<(), LedgerError> {
|
||||
self.check_asset_tax_basis(id, tax)?;
|
||||
self.assets.get_mut(&id).expect("checked").tax = Some(tax);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The validation behind [`Self::set_asset_tax_basis`].
|
||||
pub fn check_asset_tax_basis(
|
||||
&self,
|
||||
id: AssetId,
|
||||
tax: assets::TaxBasis,
|
||||
) -> Result<(), LedgerError> {
|
||||
if !self.assets.contains_key(&id) {
|
||||
return Err(LedgerError::UnknownAsset(id));
|
||||
}
|
||||
tax.check().map_err(LedgerError::InvalidTaxBasis)
|
||||
}
|
||||
|
||||
/// The asset whose current tag is `tag`, ignoring case.
|
||||
pub fn asset_by_tag(&self, tag: &str) -> Option<&Asset> {
|
||||
let id = self.asset_tags.get(&assets::tag_key(tag))?;
|
||||
|
||||
@@ -44,7 +44,8 @@ pub use arap::{
|
||||
};
|
||||
pub use assets::{
|
||||
Asset, AssetId, AssetStatus, DepreciationMethod, DisposalAmounts, NewAsset, ScheduleLine,
|
||||
accumulated_depreciation, book_value, depreciation_schedule, disposal_amounts,
|
||||
TaxBasis, TaxBasisRule, accumulated_depreciation, book_value, depreciation_schedule,
|
||||
disposal_amounts,
|
||||
};
|
||||
pub use classes::{Class, ClassId};
|
||||
pub use error::LedgerError;
|
||||
|
||||
@@ -1556,6 +1556,15 @@ pub struct AssetRegisterRow {
|
||||
pub book_value: Decimal,
|
||||
/// Disposal date and proceeds, if disposed by the date.
|
||||
pub disposed: Option<(NaiveDate, Decimal)>,
|
||||
/// The federal tax basis rule, if recorded.
|
||||
pub tax_rule: Option<crate::TaxBasisRule>,
|
||||
/// The contributing member's adjusted basis (a contribution only).
|
||||
pub contributor_basis: Option<Decimal>,
|
||||
/// The depreciable federal tax basis (`Asset::tax_basis`), if recorded.
|
||||
pub tax_basis: Option<Decimal>,
|
||||
/// Built-in gain (+) or loss (−) at contribution, IRC §704(c)
|
||||
/// (`Asset::built_in_gain`).
|
||||
pub built_in_gain: Option<Decimal>,
|
||||
}
|
||||
|
||||
/// The fixed asset register as of a date, reconciled to the general ledger.
|
||||
@@ -1572,6 +1581,11 @@ pub struct AssetRegister {
|
||||
pub total_accumulated: Decimal,
|
||||
/// Their book value.
|
||||
pub total_book_value: Decimal,
|
||||
/// The total depreciable tax basis of the assets still held that have
|
||||
/// one recorded.
|
||||
pub total_tax_basis: Decimal,
|
||||
/// How many assets still held have no tax basis recorded.
|
||||
pub missing_tax_basis: usize,
|
||||
/// The general ledger's balance (debit positive) of the assets' cost
|
||||
/// accounts on the date.
|
||||
pub ledger_cost: Decimal,
|
||||
@@ -1611,7 +1625,11 @@ impl AssetRegister {
|
||||
/// 2. accumulated is the asset's schedule through as_of
|
||||
/// (assets::accumulated_depreciation); book_value = cost −
|
||||
/// accumulated.
|
||||
/// 3. Totals are the sums over the held assets, exactly.
|
||||
/// 3. Totals are the sums over the held assets, exactly; tax basis
|
||||
/// totals over those with one recorded, and missing_tax_basis counts
|
||||
/// the rest.
|
||||
/// 5. Each row's tax fields are the asset's (Asset::tax_basis,
|
||||
/// Asset::built_in_gain), never re-derived here.
|
||||
/// 4. ledger_cost and ledger_accumulated are the general ledger's
|
||||
/// balances on as_of of the distinct cost and accumulated
|
||||
/// depreciation accounts of the registered assets, so
|
||||
@@ -1622,6 +1640,7 @@ pub fn fixed_asset_register(ledger: &Ledger, as_of: NaiveDate) -> AssetRegister
|
||||
use crate::AssetStatus;
|
||||
let mut rows = Vec::new();
|
||||
let (mut cost, mut accumulated) = (Decimal::ZERO, Decimal::ZERO);
|
||||
let (mut tax_total, mut tax_missing) = (Decimal::ZERO, 0usize);
|
||||
let mut cost_accounts = std::collections::BTreeSet::new();
|
||||
let mut accum_accounts = std::collections::BTreeSet::new();
|
||||
for a in ledger
|
||||
@@ -1644,6 +1663,10 @@ pub fn fixed_asset_register(ledger: &Ledger, as_of: NaiveDate) -> AssetRegister
|
||||
if disposed.is_none() {
|
||||
cost += a.cost.amount;
|
||||
accumulated += acc;
|
||||
match a.tax_basis() {
|
||||
Some(t) => tax_total += t,
|
||||
None => tax_missing += 1,
|
||||
}
|
||||
}
|
||||
rows.push(AssetRegisterRow {
|
||||
asset_id: a.id,
|
||||
@@ -1661,6 +1684,10 @@ pub fn fixed_asset_register(ledger: &Ledger, as_of: NaiveDate) -> AssetRegister
|
||||
a.cost.amount - acc
|
||||
},
|
||||
disposed,
|
||||
tax_rule: a.tax.map(|t| t.rule),
|
||||
contributor_basis: a.tax.and_then(|t| t.contributor_basis),
|
||||
tax_basis: a.tax_basis(),
|
||||
built_in_gain: a.built_in_gain(),
|
||||
});
|
||||
}
|
||||
let tb = trial_balance_as_of(ledger, as_of);
|
||||
@@ -1681,6 +1708,8 @@ pub fn fixed_asset_register(ledger: &Ledger, as_of: NaiveDate) -> AssetRegister
|
||||
total_cost: cost,
|
||||
total_accumulated: accumulated,
|
||||
total_book_value: cost - accumulated,
|
||||
total_tax_basis: tax_total,
|
||||
missing_tax_basis: tax_missing,
|
||||
ledger_cost: cost_accounts.iter().map(|&id| net(id)).sum(),
|
||||
ledger_accumulated: -accum_accounts.iter().map(|&id| net(id)).sum::<Decimal>(),
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ fn asset(cost: &str, salvage: &str, life: u32, purchase: NaiveDate) -> Asset {
|
||||
tag: None,
|
||||
serial: None,
|
||||
acquired_by: None,
|
||||
tax: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,693 @@
|
||||
//! Spec-derived tests for an asset's federal tax basis (rules.md B-25):
|
||||
//! `TaxBasisRule`, `Ledger::set_asset_tax_basis` / `check_asset_tax_basis`,
|
||||
//! `Asset::tax_basis`, `Asset::built_in_gain`, and the tax fields of
|
||||
//! `reports::fixed_asset_register`.
|
||||
//!
|
||||
//! Every expected value comes from the written specification: the doc
|
||||
//! comments and INVARIANTS/SCENARIOS blocks on those items, and the
|
||||
//! vendored sources (`docs/spec/sources/irs-partnership-basis.md`): cost
|
||||
//! basis (IRC §1012(a)), carryover of the contributor's adjusted basis
|
||||
//! (IRC §723), the lesser of fair market value and adjusted basis on a
|
||||
//! conversion from personal use (Treas. Reg. §1.168(i)-4(b)(1)), and
|
||||
//! built-in gain or loss as fair market value less the contributor's basis
|
||||
//! (IRC §704(c)). Amounts are worked by hand.
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::{
|
||||
AccountId, AccountType, AssetId, Currency, Ledger, LedgerError, Money, NewAsset, Side,
|
||||
TaxBasis, TaxBasisRule, fixed_asset_register,
|
||||
};
|
||||
use proptest::prelude::*;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
// ---------------------------------------------------------------- helpers
|
||||
|
||||
fn d(s: &str) -> Decimal {
|
||||
Decimal::from_str(s).expect("decimal literal")
|
||||
}
|
||||
|
||||
fn usd(s: &str) -> Money {
|
||||
Money::new(d(s), Currency::USD)
|
||||
}
|
||||
|
||||
fn date(y: i32, m: u32, day: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(y, m, day).expect("valid date")
|
||||
}
|
||||
|
||||
struct Accts {
|
||||
equipment: AccountId,
|
||||
accum: AccountId,
|
||||
depr_expense: AccountId,
|
||||
}
|
||||
|
||||
/// A USD ledger with a fixed-asset chart, funded with $50,000 cash.
|
||||
fn fixture() -> (Ledger, Accts) {
|
||||
let mut l = Ledger::new(Currency::USD);
|
||||
let mut acct = |code: &str, name: &str, ty: AccountType| {
|
||||
l.add_account(code, name, ty, None, false).unwrap()
|
||||
};
|
||||
let cash = acct("1000", "Cash", AccountType::Asset);
|
||||
let equipment = acct("1500", "Equipment", AccountType::Asset);
|
||||
let accum = acct("1510", "Accumulated Depreciation", AccountType::Asset);
|
||||
let capital = acct("3000", "Owner Capital", AccountType::Equity);
|
||||
let depr_expense = acct("6200", "Depreciation Expense", AccountType::Expense);
|
||||
let e = l.try_create_entry(date(2026, 1, 1), "funding").unwrap();
|
||||
l.add_line(e, cash, Side::Debit, usd("50000.00")).unwrap();
|
||||
l.add_line(e, capital, Side::Credit, usd("50000.00"))
|
||||
.unwrap();
|
||||
l.post_entry(e).unwrap();
|
||||
(
|
||||
l,
|
||||
Accts {
|
||||
equipment,
|
||||
accum,
|
||||
depr_expense,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn register(l: &mut Ledger, a: &Accts, description: &str, cost: &str) -> AssetId {
|
||||
l.register_asset(NewAsset {
|
||||
description: description.to_string(),
|
||||
purchase_date: date(2026, 1, 1),
|
||||
cost: usd(cost),
|
||||
salvage_value: usd("0.00"),
|
||||
useful_life_months: 36,
|
||||
cost_account: a.equipment,
|
||||
accum_depr_account: a.accum,
|
||||
expense_account: a.depr_expense,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn tb(rule: TaxBasisRule, basis: Option<&str>) -> TaxBasis {
|
||||
TaxBasis {
|
||||
rule,
|
||||
contributor_basis: basis.map(d),
|
||||
}
|
||||
}
|
||||
|
||||
/// A ledger with one asset of book cost (fair market value) `cost`.
|
||||
fn one(cost: &str) -> (Ledger, AssetId) {
|
||||
let (mut l, a) = fixture();
|
||||
let id = register(&mut l, &a, "Laptop", cost);
|
||||
(l, id)
|
||||
}
|
||||
|
||||
fn set(l: &mut Ledger, id: AssetId, rule: TaxBasisRule, basis: Option<&str>) {
|
||||
l.set_asset_tax_basis(id, tb(rule, basis)).unwrap();
|
||||
}
|
||||
|
||||
const RULES: [TaxBasisRule; 3] = [
|
||||
TaxBasisRule::Cost,
|
||||
TaxBasisRule::Carryover,
|
||||
TaxBasisRule::PersonalUseConversion,
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------- TaxBasisRule
|
||||
|
||||
/// TaxBasisRule::key: the wire names `cost`, `carryover`,
|
||||
/// `personal_use_conversion`.
|
||||
#[test]
|
||||
fn rule_keys_are_the_documented_wire_names() {
|
||||
assert_eq!(TaxBasisRule::Cost.key(), "cost");
|
||||
assert_eq!(TaxBasisRule::Carryover.key(), "carryover");
|
||||
assert_eq!(
|
||||
TaxBasisRule::PersonalUseConversion.key(),
|
||||
"personal_use_conversion"
|
||||
);
|
||||
}
|
||||
|
||||
/// TaxBasisRule::parse: the rule named `key`; round-trips with key().
|
||||
#[test]
|
||||
fn rule_parse_round_trips_with_key() {
|
||||
for r in RULES {
|
||||
assert_eq!(TaxBasisRule::parse(r.key()), Some(r));
|
||||
}
|
||||
}
|
||||
|
||||
/// Anything other than a documented wire name is no rule.
|
||||
#[test]
|
||||
fn rule_parse_refuses_other_names() {
|
||||
for bad in [
|
||||
"",
|
||||
"Cost",
|
||||
"COST",
|
||||
" cost",
|
||||
"cost ",
|
||||
"carry_over",
|
||||
"personal-use-conversion",
|
||||
"PersonalUseConversion",
|
||||
"fmv",
|
||||
] {
|
||||
assert_eq!(TaxBasisRule::parse(bad), None, "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The serde names are snake_case, the same as the wire names.
|
||||
#[test]
|
||||
fn rule_serde_names_are_the_wire_names() {
|
||||
for r in RULES {
|
||||
let j = serde_json::to_value(r).unwrap();
|
||||
assert_eq!(j, serde_json::Value::String(r.key().to_string()));
|
||||
let back: TaxBasisRule = serde_json::from_value(j).unwrap();
|
||||
assert_eq!(back, r);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- set / check
|
||||
|
||||
/// SCENARIO 1: an unknown asset is refused with UnknownAsset.
|
||||
#[test]
|
||||
fn an_unknown_asset_is_refused() {
|
||||
let (mut l, id) = one("1000.00");
|
||||
let before = l.clone();
|
||||
let unknown = id + 99;
|
||||
for t in [
|
||||
tb(TaxBasisRule::Cost, None),
|
||||
tb(TaxBasisRule::Carryover, Some("500.00")),
|
||||
] {
|
||||
assert!(matches!(
|
||||
l.check_asset_tax_basis(unknown, t),
|
||||
Err(LedgerError::UnknownAsset(x)) if x == unknown
|
||||
));
|
||||
assert!(matches!(
|
||||
l.set_asset_tax_basis(unknown, t),
|
||||
Err(LedgerError::UnknownAsset(x)) if x == unknown
|
||||
));
|
||||
}
|
||||
assert_eq!(l, before, "INVARIANT 3");
|
||||
}
|
||||
|
||||
/// INVARIANT 1: Cost takes no contributor basis.
|
||||
#[test]
|
||||
fn cost_with_a_contributor_basis_is_refused() {
|
||||
let (mut l, id) = one("1000.00");
|
||||
let before = l.clone();
|
||||
for b in ["0.00", "500.00", "1000.00"] {
|
||||
let t = tb(TaxBasisRule::Cost, Some(b));
|
||||
assert!(matches!(
|
||||
l.check_asset_tax_basis(id, t),
|
||||
Err(LedgerError::InvalidTaxBasis(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
l.set_asset_tax_basis(id, t),
|
||||
Err(LedgerError::InvalidTaxBasis(_))
|
||||
));
|
||||
}
|
||||
assert_eq!(l, before, "INVARIANT 3");
|
||||
assert_eq!(l.get_asset(id).unwrap().tax, None);
|
||||
}
|
||||
|
||||
/// INVARIANT 1: Carryover and PersonalUseConversion require one.
|
||||
#[test]
|
||||
fn a_contribution_without_a_contributor_basis_is_refused() {
|
||||
let (mut l, id) = one("1000.00");
|
||||
let before = l.clone();
|
||||
for r in [TaxBasisRule::Carryover, TaxBasisRule::PersonalUseConversion] {
|
||||
let t = tb(r, None);
|
||||
assert!(matches!(
|
||||
l.check_asset_tax_basis(id, t),
|
||||
Err(LedgerError::InvalidTaxBasis(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
l.set_asset_tax_basis(id, t),
|
||||
Err(LedgerError::InvalidTaxBasis(_))
|
||||
));
|
||||
}
|
||||
assert_eq!(l, before, "INVARIANT 3");
|
||||
}
|
||||
|
||||
/// INVARIANT 2: a contributor basis is non-negative.
|
||||
#[test]
|
||||
fn a_negative_contributor_basis_is_refused() {
|
||||
let (mut l, id) = one("1000.00");
|
||||
let before = l.clone();
|
||||
for r in [TaxBasisRule::Carryover, TaxBasisRule::PersonalUseConversion] {
|
||||
for b in ["-0.01", "-1", "-500.00"] {
|
||||
let t = tb(r, Some(b));
|
||||
assert!(
|
||||
matches!(
|
||||
l.set_asset_tax_basis(id, t),
|
||||
Err(LedgerError::InvalidTaxBasis(_))
|
||||
),
|
||||
"{r:?} {b}"
|
||||
);
|
||||
assert!(l.check_asset_tax_basis(id, t).is_err());
|
||||
}
|
||||
}
|
||||
assert_eq!(l, before, "INVARIANT 3");
|
||||
}
|
||||
|
||||
/// INVARIANT 2: in cents at most.
|
||||
#[test]
|
||||
fn more_than_two_decimal_places_is_refused() {
|
||||
let (mut l, id) = one("1000.00");
|
||||
let before = l.clone();
|
||||
for r in [TaxBasisRule::Carryover, TaxBasisRule::PersonalUseConversion] {
|
||||
for b in ["500.001", "0.005", "1.2345"] {
|
||||
let t = tb(r, Some(b));
|
||||
assert!(
|
||||
matches!(
|
||||
l.set_asset_tax_basis(id, t),
|
||||
Err(LedgerError::InvalidTaxBasis(_))
|
||||
),
|
||||
"{r:?} {b}"
|
||||
);
|
||||
assert!(l.check_asset_tax_basis(id, t).is_err());
|
||||
}
|
||||
}
|
||||
assert_eq!(l, before, "INVARIANT 3");
|
||||
}
|
||||
|
||||
/// Valid records: zero is non-negative; whole dollars and one decimal
|
||||
/// place are "in cents at most". check agrees with set, and a valid set
|
||||
/// changes nothing but the asset's tax record (book figures don't change).
|
||||
#[test]
|
||||
fn valid_records_are_accepted_and_book_figures_stay() {
|
||||
for (r, b) in [
|
||||
(TaxBasisRule::Cost, None),
|
||||
(TaxBasisRule::Carryover, Some("0")),
|
||||
(TaxBasisRule::Carryover, Some("0.00")),
|
||||
(TaxBasisRule::Carryover, Some("750")),
|
||||
(TaxBasisRule::PersonalUseConversion, Some("750.5")),
|
||||
(TaxBasisRule::PersonalUseConversion, Some("750.50")),
|
||||
(TaxBasisRule::PersonalUseConversion, Some("2500.00")),
|
||||
] {
|
||||
let (mut l, id) = one("1000.00");
|
||||
let t = tb(r, b);
|
||||
l.check_asset_tax_basis(id, t).unwrap();
|
||||
let before = l.get_asset(id).unwrap().clone();
|
||||
l.set_asset_tax_basis(id, t).unwrap();
|
||||
let after = l.get_asset(id).unwrap();
|
||||
assert_eq!(after.tax, Some(t), "{r:?} {b:?}");
|
||||
// Book figures don't change.
|
||||
let mut same = after.clone();
|
||||
same.tax = before.tax;
|
||||
assert_eq!(same, before, "{r:?} {b:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// SCENARIO 2: set again, the new record replaces the old.
|
||||
#[test]
|
||||
fn setting_again_replaces() {
|
||||
let (mut l, id) = one("1000.00");
|
||||
set(&mut l, id, TaxBasisRule::Cost, None);
|
||||
assert_eq!(l.get_asset(id).unwrap().tax_basis(), Some(d("1000.00")));
|
||||
set(&mut l, id, TaxBasisRule::Carryover, Some("400.00"));
|
||||
let a = l.get_asset(id).unwrap();
|
||||
assert_eq!(a.tax, Some(tb(TaxBasisRule::Carryover, Some("400.00"))));
|
||||
assert_eq!(a.tax_basis(), Some(d("400.00")));
|
||||
set(
|
||||
&mut l,
|
||||
id,
|
||||
TaxBasisRule::PersonalUseConversion,
|
||||
Some("1200.00"),
|
||||
);
|
||||
let a = l.get_asset(id).unwrap();
|
||||
assert_eq!(
|
||||
a.tax,
|
||||
Some(tb(TaxBasisRule::PersonalUseConversion, Some("1200.00")))
|
||||
);
|
||||
assert_eq!(a.tax_basis(), Some(d("1000.00")));
|
||||
set(&mut l, id, TaxBasisRule::Cost, None);
|
||||
let a = l.get_asset(id).unwrap();
|
||||
assert_eq!(a.tax, Some(tb(TaxBasisRule::Cost, None)));
|
||||
assert_eq!(a.built_in_gain(), None);
|
||||
}
|
||||
|
||||
/// INVARIANT 3: a refusal after a valid record leaves that record.
|
||||
#[test]
|
||||
fn a_refusal_keeps_the_earlier_record() {
|
||||
let (mut l, id) = one("1000.00");
|
||||
set(&mut l, id, TaxBasisRule::Carryover, Some("300.00"));
|
||||
let before = l.clone();
|
||||
assert!(
|
||||
l.set_asset_tax_basis(id, tb(TaxBasisRule::Cost, Some("1.00")))
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
l.set_asset_tax_basis(id, tb(TaxBasisRule::Carryover, None))
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
l.set_asset_tax_basis(id, tb(TaxBasisRule::Carryover, Some("-1.00")))
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
l.set_asset_tax_basis(id, tb(TaxBasisRule::Carryover, Some("1.001")))
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(l, before);
|
||||
assert_eq!(l.get_asset(id).unwrap().tax_basis(), Some(d("300.00")));
|
||||
}
|
||||
|
||||
/// Setting one asset's record leaves the others alone.
|
||||
#[test]
|
||||
fn a_record_belongs_to_one_asset() {
|
||||
let (mut l, a) = fixture();
|
||||
let x = register(&mut l, &a, "Laptop", "1000.00");
|
||||
let y = register(&mut l, &a, "Desk", "400.00");
|
||||
set(&mut l, x, TaxBasisRule::Carryover, Some("800.00"));
|
||||
assert_eq!(l.get_asset(y).unwrap().tax, None);
|
||||
assert_eq!(l.get_asset(y).unwrap().tax_basis(), None);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- tax_basis
|
||||
|
||||
/// Not recorded: None (and no built-in gain).
|
||||
#[test]
|
||||
fn no_record_means_no_tax_basis() {
|
||||
let (l, id) = one("1000.00");
|
||||
let a = l.get_asset(id).unwrap();
|
||||
assert_eq!(a.tax, None);
|
||||
assert_eq!(a.tax_basis(), None);
|
||||
assert_eq!(a.built_in_gain(), None);
|
||||
}
|
||||
|
||||
/// INVARIANT 1: Cost: the book cost (IRC §1012(a)).
|
||||
#[test]
|
||||
fn cost_basis_is_the_book_cost() {
|
||||
let (mut l, id) = one("1234.56");
|
||||
set(&mut l, id, TaxBasisRule::Cost, None);
|
||||
let a = l.get_asset(id).unwrap();
|
||||
assert_eq!(a.tax_basis(), Some(d("1234.56")));
|
||||
// A purchase has no built-in gain.
|
||||
assert_eq!(a.built_in_gain(), None);
|
||||
}
|
||||
|
||||
/// INVARIANT 2: Carryover: the contributor's adjusted basis (IRC §723),
|
||||
/// whether above or below fair market value.
|
||||
#[test]
|
||||
fn carryover_basis_is_the_contributors_basis() {
|
||||
for (fmv, basis, gain) in [
|
||||
("1000.00", "600.00", "400.00"), // built-in gain
|
||||
("1000.00", "1500.00", "-500.00"), // built-in loss
|
||||
("1000.00", "1000.00", "0.00"), // boundary
|
||||
("1000.00", "0.00", "1000.00"), // fully depreciated
|
||||
] {
|
||||
let (mut l, id) = one(fmv);
|
||||
set(&mut l, id, TaxBasisRule::Carryover, Some(basis));
|
||||
let a = l.get_asset(id).unwrap();
|
||||
assert_eq!(a.tax_basis(), Some(d(basis)), "{fmv} {basis}");
|
||||
assert_eq!(a.built_in_gain(), Some(d(gain)), "{fmv} {basis}");
|
||||
}
|
||||
}
|
||||
|
||||
/// INVARIANT 3: PersonalUseConversion: min(book cost, contributor's basis)
|
||||
/// (Treas. Reg. §1.168(i)-4(b)(1): "the lesser of its fair market value or
|
||||
/// its adjusted depreciable basis").
|
||||
#[test]
|
||||
fn personal_use_conversion_is_the_lesser_of_value_and_basis() {
|
||||
for (fmv, basis, tax, gain) in [
|
||||
// Paid 2,000, worth 1,200 now: basis 1,200; built-in loss −800.
|
||||
("1200.00", "2000.00", "1200.00", "-800.00"),
|
||||
// Paid 300, worth 450: basis 300; built-in gain 150.
|
||||
("450.00", "300.00", "300.00", "150.00"),
|
||||
// Boundary: value equals basis.
|
||||
("999.99", "999.99", "999.99", "0.00"),
|
||||
// A cent either side of the boundary.
|
||||
("1000.00", "1000.01", "1000.00", "-0.01"),
|
||||
("1000.00", "999.99", "999.99", "0.01"),
|
||||
] {
|
||||
let (mut l, id) = one(fmv);
|
||||
set(&mut l, id, TaxBasisRule::PersonalUseConversion, Some(basis));
|
||||
let a = l.get_asset(id).unwrap();
|
||||
assert_eq!(a.tax_basis(), Some(d(tax)), "{fmv} {basis}");
|
||||
assert_eq!(a.built_in_gain(), Some(d(gain)), "{fmv} {basis}");
|
||||
}
|
||||
}
|
||||
|
||||
/// built_in_gain: FMV (book cost) less the contributor's basis; positive
|
||||
/// for a gain, negative for a loss, zero at FMV == basis, regardless of
|
||||
/// which contribution rule (the definition does not depend on the rule).
|
||||
#[test]
|
||||
fn built_in_gain_sign_and_boundary() {
|
||||
for r in [TaxBasisRule::Carryover, TaxBasisRule::PersonalUseConversion] {
|
||||
let (mut l, id) = one("800.00");
|
||||
set(&mut l, id, r, Some("500.00"));
|
||||
let g = l.get_asset(id).unwrap().built_in_gain().unwrap();
|
||||
assert_eq!(g, d("300.00"));
|
||||
assert!(g > Decimal::ZERO, "{r:?}: FMV > basis is a gain");
|
||||
|
||||
set(&mut l, id, r, Some("1100.00"));
|
||||
let g = l.get_asset(id).unwrap().built_in_gain().unwrap();
|
||||
assert_eq!(g, d("-300.00"));
|
||||
assert!(g < Decimal::ZERO, "{r:?}: basis > FMV is a loss");
|
||||
|
||||
set(&mut l, id, r, Some("800.00"));
|
||||
let g = l.get_asset(id).unwrap().built_in_gain().unwrap();
|
||||
assert!(g.is_zero(), "{r:?}: FMV == basis is neither");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- register
|
||||
|
||||
struct RegisterBooks {
|
||||
l: Ledger,
|
||||
bought: AssetId,
|
||||
carried: AssetId,
|
||||
converted: AssetId,
|
||||
unrecorded: AssetId,
|
||||
sold: AssetId,
|
||||
}
|
||||
|
||||
/// Five assets: a purchase at cost, a carryover contribution, a
|
||||
/// personal-use conversion, one with no tax basis, and one disposed (with
|
||||
/// a tax basis, which the totals must leave out).
|
||||
fn register_books() -> RegisterBooks {
|
||||
let (mut l, a) = fixture();
|
||||
let bought = register(&mut l, &a, "Server", "3000.00");
|
||||
let carried = register(&mut l, &a, "Printer", "500.00");
|
||||
let converted = register(&mut l, &a, "Laptop", "1200.00");
|
||||
let unrecorded = register(&mut l, &a, "Desk", "700.00");
|
||||
let sold = register(&mut l, &a, "Old monitor", "360.00");
|
||||
set(&mut l, bought, TaxBasisRule::Cost, None);
|
||||
set(&mut l, carried, TaxBasisRule::Carryover, Some("650.25"));
|
||||
set(
|
||||
&mut l,
|
||||
converted,
|
||||
TaxBasisRule::PersonalUseConversion,
|
||||
Some("2000.00"),
|
||||
);
|
||||
set(&mut l, sold, TaxBasisRule::Cost, None);
|
||||
l.dispose_asset(sold, date(2026, 3, 31), usd("100.00"))
|
||||
.unwrap();
|
||||
RegisterBooks {
|
||||
l,
|
||||
bought,
|
||||
carried,
|
||||
converted,
|
||||
unrecorded,
|
||||
sold,
|
||||
}
|
||||
}
|
||||
|
||||
/// INVARIANT 5: each row's tax fields are the asset's.
|
||||
#[test]
|
||||
fn register_rows_carry_the_assets_tax_fields() {
|
||||
let b = register_books();
|
||||
let r = fixed_asset_register(&b.l, date(2026, 6, 30));
|
||||
assert_eq!(r.rows.len(), 5);
|
||||
for row in &r.rows {
|
||||
let a = b.l.get_asset(row.asset_id).unwrap();
|
||||
assert_eq!(row.tax_rule, a.tax.map(|t| t.rule));
|
||||
assert_eq!(
|
||||
row.contributor_basis,
|
||||
a.tax.and_then(|t| t.contributor_basis)
|
||||
);
|
||||
assert_eq!(row.tax_basis, a.tax_basis());
|
||||
assert_eq!(row.built_in_gain, a.built_in_gain());
|
||||
}
|
||||
let row = |id| r.rows.iter().find(|x| x.asset_id == id).unwrap();
|
||||
// By hand.
|
||||
let x = row(b.bought);
|
||||
assert_eq!(x.tax_rule, Some(TaxBasisRule::Cost));
|
||||
assert_eq!(x.contributor_basis, None);
|
||||
assert_eq!(x.tax_basis, Some(d("3000.00")));
|
||||
assert_eq!(x.built_in_gain, None);
|
||||
let x = row(b.carried);
|
||||
assert_eq!(x.tax_rule, Some(TaxBasisRule::Carryover));
|
||||
assert_eq!(x.contributor_basis, Some(d("650.25")));
|
||||
assert_eq!(x.tax_basis, Some(d("650.25")));
|
||||
assert_eq!(x.built_in_gain, Some(d("-150.25")));
|
||||
let x = row(b.converted);
|
||||
assert_eq!(x.tax_rule, Some(TaxBasisRule::PersonalUseConversion));
|
||||
assert_eq!(x.contributor_basis, Some(d("2000.00")));
|
||||
assert_eq!(x.tax_basis, Some(d("1200.00")));
|
||||
assert_eq!(x.built_in_gain, Some(d("-800.00")));
|
||||
let x = row(b.unrecorded);
|
||||
assert_eq!(x.tax_rule, None);
|
||||
assert_eq!(x.contributor_basis, None);
|
||||
assert_eq!(x.tax_basis, None);
|
||||
assert_eq!(x.built_in_gain, None);
|
||||
// A disposed asset's row still shows its record.
|
||||
let x = row(b.sold);
|
||||
assert!(x.disposed.is_some());
|
||||
assert_eq!(x.tax_rule, Some(TaxBasisRule::Cost));
|
||||
assert_eq!(x.tax_basis, Some(d("360.00")));
|
||||
}
|
||||
|
||||
/// INVARIANT 3: total_tax_basis sums held assets with a basis, exactly;
|
||||
/// missing_tax_basis counts held ones without; disposed excluded.
|
||||
#[test]
|
||||
fn register_tax_totals_cover_held_assets_only() {
|
||||
let b = register_books();
|
||||
let r = fixed_asset_register(&b.l, date(2026, 6, 30));
|
||||
// 3000.00 + 650.25 + 1200.00 (the disposed 360.00 left out).
|
||||
assert_eq!(r.total_tax_basis, d("4850.25"));
|
||||
assert_eq!(r.missing_tax_basis, 1);
|
||||
// Before the disposal date the old monitor is still held.
|
||||
let r = fixed_asset_register(&b.l, date(2026, 2, 28));
|
||||
assert_eq!(r.total_tax_basis, d("5210.25"));
|
||||
assert_eq!(r.missing_tax_basis, 1);
|
||||
}
|
||||
|
||||
/// A disposed asset without a tax basis isn't counted as missing.
|
||||
#[test]
|
||||
fn a_disposed_asset_without_a_basis_is_not_missing() {
|
||||
let (mut l, a) = fixture();
|
||||
let x = register(&mut l, &a, "Laptop", "1000.00");
|
||||
let y = register(&mut l, &a, "Desk", "400.00");
|
||||
l.dispose_asset(y, date(2026, 3, 31), usd("0.00")).unwrap();
|
||||
let r = fixed_asset_register(&l, date(2026, 6, 30));
|
||||
assert_eq!(r.missing_tax_basis, 1);
|
||||
assert_eq!(r.total_tax_basis, Decimal::ZERO);
|
||||
set(&mut l, x, TaxBasisRule::Carryover, Some("900.00"));
|
||||
let r = fixed_asset_register(&l, date(2026, 6, 30));
|
||||
assert_eq!(r.missing_tax_basis, 0);
|
||||
assert_eq!(r.total_tax_basis, d("900.00"));
|
||||
}
|
||||
|
||||
/// No assets: zero total, none missing.
|
||||
#[test]
|
||||
fn an_empty_register_has_no_tax_basis() {
|
||||
let (l, _) = fixture();
|
||||
let r = fixed_asset_register(&l, date(2026, 6, 30));
|
||||
assert!(r.rows.is_empty());
|
||||
assert_eq!(r.total_tax_basis, Decimal::ZERO);
|
||||
assert_eq!(r.missing_tax_basis, 0);
|
||||
}
|
||||
|
||||
/// Recording tax basis leaves the book totals alone.
|
||||
#[test]
|
||||
fn tax_basis_does_not_move_book_totals() {
|
||||
let (mut l, a) = fixture();
|
||||
let x = register(&mut l, &a, "Laptop", "1200.00");
|
||||
let before = fixed_asset_register(&l, date(2026, 6, 30));
|
||||
set(
|
||||
&mut l,
|
||||
x,
|
||||
TaxBasisRule::PersonalUseConversion,
|
||||
Some("2000.00"),
|
||||
);
|
||||
let after = fixed_asset_register(&l, date(2026, 6, 30));
|
||||
assert_eq!(after.total_cost, before.total_cost);
|
||||
assert_eq!(after.total_accumulated, before.total_accumulated);
|
||||
assert_eq!(after.total_book_value, before.total_book_value);
|
||||
assert_eq!(after.ledger_cost, before.ledger_cost);
|
||||
assert_eq!(after.ledger_accumulated, before.ledger_accumulated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- proptest
|
||||
|
||||
fn cents() -> impl Strategy<Value = Decimal> {
|
||||
(0i64..=100_000_000).prop_map(|c| Decimal::new(c, 2))
|
||||
}
|
||||
|
||||
fn positive_cents() -> impl Strategy<Value = Decimal> {
|
||||
(1i64..=100_000_000).prop_map(|c| Decimal::new(c, 2))
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(256))]
|
||||
|
||||
/// Asset::tax_basis INVARIANTS 1–3 and built_in_gain over random
|
||||
/// costs, bases, and rules; register totals agree.
|
||||
#[test]
|
||||
fn prop_tax_basis_follows_its_rule(
|
||||
assets in proptest::collection::vec((positive_cents(), cents(), 0usize..4), 1..8)
|
||||
) {
|
||||
let (mut l, a) = fixture();
|
||||
let mut expected_total = Decimal::ZERO;
|
||||
let mut expected_missing = 0usize;
|
||||
for (i, (cost, basis, rule)) in assets.iter().enumerate() {
|
||||
let id = register(&mut l, &a, &format!("Asset {i}"), &cost.to_string());
|
||||
let want = match rule {
|
||||
0 => {
|
||||
set(&mut l, id, TaxBasisRule::Cost, None);
|
||||
Some((*cost, None))
|
||||
}
|
||||
1 => {
|
||||
l.set_asset_tax_basis(id, TaxBasis {
|
||||
rule: TaxBasisRule::Carryover,
|
||||
contributor_basis: Some(*basis),
|
||||
}).unwrap();
|
||||
Some((*basis, Some(*cost - *basis)))
|
||||
}
|
||||
2 => {
|
||||
l.set_asset_tax_basis(id, TaxBasis {
|
||||
rule: TaxBasisRule::PersonalUseConversion,
|
||||
contributor_basis: Some(*basis),
|
||||
}).unwrap();
|
||||
Some((if cost < basis { *cost } else { *basis }, Some(*cost - *basis)))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let asset = l.get_asset(id).unwrap();
|
||||
match want {
|
||||
Some((tax, gain)) => {
|
||||
prop_assert_eq!(asset.tax_basis(), Some(tax));
|
||||
prop_assert_eq!(asset.built_in_gain(), gain);
|
||||
expected_total += tax;
|
||||
}
|
||||
None => {
|
||||
prop_assert_eq!(asset.tax_basis(), None);
|
||||
expected_missing += 1;
|
||||
}
|
||||
}
|
||||
if let Some(t) = asset.tax_basis() {
|
||||
// Personal-use conversion never exceeds fair market value.
|
||||
if *rule == 2 {
|
||||
prop_assert!(t <= *cost);
|
||||
prop_assert!(t <= *basis);
|
||||
}
|
||||
}
|
||||
}
|
||||
let r = fixed_asset_register(&l, date(2026, 6, 30));
|
||||
prop_assert_eq!(r.total_tax_basis, expected_total);
|
||||
prop_assert_eq!(r.missing_tax_basis, expected_missing);
|
||||
}
|
||||
|
||||
/// check agrees with set, and a refused record changes nothing.
|
||||
#[test]
|
||||
fn prop_check_agrees_with_set(
|
||||
cents in -1_000_000i64..1_000_000,
|
||||
scale in 0u32..5,
|
||||
rule in 0usize..3,
|
||||
some in any::<bool>(),
|
||||
) {
|
||||
let (mut l, id) = one("1000.00");
|
||||
let basis = some.then(|| Decimal::new(cents, scale));
|
||||
let t = TaxBasis { rule: RULES[rule], contributor_basis: basis };
|
||||
let before = l.clone();
|
||||
let checked = l.check_asset_tax_basis(id, t);
|
||||
let done = l.set_asset_tax_basis(id, t);
|
||||
prop_assert_eq!(checked.is_ok(), done.is_ok());
|
||||
// The spec, independently: INVARIANTS 1 and 2.
|
||||
let valid = match (RULES[rule], basis) {
|
||||
(TaxBasisRule::Cost, None) => true,
|
||||
(TaxBasisRule::Cost, Some(_)) => false,
|
||||
(_, None) => false,
|
||||
(_, Some(b)) => b >= Decimal::ZERO && b.normalize().scale() <= 2,
|
||||
};
|
||||
prop_assert_eq!(done.is_ok(), valid, "{:?} {:?}", RULES[rule], basis);
|
||||
if done.is_err() {
|
||||
prop_assert_eq!(&l, &before);
|
||||
} else {
|
||||
prop_assert_eq!(l.get_asset(id).unwrap().tax, Some(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user