diff --git a/crates/ledger-gui/src/pages/journal.rs b/crates/ledger-gui/src/pages/journal.rs index 814ec7e..e7a9fcc 100644 --- a/crates/ledger-gui/src/pages/journal.rs +++ b/crates/ledger-gui/src/pages/journal.rs @@ -267,6 +267,18 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option { }; ev(icon::BARCODE, text, asset_id.map(Route::Asset)) } + "asset.acquisition_linked" => { + let entry = p.get("entry_id").and_then(|v| v.as_u64()).unwrap_or(0); + Some(Event { + icon: icon::LINK, + text: format!( + "Linked {} to the entry that acquired it, #{entry}", + asset(asset_id) + ), + route: asset_id.map(Route::Asset), + also: Some(Route::Entry(entry)), + }) + } "asset.depreciation_posted" => with_asset( icon::TREND_DOWN, format!( diff --git a/crates/ledger-gui/tests/assets.rs b/crates/ledger-gui/tests/assets.rs index 0d4d207..b061e8a 100644 --- a/crates/ledger-gui/tests/assets.rs +++ b/crates/ledger-gui/tests/assets.rs @@ -612,3 +612,42 @@ proptest! { } } } + +/// Every event the store can journal reads as a sentence in the journal +/// and recent activity, never as its raw type: the event types come from +/// the store's own source (its replay arms), so a new event without +/// journal text fails here. (asset.acquisition_linked showed raw, +/// 2026-10-15.) +#[test] +fn every_journal_event_has_plain_language_text() { + let src = include_str!("../../ledger-store/src/store.rs"); + let mut kinds: Vec<&str> = src + .lines() + .flat_map(|l| l.split('"').skip(1).step_by(2)) + .filter(|t| { + let mut parts = t.split('.'); + matches!( + (parts.next(), parts.next(), parts.next()), + (Some(a), Some(b), None) + if !a.is_empty() && !b.is_empty() + && a.chars().chain(b.chars()).all(|c| c.is_ascii_lowercase() || c == '_') + ) + }) + .filter(|t| src.contains(&format!("\"{t}\" =>")) || src.contains(&format!("\"{t}\" |"))) + .collect(); + kinds.sort_unstable(); + kinds.dedup(); + assert!(kinds.len() > 30, "found only {kinds:?}"); + let f = fixture("every-event"); + for kind in kinds { + let line = ledger_store::JournalLine { + seq: 1, + ts: "2026-10-15T00:00:00Z".into(), + line_type: kind.to_string(), + payload: serde_json::json!({}), + prev: String::new(), + }; + let e = ledger_gui::pages::journal::describe_any(&f.books, &line); + assert_ne!(e.text, kind, "{kind} has no journal text"); + } +} diff --git a/crates/ledger-store/src/journal.rs b/crates/ledger-store/src/journal.rs index bb51f93..09dc247 100644 --- a/crates/ledger-store/src/journal.rs +++ b/crates/ledger-store/src/journal.rs @@ -27,15 +27,10 @@ pub struct JournalLine { pub seq: u64, /// UTC timestamp, RFC 3339. Recorded, never trusted for ordering. pub ts: String, - /// Event type: `account.added`, `entry.created`, `entry.line_added`, - /// `entry.posted`, `entry.reversed`, `entry.line_class_set`, - /// `receipt.attached`, `receipt.linked`, `receipt.unlinked`, - /// `document.filed`, `attachment.withdrawn`, - /// `asset.registered`, `asset.depreciation_posted`, `asset.disposed`, - /// `period.created`, `period.closed`, `line.cleared`, `customer.added`, - /// `vendor.added`, `invoice.created`, `bill.created`, - /// `invoice.payment_received`, `bill.paid`, `fx.rate_set`, - /// `payroll.run`, `class.added`, `store.created`. + /// Event type, e.g. `account.added`, `entry.posted`, `asset.tagged`: + /// every type the store writes has a replay arm in + /// `replay_line_into_ledger` (an unknown type is refused as corrupt), + /// and journal text in the viewer (`ledger-gui` tests check both). #[serde(rename = "type")] pub line_type: String, /// The full domain facts. Everything a replay needs.