merge: P4b S6, paying on terms (branch S6, 6c4480cd)
The third of the paused branches: Ledger::settle with discounts and the processor's fee (kernel::settle, Kani settle_is_exact, Lean settle_posts_a_document_step), books.receive_payment and books.pay_bill with applications, books.bills_due, the agings' detail and tie-out, the aging exports and the CPA package's aging CSVs, the handlers in tools/settlements.rs. Twelve files conflicted with the S4.4 and S5 merges, resolved as plan 8.0.2 says: both routers; both test appends (the shared match arm in arap_standing.rs spliced into both tests; the shared `.unwrap()` in store.rs); the scope table at 124; one invoices-and-bills handbook (S5's send step 1 with S6's steps 2, 3, 5 and procedures) registered once; docs/mcp-interface.md with S4.4's create docs and S6's payment docs; S5's law renumbered 35 and S6's 36 (S1's is 34); the parity rows and sections of both. S6's wire tests create their bills as S4.4's contract now requires (confirmed, the vendor's number, a line account, the vendor's invoice uploaded) and live in their own module beside S4.4's tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
commit
be58059830
45 files changed
+3949
-520
No files matched your search
@@ -1,48 +1,13 @@
|
||||
//! `books.documents`' group: the A/R and A/P documents (invoices and
|
||||
//! bills), the payments and voids against them, the read tools
|
||||
//! (`books.documents`, `books.document`), the control check and the
|
||||
//! agings. Split whole out of `server.rs` (plan/served.md 8.2 U0a): the
|
||||
//! tools, their names, descriptions and rules are unchanged
|
||||
//! (`docs/mcp-interface.md`; rules.md B-38).
|
||||
//! bills), their voids, the read tools (`books.documents`,
|
||||
//! `books.document`) and the control check. Split whole out of
|
||||
//! `server.rs` (plan/served.md 8.2 U0a); the payments and the agings live
|
||||
//! in `tools/settlements.rs` (P4b S6).
|
||||
|
||||
use super::*;
|
||||
use ledger_core::Terms;
|
||||
use ledger_core::arap::{self, PartySnapshot};
|
||||
|
||||
/// A payment's method and reference from its parameters (the method by
|
||||
/// id or label, books.instruments).
|
||||
fn payment_details(
|
||||
store: &Store,
|
||||
p: &PaymentParams,
|
||||
) -> Result<ledger_core::PaymentDetails, String> {
|
||||
Ok(ledger_core::PaymentDetails {
|
||||
instrument: p
|
||||
.instrument
|
||||
.as_deref()
|
||||
.map(|r| resolve_instrument(store, r))
|
||||
.transpose()?,
|
||||
reference: p.reference.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// What a payment tool answers: the document's figures after it, derived
|
||||
/// from its lines that stand (rules.md B-38), as of the end of the books.
|
||||
fn payment_out(
|
||||
ledger: &Ledger,
|
||||
doc: ledger_core::DocRef,
|
||||
entry_id: u64,
|
||||
) -> Result<Json<PaymentOut>, String> {
|
||||
let st = ledger
|
||||
.doc_state(doc, chrono::NaiveDate::MAX)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("{} not found", doc_label(doc)))?;
|
||||
Ok(Json(PaymentOut {
|
||||
entry_id,
|
||||
remaining: money(st.outstanding),
|
||||
status: st.status.key().to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// A document's id.
|
||||
fn doc_id(doc: ledger_core::DocRef) -> u64 {
|
||||
match doc {
|
||||
@@ -71,14 +36,6 @@ fn movement_key(m: ledger_core::Movement) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// A document's kind and id for people ("invoice 7").
|
||||
fn doc_label(doc: ledger_core::DocRef) -> String {
|
||||
match doc {
|
||||
ledger_core::DocRef::Invoice(id) => format!("invoice {id}"),
|
||||
ledger_core::DocRef::Bill(id) => format!("bill {id}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// An invoice's or bill's lines as the tool takes them: each amount
|
||||
/// parsed, each project and account resolved ([`resolve_class`],
|
||||
/// [`resolve_account`]), and quantity and unit price together, before
|
||||
@@ -847,74 +804,6 @@ impl BooksServer {
|
||||
}))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "books.receive_payment",
|
||||
title = "Receive an invoice payment",
|
||||
description = "Record a customer payment against an invoice. Posts Dr Cash / Cr A/R, the receivable line tagged with the invoice (its figures are derived from its lines that stand: a reversed payment no longer counts). Name instrument (how the money moved, books.instruments) and the processor's or bank's reference when known. The books record payments; they never collect them (D-45). Refused above what it owes, before its date, or into an account that isn't cash.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = true,
|
||||
open_world_hint = false
|
||||
)
|
||||
)]
|
||||
async fn receive_payment(
|
||||
&self,
|
||||
params: Parameters<PaymentParams>,
|
||||
) -> Result<Json<PaymentOut>, String> {
|
||||
let p = params.0;
|
||||
let mut store = self.lock()?;
|
||||
let cash = resolve_account(&store, &p.cash_account)?;
|
||||
let details = payment_details(&store, &p)?;
|
||||
let entry_id = store
|
||||
.receive_payment_with(
|
||||
p.document_id,
|
||||
parse_date(&p.date)?,
|
||||
parse_amount(&p.amount)?,
|
||||
cash,
|
||||
&details,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
payment_out(
|
||||
store.ledger(),
|
||||
ledger_core::DocRef::Invoice(p.document_id),
|
||||
entry_id,
|
||||
)
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "books.pay_bill",
|
||||
title = "Pay a bill",
|
||||
description = "Record a payment of a vendor bill. Posts Dr A/P / Cr Cash, the payable line tagged with the bill (its figures are derived from its lines that stand: a reversed payment no longer counts). Name instrument (how the money moved, books.instruments: a card payment is left out of 1099-NEC) and the payment's reference when known. The books record payments; they never make them (D-45). Refused above what it owes, before its date, or from an account that isn't cash.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = true,
|
||||
open_world_hint = false
|
||||
)
|
||||
)]
|
||||
async fn pay_bill(
|
||||
&self,
|
||||
params: Parameters<PaymentParams>,
|
||||
) -> Result<Json<PaymentOut>, String> {
|
||||
let p = params.0;
|
||||
let mut store = self.lock()?;
|
||||
let cash = resolve_account(&store, &p.cash_account)?;
|
||||
let details = payment_details(&store, &p)?;
|
||||
let entry_id = store
|
||||
.pay_bill_with(
|
||||
p.document_id,
|
||||
parse_date(&p.date)?,
|
||||
parse_amount(&p.amount)?,
|
||||
cash,
|
||||
&details,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
payment_out(
|
||||
store.ledger(),
|
||||
ledger_core::DocRef::Bill(p.document_id),
|
||||
entry_id,
|
||||
)
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "books.void_document",
|
||||
title = "Void an invoice or bill",
|
||||
@@ -1229,78 +1118,4 @@ impl BooksServer {
|
||||
let checks = ledger.control_check(as_of).map_err(|e| e.to_string())?;
|
||||
Ok(Json(control_check_out(ledger, as_of, &checks)))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "books.ar_aging",
|
||||
title = "Receivables aging",
|
||||
description = described(
|
||||
"A/R aging report as of a date: outstanding balances per customer, bucketed by days past due (current, 30, 60, 90, 90+).",
|
||||
ledger_core::calendar::PeriodKind::AsOf
|
||||
),
|
||||
annotations(read_only_hint = true, open_world_hint = false)
|
||||
)]
|
||||
async fn ar_aging(&self, params: Parameters<AgingParams>) -> Result<Json<AgingOut>, String> {
|
||||
let p = params.0;
|
||||
let as_of = self.as_of(p.as_of.as_deref())?;
|
||||
let store = self.lock()?;
|
||||
let rows = store
|
||||
.ledger()
|
||||
.ar_aging(as_of)
|
||||
.map_err(|e| e.to_string())?
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let total = r.total().map_err(|e| e.to_string())?.to_string();
|
||||
Ok(AgingRowOut {
|
||||
name: r.name,
|
||||
current: money(r.current),
|
||||
days_30: money(r.days_30),
|
||||
days_60: money(r.days_60),
|
||||
days_90: money(r.days_90),
|
||||
days_90_plus: money(r.days_90_plus),
|
||||
total,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, String>>()?;
|
||||
Ok(Json(AgingOut {
|
||||
as_of: as_of.to_string(),
|
||||
rows,
|
||||
}))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "books.ap_aging",
|
||||
title = "Payables aging",
|
||||
description = described(
|
||||
"A/P aging report as of a date: outstanding balances per vendor, bucketed by days past due (current, 30, 60, 90, 90+).",
|
||||
ledger_core::calendar::PeriodKind::AsOf
|
||||
),
|
||||
annotations(read_only_hint = true, open_world_hint = false)
|
||||
)]
|
||||
async fn ap_aging(&self, params: Parameters<AgingParams>) -> Result<Json<AgingOut>, String> {
|
||||
let p = params.0;
|
||||
let as_of = self.as_of(p.as_of.as_deref())?;
|
||||
let store = self.lock()?;
|
||||
let rows = store
|
||||
.ledger()
|
||||
.ap_aging(as_of)
|
||||
.map_err(|e| e.to_string())?
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let total = r.total().map_err(|e| e.to_string())?.to_string();
|
||||
Ok(AgingRowOut {
|
||||
name: r.name,
|
||||
current: money(r.current),
|
||||
days_30: money(r.days_30),
|
||||
days_60: money(r.days_60),
|
||||
days_90: money(r.days_90),
|
||||
days_90_plus: money(r.days_90_plus),
|
||||
total,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, String>>()?;
|
||||
Ok(Json(AgingOut {
|
||||
as_of: as_of.to_string(),
|
||||
rows,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user