P3 review: asset bases and the tax-basis K-1
Schema 13 is as shipped and the asset columns are schema 14 (shipped migrations pinned by hash); the tax-basis K-1 leaves net income open (only the return has it) and takes a contributed prepaid plan at its book amount as its section 723 basis; a disposal of contributed property names its 704(c) gain or loss; aliases compare by meaning; fixture amounts made up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
24 files changed
+860
-127
No files matched your search
@@ -18,7 +18,7 @@
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::{AccountType, AssetId, Currency, LedgerError, Money, NewAsset};
|
||||
use ledger_store::journal::read_and_verify_journal;
|
||||
use ledger_store::{JOURNAL_FILE, JournalWriter, Store, StoreError, db_file};
|
||||
use ledger_store::{JOURNAL_FILE, JournalWriter, Store, StoreError, db_file, db_file_for};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -887,6 +887,49 @@ fn downgrade_db_to_v12(db: &Path) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// A database at v13 exactly as it shipped on main before the assets'
|
||||
/// basis columns (P3 review, 2026-10-21: those columns were first written
|
||||
/// into v13 itself, so a v13 file built by the earlier commits opened as
|
||||
/// current and failed to load, "no such column"): its file is another
|
||||
/// schema's, left byte for byte, and this version builds its own from the
|
||||
/// journal, with every run, and verifies.
|
||||
#[test]
|
||||
fn a_v13_file_built_before_the_basis_columns_is_left_and_rebuilt() {
|
||||
use ledger_core::BasisRule::*;
|
||||
let (dir, mut s, _, x, y) = fixture("v13-shipped");
|
||||
s.run_depreciation(x, "2026-01").unwrap();
|
||||
s.set_asset_basis(y, contribution(Carryover723, "640.00", "1200.00"))
|
||||
.unwrap();
|
||||
drop(s);
|
||||
let v13 = dir.join(db_file_for(13));
|
||||
std::fs::rename(dir.join(db_file()), &v13).unwrap();
|
||||
let conn = rusqlite::Connection::open(&v13).unwrap();
|
||||
conn.execute_batch(
|
||||
"ALTER TABLE assets DROP COLUMN fmv_at_contribution;
|
||||
ALTER TABLE assets DROP COLUMN sec704c_method;
|
||||
ALTER TABLE assets DROP COLUMN basis_source_json;
|
||||
CREATE TABLE asset_depreciation_v12(asset_id INTEGER NOT NULL, period TEXT NOT NULL,
|
||||
entry_id INTEGER NOT NULL, amount TEXT NOT NULL, PRIMARY KEY (asset_id, period));
|
||||
INSERT INTO asset_depreciation_v12 SELECT asset_id, period, entry_id, amount FROM asset_depreciation;
|
||||
DROP TABLE asset_depreciation;
|
||||
ALTER TABLE asset_depreciation_v12 RENAME TO asset_depreciation;
|
||||
UPDATE meta SET value = '13' WHERE key = 'schema_version';",
|
||||
)
|
||||
.unwrap();
|
||||
drop(conn);
|
||||
let shipped = std::fs::read(&v13).unwrap();
|
||||
let s = Store::open(&dir).expect("books with a shipped v13 file open");
|
||||
assert_eq!(s.database_file(), db_file());
|
||||
s.verify().unwrap();
|
||||
assert_eq!(s.ledger().depreciation_runs(x).len(), 1);
|
||||
assert_eq!(
|
||||
basis_of(&s, y),
|
||||
Some(contribution(Carryover723, "640.00", "1200.00"))
|
||||
);
|
||||
drop(s);
|
||||
assert_eq!(std::fs::read(&v13).unwrap(), shipped, "left byte for byte");
|
||||
}
|
||||
|
||||
/// Downgrade the database to schema v10 (none of v11's attachment links,
|
||||
/// statements, reconciliations, or payroll_lines.employee_id).
|
||||
fn downgrade_db_to_v10(dir: &Path) {
|
||||
@@ -909,12 +952,12 @@ fn downgrade_db_to_v10(dir: &Path) {
|
||||
}
|
||||
|
||||
/// P2's database (`wanderbooks.db`, schema v12) is left as it is, byte
|
||||
/// for byte: this version builds its own v13 file from the journal, its
|
||||
/// for byte: this version builds its own file from the journal, its
|
||||
/// runs table keyed by entry with every run kept and the new columns
|
||||
/// empty, and it verifies; a basis set after then reloads from it (each
|
||||
/// schema in a file of its own: rules.md B-27).
|
||||
#[test]
|
||||
fn a_p2_database_is_left_and_v13_built_from_the_journal() {
|
||||
fn a_p2_database_is_left_and_this_versions_built_from_the_journal() {
|
||||
use ledger_core::BasisRule::*;
|
||||
let (dir, mut s, _, x, y) = fixture("v12");
|
||||
s.run_depreciation(x, "2026-01").unwrap();
|
||||
@@ -1213,3 +1256,35 @@ fn the_viewer_and_writer_agree_on_a_bad_depreciation_posted_payload() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// B-27: books an older version wrote (format 1, declaring nothing) refuse
|
||||
/// a basis while they wait for the owner's format upgrade, a purchase's
|
||||
/// too, journaling nothing; the feature is never declared on the way (P3
|
||||
/// review, 2026-10-21).
|
||||
#[test]
|
||||
fn a_basis_in_books_pending_their_upgrade_is_refused_and_journals_nothing() {
|
||||
use ledger_core::BasisRule::*;
|
||||
let dir = test_dir("basis-pending");
|
||||
let mut s = Store::create_in_format(
|
||||
&dir,
|
||||
Currency::USD,
|
||||
&ledger_store::format::JournalFormat::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let a = accounts(&mut s);
|
||||
let x = laptop(&mut s, &a, "Laptop A");
|
||||
let was = std::fs::read(dir.join(JOURNAL_FILE)).unwrap();
|
||||
for b in [
|
||||
ledger_core::AssetBasis::purchase(),
|
||||
contribution(Carryover723, "640.00", "1200.00"),
|
||||
] {
|
||||
match s.set_asset_basis(x, b) {
|
||||
Err(StoreError::FormatUpgradePending { need }) => {
|
||||
assert!(need.contains(ledger_store::format::ASSET_BASIS), "{need:?}")
|
||||
}
|
||||
other => panic!("expected FormatUpgradePending, got {other:?}"),
|
||||
}
|
||||
}
|
||||
assert_eq!(std::fs::read(dir.join(JOURNAL_FILE)).unwrap(), was);
|
||||
assert_eq!(basis_of(&s, x), None);
|
||||
}
|
||||
@@ -369,3 +369,36 @@ fn new_books_get_their_schemas_own_file() {
|
||||
assert_eq!(ledger_store::is_database_file(name), is, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A shipped migration's text never changes (P3 review, 2026-10-21: the
|
||||
/// assets' v13 columns were first added to the v13 already on main, so a
|
||||
/// v13 file built before them would open without them). Each schema's
|
||||
/// file is built once by these scripts; a change is a new migration. The
|
||||
/// SHA-256 of each shipped one, v1 to v13 (append the next as it ships).
|
||||
#[test]
|
||||
fn a_shipped_migration_never_changes() {
|
||||
use sha2::{Digest, Sha256};
|
||||
const SHIPPED: [&str; 13] = [
|
||||
"8b253dded247d8518a7454b1c894a147ac211ee32ea2671dca2c586c5184c046",
|
||||
"9fc19f844ed33b8f08eee8437ee3eb038e854737d5febe79ce3ac80abeb18614",
|
||||
"215af9a7a54cda382f721488fedb34f68eb36399730de1b23371d44eb16fd79d",
|
||||
"7923eb31ab8a08f20b8d7df017814095861478cb796af14994dbb85dbe558c20",
|
||||
"95caa91ee3327b5590ad1ddefa76bad189950091cfcd140121f978e0af258e8a",
|
||||
"74181a819beb4349808b9a095fbcda0073890fa6f54bc3fa5535c5a6644e35c6",
|
||||
"df008d2a7cf832173908d1e33f795b88d272eae9cd23a5ebebe7207871d7076f",
|
||||
"d0cd600ef4aa7bc2301e682bace23f3ce311b5f991e5d7bff447a9520ca8e89f",
|
||||
"b2e54e8cf47b7f55f42f3fd4876fce5183e229aea08059518a50cbbffdca2db2",
|
||||
"8881527992c586f69644276db921e640fb6548ee1d2f7c313a46c53e1e63430e",
|
||||
"cc18b31e929b5889c0d09190b3211ccefd9d73df8e1e05c93f9e460097e1553a",
|
||||
"dd2a93d760350f2edd9462f4a52888c0645040582739d3a93b17a3195238d501",
|
||||
"9aa8a0ea7ea15315a4e38281e3a6d4e14beb26333991ebb2824d9c8cc5a51941",
|
||||
];
|
||||
for (i, want) in SHIPPED.iter().enumerate() {
|
||||
let got: String = Sha256::digest(MIGRATIONS[i].as_bytes())
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect();
|
||||
assert_eq!(&got, want, "migration v{} changed after it shipped", i + 1);
|
||||
}
|
||||
assert_eq!(MIGRATIONS.len() as u32, SCHEMA_VERSION);
|
||||
}
|
||||
Reference in new issue
Block a user