cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit dbf061a57afaa92569c876f1a4606233cff0ef47
parent 2e046b2f44fdd72147a3c120fa5ffe360732eeb8
Author: triesap <tyson@radroots.org>
Date:   Wed, 29 Apr 2026 19:34:21 +0000

order: reject unknown submit bins

Diffstat:
Msrc/runtime/order.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Mtests/target_cli.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 100 insertions(+), 0 deletions(-)

diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -2942,6 +2942,7 @@ fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsF price_qty_amt: None, price_qty_unit: None, listing_addr: Some(listing_addr.to_owned()), + primary_bin_id: None, notes: None, } } @@ -3323,6 +3324,43 @@ fn order_submit_quantity_preflight_view( } }; + let Some(primary_bin_id) = product.primary_bin_id.as_deref().and_then(non_empty_ref) else { + return Ok(Some(order_submit_invalid_quantity_view( + config, + loaded, + args, + "order listing bin identity is missing in the local replica", + vec![issue_with_code( + "listing_primary_bin_missing", + "inventory.primary_bin_id", + "current local replica listing primary bin is required before submit", + )], + ))); + }; + + let mut bin_issues = Vec::new(); + for (index, item) in loaded.document.order.items.iter().enumerate() { + if item.bin_id != primary_bin_id { + bin_issues.push(issue_with_code( + "order_bin_unknown", + format!("order.items[{index}].bin_id"), + format!( + "draft bin `{}` is not in the current local listing bin set; expected primary bin `{primary_bin_id}`", + item.bin_id + ), + )); + } + } + if !bin_issues.is_empty() { + return Ok(Some(order_submit_invalid_quantity_view( + config, + loaded, + args, + "order draft references a bin outside the current local listing", + bin_issues, + ))); + } + let available_count = match product.qty_avail { Some(value) if value >= 0 => value as u64, Some(value) => { @@ -4091,6 +4129,14 @@ fn non_empty_string(value: String) -> Option<String> { } } +fn non_empty_ref(value: &str) -> Option<&str> { + if value.trim().is_empty() { + None + } else { + Some(value) + } +} + fn modified_unix(path: &Path) -> Option<u64> { let modified = fs::metadata(path).ok()?.modified().ok()?; modified diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -1168,6 +1168,20 @@ fn create_ready_order(sandbox: &RadrootsCliSandbox, basket_id: &str) -> String { .to_owned() } +fn rewrite_order_bin(sandbox: &RadrootsCliSandbox, order_id: &str, bin_id: &str) { + let path = sandbox + .root() + .join("data/apps/cli/orders/drafts") + .join(format!("{order_id}.toml")); + let contents = fs::read_to_string(&path).expect("read order draft"); + let updated = contents.replace( + "bin_id = \"bin-1\"", + format!("bin_id = \"{bin_id}\"").as_str(), + ); + assert_ne!(updated, contents); + fs::write(path, updated).expect("rewrite order draft bin"); +} + #[test] fn buyer_target_flow_acceptance_uses_target_operations() { let sandbox = RadrootsCliSandbox::new(); @@ -1517,6 +1531,46 @@ fn order_submit_rejects_over_available_quantity_before_publish() { } #[test] +fn order_submit_rejects_unknown_local_listing_bin_before_publish() { + let sandbox = RadrootsCliSandbox::new(); + let order_id = create_ready_order(&sandbox, "unknown_bin"); + rewrite_order_bin(&sandbox, order_id.as_str(), "unknown-bin"); + + let (output, value) = sandbox.json_output(&[ + "--format", + "json", + "--relay", + "ws://127.0.0.1:9", + "--approval-token", + "approve", + "order", + "submit", + order_id.as_str(), + ]); + + assert!(!output.status.success()); + assert_eq!(output.status.code(), Some(10)); + assert_eq!(value["operation_id"], "order.submit"); + assert_eq!(value["errors"][0]["code"], "validation_failed"); + assert_eq!( + value["errors"][0]["detail"]["issues"][0]["code"], + "order_bin_unknown" + ); + assert_eq!( + value["errors"][0]["detail"]["issues"][0]["field"], + "order.items[0].bin_id" + ); + assert!( + value["errors"][0]["detail"]["issues"][0]["message"] + .as_str() + .expect("issue message") + .contains("expected primary bin `bin-1`") + ); + assert_no_removed_command_reference(&value, &["order", "submit"]); + assert_no_daemon_runtime_reference(&value, &["order", "submit"]); +} + +#[test] fn order_submit_dry_run_rejects_over_available_quantity_before_relay_preflight() { let sandbox = RadrootsCliSandbox::new(); sandbox.json_success(&["--format", "json", "account", "create"]);