P3: dated 1099 decisions, payment methods and the card exception (defects 23, 12, 18, D-40)
check / check (push) Canceled after 0s

A vendor's 1099 decision is its own dated fact (files NEC or MISC, not
reportable with its reason, or undetermined), applied by each payment's
date. Payment methods are recorded; card and network payments are left
out and listed; a member's check through financing counts; a bank payment
with no method counts (D-40); $2,000 for payments after 2025. One
worksheet for the tool, the viewer and the CPA package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-21 12:12:26 -06:00
1 parent c2ceabbae9
commit 1304b9c0ad
77 files changed
+11455 -970

No files matched your search

+135 -35
View File
@@ -764,7 +764,7 @@ fn run_recurring_skips_occurrences_in_a_closed_period() {
#[test]
fn vendor_1099_tags_replay_and_project() {
use ledger_core::arap::DocumentLine;
use ledger_core::{Form1099, vendor_1099_report};
use ledger_core::{Dated1099, Decision1099, Form1099, PaymentAccounts, vendor_1099_report};
let dir = test_dir("form1099");
let mut store = Store::create(&dir, Currency::USD).unwrap();
let cash = store
@@ -774,10 +774,36 @@ fn vendor_1099_tags_replay_and_project() {
.add_account("6000", "Contract Labor", AccountType::Expense, None, false)
.unwrap();
let dana = store.add_vendor("Dana").unwrap();
store
.set_vendor_1099(dana, Some(Form1099::Nec), true)
.unwrap();
let d = |m, day| NaiveDate::from_ymd_opt(2026, m, day).unwrap();
store
.set_vendor_1099(
dana,
&Dated1099::from_beginning(
Decision1099::Files(Form1099::Nec),
Some(ledger_core::settings::Source {
decided_by: "Riley Chen".into(),
decided_on: d(1, 5),
citation: None,
note: None,
}),
),
Some(true),
)
.unwrap();
let report = |l: &ledger_core::Ledger| {
vendor_1099_report(
l,
2026,
None,
&PaymentAccounts {
cash: vec![cash],
financing: vec![],
},
&[],
None,
)
.unwrap()
};
let bill = store
.create_bill(
dana,
@@ -803,7 +829,7 @@ fn vendor_1099_tags_replay_and_project() {
store.post_entry(dup).unwrap();
store.set_entry_vendor(dup, Some(dana)).unwrap();
store.reverse_entry(dup, d(6, 3), "duplicate").unwrap();
let before = vendor_1099_report(store.ledger(), 2026, None).unwrap();
let before = report(store.ledger());
assert_eq!(before.vendors[0].paid, Decimal::from(2200));
assert!(before.vendors[0].reportable);
drop(store);
@@ -814,10 +840,7 @@ fn vendor_1099_tags_replay_and_project() {
std::fs::remove_file(dir.join(ledger_store::DB_FILE)).unwrap();
}
let store = Store::open(&dir).unwrap();
assert_eq!(
vendor_1099_report(store.ledger(), 2026, None).unwrap(),
before
);
assert_eq!(report(store.ledger()), before);
let conn = rusqlite::Connection::open(dir.join(ledger_store::DB_FILE)).unwrap();
let tagged: i64 = conn
.query_row(
@@ -832,6 +855,16 @@ fn vendor_1099_tags_replay_and_project() {
.query_row("SELECT form_1099 FROM vendors", [], |r| r.get(0))
.unwrap();
assert_eq!(form, "nec");
// The decision, with who made it, line by line.
let (decided, by): (String, String) = conn
.query_row(
"SELECT form_1099, source_json FROM vendor_1099_decisions",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert_eq!(decided, "nec");
assert!(by.contains("Riley Chen"), "{by}");
}
}
@@ -972,7 +1005,14 @@ mod refusals {
AddClass(u8),
SetLineClass(u64, usize, Option<u32>),
SetEntryVendor(u64, Option<u64>),
SetVendor1099(u64, bool),
/// A 1099 decision (0 undetermined, 1 nec, 2 a corporation, 3 a
/// blank reason, 4 no source), dated or not, with a W-9 flag or
/// none.
SetVendor1099(u64, u8, Option<u32>, Option<bool>),
SetW9Flag(u64, bool),
/// A payment method: kind, label, account.
AddInstrument(u8, u8, u64),
SetEntryInstrument(u64, Option<u64>),
SetAccountTax(u64, u8),
Renumber(u64, u8),
// P3 (defects 8, 15): renames, accounts added with a category,
@@ -1051,7 +1091,16 @@ mod refusals {
(id.clone(), 0usize..4, prop::option::of(0u32..4))
.prop_map(|(e, i, c)| Op::SetLineClass(e, i, c)),
(id.clone(), prop::option::of(0u64..4)).prop_map(|(e, v)| Op::SetEntryVendor(e, v)),
(0u64..4, any::<bool>()).prop_map(|(v, t)| Op::SetVendor1099(v, t)),
(
0u64..4,
0u8..5,
prop::option::of(0u32..120),
prop::option::of(any::<bool>())
)
.prop_map(|(v, k, d, t)| Op::SetVendor1099(v, k, d, t)),
(0u64..4, any::<bool>()).prop_map(|(v, t)| Op::SetW9Flag(v, t)),
(0u8..6, 0u8..3, 0u64..12).prop_map(|(k, l, a)| Op::AddInstrument(k, l, a)),
(id.clone(), prop::option::of(0u64..4)).prop_map(|(e, i)| Op::SetEntryInstrument(e, i)),
(0u64..10, 0u8..4).prop_map(|(a, k)| Op::SetAccountTax(a, k)),
(0u64..10, 0u8..9).prop_map(|(a, c)| Op::Renumber(a, c)),
(0u64..12, 0u8..8).prop_map(|(a, n)| Op::Rename(a, n)),
@@ -1222,7 +1271,51 @@ mod refusals {
Op::AddClass(n) => store.add_class(["A", "B", ""][n as usize]).map(drop),
Op::SetLineClass(e, i, c) => store.set_line_class(e, i, c),
Op::SetEntryVendor(e, v) => store.set_entry_vendor(e, v),
Op::SetVendor1099(v, t) => store.set_vendor_1099(v, Some(Form1099::Nec), t),
Op::SetVendor1099(v, k, d, t) => {
let decision = match k {
0 => ledger_core::Decision1099::Undetermined,
1 => ledger_core::Decision1099::Files(Form1099::Nec),
2 => ledger_core::Decision1099::not_reportable(
ledger_core::ExemptReason::Corporation,
"a corporation by name",
)
.unwrap(),
_ => ledger_core::Decision1099::NotReportable {
kind: ledger_core::ExemptReason::Other,
reason: " ".into(),
},
};
let source = (k != 4).then(|| ledger_core::settings::Source {
decided_by: "Riley Chen".into(),
decided_on: day(0),
citation: None,
note: None,
});
let dated = ledger_core::Dated1099 {
effective: d.map(day),
decision,
source,
};
store.set_vendor_1099(v, &dated, t).map(drop)
}
Op::SetW9Flag(v, t) => store.set_vendor_w9_flag(v, t).map(drop),
Op::AddInstrument(k, l, a) => store
.add_instrument(
[
ledger_core::InstrumentKind::Bank,
ledger_core::InstrumentKind::BusinessCard,
ledger_core::InstrumentKind::MemberCard,
ledger_core::InstrumentKind::MemberCashOrCheck,
ledger_core::InstrumentKind::Platform,
ledger_core::InstrumentKind::Bank,
][k as usize],
["Harbor Bank", "Riley's Visa", ""][l as usize],
(k == 5).then_some("12345"),
None,
a,
)
.map(drop),
Op::SetEntryInstrument(e, i) => store.set_entry_instrument(e, i).map(drop),
Op::SetAccountTax(a, k) => store.set_account_tax(a, TAX[k as usize], None),
// The chart's codes, control codes (payroll's 6100 and 2110,
// revenue, receivables), a blank, and fresh ones.
@@ -1512,6 +1605,7 @@ mod refusals {
let journal = dir.join(ledger_store::JOURNAL_FILE);
for op in ops {
check_invariants(&store)?;
let ledger_before = store.ledger().clone();
let mut before = std::fs::read_to_string(&journal).unwrap();
let result = apply(&mut store, op, &mut || {
before = std::fs::read_to_string(&journal).unwrap();
@@ -1524,21 +1618,34 @@ mod refusals {
Err(e) => eprintln!("REFUSED {kind} {e}"),
}
}
// The converse law: an accepted command is on the record. Only
// a batch run may find nothing to do, and a setting changed to
// the value it has changes nothing.
if result.is_ok()
&& !matches!(
op,
Op::RunDepreciation(..) | Op::RunRecurring(..) | Op::SetTaxPolicy(..)
)
{
// The converse law: an accepted command is on the record, or
// changed nothing at all (a batch run with nothing due; a
// setting changed to the value it has; a 1099 decision, W-9
// flag or payment method already so).
if result.is_ok() {
let after = std::fs::read_to_string(&journal).unwrap();
prop_assert!(
after.len() > before.len(),
"accepted {:?} but journaled nothing",
op
);
if after.len() == before.len() {
let parts = ledger_before.differs_from(store.ledger());
prop_assert!(
parts.is_empty(),
"accepted {:?}, journaled nothing, but changed {:?}",
op,
parts
);
prop_assert!(
matches!(
op,
Op::RunDepreciation(..)
| Op::RunRecurring(..)
| Op::SetTaxPolicy(..)
| Op::SetVendor1099(..)
| Op::SetW9Flag(..)
| Op::SetEntryInstrument(..)
),
"accepted {:?} but journaled nothing",
op
);
}
}
if let Err(e) = result {
let after = std::fs::read_to_string(&journal).unwrap();
@@ -1929,15 +2036,8 @@ fn every_figure_of(l: &ledger_core::Ledger) -> (usize, usize) {
financing: vec![],
};
tally(
ledger_core::form1099::vendor_1099_report_with_members(
l,
year,
None,
None,
&sources,
&[],
)
.map(|_| ()),
ledger_core::form1099::vendor_1099_report(l, year, None, &sources, &[], None)
.map(|_| ()),
);
}
let money = ledger_core::parties::money_accounts(l, &cash, &[]);