cli

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

commit b4852735871eb3fa7deb96eee8f3622656c90c71
parent e8a7ac48433222b97fe9f327b2ab65f4f70efb02
Author: triesap <tyson@radroots.org>
Date:   Wed,  6 May 2026 13:28:51 +0000

cli: defer payment commands with not_implemented

- add structured not_implemented output for reserved payment operations
- return deferred payment errors before approval and relay preflight
- reclassify payment routes as non-mutating reserved command surfaces
- cover target and service paths for payment and settlement deferral

Diffstat:
Msrc/main.rs | 23+++++++++++++++++------
Msrc/operation_adapter.rs | 38++++++++++++++++++++++++++++++++++++++
Msrc/operation_order.rs | 478++++++-------------------------------------------------------------------------
Msrc/operation_registry.rs | 30++++++++++++------------------
Msrc/runtime/order.rs | 2++
Msrc/runtime_args.rs | 3+++
Mtests/target_cli.rs | 296++++++++++++++++++++++++++-----------------------------------------------------
7 files changed, 205 insertions(+), 665 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -353,6 +353,12 @@ fn validate_request_contract( message: format!("`{}` does not support --dry-run", spec.cli_path), }); } + if 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(()) @@ -426,9 +432,6 @@ fn dry_run_requires_network(operation_id: &str) -> bool { | "order.revision.decline" | "order.fulfillment.update" | "order.receipt.record" - | "order.payment.record" - | "order.settlement.accept" - | "order.settlement.reject" ) } @@ -451,15 +454,23 @@ fn external_network_operation(operation_id: &str) -> bool { | "order.revision.decline" | "order.fulfillment.update" | "order.receipt.record" - | "order.payment.record" - | "order.settlement.accept" - | "order.settlement.reject" | "order.status.get" | "order.event.list" | "order.event.watch" ) } +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 @@ -381,6 +381,11 @@ pub enum OperationAdapterError { operation_id: String, message: String, }, + #[error("operation `{operation_id}` is not implemented: {message}")] + NotImplemented { + operation_id: String, + message: String, + }, #[error("operation `{operation_id}` failed: {message}")] DetailedFailure { operation_id: String, @@ -450,6 +455,13 @@ impl OperationAdapterError { } } + pub fn not_implemented(operation_id: &str, message: String) -> Self { + Self::NotImplemented { + operation_id: operation_id.to_owned(), + message, + } + } + pub fn network_unavailable_with_detail( operation_id: &str, message: String, @@ -678,6 +690,16 @@ impl OperationAdapterError { message, CliExitCode::RuntimeUnavailable, ), + Self::NotImplemented { + operation_id, + message, + } => runtime_output_error( + "not_implemented", + operation_id, + "operation", + message, + CliExitCode::RuntimeUnavailable, + ), Self::DetailedFailure { operation_id, code, @@ -1912,6 +1934,22 @@ mod tests { } #[test] + fn not_implemented_errors_map_to_structured_exit_code() { + let error = OperationAdapterError::not_implemented( + "order.payment.record", + "coming soon".to_owned(), + ); + let output_error = error.to_output_error(); + + assert_eq!(output_error.code, "not_implemented"); + assert_eq!(output_error.exit_code, 3); + assert_eq!( + output_error.detail.expect("detail")["operation_id"], + "order.payment.record" + ); + } + + #[test] fn runtime_failures_map_to_specific_machine_codes() { let cases = [ ( diff --git a/src/operation_order.rs b/src/operation_order.rs @@ -3,8 +3,8 @@ use serde_json::{Value, json}; use crate::domain::runtime::{ CommandDisposition, OrderCancellationView, OrderDecisionView, OrderFulfillmentView, - OrderPaymentView, OrderReceiptView, OrderRevisionDecisionView, OrderRevisionProposalView, - OrderSettlementView, OrderStatusView, OrderSubmitView, + OrderReceiptView, OrderRevisionDecisionView, OrderRevisionProposalView, OrderStatusView, + OrderSubmitView, }; use crate::operation_adapter::{ OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload, @@ -22,9 +22,8 @@ use crate::operation_adapter::{ use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; use crate::runtime_args::{ - OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs, OrderFulfillmentArgs, OrderPaymentArgs, - OrderReceiptArgs, OrderRevisionDecisionArg, OrderRevisionDecisionArgs, - OrderRevisionProposeArgs, OrderSettlementArgs, OrderSettlementDecisionArg, OrderStatusArgs, + OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs, OrderFulfillmentArgs, OrderReceiptArgs, + OrderRevisionDecisionArg, OrderRevisionDecisionArgs, OrderRevisionProposeArgs, OrderStatusArgs, OrderSubmitArgs, OrderWatchArgs, RecordLookupArgs, }; @@ -445,64 +444,7 @@ impl OperationService<OrderPaymentRecordRequest> for OrderOperationService<'_> { &self, request: OperationRequest<OrderPaymentRecordRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let amount = string_input(&request, "amount") - .map(|amount| amount.trim().to_owned()) - .filter(|amount| !amount.is_empty()) - .ok_or_else(|| { - invalid_input( - request.operation_id(), - "missing required payment amount input".to_owned(), - ) - })?; - let currency = string_input(&request, "currency") - .map(|currency| currency.trim().to_owned()) - .filter(|currency| !currency.is_empty()) - .ok_or_else(|| { - invalid_input( - request.operation_id(), - "missing required payment currency input".to_owned(), - ) - })?; - let method = string_input(&request, "method") - .map(|method| method.trim().to_owned()) - .filter(|method| !method.is_empty()) - .ok_or_else(|| { - invalid_input( - request.operation_id(), - "missing required payment method input".to_owned(), - ) - })?; - let reference = string_input(&request, "reference") - .map(|reference| reference.trim().to_owned()) - .filter(|reference| !reference.is_empty()); - let paid_at = u64_input(&request, "paid_at"); - if request.context.requires_approval_token() { - return Err(OperationAdapterError::approval_required( - request.operation_id(), - )); - } - - let args = OrderPaymentArgs { - key: required_order_key(&request)?, - amount, - currency, - method, - reference, - paid_at, - idempotency_key: request - .context - .idempotency_key - .clone() - .or_else(|| string_input(&request, "idempotency_key")), - }; - let mut config = self.config.clone(); - if request.context.dry_run { - config.output.dry_run = true; - } - let view = crate::runtime::order::payment_record(&config, &args).map_err(|error| { - OperationAdapterError::runtime_failure(request.operation_id(), error) - })?; - payment_result::<OrderPaymentRecordResult>(request.operation_id(), &view) + Err(deferred_payment_error(request.operation_id())) } } @@ -513,32 +455,7 @@ impl OperationService<OrderSettlementAcceptRequest> for OrderOperationService<'_ &self, request: OperationRequest<OrderSettlementAcceptRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let payment_event_id = required_payment_event_id(&request)?; - if request.context.requires_approval_token() { - return Err(OperationAdapterError::approval_required( - request.operation_id(), - )); - } - - let args = OrderSettlementArgs { - key: required_order_key(&request)?, - payment_event_id, - decision: OrderSettlementDecisionArg::Accept, - reason: None, - idempotency_key: request - .context - .idempotency_key - .clone() - .or_else(|| string_input(&request, "idempotency_key")), - }; - let mut config = self.config.clone(); - if request.context.dry_run { - config.output.dry_run = true; - } - let view = crate::runtime::order::settlement_decision(&config, &args).map_err(|error| { - OperationAdapterError::runtime_failure(request.operation_id(), error) - })?; - settlement_result::<OrderSettlementAcceptResult>(request.operation_id(), &view) + Err(deferred_payment_error(request.operation_id())) } } @@ -549,41 +466,7 @@ impl OperationService<OrderSettlementRejectRequest> for OrderOperationService<'_ &self, request: OperationRequest<OrderSettlementRejectRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let payment_event_id = required_payment_event_id(&request)?; - let reason = string_input(&request, "reason") - .map(|reason| reason.trim().to_owned()) - .filter(|reason| !reason.is_empty()) - .ok_or_else(|| { - invalid_input( - request.operation_id(), - "missing required settlement rejection reason input".to_owned(), - ) - })?; - if request.context.requires_approval_token() { - return Err(OperationAdapterError::approval_required( - request.operation_id(), - )); - } - - let args = OrderSettlementArgs { - key: required_order_key(&request)?, - payment_event_id, - decision: OrderSettlementDecisionArg::Reject, - reason: Some(reason), - idempotency_key: request - .context - .idempotency_key - .clone() - .or_else(|| string_input(&request, "idempotency_key")), - }; - let mut config = self.config.clone(); - if request.context.dry_run { - config.output.dry_run = true; - } - let view = crate::runtime::order::settlement_decision(&config, &args).map_err(|error| { - OperationAdapterError::runtime_failure(request.operation_id(), error) - })?; - settlement_result::<OrderSettlementRejectResult>(request.operation_id(), &view) + Err(deferred_payment_error(request.operation_id())) } } @@ -1142,198 +1025,6 @@ where } } -fn payment_result<R>( - operation_id: &str, - view: &OrderPaymentView, -) -> Result<OperationResult<R>, OperationAdapterError> -where - R: OperationResultData, -{ - match view.disposition() { - CommandDisposition::Success => serialized_target_result::<R, _>(view), - CommandDisposition::ValidationFailed => { - let message = view.reason.clone().unwrap_or_else(|| { - format!( - "order payment record failed validation with state `{}`", - view.state - ) - }); - Err(OperationAdapterError::validation_failed_with_detail( - operation_id, - message, - order_payment_error_detail(view), - )) - } - disposition => { - let message = view.reason.clone().unwrap_or_else(|| { - format!("order payment record finished with state `{}`", view.state) - }); - if disposition == CommandDisposition::ExternalUnavailable { - let detail = order_payment_error_detail(view); - if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { - Err(OperationAdapterError::network_unavailable_with_detail( - operation_id, - message, - detail, - )) - } else { - Err(OperationAdapterError::operation_unavailable_with_detail( - operation_id, - message, - detail, - )) - } - } else if disposition == CommandDisposition::Unconfigured { - Err(OperationAdapterError::operation_unavailable_with_detail( - operation_id, - message, - order_payment_error_detail(view), - )) - } else { - Err(OperationAdapterError::from_command_disposition( - operation_id, - disposition, - message, - )) - } - } - } -} - -fn settlement_result<R>( - operation_id: &str, - view: &OrderSettlementView, -) -> Result<OperationResult<R>, OperationAdapterError> -where - R: OperationResultData, -{ - match view.disposition() { - CommandDisposition::Success => serialized_target_result::<R, _>(view), - CommandDisposition::ValidationFailed => { - let message = view.reason.clone().unwrap_or_else(|| { - format!( - "order settlement decision failed validation with state `{}`", - view.state - ) - }); - Err(OperationAdapterError::validation_failed_with_detail( - operation_id, - message, - order_settlement_error_detail(view), - )) - } - disposition => { - let message = view.reason.clone().unwrap_or_else(|| { - format!( - "order settlement decision finished with state `{}`", - view.state - ) - }); - if disposition == CommandDisposition::ExternalUnavailable { - let detail = order_settlement_error_detail(view); - if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { - Err(OperationAdapterError::network_unavailable_with_detail( - operation_id, - message, - detail, - )) - } else { - Err(OperationAdapterError::operation_unavailable_with_detail( - operation_id, - message, - detail, - )) - } - } else if disposition == CommandDisposition::Unconfigured { - Err(OperationAdapterError::operation_unavailable_with_detail( - operation_id, - message, - order_settlement_error_detail(view), - )) - } else { - Err(OperationAdapterError::from_command_disposition( - operation_id, - disposition, - message, - )) - } - } - } -} - -fn order_payment_error_detail(view: &OrderPaymentView) -> Value { - json!({ - "state": &view.state, - "order_id": &view.order_id, - "listing_addr": &view.listing_addr, - "request_event_id": &view.request_event_id, - "agreement_event_id": &view.agreement_event_id, - "root_event_id": &view.root_event_id, - "prev_event_id": &view.prev_event_id, - "event_id": &view.event_id, - "event_kind": view.event_kind, - "buyer_pubkey": &view.buyer_pubkey, - "seller_pubkey": &view.seller_pubkey, - "quote_id": &view.quote_id, - "quote_version": view.quote_version, - "economics_digest": &view.economics_digest, - "amount": &view.amount, - "currency": &view.currency, - "method": &view.method, - "reference": &view.reference, - "paid_at": &view.paid_at, - "dry_run": view.dry_run, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "acknowledged_relays": &view.acknowledged_relays, - "failed_relays": &view.failed_relays, - "fetched_count": view.fetched_count, - "decoded_count": view.decoded_count, - "skipped_count": view.skipped_count, - "idempotency_key": &view.idempotency_key, - "signer_mode": &view.signer_mode, - "issues": &view.issues, - "actions": &view.actions, - }) -} - -fn order_settlement_error_detail(view: &OrderSettlementView) -> Value { - json!({ - "state": &view.state, - "order_id": &view.order_id, - "listing_addr": &view.listing_addr, - "request_event_id": &view.request_event_id, - "agreement_event_id": &view.agreement_event_id, - "root_event_id": &view.root_event_id, - "prev_event_id": &view.prev_event_id, - "payment_event_id": &view.payment_event_id, - "event_id": &view.event_id, - "event_kind": view.event_kind, - "buyer_pubkey": &view.buyer_pubkey, - "seller_pubkey": &view.seller_pubkey, - "quote_id": &view.quote_id, - "quote_version": view.quote_version, - "economics_digest": &view.economics_digest, - "amount": &view.amount, - "currency": &view.currency, - "decision": &view.decision, - "settlement_reason": &view.settlement_reason, - "reason": &view.reason, - "dry_run": view.dry_run, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "acknowledged_relays": &view.acknowledged_relays, - "failed_relays": &view.failed_relays, - "fetched_count": view.fetched_count, - "decoded_count": view.decoded_count, - "skipped_count": view.skipped_count, - "idempotency_key": &view.idempotency_key, - "signer_mode": &view.signer_mode, - "issues": &view.issues, - "actions": &view.actions, - }) -} - fn order_receipt_error_detail(view: &OrderReceiptView) -> Value { json!({ "state": &view.state, @@ -1503,15 +1194,6 @@ where }) } -fn required_payment_event_id<P>( - request: &OperationRequest<P>, -) -> Result<String, OperationAdapterError> -where - P: OperationRequestPayload + OperationRequestData, -{ - required_string_input(request, "payment_event_id") -} - fn required_string_input<P>( request: &OperationRequest<P>, key: &str, @@ -1584,6 +1266,13 @@ fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapter result.map_err(|error| OperationAdapterError::Runtime(error.to_string())) } +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(), + ) +} + fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError { OperationAdapterError::InvalidInput { operation_id: operation_id.to_owned(), @@ -1948,141 +1637,44 @@ mod tests { } #[test] - fn order_payment_record_requires_amount_before_approval() { + fn deferred_payment_commands_return_not_implemented_before_input_or_approval() { let dir = tempdir().expect("tempdir"); let config = sample_config(dir.path()); let service = OperationAdapter::new(OrderOperationService::new(&config)); - let payment = OperationRequest::new( - OperationContext::default(), - OrderPaymentRecordRequest::from_data(data(&[ - ("order_id", "ord_pending"), - ("currency", "USD"), - ("method", "cash"), - ])), - ) - .expect("order payment request"); - let error = service.execute(payment).expect_err("amount required"); - let output_error = error.to_output_error(); - - assert_eq!(output_error.code, "invalid_input"); - assert!(output_error.message.contains("amount")); - } - - #[test] - fn order_payment_record_requires_method_before_approval() { - let dir = tempdir().expect("tempdir"); - let config = sample_config(dir.path()); - let service = OperationAdapter::new(OrderOperationService::new(&config)); - let payment = OperationRequest::new( - OperationContext::default(), - OrderPaymentRecordRequest::from_data(data(&[ - ("order_id", "ord_pending"), - ("amount", "12"), - ("currency", "USD"), - ])), - ) - .expect("order payment request"); - let error = service.execute(payment).expect_err("method required"); - let output_error = error.to_output_error(); - - assert_eq!(output_error.code, "invalid_input"); - assert!(output_error.message.contains("method")); - } - #[test] - fn order_payment_record_requires_approval_token() { - let dir = tempdir().expect("tempdir"); - let config = sample_config(dir.path()); - let service = OperationAdapter::new(OrderOperationService::new(&config)); let payment = OperationRequest::new( OperationContext::default(), - OrderPaymentRecordRequest::from_data(data(&[ - ("order_id", "ord_pending"), - ("amount", "12"), - ("currency", "USD"), - ("method", "cash"), - ])), + OrderPaymentRecordRequest::from_data(data(&[("order_id", "ord_pending")])), ) .expect("order payment request"); - let error = service.execute(payment).expect_err("approval required"); + let payment_error = service.execute(payment).expect_err("payment deferred"); + assert_eq!(payment_error.to_output_error().code, "not_implemented"); - assert_eq!(error.to_output_error().code, "approval_required"); - } - - #[test] - fn order_settlement_accept_requires_payment_event_before_approval() { - let dir = tempdir().expect("tempdir"); - let config = sample_config(dir.path()); - let service = OperationAdapter::new(OrderOperationService::new(&config)); - let settlement = OperationRequest::new( + let settlement_accept = OperationRequest::new( OperationContext::default(), OrderSettlementAcceptRequest::from_data(data(&[("order_id", "ord_pending")])), ) .expect("order settlement accept request"); - let error = service - .execute(settlement) - .expect_err("payment event required"); - let output_error = error.to_output_error(); - - assert_eq!(output_error.code, "invalid_input"); - assert!(output_error.message.contains("payment_event_id")); - } - - #[test] - fn order_settlement_accept_requires_approval_token() { - let dir = tempdir().expect("tempdir"); - let config = sample_config(dir.path()); - let service = OperationAdapter::new(OrderOperationService::new(&config)); - let settlement = OperationRequest::new( - OperationContext::default(), - OrderSettlementAcceptRequest::from_data(data(&[ - ("order_id", "ord_pending"), - ("payment_event_id", "pay_pending"), - ])), - ) - .expect("order settlement accept request"); - let error = service.execute(settlement).expect_err("approval required"); - - assert_eq!(error.to_output_error().code, "approval_required"); - } - - #[test] - fn order_settlement_reject_requires_reason_before_approval() { - let dir = tempdir().expect("tempdir"); - let config = sample_config(dir.path()); - let service = OperationAdapter::new(OrderOperationService::new(&config)); - let settlement = OperationRequest::new( - OperationContext::default(), - OrderSettlementRejectRequest::from_data(data(&[ - ("order_id", "ord_pending"), - ("payment_event_id", "pay_pending"), - ])), - ) - .expect("order settlement reject request"); - let error = service.execute(settlement).expect_err("reason required"); - let output_error = error.to_output_error(); - - assert_eq!(output_error.code, "invalid_input"); - assert!(output_error.message.contains("reason")); - } + let settlement_accept_error = service + .execute(settlement_accept) + .expect_err("settlement accept deferred"); + assert_eq!( + settlement_accept_error.to_output_error().code, + "not_implemented" + ); - #[test] - fn order_settlement_reject_requires_approval_token() { - let dir = tempdir().expect("tempdir"); - let config = sample_config(dir.path()); - let service = OperationAdapter::new(OrderOperationService::new(&config)); - let settlement = OperationRequest::new( + let settlement_reject = OperationRequest::new( OperationContext::default(), - OrderSettlementRejectRequest::from_data(data(&[ - ("order_id", "ord_pending"), - ("payment_event_id", "pay_pending"), - ("reason", "reference mismatch"), - ])), + OrderSettlementRejectRequest::from_data(data(&[("order_id", "ord_pending")])), ) .expect("order settlement reject request"); - let error = service.execute(settlement).expect_err("approval required"); - - assert_eq!(error.to_output_error().code, "approval_required"); + let settlement_reject_error = service + .execute(settlement_reject) + .expect_err("settlement reject deferred"); + assert_eq!( + settlement_reject_error.to_output_error().code, + "not_implemented" + ); } #[test] diff --git a/src/operation_registry.rs b/src/operation_registry.rs @@ -998,11 +998,11 @@ pub const OPERATION_REGISTRY: &[OperationSpec] = &[ "order_payment_record", "OrderPaymentRecordRequest", "OrderPaymentRecordResult", - "Record buyer manual payment.", + "Reserved future buyer manual payment command.", Buyer, - true, - Required, - High, + false, + None, + Low, false, true ), @@ -1013,11 +1013,11 @@ pub const OPERATION_REGISTRY: &[OperationSpec] = &[ "order_settlement_accept", "OrderSettlementAcceptRequest", "OrderSettlementAcceptResult", - "Accept seller settlement of a recorded payment.", + "Reserved future seller settlement acceptance command.", Seller, - true, - Required, - High, + false, + None, + Low, false, true ), @@ -1028,11 +1028,11 @@ pub const OPERATION_REGISTRY: &[OperationSpec] = &[ "order_settlement_reject", "OrderSettlementRejectRequest", "OrderSettlementRejectResult", - "Reject seller settlement of a recorded payment.", + "Reserved future seller settlement rejection command.", Seller, - true, - Required, - High, + false, + None, + Low, false, true ), @@ -1211,9 +1211,6 @@ mod tests { "order.revision.decline", "order.fulfillment.update", "order.receipt.record", - "order.payment.record", - "order.settlement.accept", - "order.settlement.reject", ]; const INTENTIONALLY_UNSUPPORTED_MUTATING_DRY_RUN_OPERATION_IDS: &[&str] = &[]; @@ -1281,9 +1278,6 @@ mod tests { "order.revision.decline", "order.fulfillment.update", "order.receipt.record", - "order.payment.record", - "order.settlement.accept", - "order.settlement.reject", ] .into_iter() .collect::<BTreeSet<_>>(); diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::fs; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/src/runtime_args.rs b/src/runtime_args.rs @@ -245,6 +245,7 @@ pub struct OrderReceiptArgs { } #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct OrderPaymentArgs { pub key: String, pub amount: String, @@ -301,12 +302,14 @@ pub struct OrderRevisionDecisionArgs { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] pub enum OrderSettlementDecisionArg { Accept, Reject, } #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct OrderSettlementArgs { pub key: String, pub payment_event_id: String, diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -262,6 +262,104 @@ fn seller_order_decision_and_status_commands_are_public() { } #[test] +fn payment_commands_return_not_implemented_before_mutation_preflight() { + let sandbox = RadrootsCliSandbox::new(); + + for (operation_id, args) in [ + ( + "order.payment.record", + [ + "--format", + "json", + "order", + "payment", + "record", + "ord_pending", + ] + .as_slice(), + ), + ( + "order.payment.record", + [ + "--format", + "json", + "--offline", + "order", + "payment", + "record", + "ord_pending", + ] + .as_slice(), + ), + ( + "order.settlement.accept", + [ + "--format", + "json", + "order", + "settlement", + "accept", + "ord_pending", + ] + .as_slice(), + ), + ( + "order.settlement.accept", + [ + "--format", + "json", + "--online", + "order", + "settlement", + "accept", + "ord_pending", + ] + .as_slice(), + ), + ( + "order.settlement.reject", + [ + "--format", + "json", + "order", + "settlement", + "reject", + "ord_pending", + ] + .as_slice(), + ), + ( + "order.settlement.reject", + [ + "--format", + "json", + "--offline", + "--dry-run", + "order", + "settlement", + "reject", + "ord_pending", + ] + .as_slice(), + ), + ] { + let (output, value) = sandbox.json_output(args); + + assert!(!output.status.success()); + assert_eq!(output.status.code(), Some(3)); + assert_eq!(value["operation_id"], operation_id); + assert_eq!(value["result"], Value::Null); + assert_eq!(value["errors"][0]["code"], "not_implemented"); + assert_eq!(value["errors"][0]["exit_code"], 3); + let message = value["errors"][0]["message"].as_str().expect("message"); + assert!(message.contains("not implemented")); + assert!(message.contains("future phase")); + assert!(!message.contains("approval_token")); + assert!(!message.contains("relay")); + } +} + +#[test] fn target_outputs_do_not_suggest_removed_command_families() { let sandbox = RadrootsCliSandbox::new(); @@ -655,57 +753,6 @@ fn offline_forbids_external_network_operations() { ] .as_slice(), ), - ( - "order.payment.record", - [ - "--format", - "json", - "--offline", - "order", - "payment", - "record", - "ord_offline_payment", - "--amount", - "12", - "--currency", - "USD", - "--method", - "cash", - ] - .as_slice(), - ), - ( - "order.settlement.accept", - [ - "--format", - "json", - "--offline", - "order", - "settlement", - "accept", - "ord_offline_settlement", - "--payment-event-id", - "pay_event", - ] - .as_slice(), - ), - ( - "order.settlement.reject", - [ - "--format", - "json", - "--offline", - "order", - "settlement", - "reject", - "ord_offline_settlement", - "--payment-event-id", - "pay_event", - "--reason", - "reference mismatch", - ] - .as_slice(), - ), ] { let output = radroots() .args(args) @@ -875,60 +922,6 @@ fn offline_rejects_order_decision_dry_run() { ] .as_slice(), ), - ( - "order.payment.record", - [ - "--format", - "json", - "--offline", - "--dry-run", - "order", - "payment", - "record", - "ord_offline_decision", - "--amount", - "12", - "--currency", - "USD", - "--method", - "manual_transfer", - ] - .as_slice(), - ), - ( - "order.settlement.accept", - [ - "--format", - "json", - "--offline", - "--dry-run", - "order", - "settlement", - "accept", - "ord_offline_decision", - "--payment-event-id", - "pay_event", - ] - .as_slice(), - ), - ( - "order.settlement.reject", - [ - "--format", - "json", - "--offline", - "--dry-run", - "order", - "settlement", - "reject", - "ord_offline_decision", - "--payment-event-id", - "pay_event", - "--reason", - "reference mismatch", - ] - .as_slice(), - ), ] { let output = radroots() .args(args) @@ -1126,57 +1119,6 @@ fn online_requires_relay_for_external_network_operations() { ] .as_slice(), ), - ( - "order.payment.record", - [ - "--format", - "json", - "--online", - "order", - "payment", - "record", - "ord_missing", - "--amount", - "12", - "--currency", - "USD", - "--method", - "cash", - ] - .as_slice(), - ), - ( - "order.settlement.accept", - [ - "--format", - "json", - "--online", - "order", - "settlement", - "accept", - "ord_missing", - "--payment-event-id", - "pay_event", - ] - .as_slice(), - ), - ( - "order.settlement.reject", - [ - "--format", - "json", - "--online", - "order", - "settlement", - "reject", - "ord_missing", - "--payment-event-id", - "pay_event", - "--reason", - "reference mismatch", - ] - .as_slice(), - ), ] { let output = radroots() .args(args) @@ -1681,48 +1623,6 @@ fn required_approval_token_rejects_absent_empty_and_whitespace_values() { "order.receipt.record", &["order", "receipt", "record", "ord_pending", "--received"], ); - assert_required_approval_token_rejected( - &sandbox, - "order.payment.record", - &[ - "order", - "payment", - "record", - "ord_pending", - "--amount", - "12", - "--currency", - "USD", - "--method", - "cash", - ], - ); - assert_required_approval_token_rejected( - &sandbox, - "order.settlement.accept", - &[ - "order", - "settlement", - "accept", - "ord_pending", - "--payment-event-id", - "pay_pending", - ], - ); - assert_required_approval_token_rejected( - &sandbox, - "order.settlement.reject", - &[ - "order", - "settlement", - "reject", - "ord_pending", - "--payment-event-id", - "pay_pending", - "--reason", - "reference mismatch", - ], - ); } fn assert_required_approval_token_rejected(