cli

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

commit 1fe750c28141106e940127cdac05bf2360d73fd6
parent 61bc212e4f208ab8d5c83280973322f650691d8f
Author: triesap <tyson@radroots.org>
Date:   Wed,  6 May 2026 15:08:18 +0000

cli: harden deferred payment precedence

Diffstat:
Asrc/deferred_payment.rs | 12++++++++++++
Msrc/main.rs | 35+++++++++++++++++++----------------
Msrc/operation_adapter.rs | 7+------
Msrc/operation_order.rs | 6++----
Msrc/target_cli.rs | 41++++++++++++++++++-----------------------
Mtests/target_cli.rs | 32++++++++++++++++++++++++++++++++
6 files changed, 84 insertions(+), 49 deletions(-)

diff --git a/src/deferred_payment.rs b/src/deferred_payment.rs @@ -0,0 +1,12 @@ +pub const DEFERRED_PAYMENT_MESSAGE: &str = "payments and settlement are not implemented in this Radroots release; order coordination is available now, and payment support is planned for a future phase"; + +pub fn is_deferred_payment_operation(operation_id: &str) -> bool { + matches!( + operation_id, + "order.payment.record" | "order.settlement.accept" | "order.settlement.reject" + ) +} + +pub fn deferred_payment_message() -> String { + DEFERRED_PAYMENT_MESSAGE.to_owned() +} diff --git a/src/main.rs b/src/main.rs @@ -1,5 +1,6 @@ #![forbid(unsafe_code)] +mod deferred_payment; mod domain; mod operation_adapter; mod operation_basket; @@ -22,6 +23,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use clap::Parser; +use crate::deferred_payment::{deferred_payment_message, is_deferred_payment_operation}; use crate::operation_adapter::{ OperationAdapter, OperationAdapterError, OperationNetworkMode, OperationOutputFormat, OperationRequest, OperationRequestPayload, OperationResultPayload, OperationService, @@ -56,10 +58,15 @@ fn run() -> Result<ExitCode, runtime::RuntimeError> { debug_assert!(operation_registry::registry_linkage_is_valid()); debug_assert!(operation_adapter::adapter_registry_linkage_is_valid()); let args = TargetCliArgs::parse(); - let config = RuntimeConfig::from_system(&runtime_args_from_target(&args))?; - let logging = initialize_logging(&config.logging)?; let request = TargetOperationRequest::from_target_args(&args).map_err(operation_config_error)?; + if let Err(error) = validate_pre_runtime_request_contract(&request) { + let envelope = failure_envelope(&request, error); + render_envelope(&envelope, args.format)?; + return Ok(envelope_exit_code(&envelope)); + } + let config = RuntimeConfig::from_system(&runtime_args_from_target(&args))?; + let logging = initialize_logging(&config.logging)?; let envelope = match validate_request_contract(&request, &config) { Ok(()) => execute_request(request, &config, &logging), Err(error) => failure_envelope(&request, error), @@ -336,6 +343,15 @@ fn validate_request_contract( request: &TargetOperationRequest, config: &RuntimeConfig, ) -> Result<(), OperationAdapterError> { + validate_pre_runtime_request_contract(request)?; + validate_signer_mode_contract(request, config)?; + validate_network_contract(request, config)?; + Ok(()) +} + +fn validate_pre_runtime_request_contract( + request: &TargetOperationRequest, +) -> Result<(), OperationAdapterError> { let spec = request.spec(); if matches!( request.context().output_format, @@ -353,14 +369,12 @@ fn validate_request_contract( message: format!("`{}` does not support --dry-run", spec.cli_path), }); } - if deferred_payment_operation(spec.operation_id) { + if is_deferred_payment_operation(spec.operation_id) { return Err(OperationAdapterError::not_implemented( spec.operation_id, deferred_payment_message(), )); } - validate_signer_mode_contract(request, config)?; - validate_network_contract(request, config)?; Ok(()) } @@ -460,17 +474,6 @@ fn external_network_operation(operation_id: &str) -> bool { ) } -fn deferred_payment_operation(operation_id: &str) -> bool { - matches!( - operation_id, - "order.payment.record" | "order.settlement.accept" | "order.settlement.reject" - ) -} - -fn deferred_payment_message() -> String { - "payments and settlement are not implemented in this Radroots release; order coordination is available now, and payment support is planned for a future phase".to_owned() -} - fn failure_envelope( request: &TargetOperationRequest, error: OperationAdapterError, diff --git a/src/operation_adapter.rs b/src/operation_adapter.rs @@ -1287,12 +1287,7 @@ fn target_operation_input(command: &crate::target_cli::TargetCommand) -> Operati insert_string(&mut input, "order_id", &args.order_id); insert_string(&mut input, "amount", &args.amount); insert_string(&mut input, "currency", &args.currency); - if let Some(method) = args.method { - input.insert( - "method".to_owned(), - Value::String(method.as_protocol_method().to_owned()), - ); - } + insert_string(&mut input, "method", &args.method); insert_string(&mut input, "reference", &args.reference); if let Some(paid_at) = args.paid_at { input.insert( diff --git a/src/operation_order.rs b/src/operation_order.rs @@ -1,6 +1,7 @@ use serde::Serialize; use serde_json::{Value, json}; +use crate::deferred_payment::deferred_payment_message; use crate::domain::runtime::{ CommandDisposition, OrderCancellationView, OrderDecisionView, OrderFulfillmentView, OrderReceiptView, OrderRevisionDecisionView, OrderRevisionProposalView, OrderStatusView, @@ -1267,10 +1268,7 @@ fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapter } fn deferred_payment_error(operation_id: &str) -> OperationAdapterError { - OperationAdapterError::not_implemented( - operation_id, - "payments and settlement are not implemented in this Radroots release; order coordination is available now, and payment support is planned for a future phase".to_owned(), - ) + OperationAdapterError::not_implemented(operation_id, deferred_payment_message()) } fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError { diff --git a/src/target_cli.rs b/src/target_cli.rs @@ -917,32 +917,14 @@ pub struct OrderPaymentRecordArgs { pub amount: Option<String>, #[arg(long)] pub currency: Option<String>, - #[arg(long, value_enum)] - pub method: Option<OrderPaymentMethodArg>, + #[arg(long)] + pub method: Option<String>, #[arg(long)] pub reference: Option<String>, #[arg(long)] pub paid_at: Option<u64>, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -#[value(rename_all = "snake_case")] -pub enum OrderPaymentMethodArg { - Cash, - ManualTransfer, - Other, -} - -impl OrderPaymentMethodArg { - pub const fn as_protocol_method(self) -> &'static str { - match self { - Self::Cash => "cash", - Self::ManualTransfer => "manual_transfer", - Self::Other => "other", - } - } -} - #[derive(Debug, Clone, Args)] pub struct OrderSettlementArgs { #[command(subcommand)] @@ -1008,8 +990,8 @@ mod tests { use super::{ OrderCommand, OrderFulfillmentCommand, OrderFulfillmentStateArg, OrderPaymentCommand, - OrderPaymentMethodArg, OrderReceiptCommand, OrderRevisionCommand, OrderSettlementCommand, - TargetCliArgs, TargetOutputFormat, + OrderReceiptCommand, OrderRevisionCommand, OrderSettlementCommand, TargetCliArgs, + TargetOutputFormat, }; use crate::operation_registry::OPERATION_REGISTRY; @@ -1311,9 +1293,22 @@ mod tests { assert_eq!(args.order_id.as_deref(), Some("ord_test")); assert_eq!(args.amount.as_deref(), Some("12")); assert_eq!(args.currency.as_deref(), Some("USD")); - assert_eq!(args.method, Some(OrderPaymentMethodArg::ManualTransfer)); + assert_eq!(args.method.as_deref(), Some("manual_transfer")); assert_eq!(args.reference.as_deref(), Some("memo-1")); assert_eq!(args.paid_at, Some(1_777_666_000)); + + let future_method = TargetCliArgs::try_parse_from([ + "radroots", "order", "payment", "record", "ord_test", "--method", "card", + ]) + .expect("target args parse"); + let crate::target_cli::TargetCommand::Order(order) = future_method.command else { + panic!("expected order command") + }; + let OrderCommand::Payment(payment) = order.command else { + panic!("expected order payment command") + }; + let OrderPaymentCommand::Record(args) = payment.command; + assert_eq!(args.method.as_deref(), Some("card")); } #[test] diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -283,6 +283,22 @@ fn payment_commands_return_not_implemented_before_mutation_preflight() { [ "--format", "json", + "--relay", + "not-a-url", + "order", + "payment", + "record", + "ord_pending", + "--method", + "card", + ] + .as_slice(), + ), + ( + "order.payment.record", + [ + "--format", + "json", "--offline", "order", "payment", @@ -308,7 +324,23 @@ fn payment_commands_return_not_implemented_before_mutation_preflight() { [ "--format", "json", + "--relay", + "not-a-url", + "order", + "settlement", + "accept", + "ord_pending", + ] + .as_slice(), + ), + ( + "order.settlement.accept", + [ + "--format", + "json", "--online", + "--relay", + "not-a-url", "order", "settlement", "accept",