feat(arap): record an invoice's sends as invoice.sent (P4b S5)
The books never send invoices (A-4), but they must remember that one
went out: who sent it, how, to whom, when, whether it was the original
or a reminder, and the channel's own reference. That record is a fact
of the document's life, not a figure: a send moves no money, so it is
one journal line (`invoice.sent`) and no entry, and every derived
figure (outstanding, aging, DocState) is exactly as it was.
`Ledger::mark_sent` refuses a send on a void invoice (it should never
have reached the customer), a send dated before the invoice, no
recipients, and any recipient, reference or note that would put a full
TIN or control characters into the journal. Open, partial, paid and
written-off invoices can be sent (a copy for the customer's records, a
reminder).
The event is `documents_v2`'s (D-52): an older version refuses the
type rather than dropping the record. Replay restores it and the
projection keeps it in schema 16's `invoice_sends` table, so live,
replay and the viewer agree. The new old-books fixture `p4_s5_before`
was written by eeee9658 and opens to the same figures; a hand-written
send in `books_written_under_older_rules_still_open` proves an older
journal with the new line still opens.
Tests: mark_sent_waits_for_documents_v2, sends_replay_and_project,
a_send_changes_no_figure, mark_sent_on_void_refused, Op::MarkSent in
status_is_a_function_of_journal_and_date,
an_older_version_stops_at_invoice_sent.
This commit is contained in:
1 parent
ed637f0e27
commit
d1fb512e4b
16 files changed
+979
-5
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 s5_before -- OUT (p4_s5_before: run against eeee9658,
|
||||
# the revision before P4b S5)
|
||||
# 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)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Writes `p4_s5_before`: books the version before P4b S5 (main eeee9658)
|
||||
//! wrote through its Store API: a chart, a customer's invoice part paid,
|
||||
//! and a vendor's bill. No send exists in that version; this version opens
|
||||
//! the same journal and must report the same figures.
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::{AccountType, Currency, DocRef, DocumentLine, Money, Side};
|
||||
use ledger_store::Store;
|
||||
use rust_decimal::Decimal;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(y, m, day).unwrap()
|
||||
}
|
||||
fn cents(c: i64) -> Decimal {
|
||||
Decimal::new(c, 2)
|
||||
}
|
||||
fn line(what: &str, c: i64) -> DocumentLine {
|
||||
DocumentLine {
|
||||
description: what.into(),
|
||||
amount: cents(c),
|
||||
class_id: None,
|
||||
account: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::args().nth(1).unwrap());
|
||||
let dir = root.join("p4_s5_before");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut s = Store::create(&dir, Currency::USD).unwrap();
|
||||
let bank = s
|
||||
.add_account("1000", "Bank", AccountType::Asset, None, false)
|
||||
.unwrap();
|
||||
let capital = s
|
||||
.add_account("3000", "Capital", AccountType::Equity, None, false)
|
||||
.unwrap();
|
||||
let seed = s.create_entry(d(2026, 1, 1), "Opening capital").unwrap();
|
||||
let m = Money::new(Decimal::from(1000), Currency::USD);
|
||||
s.add_line(seed, bank, Side::Debit, m).unwrap();
|
||||
s.add_line(seed, capital, Side::Credit, m).unwrap();
|
||||
s.post_entry(seed).unwrap();
|
||||
let fernhill = s.add_customer("Fernhill Labs").unwrap();
|
||||
let vendor = s.add_vendor("Tobin Harlow Supply").unwrap();
|
||||
let inv = s
|
||||
.create_invoice(
|
||||
fernhill,
|
||||
d(2026, 2, 1),
|
||||
d(2026, 3, 3),
|
||||
vec![line("Design", 120_000)],
|
||||
)
|
||||
.unwrap();
|
||||
s.receive_payment(inv, d(2026, 2, 15), cents(40_000), bank)
|
||||
.unwrap();
|
||||
s.create_bill(
|
||||
vendor,
|
||||
d(2026, 2, 10),
|
||||
d(2026, 3, 12),
|
||||
vec![line("Drafting", 30_000)],
|
||||
)
|
||||
.unwrap();
|
||||
println!("WROTE {:?} (invoice {inv}, {:?})", dir, DocRef::Invoice(inv));
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
{
|
||||
"assets": [],
|
||||
"bills": [
|
||||
{
|
||||
"amount_paid": "0",
|
||||
"date": "2026-02-10",
|
||||
"due_date": "2026-03-12",
|
||||
"entry_id": 4,
|
||||
"id": 1,
|
||||
"lines": [
|
||||
{
|
||||
"amount": "300.00",
|
||||
"description": "Drafting"
|
||||
}
|
||||
],
|
||||
"status": "Open",
|
||||
"vendor_id": 1
|
||||
}
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"date": "2026-01-01",
|
||||
"description": "Opening capital",
|
||||
"id": 1,
|
||||
"lines": [
|
||||
{
|
||||
"account": 1,
|
||||
"amount": {
|
||||
"amount": "1000",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 2,
|
||||
"amount": {
|
||||
"amount": "1000",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-01",
|
||||
"description": "Invoice 1",
|
||||
"id": 2,
|
||||
"lines": [
|
||||
{
|
||||
"account": 3,
|
||||
"amount": {
|
||||
"amount": "1200.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 4,
|
||||
"amount": {
|
||||
"amount": "1200.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-15",
|
||||
"description": "Payment on invoice 1",
|
||||
"id": 3,
|
||||
"lines": [
|
||||
{
|
||||
"account": 1,
|
||||
"amount": {
|
||||
"amount": "400.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 3,
|
||||
"amount": {
|
||||
"amount": "400.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-10",
|
||||
"description": "Bill 1",
|
||||
"id": 4,
|
||||
"lines": [
|
||||
{
|
||||
"account": 5,
|
||||
"amount": {
|
||||
"amount": "300.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 6,
|
||||
"amount": {
|
||||
"amount": "300.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
}
|
||||
],
|
||||
"entry_parties": [
|
||||
[
|
||||
2,
|
||||
[
|
||||
{
|
||||
"party_id": 1,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
3,
|
||||
[
|
||||
{
|
||||
"party_id": 1,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
4,
|
||||
[
|
||||
{
|
||||
"party_id": 2,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
]
|
||||
],
|
||||
"invoices": [
|
||||
{
|
||||
"amount_paid": "400.00",
|
||||
"customer_id": 1,
|
||||
"date": "2026-02-01",
|
||||
"due_date": "2026-03-03",
|
||||
"entry_id": 2,
|
||||
"id": 1,
|
||||
"lines": [
|
||||
{
|
||||
"amount": "1200.00",
|
||||
"description": "Design"
|
||||
}
|
||||
],
|
||||
"status": "Partial"
|
||||
}
|
||||
],
|
||||
"payroll_runs": [],
|
||||
"periods": [],
|
||||
"recurring": [],
|
||||
"schedules": [],
|
||||
"snapshot": {
|
||||
"Ok": {
|
||||
"balance_sheet": {
|
||||
"assets": "2200",
|
||||
"balanced": true,
|
||||
"current_earnings": "900",
|
||||
"equity": "1900",
|
||||
"liabilities": "300"
|
||||
},
|
||||
"profit_and_loss": {
|
||||
"expenses": "300",
|
||||
"net_income": "900",
|
||||
"revenue": "1200"
|
||||
},
|
||||
"trial_balance": {
|
||||
"lines": [
|
||||
{
|
||||
"code": "1000",
|
||||
"credit": "0",
|
||||
"debit": "1400",
|
||||
"name": "Bank"
|
||||
},
|
||||
{
|
||||
"code": "1200",
|
||||
"credit": "0",
|
||||
"debit": "800",
|
||||
"name": "Accounts Receivable"
|
||||
},
|
||||
{
|
||||
"code": "2000",
|
||||
"credit": "300",
|
||||
"debit": "0",
|
||||
"name": "Accounts Payable"
|
||||
},
|
||||
{
|
||||
"code": "3000",
|
||||
"credit": "1000",
|
||||
"debit": "0",
|
||||
"name": "Capital"
|
||||
},
|
||||
{
|
||||
"code": "4000",
|
||||
"credit": "1200",
|
||||
"debit": "0",
|
||||
"name": "Revenue"
|
||||
},
|
||||
{
|
||||
"code": "6999",
|
||||
"credit": "0",
|
||||
"debit": "300",
|
||||
"name": "General Expenses"
|
||||
}
|
||||
],
|
||||
"total_credits": "2500",
|
||||
"total_debits": "2500"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verify": {
|
||||
"Ok": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{"payload":{"base_currency":"USD"},"prev":"0000000000000000000000000000000000000000000000000000000000000000","seq":1,"ts":"2026-10-27T12:07:38Z","type":"store.created"}
|
||||
{"payload":{"features":["account_renamed","asset_basis","depreciation_catch_up","disposal_before_service","document_line_projects","document_standing","documents_v2","members_cumulative","month_reposting","party_creditors","payment_instruments","projects","require_tax_category","schedule_convention","tax_categories_gain_exempt","vendor_1099_decisions"],"format":2},"prev":"0bbd745853ef336fa261af81109c83ac2b5c62be3d9243eecbd7d1abc4810afc","seq":2,"ts":"2026-10-27T12:07:38Z","type":"journal.format_set"}
|
||||
{"payload":{"account_type":"Asset","code":"1000","currency":"USD","id":1,"name":"Bank","parent":null,"placeholder":false},"prev":"5b4b18ca6b1e3e4dd2dd8d2ba522d2ec8ffdf3cd2c10472092983453c5675472","seq":3,"ts":"2026-10-27T12:07:38Z","type":"account.added"}
|
||||
{"payload":{"account_type":"Equity","code":"3000","currency":"USD","id":2,"name":"Capital","parent":null,"placeholder":false},"prev":"c0ab8146ae450a9204c1f2dd338f49330900ec2c4edd29bda362ce5c73665275","seq":4,"ts":"2026-10-27T12:07:38Z","type":"account.added"}
|
||||
{"payload":{"date":"2026-01-01","description":"Opening capital","id":1},"prev":"ade211d7ae85ac8a12866d529418cba0785290e559183a0700dd91c95a7d41b0","seq":5,"ts":"2026-10-27T12:07:38Z","type":"entry.created"}
|
||||
{"payload":{"account":1,"amount":"1000","currency":"USD","id":1,"side":"debit"},"prev":"0b1c4e86907d5b19faed67cba9633120de6726217fec3602930e09cb1c3ac6e1","seq":6,"ts":"2026-10-27T12:07:38Z","type":"entry.line_added"}
|
||||
{"payload":{"account":2,"amount":"1000","currency":"USD","id":1,"side":"credit"},"prev":"f300e939ff3967ad7b04c781ec6ec0c2790cbfb938a1ad57d8428239f3c05ecb","seq":7,"ts":"2026-10-27T12:07:38Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-01-01","description":"Opening capital","id":1,"lines":[{"account":1,"amount":"1000","currency":"USD","side":"debit"},{"account":2,"amount":"1000","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"4c31e09e179aa377d03f09ff07298a94f7bcdb5463b9a56e7d9ec1649fa9b859","seq":8,"ts":"2026-10-27T12:07:38Z","type":"entry.posted"}
|
||||
{"payload":{"id":1,"name":"Fernhill Labs"},"prev":"88a5469ed669976c63355000e7ce6b358bc3d8731527da98f45833ae28cd04f8","seq":9,"ts":"2026-10-27T12:07:38Z","type":"customer.added"}
|
||||
{"payload":{"id":1,"name":"Tobin Harlow Supply"},"prev":"84f6fd180671d63e367eaa0465867732877995d282996446ed2514e2b164f06d","seq":10,"ts":"2026-10-27T12:07:38Z","type":"vendor.added"}
|
||||
{"payload":{"account_type":"Asset","code":"1200","currency":"USD","id":3,"name":"Accounts Receivable","parent":null,"placeholder":false},"prev":"af015074dc5235c7efd9df38e447866e46c5d552d049f07ceec7f5cc94655770","seq":11,"ts":"2026-10-27T12:07:38Z","type":"account.added"}
|
||||
{"payload":{"account_type":"Revenue","code":"4000","currency":"USD","id":4,"name":"Revenue","parent":null,"placeholder":false},"prev":"a17b19e0d8229d378f2cf950079ff6495b21799ecc0b4608de72853d62d5eaf2","seq":12,"ts":"2026-10-27T12:07:39Z","type":"account.added"}
|
||||
{"payload":{"date":"2026-02-01","description":"Invoice 1","id":2},"prev":"7e1bf5a0142e15f2c4471f6dd413e428586673cdf5ce94d46e40bb951d487423","seq":13,"ts":"2026-10-27T12:07:39Z","type":"entry.created"}
|
||||
{"payload":{"account":3,"amount":"1200.00","currency":"USD","id":2,"side":"debit"},"prev":"0b0d2bc9815cac0aaf428d114031cd18d6c133cb14aff6e374324b9a973fe50f","seq":14,"ts":"2026-10-27T12:07:39Z","type":"entry.line_added"}
|
||||
{"payload":{"account":4,"amount":"1200.00","currency":"USD","id":2,"side":"credit"},"prev":"fa319df9f4858df95f47b4035c0f83901674dca7fbc5978b820a8d93bd27e196","seq":15,"ts":"2026-10-27T12:07:39Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-02-01","description":"Invoice 1","id":2,"lines":[{"account":3,"amount":"1200.00","currency":"USD","side":"debit"},{"account":4,"amount":"1200.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"e0cbb496c5116f927a0d2d897c651bf7df706af670d44c468703a81cdbb6a598","seq":16,"ts":"2026-10-27T12:07:39Z","type":"entry.posted"}
|
||||
{"payload":{"customer_id":1,"date":"2026-02-01","due_date":"2026-03-03","entry_id":2,"id":1,"lines":[{"amount":"1200.00","description":"Design"}]},"prev":"72eee837c1eee71d7b45bc63e840224fe85d4f08f423b7cdadd5431d2e0c730c","seq":17,"ts":"2026-10-27T12:07:39Z","type":"invoice.created"}
|
||||
{"payload":{"date":"2026-02-15","description":"Payment on invoice 1","id":3},"prev":"915870654047decc4b93972fb04bd76bf8ea65b5710f58a866d3661c0a3c09f5","seq":18,"ts":"2026-10-27T12:07:39Z","type":"entry.created"}
|
||||
{"payload":{"account":1,"amount":"400.00","currency":"USD","id":3,"side":"debit"},"prev":"433c71f0884859468a8b1e48038b807bc9f910e23e22bd646518913287f5a1d6","seq":19,"ts":"2026-10-27T12:07:39Z","type":"entry.line_added"}
|
||||
{"payload":{"account":3,"amount":"400.00","currency":"USD","id":3,"side":"credit"},"prev":"86db1b12b1c6585ed1fadbee2543d33e0adede5e9a02f0d36626a6f8acc2179a","seq":20,"ts":"2026-10-27T12:07:39Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-02-15","description":"Payment on invoice 1","id":3,"lines":[{"account":1,"amount":"400.00","currency":"USD","side":"debit"},{"account":3,"amount":"400.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"c4a0a4fb8495af3b43b2b5d5e9602067998028b65d42a301e407f37017d6e6b0","seq":21,"ts":"2026-10-27T12:07:39Z","type":"entry.posted"}
|
||||
{"payload":{"amount":"400.00","cash_account":1,"date":"2026-02-15","entry_id":3,"invoice_id":1},"prev":"5a29d6bff70696aeb707eb69fac9ceaffb7c5167e10434b23aa18ba9aeb4b8ff","seq":22,"ts":"2026-10-27T12:07:39Z","type":"invoice.payment_received"}
|
||||
{"payload":{"account_type":"Expense","code":"6999","currency":"USD","id":5,"name":"General Expenses","parent":null,"placeholder":false},"prev":"2dc14a4d6d23f42a34c1172475ac86b9706be79ee17ba9bfd4041fde99c43231","seq":23,"ts":"2026-10-27T12:07:39Z","type":"account.added"}
|
||||
{"payload":{"account_type":"Liability","code":"2000","currency":"USD","id":6,"name":"Accounts Payable","parent":null,"placeholder":false},"prev":"4a44d6570b6b9e1f3b705b383d41939aab2952d32902ab151f2b4d02ad3c1237","seq":24,"ts":"2026-10-27T12:07:39Z","type":"account.added"}
|
||||
{"payload":{"date":"2026-02-10","description":"Bill 1","id":4},"prev":"bd11681484634f272371a9245cfabdff52024ffe516380143b650275b9af034a","seq":25,"ts":"2026-10-27T12:07:39Z","type":"entry.created"}
|
||||
{"payload":{"account":5,"amount":"300.00","currency":"USD","id":4,"side":"debit"},"prev":"3df6859cd10b0f3ec4a50ff2abcfd619291db2c290e3d844399d0434be31754c","seq":26,"ts":"2026-10-27T12:07:39Z","type":"entry.line_added"}
|
||||
{"payload":{"account":6,"amount":"300.00","currency":"USD","id":4,"side":"credit"},"prev":"86f685ccc47f69411c2cd424017510d10823f05ab3c9018fbf636ff15455b548","seq":27,"ts":"2026-10-27T12:07:39Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-02-10","description":"Bill 1","id":4,"lines":[{"account":5,"amount":"300.00","currency":"USD","side":"debit"},{"account":6,"amount":"300.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"c25fce42aa1755ee4e915191e4903f13874e2ea327bfd915ba39df474a04ca4e","seq":28,"ts":"2026-10-27T12:07:39Z","type":"entry.posted"}
|
||||
{"payload":{"date":"2026-02-10","due_date":"2026-03-12","entry_id":4,"id":1,"lines":[{"amount":"300.00","description":"Drafting"}],"vendor_id":1},"prev":"55ec1c5c73e4ac37996b917c87d6bc9a841ce1eb43ac57882d2ca9a1fdf4820a","seq":29,"ts":"2026-10-27T12:07:39Z","type":"bill.created"}
|
||||
Reference in new issue
Block a user