P3: dated 1099 decisions, payment methods and the card exception (defects 23, 12, 18, D-40)
check / check (push) Canceled after 0s
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:
77 files changed
+11455
-970
No files matched your search
@@ -7,6 +7,8 @@
|
||||
# cargo run --bin extras -- OUT (the edges of chrono and Decimal)
|
||||
# cargo run --bin extras2 -- OUT (depreciation for 10000-01; 1,044 drafts)
|
||||
# cargo run --bin older -- OUT (x_older_rules, with its JournalWriter)
|
||||
# cargo run --bin p2_1099 -- OUT (p2_vendor_1099: P2's 1099 marks, run
|
||||
# against 9c99a0cf, the revision before P3)
|
||||
# cargo run --bin expect -- OUT (expected.json beside each journal)
|
||||
# and copy each journal.jsonl, blobs/ and expected.json into old-books/.
|
||||
# x_member_closes was written later, the same way, by main 00226a4c
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//! The P2 books of 1099 marks: a vendor marked 1099-NEC, two marked
|
||||
//! "none" (books.set_vendor_1099 "none" and ""), one undecided whose W-9
|
||||
//! sync wrote a null, and payments out of the bank (1000), the members'
|
||||
//! financing (2100) and a card (2050). Every name is fictional.
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::{AccountType, Currency, Form1099, Money, Side};
|
||||
use ledger_store::Store;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
fn main() {
|
||||
let out = std::path::PathBuf::from(std::env::args().nth(1).unwrap()).join("p2_vendor_1099");
|
||||
let _ = std::fs::remove_dir_all(&out);
|
||||
let mut s = Store::create(&out, Currency::USD).unwrap();
|
||||
let a = |s: &mut Store, c: &str, n: &str, t| s.add_account(c, n, t, None, false).unwrap();
|
||||
let bank = a(&mut s, "1000", "Bank", AccountType::Asset);
|
||||
let card = a(&mut s, "2050", "Company card", AccountType::Liability);
|
||||
let due = a(&mut s, "2100", "Due to Members", AccountType::Liability);
|
||||
let labor = a(&mut s, "6000", "Contract Labor", AccountType::Expense);
|
||||
let harbor = s.add_vendor("Harbor Design LLC").unwrap();
|
||||
let bigco = s.add_vendor("Bigco Supply").unwrap();
|
||||
let cedar = s.add_vendor("Cedar Studio").unwrap();
|
||||
let dana = s.add_vendor("Dana Studio").unwrap();
|
||||
s.set_vendor_1099(harbor, Some(Form1099::Nec), false).unwrap(); // "nec"
|
||||
s.set_vendor_1099(bigco, None, false).unwrap(); // "none"
|
||||
s.set_vendor_1099(cedar, None, false).unwrap(); // ""
|
||||
s.set_vendor_1099(dana, None, true).unwrap(); // set_w9's sync, undecided
|
||||
let d = |y, m, day| NaiveDate::from_ymd_opt(y, m, day).unwrap();
|
||||
let mut pay = |date, vendor, cr, cents: i64| {
|
||||
let e = s.create_entry(date, "work").unwrap();
|
||||
let m = Money::new(Decimal::new(cents, 2), Currency::USD);
|
||||
s.add_line(e, labor, Side::Debit, m).unwrap();
|
||||
s.add_line(e, cr, Side::Credit, m).unwrap();
|
||||
s.post_entry(e).unwrap();
|
||||
s.set_entry_vendor(e, Some(vendor)).unwrap();
|
||||
};
|
||||
pay(d(2025, 6, 1), dana, due, 65_000);
|
||||
pay(d(2026, 3, 1), harbor, bank, 250_000);
|
||||
pay(d(2026, 4, 1), harbor, due, 70_000);
|
||||
pay(d(2026, 5, 1), harbor, card, 40_000);
|
||||
pay(d(2026, 6, 1), bigco, bank, 90_000);
|
||||
println!("WROTE {}", out.display());
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
{
|
||||
"assets": [],
|
||||
"bills": [],
|
||||
"entries": [
|
||||
{
|
||||
"date": "2025-06-01",
|
||||
"description": "work",
|
||||
"id": 1,
|
||||
"lines": [
|
||||
{
|
||||
"account": 4,
|
||||
"amount": {
|
||||
"amount": "650.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 3,
|
||||
"amount": {
|
||||
"amount": "650.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-01",
|
||||
"description": "work",
|
||||
"id": 2,
|
||||
"lines": [
|
||||
{
|
||||
"account": 4,
|
||||
"amount": {
|
||||
"amount": "2500.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 1,
|
||||
"amount": {
|
||||
"amount": "2500.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-01",
|
||||
"description": "work",
|
||||
"id": 3,
|
||||
"lines": [
|
||||
{
|
||||
"account": 4,
|
||||
"amount": {
|
||||
"amount": "700.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 3,
|
||||
"amount": {
|
||||
"amount": "700.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-01",
|
||||
"description": "work",
|
||||
"id": 4,
|
||||
"lines": [
|
||||
{
|
||||
"account": 4,
|
||||
"amount": {
|
||||
"amount": "400.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 2,
|
||||
"amount": {
|
||||
"amount": "400.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-01",
|
||||
"description": "work",
|
||||
"id": 5,
|
||||
"lines": [
|
||||
{
|
||||
"account": 4,
|
||||
"amount": {
|
||||
"amount": "900.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 1,
|
||||
"amount": {
|
||||
"amount": "900.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
}
|
||||
],
|
||||
"entry_parties": [
|
||||
[
|
||||
1,
|
||||
[
|
||||
{
|
||||
"party_id": 4,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
2,
|
||||
[
|
||||
{
|
||||
"party_id": 1,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
3,
|
||||
[
|
||||
{
|
||||
"party_id": 1,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
4,
|
||||
[
|
||||
{
|
||||
"party_id": 1,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
5,
|
||||
[
|
||||
{
|
||||
"party_id": 2,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
]
|
||||
],
|
||||
"invoices": [],
|
||||
"payroll_runs": [],
|
||||
"periods": [],
|
||||
"recurring": [],
|
||||
"schedules": [],
|
||||
"snapshot": {
|
||||
"Ok": {
|
||||
"balance_sheet": {
|
||||
"assets": "-3400",
|
||||
"balanced": true,
|
||||
"current_earnings": "-5150",
|
||||
"equity": "-5150",
|
||||
"liabilities": "1750"
|
||||
},
|
||||
"profit_and_loss": {
|
||||
"expenses": "5150",
|
||||
"net_income": "-5150",
|
||||
"revenue": "0"
|
||||
},
|
||||
"trial_balance": {
|
||||
"lines": [
|
||||
{
|
||||
"code": "1000",
|
||||
"credit": "3400",
|
||||
"debit": "0",
|
||||
"name": "Bank"
|
||||
},
|
||||
{
|
||||
"code": "2050",
|
||||
"credit": "400",
|
||||
"debit": "0",
|
||||
"name": "Company card"
|
||||
},
|
||||
{
|
||||
"code": "2100",
|
||||
"credit": "1350",
|
||||
"debit": "0",
|
||||
"name": "Due to Members"
|
||||
},
|
||||
{
|
||||
"code": "6000",
|
||||
"credit": "0",
|
||||
"debit": "5150",
|
||||
"name": "Contract Labor"
|
||||
}
|
||||
],
|
||||
"total_credits": "5150",
|
||||
"total_debits": "5150"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verify": {
|
||||
"Ok": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{"payload":{"base_currency":"USD"},"prev":"0000000000000000000000000000000000000000000000000000000000000000","seq":1,"ts":"2026-10-21T03:02:53Z","type":"store.created"}
|
||||
{"payload":{"account_type":"Asset","code":"1000","currency":"USD","id":1,"name":"Bank","parent":null,"placeholder":false},"prev":"faa687f5a03c5dd12cd38d92bb550079de448ca93b0277fd87734c450f762985","seq":2,"ts":"2026-10-21T03:02:53Z","type":"account.added"}
|
||||
{"payload":{"account_type":"Liability","code":"2050","currency":"USD","id":2,"name":"Company card","parent":null,"placeholder":false},"prev":"35820b1110264a25167acc22ae911dfef3d362c497b17acca930ee921b97b7d6","seq":3,"ts":"2026-10-21T03:02:53Z","type":"account.added"}
|
||||
{"payload":{"account_type":"Liability","code":"2100","currency":"USD","id":3,"name":"Due to Members","parent":null,"placeholder":false},"prev":"f6431387d04e613df69f33702293c8c23f902f15e555b5d4c2aca49e545842bf","seq":4,"ts":"2026-10-21T03:02:53Z","type":"account.added"}
|
||||
{"payload":{"account_type":"Expense","code":"6000","currency":"USD","id":4,"name":"Contract Labor","parent":null,"placeholder":false},"prev":"bb8832a8ca94611084d543b5ba32e0343944e55440b2849c2c8e1d2b6a8d2b99","seq":5,"ts":"2026-10-21T03:02:53Z","type":"account.added"}
|
||||
{"payload":{"id":1,"name":"Harbor Design LLC"},"prev":"2dcde6845ee05cfb4ef2f23194a161986fe52b246c928e76745990e97409e091","seq":6,"ts":"2026-10-21T03:02:53Z","type":"vendor.added"}
|
||||
{"payload":{"id":2,"name":"Bigco Supply"},"prev":"0721eef61ec9b3ed1ead9282f481e8a67191183bfd00d51184a8262a849de4b6","seq":7,"ts":"2026-10-21T03:02:53Z","type":"vendor.added"}
|
||||
{"payload":{"id":3,"name":"Cedar Studio"},"prev":"4c6928038c741dcd6cd7c5dea501b3a7fbed9227002baec525323689097bb447","seq":8,"ts":"2026-10-21T03:02:53Z","type":"vendor.added"}
|
||||
{"payload":{"id":4,"name":"Dana Studio"},"prev":"1f5a42c9b9512441793dd94dfefc80316b656af83a521c9e9c5f0fa339cd383f","seq":9,"ts":"2026-10-21T03:02:53Z","type":"vendor.added"}
|
||||
{"payload":{"form_1099":"nec","tin_on_file":false,"vendor_id":1},"prev":"28175697977371c53fd918f0b858ba22f03eb7f8698e0ac4d94d4b08da26a96c","seq":10,"ts":"2026-10-21T03:02:53Z","type":"vendor.1099_set"}
|
||||
{"payload":{"form_1099":null,"tin_on_file":false,"vendor_id":2},"prev":"22f935f5a504501919d62eeaab1c109d9878adf718b698fb77d947e33ebf8cf8","seq":11,"ts":"2026-10-21T03:02:53Z","type":"vendor.1099_set"}
|
||||
{"payload":{"form_1099":null,"tin_on_file":false,"vendor_id":3},"prev":"1b4cdc7b0207eef330446cfafbb85acffe255268bba7a525c9a2281cb9491cd1","seq":12,"ts":"2026-10-21T03:02:53Z","type":"vendor.1099_set"}
|
||||
{"payload":{"form_1099":null,"tin_on_file":true,"vendor_id":4},"prev":"fa22b64dc51595facd1613fd593971999599200ce695d5136645baf429f57c5c","seq":13,"ts":"2026-10-21T03:02:53Z","type":"vendor.1099_set"}
|
||||
{"payload":{"date":"2025-06-01","description":"work","id":1},"prev":"aada8778c28f9bd17317f894db215169cbd033feb73f9024683c9a87034ecbe6","seq":14,"ts":"2026-10-21T03:02:53Z","type":"entry.created"}
|
||||
{"payload":{"account":4,"amount":"650.00","currency":"USD","id":1,"side":"debit"},"prev":"a6c85a896b3525734f943e9e6c1d802f0dbb81da242af57160ecd1fb4a6994ba","seq":15,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"account":3,"amount":"650.00","currency":"USD","id":1,"side":"credit"},"prev":"65f150b229ce65ce0fa00be0db83cc9b48a2b7bbf9b66f977186ca180661f517","seq":16,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2025-06-01","description":"work","id":1,"lines":[{"account":4,"amount":"650.00","currency":"USD","side":"debit"},{"account":3,"amount":"650.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"15cab2a9658cdcbffd3f754d0dfb8bf50915aeeff62ad86b29dfeae085243d1a","seq":17,"ts":"2026-10-21T03:02:53Z","type":"entry.posted"}
|
||||
{"payload":{"entry_id":1,"vendor_id":4},"prev":"75a257d69a1e96e999076f04a75caf1011ca684aa1eb4e0c35ab19d2a30d4715","seq":18,"ts":"2026-10-21T03:02:53Z","type":"entry.vendor_set"}
|
||||
{"payload":{"date":"2026-03-01","description":"work","id":2},"prev":"965a3395b67428dc095d1d6ed8ed75873474bef20b49a976cfa088f3676d024a","seq":19,"ts":"2026-10-21T03:02:53Z","type":"entry.created"}
|
||||
{"payload":{"account":4,"amount":"2500.00","currency":"USD","id":2,"side":"debit"},"prev":"c7ecf610d918f3b346db3239226a9d4ac586dea59a45f627519fcbb4eb11f466","seq":20,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"account":1,"amount":"2500.00","currency":"USD","id":2,"side":"credit"},"prev":"4a25b78987fff2213ade495a096a9cda705c5a6c44c285b065794b5e268d2765","seq":21,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-03-01","description":"work","id":2,"lines":[{"account":4,"amount":"2500.00","currency":"USD","side":"debit"},{"account":1,"amount":"2500.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"8d16aeafb1d1fe79d085cd574c0bd6bc5167807a462c7994ddf8625bb03fc92f","seq":22,"ts":"2026-10-21T03:02:53Z","type":"entry.posted"}
|
||||
{"payload":{"entry_id":2,"vendor_id":1},"prev":"bce9b391e47b12594c9ae5302b9b5e4c75e0eef0d0e92dd5c78e3a8e0de8bc51","seq":23,"ts":"2026-10-21T03:02:53Z","type":"entry.vendor_set"}
|
||||
{"payload":{"date":"2026-04-01","description":"work","id":3},"prev":"cfa11ea24eef3250c1c490d5a7c7748c5fd9c5d92d657bbd67b3115859c00546","seq":24,"ts":"2026-10-21T03:02:53Z","type":"entry.created"}
|
||||
{"payload":{"account":4,"amount":"700.00","currency":"USD","id":3,"side":"debit"},"prev":"645f86b699b6d72c89a6396a2ce7151a633e05a160586a1d7e91ff9638752067","seq":25,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"account":3,"amount":"700.00","currency":"USD","id":3,"side":"credit"},"prev":"3da5a99cfa096a7de27f8f8e117aafafde22c8f8516d8753235e688c18566ab6","seq":26,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-04-01","description":"work","id":3,"lines":[{"account":4,"amount":"700.00","currency":"USD","side":"debit"},{"account":3,"amount":"700.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"7e9c4bce2009f0dc611e45aaa173dc2c973f999a06da1a6ef498be0beae9ad45","seq":27,"ts":"2026-10-21T03:02:53Z","type":"entry.posted"}
|
||||
{"payload":{"entry_id":3,"vendor_id":1},"prev":"6357688b27ccbbb24d545fbe1bc454d3c73fa0aef4daf5d3ac666cfe7666d066","seq":28,"ts":"2026-10-21T03:02:53Z","type":"entry.vendor_set"}
|
||||
{"payload":{"date":"2026-05-01","description":"work","id":4},"prev":"29b5ac4f6c46158b22f5cb6a18c2a9f88a386637d8424a283a587a17dbc56569","seq":29,"ts":"2026-10-21T03:02:53Z","type":"entry.created"}
|
||||
{"payload":{"account":4,"amount":"400.00","currency":"USD","id":4,"side":"debit"},"prev":"f8eef20eca997c3a82f6963177f5f3482230852d2fa7abc965581d50e6ba0886","seq":30,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"account":2,"amount":"400.00","currency":"USD","id":4,"side":"credit"},"prev":"622cac7da511368e75195f733595a7da35d35ba2e70594e8b54aba20e888584e","seq":31,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-05-01","description":"work","id":4,"lines":[{"account":4,"amount":"400.00","currency":"USD","side":"debit"},{"account":2,"amount":"400.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"4541dabebf1bb9f268645a4398980e9542de4ea14b47d906a22dc42920579cc8","seq":32,"ts":"2026-10-21T03:02:53Z","type":"entry.posted"}
|
||||
{"payload":{"entry_id":4,"vendor_id":1},"prev":"ccfdcf25dde07c0cfd75f5fb30ab42761fe2f3a901e89b63fb4836bf851702a1","seq":33,"ts":"2026-10-21T03:02:53Z","type":"entry.vendor_set"}
|
||||
{"payload":{"date":"2026-06-01","description":"work","id":5},"prev":"52a15b817fa526b761c3f3ec4e9d2c8684a42f0ed7dd2fa6c96fd8b378063d57","seq":34,"ts":"2026-10-21T03:02:53Z","type":"entry.created"}
|
||||
{"payload":{"account":4,"amount":"900.00","currency":"USD","id":5,"side":"debit"},"prev":"4e20498034f7dc588e1515af8a32a8df74009203bd1a78b27f2e1ba5d7cf9666","seq":35,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"account":1,"amount":"900.00","currency":"USD","id":5,"side":"credit"},"prev":"199142dd14c4e3096c81a6426ae9e94e3e920b846656ac8eb0ee1fab322ba3a6","seq":36,"ts":"2026-10-21T03:02:53Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-06-01","description":"work","id":5,"lines":[{"account":4,"amount":"900.00","currency":"USD","side":"debit"},{"account":1,"amount":"900.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"7c071ebfd5242722f07a46d87fecdf5549125894e4085b509c4789c7ca403bc9","seq":37,"ts":"2026-10-21T03:02:53Z","type":"entry.posted"}
|
||||
{"payload":{"entry_id":5,"vendor_id":2},"prev":"fb00c3db22192ed708b2db0838b423a990d57560e43f30d62e150c815434e1d7","seq":38,"ts":"2026-10-21T03:02:53Z","type":"entry.vendor_set"}
|
||||
@@ -0,0 +1,690 @@
|
||||
//! 1099 decisions and payment methods in the journal (plan P3, defects
|
||||
//! 12, 18 and 23; D-40): declared as features before their first line,
|
||||
//! replayed and projected exactly, refused without a trace, and books
|
||||
//! written before them read as they always meant. Every name is
|
||||
//! fictional.
|
||||
//!
|
||||
//! ```text
|
||||
//! GOAL: the decision ("no 1099" included) and how each payment was made
|
||||
//! are kept as records, and no version ever misreads them.
|
||||
//! SOURCE: rules.md B-19, B-27; ledger_store::form1099 INVARIANTS 1-5.
|
||||
//! ```
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::form1099::Applied;
|
||||
use ledger_core::settings::Source;
|
||||
use ledger_core::{
|
||||
AccountType, Currency, Dated1099, Decision1099, ExemptReason, Form1099, InstrumentKind, Money,
|
||||
Side,
|
||||
};
|
||||
use ledger_store::format::{FORMAT_SET, PAYMENT_INSTRUMENTS, VENDOR_1099_DECISIONS};
|
||||
use ledger_store::journal::read_and_verify_journal;
|
||||
use ledger_store::{DB_FILE, JOURNAL_FILE, JournalWriter, Snapshot, Store, StoreError};
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::json;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A test's dir, removed when the test ends, pass or panic.
|
||||
struct TestDir(PathBuf);
|
||||
|
||||
impl std::ops::Deref for TestDir {
|
||||
type Target = Path;
|
||||
fn deref(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_dir(name: &str) -> TestDir {
|
||||
let dir = std::env::temp_dir().join(format!("wanderbooks-1099-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
TestDir(dir)
|
||||
}
|
||||
|
||||
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(y, m, day).unwrap()
|
||||
}
|
||||
|
||||
fn src() -> Source {
|
||||
Source {
|
||||
decided_by: "Riley Chen".into(),
|
||||
decided_on: d(2026, 10, 16),
|
||||
citation: Some("Treas. Reg. §1.6041-3(p)(1)".into()),
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_beginning(decision: Decision1099) -> Dated1099 {
|
||||
Dated1099::from_beginning(decision, Some(src()))
|
||||
}
|
||||
|
||||
/// Books: bank 1000, Due to Members 2100, contract labor 6000; Harbor
|
||||
/// Design LLC paid 2,500.00 by a member (Dr 6000 / Cr 2100) and reversed,
|
||||
/// then paid again.
|
||||
struct Books {
|
||||
store: Store,
|
||||
due: u64,
|
||||
harbor: u64,
|
||||
paid: u64,
|
||||
reversal: u64,
|
||||
}
|
||||
|
||||
fn books(dir: &Path) -> Books {
|
||||
let mut store = Store::create(dir, Currency::USD).unwrap();
|
||||
store
|
||||
.add_account("1000", "Bank", AccountType::Asset, None, false)
|
||||
.unwrap();
|
||||
let due = store
|
||||
.add_account(
|
||||
"2100",
|
||||
"Due to Members",
|
||||
AccountType::Liability,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let labor = store
|
||||
.add_account("6000", "Contract Labor", AccountType::Expense, None, false)
|
||||
.unwrap();
|
||||
let harbor = store.add_vendor("Harbor Design LLC").unwrap();
|
||||
let pay = |store: &mut Store, day| {
|
||||
let e = store.create_entry(d(2026, 5, day), "Design work").unwrap();
|
||||
let m = Money::new(Decimal::new(250_000, 2), Currency::USD);
|
||||
store.add_line(e, labor, Side::Debit, m).unwrap();
|
||||
store.add_line(e, due, Side::Credit, m).unwrap();
|
||||
store.post_entry(e).unwrap();
|
||||
store.set_entry_vendor(e, Some(harbor)).unwrap();
|
||||
e
|
||||
};
|
||||
let paid = pay(&mut store, 1);
|
||||
let reversal = store.reverse_entry(paid, d(2026, 5, 2), "twice").unwrap();
|
||||
pay(&mut store, 3);
|
||||
Books {
|
||||
store,
|
||||
due,
|
||||
harbor,
|
||||
paid,
|
||||
reversal,
|
||||
}
|
||||
}
|
||||
|
||||
/// A worksheet line: name, paid, undetermined, excluded card, status,
|
||||
/// decision needed.
|
||||
type Row1099 = (String, Decimal, Decimal, Decimal, &'static str, bool);
|
||||
|
||||
/// A row of the projection's vendor_1099_decisions: effective, form,
|
||||
/// reason kind, source.
|
||||
type DecisionRow = (Option<String>, Option<String>, Option<String>, String);
|
||||
|
||||
fn types(dir: &Path) -> Vec<(String, serde_json::Value)> {
|
||||
read_and_verify_journal(&dir.join(JOURNAL_FILE))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|l| (l.line_type, l.payload))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// B-27: the first line that needs a feature comes after a declaration of
|
||||
/// it (with every feature already in force), once. A version that doesn't
|
||||
/// know the feature stops at the declaration (`tests/format.rs`
|
||||
/// old_reader_gets_newer_journal, for any feature it doesn't know).
|
||||
#[test]
|
||||
fn each_feature_is_declared_once_before_its_first_line() {
|
||||
let dir = test_dir("declare");
|
||||
let mut b = books(&dir);
|
||||
let before = types(&dir).len();
|
||||
assert!(
|
||||
!types(&dir).iter().any(|(t, _)| t == FORMAT_SET),
|
||||
"books without either feature declare nothing"
|
||||
);
|
||||
b.store
|
||||
.set_vendor_1099(
|
||||
b.harbor,
|
||||
&from_beginning(Decision1099::Files(Form1099::Nec)),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
b.store.set_vendor_w9_flag(b.harbor, true).unwrap();
|
||||
let card = b
|
||||
.store
|
||||
.add_instrument(
|
||||
InstrumentKind::MemberCard,
|
||||
"Riley's Visa",
|
||||
Some("4242"),
|
||||
None,
|
||||
b.due,
|
||||
)
|
||||
.unwrap();
|
||||
b.store.set_entry_instrument(b.paid, Some(card)).unwrap();
|
||||
b.store
|
||||
.add_instrument(
|
||||
InstrumentKind::MemberCashOrCheck,
|
||||
"Riley's checks",
|
||||
None,
|
||||
None,
|
||||
b.due,
|
||||
)
|
||||
.unwrap();
|
||||
let added: Vec<String> = types(&dir)[before..]
|
||||
.iter()
|
||||
.map(|(t, p)| match t.as_str() {
|
||||
FORMAT_SET => format!("{t} {}", p["features"]),
|
||||
_ => t.clone(),
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
added,
|
||||
[
|
||||
format!("{FORMAT_SET} [\"{VENDOR_1099_DECISIONS}\"]"),
|
||||
"vendor.1099_set".to_string(),
|
||||
"vendor.w9_flag_set".to_string(),
|
||||
format!("{FORMAT_SET} [\"{PAYMENT_INSTRUMENTS}\",\"{VENDOR_1099_DECISIONS}\"]"),
|
||||
"instrument.added".to_string(),
|
||||
"entry.instrument_set".to_string(),
|
||||
"instrument.added".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(b.store.journal_format().format, 2);
|
||||
}
|
||||
|
||||
/// Every record replays and projects exactly: reopening gives back the
|
||||
/// same ledger, part for part; a projection rebuilt from the journal
|
||||
/// equals the one the writer kept, table by table (verify); the viewer's
|
||||
/// snapshot reads the same; and the projection holds each decision with
|
||||
/// who made it, each method, and the tag on the whole reversal chain.
|
||||
#[test]
|
||||
fn decisions_and_methods_replay_and_project() {
|
||||
let dir = test_dir("replay");
|
||||
let mut b = books(&dir);
|
||||
let corp = Decision1099::not_reportable(
|
||||
ExemptReason::Corporation,
|
||||
"Delaware public benefit corporation, from the public record",
|
||||
)
|
||||
.unwrap();
|
||||
b.store
|
||||
.set_vendor_1099(
|
||||
b.harbor,
|
||||
&from_beginning(Decision1099::Files(Form1099::Nec)),
|
||||
Some(true),
|
||||
)
|
||||
.unwrap();
|
||||
b.store
|
||||
.set_vendor_1099(
|
||||
b.harbor,
|
||||
&Dated1099 {
|
||||
effective: Some(d(2026, 12, 30)),
|
||||
decision: corp.clone(),
|
||||
source: Some(src()),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let card = b
|
||||
.store
|
||||
.add_instrument(
|
||||
InstrumentKind::MemberCard,
|
||||
"Riley's Visa",
|
||||
Some("4242"),
|
||||
None,
|
||||
b.due,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
b.store
|
||||
.set_entry_instrument(b.reversal, Some(card))
|
||||
.unwrap()
|
||||
);
|
||||
let chain = [b.paid as i64, b.reversal as i64];
|
||||
let live = b.store.ledger().clone();
|
||||
let v = live.get_vendor(b.harbor).unwrap();
|
||||
assert!(v.tin_on_file);
|
||||
assert_eq!(v.decisions_1099.len(), 2);
|
||||
assert_eq!(v.decision_on(d(2026, 12, 31)), Some(&corp));
|
||||
assert_eq!(live.entry_instrument(b.paid), Some(card));
|
||||
drop(b);
|
||||
let store = Store::open(&dir).unwrap();
|
||||
assert!(live.differs_from(store.ledger()).is_empty());
|
||||
store.verify().unwrap();
|
||||
drop(store);
|
||||
std::fs::remove_file(dir.join(DB_FILE)).unwrap();
|
||||
let store = Store::open(&dir).unwrap();
|
||||
store.verify().unwrap();
|
||||
assert!(live.differs_from(store.ledger()).is_empty());
|
||||
drop(store);
|
||||
let snap = Snapshot::load(&dir).unwrap();
|
||||
assert!(live.differs_from(snap.ledger()).is_empty());
|
||||
let conn = rusqlite::Connection::open(dir.join(DB_FILE)).unwrap();
|
||||
let decisions: Vec<DecisionRow> = conn
|
||||
.prepare("SELECT effective, form_1099, reason_kind, source_json FROM vendor_1099_decisions ORDER BY seq")
|
||||
.unwrap()
|
||||
.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)))
|
||||
.unwrap()
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap();
|
||||
assert_eq!(decisions.len(), 2);
|
||||
assert_eq!(
|
||||
(decisions[0].0.as_deref(), decisions[0].1.as_deref()),
|
||||
(None, Some("nec"))
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
decisions[1].0.as_deref(),
|
||||
decisions[1].1.as_deref(),
|
||||
decisions[1].2.as_deref()
|
||||
),
|
||||
(
|
||||
Some("2026-12-30"),
|
||||
Some("not_reportable"),
|
||||
Some("corporation")
|
||||
)
|
||||
);
|
||||
assert!(decisions[1].3.contains("Riley Chen"));
|
||||
let (form, on_file): (String, bool) = conn
|
||||
.query_row("SELECT form_1099, tin_on_file FROM vendors", [], |r| {
|
||||
Ok((r.get(0)?, r.get(1)?))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(form.as_str(), on_file),
|
||||
("nec", true),
|
||||
"the decision from the beginning"
|
||||
);
|
||||
let tagged: Vec<i64> = conn
|
||||
.prepare("SELECT entry_id FROM entry_instruments ORDER BY entry_id")
|
||||
.unwrap()
|
||||
.query_map([], |r| r.get(0))
|
||||
.unwrap()
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap();
|
||||
assert_eq!(tagged, chain);
|
||||
}
|
||||
|
||||
/// The engine-side half of verification item 13a (P3 review R14): a 1099
|
||||
/// decision, a W-9 flag, a payment method and a payment-method tag change
|
||||
/// no entry, no balance and no period.
|
||||
#[test]
|
||||
fn decision_and_method_records_change_no_entry() {
|
||||
let dir = test_dir("no-entry");
|
||||
let mut b = books(&dir);
|
||||
let figures = |s: &Store| {
|
||||
let l = s.ledger();
|
||||
(
|
||||
ledger_core::reports::snapshot(l).unwrap(),
|
||||
l.posted_entries().cloned().collect::<Vec<_>>(),
|
||||
l.list_periods().into_iter().cloned().collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
let start = figures(&b.store);
|
||||
b.store
|
||||
.set_vendor_1099(
|
||||
b.harbor,
|
||||
&from_beginning(Decision1099::Files(Form1099::Misc)),
|
||||
Some(false),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(figures(&b.store), start);
|
||||
b.store.set_vendor_w9_flag(b.harbor, true).unwrap();
|
||||
assert_eq!(figures(&b.store), start);
|
||||
let i = b
|
||||
.store
|
||||
.add_instrument(InstrumentKind::Platform, "Harbor Pay", None, None, b.due)
|
||||
.unwrap();
|
||||
assert_eq!(figures(&b.store), start);
|
||||
b.store.set_entry_instrument(b.paid, Some(i)).unwrap();
|
||||
assert_eq!(figures(&b.store), start);
|
||||
b.store.set_entry_instrument(b.paid, None).unwrap();
|
||||
assert_eq!(figures(&b.store), start);
|
||||
}
|
||||
|
||||
/// A refused command journals nothing, not even a feature declaration;
|
||||
/// one that changes nothing journals nothing either (P3 review R8).
|
||||
#[test]
|
||||
fn refusals_and_no_ops_journal_nothing() {
|
||||
let dir = test_dir("refused");
|
||||
let mut b = books(&dir);
|
||||
let journal = || std::fs::read(dir.join(JOURNAL_FILE)).unwrap();
|
||||
let at_start = journal();
|
||||
let nec = Decision1099::Files(Form1099::Nec);
|
||||
// No source.
|
||||
let err = b
|
||||
.store
|
||||
.set_vendor_1099(
|
||||
b.harbor,
|
||||
&Dated1099::from_beginning(nec.clone(), None),
|
||||
None,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("decided_by"), "{err}");
|
||||
// Dated with nothing decided before it.
|
||||
let dated = Dated1099 {
|
||||
effective: Some(d(2026, 7, 1)),
|
||||
decision: nec.clone(),
|
||||
source: Some(src()),
|
||||
};
|
||||
assert!(b.store.set_vendor_1099(b.harbor, &dated, None).is_err());
|
||||
// A blank reason; an unknown vendor.
|
||||
let blank = Dated1099::from_beginning(
|
||||
Decision1099::NotReportable {
|
||||
kind: ExemptReason::Other,
|
||||
reason: " ".into(),
|
||||
},
|
||||
Some(src()),
|
||||
);
|
||||
assert!(b.store.set_vendor_1099(b.harbor, &blank, None).is_err());
|
||||
assert!(
|
||||
b.store
|
||||
.set_vendor_1099(99, &from_beginning(nec.clone()), None)
|
||||
.is_err()
|
||||
);
|
||||
assert!(b.store.set_vendor_w9_flag(99, true).is_err());
|
||||
// A bad method, an unknown entry or method.
|
||||
assert!(
|
||||
b.store
|
||||
.add_instrument(InstrumentKind::Bank, "Bank", Some("12345"), None, b.due)
|
||||
.is_err()
|
||||
);
|
||||
assert!(b.store.set_entry_instrument(999, None).is_err());
|
||||
assert!(b.store.set_entry_instrument(b.paid, Some(7)).is_err());
|
||||
assert_eq!(journal(), at_start, "refusals left a trace");
|
||||
// A flag already so, a tag already so: nothing.
|
||||
assert!(!b.store.set_vendor_w9_flag(b.harbor, false).unwrap());
|
||||
assert!(!b.store.set_entry_instrument(b.paid, None).unwrap());
|
||||
assert_eq!(journal(), at_start);
|
||||
// Recorded once; the same again: unchanged, nothing more.
|
||||
assert_eq!(
|
||||
b.store
|
||||
.set_vendor_1099(b.harbor, &from_beginning(nec.clone()), None)
|
||||
.unwrap(),
|
||||
Applied::Set
|
||||
);
|
||||
let once = journal();
|
||||
assert_eq!(
|
||||
b.store
|
||||
.set_vendor_1099(b.harbor, &from_beginning(nec.clone()), None)
|
||||
.unwrap(),
|
||||
Applied::Unchanged
|
||||
);
|
||||
assert_eq!(journal(), once);
|
||||
// The same decision with a W-9 flag: only the flag is journaled.
|
||||
assert_eq!(
|
||||
b.store
|
||||
.set_vendor_1099(b.harbor, &from_beginning(nec), Some(true))
|
||||
.unwrap(),
|
||||
Applied::Unchanged
|
||||
);
|
||||
let last = types(&dir).pop().unwrap();
|
||||
assert_eq!(last.0, "vendor.w9_flag_set");
|
||||
}
|
||||
|
||||
/// P3 review R8: marking a W-9 on file keeps the 1099 decision and who
|
||||
/// made it, verbatim.
|
||||
#[test]
|
||||
fn marking_a_w9_keeps_who_decided_the_1099() {
|
||||
let dir = test_dir("w9");
|
||||
let mut b = books(&dir);
|
||||
let decision = from_beginning(Decision1099::Files(Form1099::Nec));
|
||||
b.store
|
||||
.set_vendor_1099(b.harbor, &decision, Some(false))
|
||||
.unwrap();
|
||||
assert!(b.store.set_vendor_w9_flag(b.harbor, true).unwrap());
|
||||
let v = b.store.ledger().get_vendor(b.harbor).unwrap();
|
||||
assert!(v.tin_on_file);
|
||||
assert_eq!(v.decisions_1099, [decision]);
|
||||
}
|
||||
|
||||
/// B-27: a value only a newer version writes, in a line of a type this
|
||||
/// version knows (a 1099 decision, a reason kind, a payment method's
|
||||
/// kind), is newer, never corrupt; a malformed one is corrupt.
|
||||
#[test]
|
||||
fn an_unknown_1099_value_is_newer_not_corrupt() {
|
||||
let source = json!({"decided_by": "Riley Chen", "decided_on": "2026-10-16"});
|
||||
let cases = [
|
||||
(
|
||||
"vendor.1099_set",
|
||||
json!({"vendor_id": 1, "form_1099": "1099-k", "tin_on_file": false, "source": source}),
|
||||
Some("the 1099 decision \"1099-k\""),
|
||||
),
|
||||
(
|
||||
"vendor.1099_set",
|
||||
json!({"vendor_id": 1, "form_1099": "not_reportable", "reason_kind": "charity_lottery", "reason": "x", "tin_on_file": false, "source": source}),
|
||||
Some("the 1099 reason kind \"charity_lottery\""),
|
||||
),
|
||||
(
|
||||
"instrument.added",
|
||||
json!({"instrument_id": 1, "kind": "crypto_wallet", "label": "x", "account_id": 1}),
|
||||
Some("the payment method kind \"crypto_wallet\""),
|
||||
),
|
||||
(
|
||||
"vendor.1099_set",
|
||||
json!({"vendor_id": 1, "form_1099": "not_reportable", "reason_kind": "corporation", "tin_on_file": false, "source": source}),
|
||||
None,
|
||||
),
|
||||
];
|
||||
for (i, (t, payload, need)) in cases.into_iter().enumerate() {
|
||||
let dir = test_dir(&format!("newer-{i}"));
|
||||
let b = books(&dir);
|
||||
drop(b);
|
||||
let mut w = JournalWriter::open(&dir.join(JOURNAL_FILE)).unwrap();
|
||||
w.append(
|
||||
FORMAT_SET,
|
||||
json!({"format": 2, "features": [PAYMENT_INSTRUMENTS, VENDOR_1099_DECISIONS]}),
|
||||
)
|
||||
.unwrap();
|
||||
let seq = w.append(t, payload).unwrap().seq;
|
||||
drop(w);
|
||||
std::fs::remove_file(dir.join(DB_FILE)).unwrap();
|
||||
match (Store::open(&dir), need) {
|
||||
(
|
||||
Err(StoreError::NewerJournal {
|
||||
seq: s, need: n, ..
|
||||
}),
|
||||
Some(want),
|
||||
) => {
|
||||
assert_eq!((s, n.as_str()), (seq, want));
|
||||
}
|
||||
(Err(StoreError::JournalCorrupt(_)), None) => {}
|
||||
(got, want) => panic!("case {i}: {:?} for {want:?}", got.err()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plan 5.5, D-40: books written before decisions were kept (lines with a
|
||||
/// form or null and a W-9 flag, no source, no date) open unchanged, byte
|
||||
/// for byte, with no feature declared: each old line reads as a decision
|
||||
/// from the beginning (a null as undetermined, never "not reportable"),
|
||||
/// and the projection is the one those books always had.
|
||||
#[test]
|
||||
fn books_written_before_decisions_read_as_they_always_meant() {
|
||||
let dir = test_dir("legacy");
|
||||
let b = books(&dir);
|
||||
let (harbor, paid) = (b.harbor, b.paid);
|
||||
drop(b);
|
||||
let mut w = JournalWriter::open(&dir.join(JOURNAL_FILE)).unwrap();
|
||||
for (vendor, name) in [(2u64, "Pinecrest Supply"), (3, "Ledgerline Bookkeeping")] {
|
||||
w.append("vendor.added", json!({"id": vendor, "name": name}))
|
||||
.unwrap();
|
||||
}
|
||||
for p in [
|
||||
json!({"vendor_id": harbor, "form_1099": "nec", "tin_on_file": false}),
|
||||
json!({"vendor_id": 2, "form_1099": null, "tin_on_file": false}),
|
||||
// The old W-9 sync on an undecided vendor.
|
||||
json!({"vendor_id": 3, "form_1099": null, "tin_on_file": true}),
|
||||
] {
|
||||
w.append("vendor.1099_set", p).unwrap();
|
||||
}
|
||||
drop(w);
|
||||
std::fs::remove_file(dir.join(DB_FILE)).unwrap();
|
||||
let journal = std::fs::read(dir.join(JOURNAL_FILE)).unwrap();
|
||||
let store = Store::open(&dir).unwrap();
|
||||
store.verify().unwrap();
|
||||
let l = store.ledger();
|
||||
let h = l.get_vendor(harbor).unwrap();
|
||||
assert_eq!(
|
||||
h.decisions_1099,
|
||||
[Dated1099::from_beginning(
|
||||
Decision1099::Files(Form1099::Nec),
|
||||
None
|
||||
)]
|
||||
);
|
||||
for v in [2, 3] {
|
||||
assert_eq!(
|
||||
l.get_vendor(v).unwrap().decisions_1099,
|
||||
[Dated1099::from_beginning(Decision1099::Undetermined, None)]
|
||||
);
|
||||
}
|
||||
assert!(l.get_vendor(3).unwrap().tin_on_file);
|
||||
assert_eq!(l.entry_instrument(paid), None);
|
||||
assert_eq!(store.journal_format().features, Vec::<String>::new());
|
||||
drop(store);
|
||||
assert_eq!(std::fs::read(dir.join(JOURNAL_FILE)).unwrap(), journal);
|
||||
let conn = rusqlite::Connection::open(dir.join(DB_FILE)).unwrap();
|
||||
let forms: Vec<(i64, Option<String>, bool)> = conn
|
||||
.prepare("SELECT id, form_1099, tin_on_file FROM vendors ORDER BY id")
|
||||
.unwrap()
|
||||
.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))
|
||||
.unwrap()
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
forms,
|
||||
[
|
||||
(1, Some("nec".to_string()), false),
|
||||
(2, None, false),
|
||||
(3, None, true)
|
||||
]
|
||||
);
|
||||
let n: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM vendor_1099_decisions", [], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(n, 0, "nothing an old line didn't say");
|
||||
}
|
||||
|
||||
/// P3 brief 8.2, compatibility: tests/fixtures/old-books/p2_vendor_1099,
|
||||
/// written through the Store API of the version before P3 (a vendor
|
||||
/// marked 1099-NEC, two marked "none" and "", an undecided one whose W-9
|
||||
/// sync wrote a null; payments out of the bank 1000, the members' 2100 and
|
||||
/// a card 2050), opens to its record: its journal byte for byte, no
|
||||
/// feature declared, every null undetermined and "nec" a 1099-NEC from
|
||||
/// the beginning that nobody is named for; and the 1099 figures, by hand,
|
||||
/// for cash 1000 and financing 2100:
|
||||
/// - 2026, threshold 2,000.00: Harbor Design LLC paid 2,500.00 from the
|
||||
/// bank (counts), 700.00 by a member (undetermined) and 400.00 on the
|
||||
/// card (no payment method names 2050: no payment line): reportable,
|
||||
/// 2,500.00 counting whatever the 700.00 turns out. Bigco Supply,
|
||||
/// undecided, 900.00 from the bank: under the threshold, a decision
|
||||
/// needed. Cedar Studio and Dana Studio: nothing in 2026, not listed.
|
||||
/// - 2025, threshold 600.00: Dana Studio, undecided, 650.00 by a member
|
||||
/// with no method: undetermined (it could reach 600.00), W-9 on file;
|
||||
/// Harbor Design LLC listed (decided for a 1099), nothing paid.
|
||||
#[test]
|
||||
fn books_written_under_p2_open_to_their_record() {
|
||||
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/old-books/p2_vendor_1099/journal.jsonl");
|
||||
let dir = test_dir("p2-fixture");
|
||||
std::fs::copy(&fixture, dir.join(JOURNAL_FILE)).unwrap();
|
||||
let journal = std::fs::read(dir.join(JOURNAL_FILE)).unwrap();
|
||||
let store = Store::open(&dir).unwrap();
|
||||
store.verify().unwrap();
|
||||
assert!(store.journal_format().features.is_empty());
|
||||
let l = store.ledger();
|
||||
let by_name = |n: &str| l.list_vendors().into_iter().find(|v| v.name == n).unwrap();
|
||||
assert_eq!(
|
||||
by_name("Harbor Design LLC").decisions_1099,
|
||||
[Dated1099::from_beginning(
|
||||
Decision1099::Files(Form1099::Nec),
|
||||
None
|
||||
)]
|
||||
);
|
||||
for n in ["Bigco Supply", "Cedar Studio", "Dana Studio"] {
|
||||
assert_eq!(
|
||||
by_name(n).decisions_1099,
|
||||
[Dated1099::from_beginning(Decision1099::Undetermined, None)],
|
||||
"{n}"
|
||||
);
|
||||
}
|
||||
assert!(by_name("Dana Studio").tin_on_file);
|
||||
let code = |c: &str| l.chart().find_by_code(c).unwrap().id;
|
||||
let report = |year| {
|
||||
ledger_core::vendor_1099_report(
|
||||
l,
|
||||
year,
|
||||
None,
|
||||
&ledger_core::PaymentAccounts {
|
||||
cash: vec![code("1000")],
|
||||
financing: vec![code("2100")],
|
||||
},
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let rows = |r: &ledger_core::Vendor1099Report| -> Vec<Row1099> {
|
||||
r.vendors
|
||||
.iter()
|
||||
.map(|v| {
|
||||
(
|
||||
v.name.clone(),
|
||||
v.paid,
|
||||
v.undetermined,
|
||||
v.excluded_card,
|
||||
v.status.key(),
|
||||
v.decision_needed,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let c = |n: i64| Decimal::new(n, 2);
|
||||
assert_eq!(
|
||||
rows(&report(2026)),
|
||||
[
|
||||
(
|
||||
"Bigco Supply".into(),
|
||||
c(90_000),
|
||||
c(0),
|
||||
c(0),
|
||||
"under_threshold",
|
||||
true
|
||||
),
|
||||
(
|
||||
"Harbor Design LLC".into(),
|
||||
c(250_000),
|
||||
c(70_000),
|
||||
c(0),
|
||||
"reportable",
|
||||
false
|
||||
),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
rows(&report(2025)),
|
||||
[
|
||||
(
|
||||
"Dana Studio".into(),
|
||||
c(0),
|
||||
c(65_000),
|
||||
c(0),
|
||||
"undetermined",
|
||||
true
|
||||
),
|
||||
// Decided for a 1099 from the beginning: listed, nothing paid.
|
||||
(
|
||||
"Harbor Design LLC".into(),
|
||||
c(0),
|
||||
c(0),
|
||||
c(0),
|
||||
"under_threshold",
|
||||
false
|
||||
),
|
||||
]
|
||||
);
|
||||
drop(store);
|
||||
assert_eq!(std::fs::read(dir.join(JOURNAL_FILE)).unwrap(), journal);
|
||||
}
|
||||
@@ -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, &[]);
|
||||
|
||||
Reference in new issue
Block a user