commit dbf061a57afaa92569c876f1a4606233cff0ef47
parent 2e046b2f44fdd72147a3c120fa5ffe360732eeb8
Author: triesap <tyson@radroots.org>
Date: Wed, 29 Apr 2026 19:34:21 +0000
order: reject unknown submit bins
Diffstat:
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"]);