Vendors carry form_1099 (NEC/MISC) and tin_on_file (never the TIN). Entries carry the vendor they paid; bills, bill payments, and reversals are tagged automatically. books.vendor_1099 counts cash paid per vendor per calendar year (card payments excluded) against $600 through 2025 and $2,000 from 2026, or an override, and flags missing W-9s. Four new tools (64), two journal events, schema v7. Docs: finished R-FUTURE work is no longer listed as future. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
148 lines
5.1 KiB
Rust
148 lines
5.1 KiB
Rust
//! 1099 tracking. Numbers computed by hand.
|
|
//!
|
|
//! 2026 payments:
|
|
//! - Dana (1099-NEC, W-9): bill 1,500 paid by check + 700 direct = 2,200,
|
|
//! over the 2,000 threshold. A 300 payment on 2025-12-30 counts in 2025.
|
|
//! - Ace Rentals (1099-MISC): 1,200 twice, one reversed = 1,200, under.
|
|
//! - Sam (1099-NEC): 5,000 by credit card = 0 cash: the card processor's
|
|
//! 1099-K covers it.
|
|
//! - Bigco (no 1099): 9,000, listed but never reportable.
|
|
|
|
use chrono::NaiveDate;
|
|
use ledger_core::arap::DocumentLine;
|
|
use ledger_core::form1099::default_threshold;
|
|
use ledger_core::{
|
|
AccountType, Currency, Form1099, Ledger, LedgerError, Money, Side, vendor_1099_report,
|
|
};
|
|
use rust_decimal::Decimal;
|
|
use std::str::FromStr;
|
|
|
|
fn dec(s: &str) -> Decimal {
|
|
Decimal::from_str(s).unwrap()
|
|
}
|
|
|
|
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
|
NaiveDate::from_ymd_opt(y, m, day).unwrap()
|
|
}
|
|
|
|
fn books() -> Ledger {
|
|
let mut l = Ledger::new(Currency::USD);
|
|
let bank = l
|
|
.add_account("1000", "Bank", AccountType::Asset, None, false)
|
|
.unwrap();
|
|
let card = l
|
|
.add_account("2100", "Credit Card", AccountType::Liability, None, false)
|
|
.unwrap();
|
|
let contract = l
|
|
.add_account("6000", "Contract Labor", AccountType::Expense, None, false)
|
|
.unwrap();
|
|
let rent = l
|
|
.add_account("6600", "Rent", AccountType::Expense, None, false)
|
|
.unwrap();
|
|
let dana = l.add_vendor("Dana").unwrap();
|
|
let ace = l.add_vendor("Ace Rentals").unwrap();
|
|
let sam = l.add_vendor("Sam").unwrap();
|
|
let bigco = l.add_vendor("Bigco").unwrap();
|
|
l.set_vendor_1099(dana, Some(Form1099::Nec), true).unwrap();
|
|
l.set_vendor_1099(ace, Some(Form1099::Misc), false).unwrap();
|
|
l.set_vendor_1099(sam, Some(Form1099::Nec), true).unwrap();
|
|
let pay = |l: &mut Ledger, date, vendor, dr, cr, amt: &str| {
|
|
let e = l.create_entry(date, "payment");
|
|
let m = Money::new(dec(amt), Currency::USD);
|
|
l.add_line(e, dr, Side::Debit, m).unwrap();
|
|
l.add_line(e, cr, Side::Credit, m).unwrap();
|
|
l.post_entry(e).unwrap();
|
|
l.set_entry_vendor(e, Some(vendor)).unwrap();
|
|
e
|
|
};
|
|
let bill = l
|
|
.create_bill(
|
|
dana,
|
|
d(2026, 3, 1),
|
|
d(2026, 3, 31),
|
|
vec![DocumentLine {
|
|
description: "Design work".into(),
|
|
amount: dec("1500"),
|
|
}],
|
|
)
|
|
.unwrap();
|
|
l.pay_bill(bill, d(2026, 3, 20), dec("1500"), bank).unwrap();
|
|
pay(&mut l, d(2026, 6, 1), dana, contract, bank, "700");
|
|
pay(&mut l, d(2025, 12, 30), dana, contract, bank, "300");
|
|
pay(&mut l, d(2026, 1, 1), ace, rent, bank, "1200");
|
|
let wrong = pay(&mut l, d(2026, 2, 1), ace, rent, bank, "1200");
|
|
l.reverse_entry(wrong, d(2026, 2, 2), "duplicate").unwrap();
|
|
pay(&mut l, d(2026, 4, 1), sam, contract, card, "5000");
|
|
pay(&mut l, d(2026, 5, 1), bigco, contract, bank, "9000");
|
|
// Untagged spending is not anyone's 1099.
|
|
let e = l.create_entry(d(2026, 7, 1), "untagged");
|
|
let m = Money::new(dec("50"), Currency::USD);
|
|
l.add_line(e, contract, Side::Debit, m).unwrap();
|
|
l.add_line(e, bank, Side::Credit, m).unwrap();
|
|
l.post_entry(e).unwrap();
|
|
l
|
|
}
|
|
|
|
#[test]
|
|
fn cash_paid_per_vendor_and_who_is_reportable() {
|
|
let l = books();
|
|
let r = vendor_1099_report(&l, 2026, None);
|
|
assert_eq!(r.threshold, dec("2000"));
|
|
let rows: Vec<(&str, Decimal, bool)> = r
|
|
.vendors
|
|
.iter()
|
|
.map(|v| (v.name.as_str(), v.paid, v.reportable))
|
|
.collect();
|
|
assert_eq!(
|
|
rows,
|
|
[
|
|
("Ace Rentals", dec("1200"), false),
|
|
("Bigco", dec("9000"), false),
|
|
("Dana", dec("2200"), true),
|
|
("Sam", dec("0"), false),
|
|
]
|
|
);
|
|
let ace = &r.vendors[0];
|
|
assert_eq!(ace.form, Some(Form1099::Misc));
|
|
assert!(!ace.tin_on_file);
|
|
}
|
|
|
|
#[test]
|
|
fn years_and_thresholds() {
|
|
let l = books();
|
|
assert_eq!(default_threshold(2025), dec("600"));
|
|
assert_eq!(default_threshold(2026), dec("2000"));
|
|
let y2025 = vendor_1099_report(&l, 2025, None);
|
|
let dana = y2025.vendors.iter().find(|v| v.name == "Dana").unwrap();
|
|
assert_eq!(dana.paid, dec("300"));
|
|
assert!(!dana.reportable, "300 < 600");
|
|
// An override: at 1,000, Ace's 1,200 is reportable.
|
|
let r = vendor_1099_report(&l, 2026, Some(dec("1000")));
|
|
assert!(
|
|
r.vendors
|
|
.iter()
|
|
.any(|v| v.name == "Ace Rentals" && v.reportable)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tags_need_real_entries_and_vendors() {
|
|
let mut l = books();
|
|
assert!(matches!(
|
|
l.set_entry_vendor(999, Some(1)),
|
|
Err(LedgerError::UnknownEntry(999))
|
|
));
|
|
assert!(matches!(
|
|
l.set_entry_vendor(1, Some(999)),
|
|
Err(LedgerError::UnknownVendor(999))
|
|
));
|
|
// The bill and its payment were tagged automatically.
|
|
assert_eq!(l.entry_vendor(1), Some(1));
|
|
assert_eq!(l.entry_vendor(2), Some(1));
|
|
l.set_entry_vendor(2, None).unwrap();
|
|
assert_eq!(l.entry_vendor(2), None);
|
|
assert_eq!(Form1099::parse("1099-NEC"), Some(Form1099::Nec));
|
|
assert_eq!(Form1099::parse("misc"), Some(Form1099::Misc));
|
|
assert_eq!(Form1099::parse("K"), None);
|
|
}
|