refactor(mcp): the party_id, document_id and cash_account aliases are removed (P5)

Agent reason: one name for one thing. S4 and S6 kept the old spellings "until
P5" so an agent that learned them did not break; P5 removes them, so the
schema an agent reads has one parameter for each fact and a call cannot name
the same thing twice with different answers.

books.create_invoice takes customer and books.create_bill vendor (a record's
id, its party's id or its name; required), no party_id.
books.receive_payment and books.pay_bill take invoice or bill and
deposit_to or paid_from, no document_id or cash_account; a payment names its
document and its account or is refused saying which to pass. A number that is
one record's id and another's name is still refused, never guessed (INVARIANT
6).

The over-the-wire tests that used the old spellings now use the new
(document_id becomes invoice or bill, cash_account becomes deposit_to or
paid_from, party_id becomes customer or vendor given as the id's text); the
one test that named the alias, party_id_alias_still_works, is now
a_customer_by_id_and_by_name_is_one_record and checks the same two spellings
of a customer, by id and by name, name one record.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-29 15:48:27 -06:00
1 parent 70a3f795f4
commit a319a64c3a
13 files changed
+81 -135

No files matched your search

+15 -15
View File
@@ -496,18 +496,18 @@ fn terms_fall_back_in_order() {
assert_eq!(bill["terms"], "Net 10", "{bill}"); assert_eq!(bill["terms"], "Net 10", "{bill}");
} }
/// `party_id` is the old spelling of `customer`/`vendor` and still names /// A customer given by id and by name is the same record (the old
/// the same record (until P5); a number that is one record's id and /// `party_id` spelling is gone, P5); a number that is one record's id and
/// another's name is refused, never guessed. /// another's name is refused, never guessed.
#[test] #[test]
fn party_id_alias_still_works() { fn a_customer_by_id_and_by_name_is_one_record() {
let books = Books::new("documents-alias"); let books = Books::new("documents-alias");
let mut m = Mcp::spawn(&books); let mut m = Mcp::spawn(&books);
let harbor = m.customer("Harbor Coffee"); let harbor = m.customer("Harbor Coffee");
let line = json!([{"description": "Work", "amount": "10.00"}]); let line = json!([{"description": "Work", "amount": "10.00"}]);
let by_alias = m.call( let by_id = m.call(
"books.create_invoice", "books.create_invoice",
json!({"party_id": harbor, "date": "2026-02-01", "terms": "Net 30", json!({"customer": (harbor).to_string(), "date": "2026-02-01", "terms": "Net 30",
"lines": line, "confirmed": true}), "lines": line, "confirmed": true}),
); );
let by_name = m.call( let by_name = m.call(
@@ -520,8 +520,8 @@ fn party_id_alias_still_works() {
.iter() .iter()
.map(|e| e["payload"]["customer_id"].as_u64().unwrap()) .map(|e| e["payload"]["customer_id"].as_u64().unwrap())
.collect(); .collect();
assert_eq!(parties, [harbor, harbor], "one customer, two spellings"); assert_eq!(parties, [harbor, harbor], "one customer, an id and a name");
assert_eq!(by_alias["total"], by_name["total"]); assert_eq!(by_id["total"], by_name["total"]);
// A customer named "2" that is not customer 2: the number is // A customer named "2" that is not customer 2: the number is
// ambiguous, and is refused rather than guessed. // ambiguous, and is refused rather than guessed.
@@ -730,7 +730,7 @@ fn mark_sent_records_who_and_how() {
.unwrap(); .unwrap();
let invoice = common::answer_of(&wire.call( let invoice = common::answer_of(&wire.call(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-03-01", "due_date": "2026-03-31", json!({"customer": (customer).to_string(), "date": "2026-03-01", "due_date": "2026-03-31",
"lines": [{"description": "Design", "amount": "1200.00"}], "confirmed": true}), "lines": [{"description": "Design", "amount": "1200.00"}], "confirmed": true}),
)) ))
.unwrap()["id"] .unwrap()["id"]
@@ -876,7 +876,7 @@ mod paying_on_terms {
ok( ok(
&mut self.wire, &mut self.wire,
"books.create_invoice", "books.create_invoice",
json!({"party_id": self.customer, "date": date, "due_date": due, json!({"customer": (self.customer).to_string(), "date": date, "due_date": due,
"lines": [{"description": "Consulting", "amount": amount}], "lines": [{"description": "Consulting", "amount": amount}],
"confirmed": true}), "confirmed": true}),
)["id"] )["id"]
@@ -897,7 +897,7 @@ mod paying_on_terms {
ok( ok(
&mut self.wire, &mut self.wire,
"books.create_bill", "books.create_bill",
json!({"party_id": self.vendor, "date": date, "due_date": due, json!({"vendor": (self.vendor).to_string(), "date": date, "due_date": due,
"vendor_ref": format!("TH-{}", self.bills), "vendor_ref": format!("TH-{}", self.bills),
"lines": [{"description": "Paper", "amount": amount, "account": "6999"}], "lines": [{"description": "Paper", "amount": amount, "account": "6999"}],
"upload_id": file, "confirmed": true}), "upload_id": file, "confirmed": true}),
@@ -1258,7 +1258,7 @@ mod bad_debts {
wire, wire,
"books.create_invoice", "books.create_invoice",
json!({ json!({
"party_id": customer, "customer": (customer).to_string(),
"date": "2026-01-05", "date": "2026-01-05",
"due_date": "2026-02-04", "due_date": "2026-02-04",
"lines": [{ "description": "Design", "amount": "100.00" }], "lines": [{ "description": "Design", "amount": "100.00" }],
@@ -1286,10 +1286,10 @@ mod bad_debts {
&mut wire, &mut wire,
"books.receive_payment", "books.receive_payment",
json!({"confirmed": true, json!({"confirmed": true,
"document_id": inv, "invoice": inv,
"date": "2026-01-20", "date": "2026-01-20",
"amount": "40.00", "amount": "40.00",
"cash_account": "1000", "deposit_to": "1000",
}), }),
); );
// The write-off's evidence, uploaded as an agent uploads it (D-46). // The write-off's evidence, uploaded as an agent uploads it (D-46).
@@ -1361,10 +1361,10 @@ mod bad_debts {
&mut wire, &mut wire,
"books.receive_payment", "books.receive_payment",
json!({"confirmed": true, json!({"confirmed": true,
"document_id": inv, "invoice": inv,
"date": "2026-03-02", "date": "2026-03-02",
"amount": "60.00", "amount": "60.00",
"cash_account": "1000", "deposit_to": "1000",
}), }),
); );
let doc = ok( let doc = ok(
+2 -2
View File
@@ -767,7 +767,7 @@ fn books_p2_served_cut_over_whole() {
let invoice = a let invoice = a
.call( .call(
"books.create_invoice", "books.create_invoice",
json!({"party_id": 1, "date": "2026-01-05", "due_date": "2026-02-04", json!({"customer": (1).to_string(), "date": "2026-01-05", "due_date": "2026-02-04",
"lines": [{"description": "Design", "amount": "400.00"}], "confirmed": true}), "lines": [{"description": "Design", "amount": "400.00"}], "confirmed": true}),
) )
.unwrap()["id"] .unwrap()["id"]
@@ -789,7 +789,7 @@ fn books_p2_served_cut_over_whole() {
let unpaid = a let unpaid = a
.call( .call(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-01-10", "due_date": "2026-02-09", json!({"customer": (customer).to_string(), "date": "2026-01-10", "due_date": "2026-02-09",
"lines": [{"description": "Survey", "amount": "300.00"}], "confirmed": true}), "lines": [{"description": "Survey", "amount": "300.00"}], "confirmed": true}),
) )
.unwrap()["id"] .unwrap()["id"]
+3 -3
View File
@@ -1017,13 +1017,13 @@ fn inputs_that_once_panicked_are_refused_or_answered() {
), ),
( (
"books.create_invoice", "books.create_invoice",
json!({"party_id": client, "date": "2026-05-01", "due_date": "2026-06-01", json!({"customer": (client).to_string(), "date": "2026-05-01", "due_date": "2026-06-01",
"lines": [{"description": "a", "amount": MAX}, "lines": [{"description": "a", "amount": MAX},
{"description": "b", "amount": "1"}], "confirmed": true}), {"description": "b", "amount": "1"}], "confirmed": true}),
), ),
( (
"books.create_bill", "books.create_bill",
json!({"party_id": acme, "date": "2026-05-01", "due_date": "2026-06-01", json!({"vendor": (acme).to_string(), "date": "2026-05-01", "due_date": "2026-06-01",
"vendor_ref": "H-1", "vendor_ref": "H-1",
"lines": [{"description": "a", "amount": MAX, "account": "6000"}, "lines": [{"description": "a", "amount": MAX, "account": "6000"},
{"description": "b", "amount": "1", "account": "6000"}], {"description": "b", "amount": "1", "account": "6000"}],
@@ -1031,7 +1031,7 @@ fn inputs_that_once_panicked_are_refused_or_answered() {
), ),
( (
"books.create_invoice", "books.create_invoice",
json!({"party_id": client, "date": "2026-05-01", "due_date": "+262142-12-31", json!({"customer": (client).to_string(), "date": "2026-05-01", "due_date": "+262142-12-31",
"lines": [{"description": "a", "amount": "1"}], "confirmed": true}), "lines": [{"description": "a", "amount": "1"}], "confirmed": true}),
), ),
( (
+1 -1
View File
@@ -274,7 +274,7 @@ fn invoiced_and_billed_project_figures_are_the_same_in_the_viewer_and_from_the_s
let bill = server.tool("books.create_bill", bill_args); let bill = server.tool("books.create_bill", bill_args);
server.tool( server.tool(
"books.receive_payment", "books.receive_payment",
json!({"confirmed": true, "document_id": inv["id"], "date": "2026-02-20", "amount": "1830.00", "cash_account": "1000"}), json!({"confirmed": true, "invoice": inv["id"], "date": "2026-02-20", "amount": "1830.00", "deposit_to": "1000"}),
); );
let b = data::load_once(&Source { let b = data::load_once(&Source {
data_dir: data.clone(), data_dir: data.clone(),
+2 -2
View File
@@ -424,7 +424,7 @@ fn a_file_links_to_every_kind_of_subject() {
let rent_invoice = m.upload(b"rent invoice", "rent.pdf", "application/pdf", "receipt"); let rent_invoice = m.upload(b"rent invoice", "rent.pdf", "application/pdf", "receipt");
let bill = m.ok( let bill = m.ok(
"books.create_bill", "books.create_bill",
json!({"party_id": vendor, "date": "2026-02-01", "due_date": "2026-02-28", json!({"vendor": (vendor).to_string(), "date": "2026-02-01", "due_date": "2026-02-28",
"vendor_ref": "RENT-2", "vendor_ref": "RENT-2",
"lines": [{"description": "Rent", "amount": "300.00", "account": "6000"}], "lines": [{"description": "Rent", "amount": "300.00", "account": "6000"}],
"upload_id": rent_invoice, "confirmed": true}), "upload_id": rent_invoice, "confirmed": true}),
@@ -432,7 +432,7 @@ fn a_file_links_to_every_kind_of_subject() {
.clone(); .clone();
let invoice = m.ok( let invoice = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-02-01", "due_date": "2026-03-01", json!({"customer": (customer).to_string(), "date": "2026-02-01", "due_date": "2026-03-01",
"lines": [{"description": "Work", "amount": "500.00"}], "lines": [{"description": "Work", "amount": "500.00"}],
"confirmed": true}), "confirmed": true}),
)["id"] )["id"]
+17 -17
View File
@@ -911,7 +911,7 @@ fn happy_path(name: &str) -> Mcp {
// A/R and A/P. // A/R and A/P.
let inv = m.ok( let inv = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-02-01", "due_date": "2026-03-01", json!({"customer": (customer).to_string(), "date": "2026-02-01", "due_date": "2026-03-01",
"lines": [{"description": "Consulting", "amount": "500.00"}], "lines": [{"description": "Consulting", "amount": "500.00"}],
"confirmed": true}), "confirmed": true}),
)["id"] )["id"]
@@ -919,7 +919,7 @@ fn happy_path(name: &str) -> Mcp {
.unwrap(); .unwrap();
m.ok( m.ok(
"books.receive_payment", "books.receive_payment",
json!({"confirmed": true, "document_id": inv, "date": "2026-02-10", "amount": "200.00", "cash_account": "1000"}), json!({"confirmed": true, "invoice": inv, "date": "2026-02-10", "amount": "200.00", "deposit_to": "1000"}),
); );
let vendor_invoice = m.upload( let vendor_invoice = m.upload(
b"vendor invoice", b"vendor invoice",
@@ -929,7 +929,7 @@ fn happy_path(name: &str) -> Mcp {
); );
let bill = m.ok( let bill = m.ok(
"books.create_bill", "books.create_bill",
json!({"party_id": vendor, "date": "2026-02-01", "due_date": "2026-02-28", json!({"vendor": (vendor).to_string(), "date": "2026-02-01", "due_date": "2026-02-28",
"vendor_ref": "V-1001", "vendor_ref": "V-1001",
"lines": [{"description": "Office rent", "amount": "300.00", "account": "6000"}], "lines": [{"description": "Office rent", "amount": "300.00", "account": "6000"}],
"upload_id": vendor_invoice, "confirmed": true}), "upload_id": vendor_invoice, "confirmed": true}),
@@ -938,7 +938,7 @@ fn happy_path(name: &str) -> Mcp {
.unwrap(); .unwrap();
m.ok( m.ok(
"books.pay_bill", "books.pay_bill",
json!({"confirmed": true, "document_id": bill, "date": "2026-02-05", "amount": "300.00", "cash_account": "1000"}), json!({"confirmed": true, "bill": bill, "date": "2026-02-05", "amount": "300.00", "paid_from": "1000"}),
); );
m.ok("books.ar_aging", json!({"as_of": "2026-06-30"})); m.ok("books.ar_aging", json!({"as_of": "2026-06-30"}));
m.ok("books.ap_aging", json!({"as_of": "2026-06-30"})); m.ok("books.ap_aging", json!({"as_of": "2026-06-30"}));
@@ -946,7 +946,7 @@ fn happy_path(name: &str) -> Mcp {
// P4b S1: a document voided, and the control accounts' tie-out. // P4b S1: a document voided, and the control accounts' tie-out.
let dup = m.ok( let dup = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-02-02", "due_date": "2026-03-02", json!({"customer": (customer).to_string(), "date": "2026-02-02", "due_date": "2026-03-02",
"lines": [{"description": "Consulting, entered twice", "amount": "500.00"}], "lines": [{"description": "Consulting, entered twice", "amount": "500.00"}],
"confirmed": true}), "confirmed": true}),
)["id"] )["id"]
@@ -972,7 +972,7 @@ fn happy_path(name: &str) -> Mcp {
// P4b S7: a bad debt, its evidence uploaded and filed with it. // P4b S7: a bad debt, its evidence uploaded and filed with it.
let bad = m.ok( let bad = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-02-03", "due_date": "2026-03-03", json!({"customer": (customer).to_string(), "date": "2026-02-03", "due_date": "2026-03-03",
"lines": [{"description": "Consulting, never paid", "amount": "400.00"}], "lines": [{"description": "Consulting, never paid", "amount": "400.00"}],
"confirmed": true}), "confirmed": true}),
)["id"] )["id"]
@@ -1685,15 +1685,15 @@ fn run_op(m: &mut Mcp, k: &mut Known, op: &Op) {
vendor_ref, vendor_ref,
} => ( } => (
"books.create_bill", "books.create_bill",
json!({"party_id": party.resolve(&k.vendors), "date": "2026-03-01", json!({"vendor": (party.resolve(&k.vendors)).to_string(), "date": "2026-03-01",
"due_date": "2026-03-31", "vendor_ref": vendor_ref, "due_date": "2026-03-31", "vendor_ref": vendor_ref,
"lines": [{"description": "thing", "amount": amount, "account": "6000"}], "lines": [{"description": "thing", "amount": amount, "account": "6000"}],
"existing_attachment_id": k.vendor_invoice, "confirmed": true}), "existing_attachment_id": k.vendor_invoice, "confirmed": true}),
), ),
Op::PayBill { doc, amount } => ( Op::PayBill { doc, amount } => (
"books.pay_bill", "books.pay_bill",
json!({"confirmed": true, "document_id": doc.resolve(&k.bills), "date": "2026-03-15", "amount": amount, json!({"confirmed": true, "bill": doc.resolve(&k.bills), "date": "2026-03-15", "amount": amount,
"cash_account": "1000"}), "paid_from": "1000"}),
), ),
Op::Payout { deposit } => ( Op::Payout { deposit } => (
"books.record_payout", "books.record_payout",
@@ -1834,7 +1834,7 @@ fn inv3_other_tools_refuse_ungrammatical_amounts() {
); );
m.refuse( m.refuse(
"books.create_bill", "books.create_bill",
json!({"party_id": vendor, "date": "2026-03-01", "due_date": "2026-03-31", json!({"vendor": (vendor).to_string(), "date": "2026-03-01", "due_date": "2026-03-31",
"lines": [{"description": "x", "amount": bad}]}), "lines": [{"description": "x", "amount": bad}]}),
); );
m.refuse( m.refuse(
@@ -2591,7 +2591,7 @@ fn assigned_roles_steer_invoices() {
.unwrap(); .unwrap();
m.ok( m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-02-01", "due_date": "2026-03-01", json!({"customer": (customer).to_string(), "date": "2026-02-01", "due_date": "2026-03-01",
"lines": [{"description": "Consulting", "amount": "500.00"}], "lines": [{"description": "Consulting", "amount": "500.00"}],
"confirmed": true}), "confirmed": true}),
); );
@@ -2654,7 +2654,7 @@ fn documents_void_and_tie_out_over_the_wire() {
); );
let inv = m.ok( let inv = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-02-01", "due_date": "2026-03-01", json!({"customer": (customer).to_string(), "date": "2026-02-01", "due_date": "2026-03-01",
"lines": [{"description": "Consulting", "amount": "500.00"}], "lines": [{"description": "Consulting", "amount": "500.00"}],
"confirmed": true}), "confirmed": true}),
)["id"] )["id"]
@@ -2662,8 +2662,8 @@ fn documents_void_and_tie_out_over_the_wire() {
.unwrap(); .unwrap();
let paid = m.ok( let paid = m.ok(
"books.receive_payment", "books.receive_payment",
json!({"confirmed": true, "document_id": inv, "date": "2026-02-10", "amount": "200.00", json!({"confirmed": true, "invoice": inv, "date": "2026-02-10", "amount": "200.00",
"cash_account": "1000", "instrument": "Harbor checking", "reference": "tr_8812"}), "deposit_to": "1000", "instrument": "Harbor checking", "reference": "tr_8812"}),
); );
assert_eq!(paid["remaining"], "300.00", "{paid}"); assert_eq!(paid["remaining"], "300.00", "{paid}");
assert_eq!(paid["status"], "partial", "{paid}"); assert_eq!(paid["status"], "partial", "{paid}");
@@ -2685,7 +2685,7 @@ fn documents_void_and_tie_out_over_the_wire() {
let dup = m.ok( let dup = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": "2026-02-02", "due_date": "2026-03-02", json!({"customer": (customer).to_string(), "date": "2026-02-02", "due_date": "2026-03-02",
"lines": [{"description": "Consulting, entered twice", "amount": "500.00"}], "lines": [{"description": "Consulting, entered twice", "amount": "500.00"}],
"confirmed": true}), "confirmed": true}),
)["id"] )["id"]
@@ -2753,7 +2753,7 @@ fn documents_list_and_timeline_over_the_wire() {
let invoice = |m: &mut Mcp, date: &str, due: &str, amount: &str| { let invoice = |m: &mut Mcp, date: &str, due: &str, amount: &str| {
m.ok( m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": customer, "date": date, "due_date": due, json!({"customer": (customer).to_string(), "date": date, "due_date": due,
"lines": [{"description": "Consulting", "amount": amount}], "lines": [{"description": "Consulting", "amount": amount}],
"confirmed": true}), "confirmed": true}),
)["id"] )["id"]
@@ -2764,7 +2764,7 @@ fn documents_list_and_timeline_over_the_wire() {
let later = invoice(&mut m, "2026-02-05", "2026-04-30", "80.00"); let later = invoice(&mut m, "2026-02-05", "2026-04-30", "80.00");
let pay = m.ok( let pay = m.ok(
"books.receive_payment", "books.receive_payment",
json!({"confirmed": true, "document_id": late, "date": "2026-02-10", "amount": "200.00", "cash_account": "1000"}), json!({"confirmed": true, "invoice": late, "date": "2026-02-10", "amount": "200.00", "deposit_to": "1000"}),
)["entry_id"] )["entry_id"]
.as_u64() .as_u64()
.unwrap(); .unwrap();
+6 -6
View File
@@ -835,15 +835,15 @@ fn party_totals_match_a_hand_computed_year() {
); );
let bill = m.ok( let bill = m.ok(
"books.create_bill", "books.create_bill",
json!({"party_id": v, "date": "2026-07-01", "due_date": "2026-07-31", json!({"vendor": (v).to_string(), "date": "2026-07-01", "due_date": "2026-07-31",
"vendor_ref": "D-1041", "vendor_ref": "D-1041",
"lines": [{"description": "Logo", "amount": "800.00", "account": "6000"}], "lines": [{"description": "Logo", "amount": "800.00", "account": "6000"}],
"upload_id": logo_invoice, "confirmed": true}), "upload_id": logo_invoice, "confirmed": true}),
); );
let pay = m.ok( let pay = m.ok(
"books.pay_bill", "books.pay_bill",
json!({"confirmed": true, "document_id": bill["id"], "date": "2026-07-15", "amount": "300.00", json!({"confirmed": true, "bill": bill["id"], "date": "2026-07-15", "amount": "300.00",
"cash_account": "1000"}), "paid_from": "1000"}),
)["entry_id"] )["entry_id"]
.as_u64() .as_u64()
.unwrap(); .unwrap();
@@ -917,14 +917,14 @@ fn party_totals_for_a_customer_a_member_and_a_prepaid() {
.unwrap(); .unwrap();
let inv = m.ok( let inv = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": c, "date": "2026-03-01", "due_date": "2026-03-31", json!({"customer": (c).to_string(), "date": "2026-03-01", "due_date": "2026-03-31",
"lines": [{"description": "Work", "amount": "1000.00"}], "lines": [{"description": "Work", "amount": "1000.00"}],
"confirmed": true}), "confirmed": true}),
); );
let pay = m.ok( let pay = m.ok(
"books.receive_payment", "books.receive_payment",
json!({"confirmed": true, "document_id": inv["id"], "date": "2026-03-20", "amount": "600.00", json!({"confirmed": true, "invoice": inv["id"], "date": "2026-03-20", "amount": "600.00",
"cash_account": "1000"}), "deposit_to": "1000"}),
)["entry_id"] )["entry_id"]
.as_u64() .as_u64()
.unwrap(); .unwrap();
+10 -10
View File
@@ -1231,7 +1231,7 @@ fn invoice_and_bill_lines_count_toward_their_projects() {
); );
let inv = m.ok( let inv = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": cedar, "date": "2026-03-02", "due_date": "2026-04-01", "lines": [ json!({"customer": (cedar).to_string(), "date": "2026-03-02", "due_date": "2026-04-01", "lines": [
{"description": "Design", "amount": "3000.00", "class": "Zeta site"}, {"description": "Design", "amount": "3000.00", "class": "Zeta site"},
{"description": "Hosting", "amount": "200.00", "class": harbor.to_string()}, {"description": "Hosting", "amount": "200.00", "class": harbor.to_string()},
{"description": "Rush fee", "amount": "100.00"}, {"description": "Rush fee", "amount": "100.00"},
@@ -1264,7 +1264,7 @@ fn invoice_and_bill_lines_count_toward_their_projects() {
); );
let bill = m.ok( let bill = m.ok(
"books.create_bill", "books.create_bill",
json!({"party_id": dana_vendor, "date": "2026-03-09", "due_date": "2026-04-08", json!({"vendor": (dana_vendor).to_string(), "date": "2026-03-09", "due_date": "2026-04-08",
"vendor_ref": "DANA-77", "vendor_ref": "DANA-77",
"lines": [ "lines": [
{"description": "Illustration", "amount": "1200.00", "account": "6000", "class": "Zeta site"}, {"description": "Illustration", "amount": "1200.00", "account": "6000", "class": "Zeta site"},
@@ -1284,11 +1284,11 @@ fn invoice_and_bill_lines_count_toward_their_projects() {
// Payments are balance-sheet only: no project. // Payments are balance-sheet only: no project.
let got = m.ok( let got = m.ok(
"books.receive_payment", "books.receive_payment",
json!({"confirmed": true, "document_id": inv["id"], "date": "2026-03-20", "amount": "3800.00", "cash_account": "1000"}), json!({"confirmed": true, "invoice": inv["id"], "date": "2026-03-20", "amount": "3800.00", "deposit_to": "1000"}),
); );
let paid = m.ok( let paid = m.ok(
"books.pay_bill", "books.pay_bill",
json!({"confirmed": true, "document_id": bill["id"], "date": "2026-03-30", "amount": "1250.00", "cash_account": "1000"}), json!({"confirmed": true, "bill": bill["id"], "date": "2026-03-30", "amount": "1250.00", "paid_from": "1000"}),
); );
for e in [&got["entry_id"], &paid["entry_id"]] { for e in [&got["entry_id"], &paid["entry_id"]] {
let e = e.as_u64().unwrap(); let e = e.as_u64().unwrap();
@@ -1356,7 +1356,7 @@ fn a_document_naming_an_unknown_project_is_refused_whole() {
for (tool, party) in [("books.create_invoice", &cedar), ("books.create_bill", &v)] { for (tool, party) in [("books.create_invoice", &cedar), ("books.create_bill", &v)] {
let why = m.refuse( let why = m.refuse(
tool, tool,
json!({"party_id": party, "date": "2026-03-02", "due_date": "2026-04-01", "lines": [ json!({"customer": (party).to_string(), "date": "2026-03-02", "due_date": "2026-04-01", "lines": [
{"description": "a", "amount": "10.00", "class": "Zeta site"}, {"description": "a", "amount": "10.00", "class": "Zeta site"},
{"description": "b", "amount": "10.00", "class": "Nowhere"}, {"description": "b", "amount": "10.00", "class": "Nowhere"},
]}), ]}),
@@ -1379,7 +1379,7 @@ fn a_document_for_a_finished_project_says_so() {
let cedar = m.ok("books.add_customer", json!({"name": "Cedar"}))["id"].clone(); let cedar = m.ok("books.add_customer", json!({"name": "Cedar"}))["id"].clone();
let out = m.ok( let out = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": cedar, "date": "2026-10-05", "due_date": "2026-11-04", "lines": [ json!({"customer": (cedar).to_string(), "date": "2026-10-05", "due_date": "2026-11-04", "lines": [
{"description": "Final", "amount": "10.00", "class": "Harbor"}, {"description": "Final", "amount": "10.00", "class": "Harbor"},
{"description": "Again", "amount": "10.00", "class": "harbor"}, {"description": "Again", "amount": "10.00", "class": "harbor"},
{"description": "Old", "amount": "10.00", "class": "Dock"}, {"description": "Old", "amount": "10.00", "class": "Dock"},
@@ -1424,7 +1424,7 @@ fn an_invoice_for_an_on_spec_project_asks_about_an_agreement() {
let v = m.ok("books.add_vendor", json!({"name": "Dana"}))["id"].clone(); let v = m.ok("books.add_vendor", json!({"name": "Dana"}))["id"].clone();
let out = m.ok( let out = m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": cedar, "date": "2026-10-05", "due_date": "2026-11-04", "lines": [ json!({"customer": (cedar).to_string(), "date": "2026-10-05", "due_date": "2026-11-04", "lines": [
{"description": "Build", "amount": "10.00", "class": "Kestrel"}, {"description": "Build", "amount": "10.00", "class": "Kestrel"},
{"description": "More", "amount": "5.00", "class": "kestrel"}, {"description": "More", "amount": "5.00", "class": "kestrel"},
{"description": "Site", "amount": "7.00", "class": "Zeta"}, {"description": "Site", "amount": "7.00", "class": "Zeta"},
@@ -1450,7 +1450,7 @@ fn an_invoice_for_an_on_spec_project_asks_about_an_agreement() {
); );
let bill = m.ok( let bill = m.ok(
"books.create_bill", "books.create_bill",
json!({"party_id": v, "date": "2026-10-05", "due_date": "2026-11-04", json!({"vendor": (v).to_string(), "date": "2026-10-05", "due_date": "2026-11-04",
"vendor_ref": "V-512", "vendor_ref": "V-512",
"lines": [ "lines": [
{"description": "Icons", "amount": "10.00", "account": "6000", "class": "Kestrel"}, {"description": "Icons", "amount": "10.00", "account": "6000", "class": "Kestrel"},
@@ -1475,7 +1475,7 @@ fn books_without_the_feature_refuse_a_tagged_document() {
for (tool, party) in [("books.create_invoice", &cedar), ("books.create_bill", &v)] { for (tool, party) in [("books.create_invoice", &cedar), ("books.create_bill", &v)] {
let why = m.refuse( let why = m.refuse(
tool, tool,
json!({"party_id": party, "date": "2026-03-02", "due_date": "2026-04-01", "lines": [ json!({"customer": (party).to_string(), "date": "2026-03-02", "due_date": "2026-04-01", "lines": [
{"description": "a", "amount": "10.00", "class": "Zeta site"}, {"description": "a", "amount": "10.00", "class": "Zeta site"},
]}), ]}),
); );
@@ -1487,7 +1487,7 @@ fn books_without_the_feature_refuse_a_tagged_document() {
assert_eq!(m.journal(), head, "nothing journaled"); assert_eq!(m.journal(), head, "nothing journaled");
m.ok( m.ok(
"books.create_invoice", "books.create_invoice",
json!({"party_id": cedar, "date": "2026-03-02", "due_date": "2026-04-01", "lines": [ json!({"customer": (cedar).to_string(), "date": "2026-03-02", "due_date": "2026-04-01", "lines": [
{"description": "a", "amount": "10.00"}, {"description": "a", "amount": "10.00"},
], "confirmed": true}), ], "confirmed": true}),
); );
+2 -2
View File
@@ -278,7 +278,7 @@ fn require_tax_category_refuses_defect_8() {
); );
let bill = m.call( let bill = m.call(
"books.create_bill", "books.create_bill",
json!({"party_id": vendor["id"], "date": "2026-02-01", "due_date": "2026-03-01", json!({"vendor": (vendor["id"]).to_string(), "date": "2026-02-01", "due_date": "2026-03-01",
"vendor_ref": "CH-1", "vendor_ref": "CH-1",
"lines": [{"description": "hosting", "amount": "42.00", "account": "6150"}], "lines": [{"description": "hosting", "amount": "42.00", "account": "6150"}],
"upload_id": first_invoice, "confirmed": true}), "upload_id": first_invoice, "confirmed": true}),
@@ -303,7 +303,7 @@ fn require_tax_category_refuses_defect_8() {
); );
let again = m.call( let again = m.call(
"books.create_bill", "books.create_bill",
json!({"party_id": vendor["id"], "date": "2026-02-02", "due_date": "2026-03-02", json!({"vendor": (vendor["id"]).to_string(), "date": "2026-02-02", "due_date": "2026-03-02",
"vendor_ref": "CH-2", "vendor_ref": "CH-2",
"lines": [{"description": "hosting", "amount": "1.00", "account": "6150"}], "lines": [{"description": "hosting", "amount": "1.00", "account": "6150"}],
"upload_id": second_invoice, "confirmed": true}), "upload_id": second_invoice, "confirmed": true}),
+2 -22
View File
@@ -455,17 +455,7 @@ impl BooksServer {
) -> Result<Json<InvoiceOut>, String> { ) -> Result<Json<InvoiceOut>, String> {
let p = params.0; let p = params.0;
let mut store = self.lock()?; let mut store = self.lock()?;
let customer = match (&p.customer, p.party_id) { let customer = p.customer.trim().to_string();
(Some(c), None) => c.trim().to_string(),
(None, Some(id)) => id.to_string(),
(Some(c), Some(id)) if c.trim() == id.to_string() => c.trim().to_string(),
(Some(_), Some(_)) => {
return Err("give the customer once: customer or party_id, not both".into());
}
(None, None) => {
return Err("give customer: a customer id, a party id or a name".into());
}
};
let customer_id = resolve_customer(&store, &customer)?; let customer_id = resolve_customer(&store, &customer)?;
let date = parse_date(&p.date)?; let date = parse_date(&p.date)?;
let lines = document_lines(&store, p.lines)?; let lines = document_lines(&store, p.lines)?;
@@ -606,17 +596,7 @@ impl BooksServer {
) -> Result<Json<BillOut>, String> { ) -> Result<Json<BillOut>, String> {
let p = params.0; let p = params.0;
let mut store = self.lock()?; let mut store = self.lock()?;
let vendor = match (&p.vendor, p.party_id) { let vendor = p.vendor.trim().to_string();
(Some(v), None) => v.trim().to_string(),
(None, Some(id)) => id.to_string(),
(Some(v), Some(id)) if v.trim() == id.to_string() => v.trim().to_string(),
(Some(_), Some(_)) => {
return Err("give the vendor once: vendor or party_id, not both".into());
}
(None, None) => {
return Err("give vendor: a vendor id, a party id or a name".into());
}
};
let vendor_id = resolve_vendor_arg(&store, &vendor)?; let vendor_id = resolve_vendor_arg(&store, &vendor)?;
let date = parse_date(&p.date)?; let date = parse_date(&p.date)?;
let received_on = p.received_on.as_deref().map(parse_date).transpose()?; let received_on = p.received_on.as_deref().map(parse_date).transpose()?;
+9 -29
View File
@@ -39,35 +39,17 @@ fn doc_party(ledger: &Ledger, doc: ledger_core::DocRef) -> Result<u64, String> {
} }
} }
/// The one id a payment tool's single form names: `invoice`/`bill`, or /// The one id a payment tool's single form names: `invoice` or `bill`.
/// the older `document_id`. Both at once is refused unless they agree. fn single_document(primary: Option<u64>, what: &str) -> Result<u64, String> {
fn single_document(primary: Option<u64>, alias: Option<u64>, what: &str) -> Result<u64, String> { primary.ok_or_else(|| format!("pass {what} (or an applications list): see books.documents"))
match (primary, alias) {
(Some(a), Some(b)) if a != b => Err(format!(
"pass {what} or document_id, not both: {} and {b} differ",
a
)),
(Some(id), _) | (None, Some(id)) => Ok(id),
(None, None) => Err(format!(
"pass {what} (or an applications list): see books.documents"
)),
}
} }
/// The one account a payment tool's single form names: `deposit_to`/ /// The one account a payment tool's single form names: `deposit_to` or
/// `paid_from`, or the older `cash_account`. /// `paid_from`.
fn single_account( fn single_account(primary: &Option<String>, what: &str) -> Result<String, String> {
primary: &Option<String>, primary
alias: &Option<String>, .clone()
what: &str, .ok_or_else(|| format!("pass {what}: the account the cash moves through"))
) -> Result<String, String> {
match (primary, alias) {
(Some(a), Some(b)) if a != b => Err(format!(
"pass {what} or cash_account, not both: {a:?} and {b:?} differ"
)),
(Some(a), _) | (None, Some(a)) => Ok(a.clone()),
(None, None) => Err(format!("pass {what} (or cash_account)")),
}
} }
/// Every document a payment tool's parameters name, as the core takes /// Every document a payment tool's parameters name, as the core takes
@@ -128,7 +110,6 @@ fn applications(
} }
let id = single_document( let id = single_document(
if invoices { p.invoice } else { p.bill }, if invoices { p.invoice } else { p.bill },
p.document_id,
if invoices { "invoice" } else { "bill" }, if invoices { "invoice" } else { "bill" },
)?; )?;
let amount = p let amount = p
@@ -178,7 +159,6 @@ fn settlement(
} else { } else {
&p.paid_from &p.paid_from
}, },
&p.cash_account,
if invoices { "deposit_to" } else { "paid_from" }, if invoices { "deposit_to" } else { "paid_from" },
)?, )?,
)?; )?;
+2 -16
View File
@@ -3265,10 +3265,7 @@ pub struct CreateInvoiceParams {
pub preview: Option<String>, pub preview: Option<String>,
/// The customer: a customer id, its party's id, or its name. A name /// The customer: a customer id, its party's id, or its name. A name
/// that two customers answer to is refused. /// that two customers answer to is refused.
pub customer: Option<String>, pub customer: String,
/// The old spelling of `customer` (accepted until P5): a customer id.
#[serde(default)]
pub party_id: Option<u64>,
/// The invoice date, YYYY-MM-DD. /// The invoice date, YYYY-MM-DD.
pub date: String, pub date: String,
/// Its payment terms ("Net 30", "Net 15 EOM", "2/10 net 30", "Due on /// Its payment terms ("Net 30", "Net 15 EOM", "2/10 net 30", "Due on
@@ -3300,10 +3297,7 @@ pub struct CreateBillParams {
pub preview: Option<String>, pub preview: Option<String>,
/// The vendor: a vendor id, its party's id, or its name. A name that /// The vendor: a vendor id, its party's id, or its name. A name that
/// two vendors answer to is refused. /// two vendors answer to is refused.
pub vendor: Option<String>, pub vendor: String,
/// The old spelling of `vendor` (accepted until P5): a vendor id.
#[serde(default)]
pub party_id: Option<u64>,
/// The bill's date, YYYY-MM-DD. /// The bill's date, YYYY-MM-DD.
pub date: String, pub date: String,
/// The day the bill was received, when it differs from its date. /// The day the bill was received, when it differs from its date.
@@ -3383,10 +3377,6 @@ pub struct PaymentParams {
/// preview: nothing posts. /// preview: nothing posts.
#[serde(default)] #[serde(default)]
pub confirmed: Option<bool>, pub confirmed: Option<bool>,
/// Invoice id (receive_payment) or bill id (pay_bill); the older name
/// of `invoice` / `bill`.
#[serde(default)]
pub document_id: Option<u64>,
/// The invoice, by id (books.receive_payment). /// The invoice, by id (books.receive_payment).
#[serde(default)] #[serde(default)]
pub invoice: Option<u64>, pub invoice: Option<u64>,
@@ -3399,10 +3389,6 @@ pub struct PaymentParams {
/// applications' amounts are the cash; a discount is beside it. /// applications' amounts are the cash; a discount is beside it.
#[serde(default)] #[serde(default)]
pub amount: Option<String>, pub amount: Option<String>,
/// Cash account id or code; the older name of `deposit_to` /
/// `paid_from`.
#[serde(default)]
pub cash_account: Option<String>,
/// The account the cash reaches (receive_payment): a bank or card that /// The account the cash reaches (receive_payment): a bank or card that
/// holds no role. /// holds no role.
#[serde(default)] #[serde(default)]
+10 -10
View File
@@ -2195,8 +2195,8 @@ snapshots; see plan 8.2 S4.
receivable line tagged with the invoice and the receivable side one line receivable line tagged with the invoice and the receivable side one line
per (account, project) part. per (account, project) part.
- `customer` is the customer's id, its party's id or its name (a name - `customer` is the customer's id, its party's id or its name (a name
two customers answer to is refused); `party_id` is the old spelling of two customers answer to is refused); it is required (the old `party_id`
the same thing (a customer id), kept until P5. spelling is gone, P5).
- The due date: `due_date` as given (and it overrides `terms`); else - The due date: `due_date` as given (and it overrides `terms`); else
`terms` ("Net 30", "Net 15 EOM", "2/10 net 30", "Due on receipt", `terms` ("Net 30", "Net 15 EOM", "2/10 net 30", "Due on receipt",
"Due 2026-11-15", rules.md B-39); else the customer's "Due 2026-11-15", rules.md B-39); else the customer's
@@ -2247,8 +2247,8 @@ snapshots; see plan 8.2 S4.
sha256 }, warnings?, notes? }`. sha256 }, warnings?, notes? }`.
Confirmed it posts Dr each line's account / Cr `2000` Accounts Payable, Confirmed it posts Dr each line's account / Cr `2000` Accounts Payable,
the payable line tagged with the bill and one debit line per (account, the payable line tagged with the bill and one debit line per (account,
project) part; `vendor` is a vendor id, its party's id or its name, and project) part; `vendor` is a vendor id, its party's id or its name
`party_id` the old spelling (a vendor id). (required; the old `party_id` spelling is gone, P5).
- `vendor_ref` is the vendor's own invoice number: required in books - `vendor_ref` is the vendor's own invoice number: required in books
that have declared `documents_v2` (B-40). The same vendor and number that have declared `documents_v2` (B-40). The same vendor and number
twice is refused unless `duplicate_reason` says why, which is twice is refused unless `duplicate_reason` says why, which is
@@ -2271,8 +2271,8 @@ snapshots; see plan 8.2 S4.
- `warnings` names an expense account it created with no tax category; - `warnings` names an expense account it created with no tax category;
`notes` one per finished or abandoned project a line names (none for `notes` one per finished or abandoned project a line names (none for
on-spec: a cost of on-spec work is an expense like any other). on-spec: a cost of on-spec work is an expense like any other).
- `books.receive_payment { date, deposit_to | cash_account, instrument?, - `books.receive_payment { date, deposit_to, instrument?,
reference?, invoice | document_id, amount? | applications: [{ invoice, reference?, invoice, amount? | applications: [{ invoice,
amount, discount?, discount_reason? }], fee?: { amount, account?, amount, discount?, discount_reason? }], fee?: { amount, account?,
vendor? }, paid_in_full_marking?, confirmed?, preview? }` → `{ preview?, vendor? }, paid_in_full_marking?, confirmed?, preview? }` → `{ preview?,
entry_id, documents: [{ ref, number?, outstanding, status }], deposited, entry_id, documents: [{ ref, number?, outstanding, status }], deposited,
@@ -2294,11 +2294,11 @@ snapshots; see plan 8.2 S4.
repeat the single document's figures. An overpayment is refused with the repeat the single document's figures. An overpayment is refused with the
excess and the remedy (a plain entry to 2300 Customer Deposits). excess and the remedy (a plain entry to 2300 Customer Deposits).
`paid_in_full_marking: true` adds a UCC §3-311 warning and records `paid_in_full_marking: true` adds a UCC §3-311 warning and records
nothing else. `document_id` and `cash_account` are the older names of nothing else. (The older names `document_id` and `cash_account` are
`invoice`/`bill` and `deposit_to`/`paid_from`. The books record gone, P5.) The books record
payments; they never collect them (D-45). payments; they never collect them (D-45).
- `books.pay_bill { date, paid_from | cash_account, instrument?, - `books.pay_bill { date, paid_from, instrument?,
reference?, bill | document_id, amount? | applications: [{ bill, amount, reference?, bill, amount? | applications: [{ bill, amount,
discount?, discount_reason? }], confirmed?, preview? }` → as `receive_payment`, per bill and discount?, discount_reason? }], confirmed?, preview? }` → as `receive_payment`, per bill and
one vendor (B-42): Dr A/P each bill's cash and discount, Cr the payer one vendor (B-42): Dr A/P each bill's cash and discount, Cr the payer
account the cash, Cr purchase discounts the discounts. A discount on a account the cash, Cr purchase discounts the discounts. A discount on a