cli

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

order.rs (73901B)


      1 use std::path::PathBuf;
      2 
      3 use serde::Serialize;
      4 use serde_json::{Value, json};
      5 
      6 use crate::cli::global::{
      7     OrderAppRecordExportArgs, OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs,
      8     OrderRebindArgs, OrderRevisionDecisionArg, OrderRevisionDecisionArgs, OrderRevisionProposeArgs,
      9     OrderStatusArgs, OrderSubmitArgs, RecordLookupArgs,
     10 };
     11 use crate::ops::{
     12     OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
     13     OperationResult, OperationResultData, OperationService, OrderAcceptRequest, OrderAcceptResult,
     14     OrderAppExportRequest, OrderAppExportResult, OrderAppListRequest, OrderAppListResult,
     15     OrderCancelRequest, OrderCancelResult, OrderDeclineRequest, OrderDeclineResult,
     16     OrderEventListRequest, OrderEventListResult, OrderEventWatchRequest, OrderEventWatchResult,
     17     OrderGetRequest, OrderGetResult, OrderListRequest, OrderListResult, OrderRebindRequest,
     18     OrderRebindResult, OrderRevisionAcceptRequest, OrderRevisionAcceptResult,
     19     OrderRevisionDeclineRequest, OrderRevisionDeclineResult, OrderRevisionProposeRequest,
     20     OrderRevisionProposeResult, OrderStatusGetRequest, OrderStatusGetResult, OrderSubmitRequest,
     21     OrderSubmitResult,
     22 };
     23 use crate::runtime::RuntimeError;
     24 use crate::runtime::config::RuntimeConfig;
     25 use crate::view::runtime::{
     26     CommandDisposition, OrderAppRecordExportView, OrderCancellationView, OrderDecisionView,
     27     OrderRebindView, OrderRevisionDecisionView, OrderRevisionProposalView, OrderStatusView,
     28     OrderSubmitView,
     29 };
     30 
     31 const ORDER_EVENT_WATCH_DEFERRED_REASON: &str = "relay-backed order event watch is not implemented";
     32 
     33 pub struct OrderOperationService<'a> {
     34     config: &'a RuntimeConfig,
     35 }
     36 
     37 impl<'a> OrderOperationService<'a> {
     38     pub fn new(config: &'a RuntimeConfig) -> Self {
     39         Self { config }
     40     }
     41 }
     42 
     43 impl OperationService<OrderSubmitRequest> for OrderOperationService<'_> {
     44     type Result = OrderSubmitResult;
     45 
     46     fn execute(
     47         &self,
     48         request: OperationRequest<OrderSubmitRequest>,
     49     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     50         if request.context.requires_approval_token() {
     51             return Err(OperationAdapterError::approval_required(
     52                 request.operation_id(),
     53             ));
     54         }
     55 
     56         let key = required_order_key(&request)?;
     57         let args = OrderSubmitArgs {
     58             key,
     59             idempotency_key: request
     60                 .context
     61                 .idempotency_key
     62                 .clone()
     63                 .or_else(|| string_input(&request, "idempotency_key")),
     64         };
     65         let mut config = self.config.clone();
     66         if request.context.dry_run {
     67             config.output.dry_run = true;
     68         }
     69         let view = crate::runtime::order::submit(&config, &args).map_err(|error| {
     70             OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
     71         })?;
     72         submit_result::<OrderSubmitResult>(request.operation_id(), &view)
     73     }
     74 }
     75 
     76 impl OperationService<OrderGetRequest> for OrderOperationService<'_> {
     77     type Result = OrderGetResult;
     78 
     79     fn execute(
     80         &self,
     81         request: OperationRequest<OrderGetRequest>,
     82     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     83         let args = RecordLookupArgs {
     84             key: required_order_key(&request)?,
     85         };
     86         let view = map_runtime(crate::runtime::order::get(self.config, &args))?;
     87         serialized_target_result::<OrderGetResult, _>(&view)
     88     }
     89 }
     90 
     91 impl OperationService<OrderListRequest> for OrderOperationService<'_> {
     92     type Result = OrderListResult;
     93 
     94     fn execute(
     95         &self,
     96         _request: OperationRequest<OrderListRequest>,
     97     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     98         let view = map_runtime(crate::runtime::order::list(self.config))?;
     99         serialized_target_result::<OrderListResult, _>(&view)
    100     }
    101 }
    102 
    103 impl OperationService<OrderAppListRequest> for OrderOperationService<'_> {
    104     type Result = OrderAppListResult;
    105 
    106     fn execute(
    107         &self,
    108         _request: OperationRequest<OrderAppListRequest>,
    109     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    110         let view = map_runtime(crate::runtime::order::app_record_list(self.config))?;
    111         serialized_target_result::<OrderAppListResult, _>(&view)
    112     }
    113 }
    114 
    115 impl OperationService<OrderAppExportRequest> for OrderOperationService<'_> {
    116     type Result = OrderAppExportResult;
    117 
    118     fn execute(
    119         &self,
    120         request: OperationRequest<OrderAppExportRequest>,
    121     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    122         let args = OrderAppRecordExportArgs {
    123             record_id: required_string_input(&request, "record_id")?,
    124             output: optional_path_input(&request, "output"),
    125         };
    126         let mut config = self.config.clone();
    127         if request.context.dry_run {
    128             config.output.dry_run = true;
    129         }
    130         let view = crate::runtime::order::app_record_export(&config, &args).map_err(|error| {
    131             OperationAdapterError::runtime_failure(request.operation_id(), error)
    132         })?;
    133         order_app_record_export_result::<OrderAppExportResult>(request.operation_id(), &view)
    134     }
    135 }
    136 
    137 impl OperationService<OrderRebindRequest> for OrderOperationService<'_> {
    138     type Result = OrderRebindResult;
    139 
    140     fn execute(
    141         &self,
    142         request: OperationRequest<OrderRebindRequest>,
    143     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    144         let args = OrderRebindArgs {
    145             key: required_order_key(&request)?,
    146             selector: required_string_input(&request, "selector")?,
    147         };
    148         if request.context.dry_run {
    149             let view =
    150                 crate::runtime::order::rebind_preflight(self.config, &args).map_err(|error| {
    151                     OperationAdapterError::runtime_failure(request.operation_id(), error)
    152                 })?;
    153             return order_rebind_result::<OrderRebindResult>(request.operation_id(), &view);
    154         }
    155         if request.context.requires_approval_token() {
    156             return Err(OperationAdapterError::approval_required(
    157                 request.operation_id(),
    158             ));
    159         }
    160 
    161         let view = crate::runtime::order::rebind(self.config, &args).map_err(|error| {
    162             OperationAdapterError::runtime_failure(request.operation_id(), error)
    163         })?;
    164         order_rebind_result::<OrderRebindResult>(request.operation_id(), &view)
    165     }
    166 }
    167 
    168 impl OperationService<OrderAcceptRequest> for OrderOperationService<'_> {
    169     type Result = OrderAcceptResult;
    170 
    171     fn execute(
    172         &self,
    173         request: OperationRequest<OrderAcceptRequest>,
    174     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    175         if request.context.requires_approval_token() {
    176             return Err(OperationAdapterError::approval_required(
    177                 request.operation_id(),
    178             ));
    179         }
    180 
    181         let args = OrderDecisionArgs {
    182             key: required_order_key(&request)?,
    183             decision: OrderDecisionArg::Accept,
    184             reason: None,
    185             idempotency_key: request
    186                 .context
    187                 .idempotency_key
    188                 .clone()
    189                 .or_else(|| string_input(&request, "idempotency_key")),
    190         };
    191         let mut config = self.config.clone();
    192         if request.context.dry_run {
    193             config.output.dry_run = true;
    194         }
    195         let view = crate::runtime::order::decide(&config, &args).map_err(|error| {
    196             OperationAdapterError::runtime_failure(request.operation_id(), error)
    197         })?;
    198         decision_result::<OrderAcceptResult>(request.operation_id(), &view)
    199     }
    200 }
    201 
    202 impl OperationService<OrderDeclineRequest> for OrderOperationService<'_> {
    203     type Result = OrderDeclineResult;
    204 
    205     fn execute(
    206         &self,
    207         request: OperationRequest<OrderDeclineRequest>,
    208     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    209         let reason = string_input(&request, "reason")
    210             .map(|reason| reason.trim().to_owned())
    211             .filter(|reason| !reason.is_empty())
    212             .ok_or_else(|| {
    213                 invalid_input(
    214                     request.operation_id(),
    215                     "missing required `reason` input".to_owned(),
    216                 )
    217             })?;
    218         if request.context.requires_approval_token() {
    219             return Err(OperationAdapterError::approval_required(
    220                 request.operation_id(),
    221             ));
    222         }
    223 
    224         let args = OrderDecisionArgs {
    225             key: required_order_key(&request)?,
    226             decision: OrderDecisionArg::Decline,
    227             reason: Some(reason),
    228             idempotency_key: request
    229                 .context
    230                 .idempotency_key
    231                 .clone()
    232                 .or_else(|| string_input(&request, "idempotency_key")),
    233         };
    234         let mut config = self.config.clone();
    235         if request.context.dry_run {
    236             config.output.dry_run = true;
    237         }
    238         let view = crate::runtime::order::decide(&config, &args).map_err(|error| {
    239             OperationAdapterError::runtime_failure(request.operation_id(), error)
    240         })?;
    241         decision_result::<OrderDeclineResult>(request.operation_id(), &view)
    242     }
    243 }
    244 
    245 impl OperationService<OrderCancelRequest> for OrderOperationService<'_> {
    246     type Result = OrderCancelResult;
    247 
    248     fn execute(
    249         &self,
    250         request: OperationRequest<OrderCancelRequest>,
    251     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    252         let reason = string_input(&request, "reason")
    253             .map(|reason| reason.trim().to_owned())
    254             .filter(|reason| !reason.is_empty())
    255             .ok_or_else(|| {
    256                 invalid_input(
    257                     request.operation_id(),
    258                     "missing required `reason` input".to_owned(),
    259                 )
    260             })?;
    261         if request.context.requires_approval_token() {
    262             return Err(OperationAdapterError::approval_required(
    263                 request.operation_id(),
    264             ));
    265         }
    266 
    267         let args = OrderCancelArgs {
    268             key: required_order_key(&request)?,
    269             reason,
    270             idempotency_key: request
    271                 .context
    272                 .idempotency_key
    273                 .clone()
    274                 .or_else(|| string_input(&request, "idempotency_key")),
    275         };
    276         let mut config = self.config.clone();
    277         if request.context.dry_run {
    278             config.output.dry_run = true;
    279         }
    280         let view = crate::runtime::order::cancel(&config, &args).map_err(|error| {
    281             OperationAdapterError::runtime_failure(request.operation_id(), error)
    282         })?;
    283         cancellation_result::<OrderCancelResult>(request.operation_id(), &view)
    284     }
    285 }
    286 
    287 impl OperationService<OrderRevisionProposeRequest> for OrderOperationService<'_> {
    288     type Result = OrderRevisionProposeResult;
    289 
    290     fn execute(
    291         &self,
    292         request: OperationRequest<OrderRevisionProposeRequest>,
    293     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    294         let reason = string_input(&request, "reason")
    295             .map(|reason| reason.trim().to_owned())
    296             .filter(|reason| !reason.is_empty())
    297             .ok_or_else(|| {
    298                 invalid_input(
    299                     request.operation_id(),
    300                     "missing required `reason` input".to_owned(),
    301                 )
    302             })?;
    303         if request.context.requires_approval_token() {
    304             return Err(OperationAdapterError::approval_required(
    305                 request.operation_id(),
    306             ));
    307         }
    308 
    309         let args = OrderRevisionProposeArgs {
    310             key: required_order_key(&request)?,
    311             reason,
    312             bin_id: string_input(&request, "bin_id"),
    313             bin_count: u32_input(&request, "bin_count"),
    314             adjustment_id: string_input(&request, "adjustment_id"),
    315             adjustment_effect: string_input(&request, "adjustment_effect"),
    316             adjustment_amount: string_input(&request, "adjustment_amount"),
    317             adjustment_currency: string_input(&request, "adjustment_currency"),
    318             adjustment_reason: string_input(&request, "adjustment_reason"),
    319             idempotency_key: request
    320                 .context
    321                 .idempotency_key
    322                 .clone()
    323                 .or_else(|| string_input(&request, "idempotency_key")),
    324         };
    325         let mut config = self.config.clone();
    326         if request.context.dry_run {
    327             config.output.dry_run = true;
    328         }
    329         let view = crate::runtime::order::revision_propose(&config, &args).map_err(|error| {
    330             OperationAdapterError::runtime_failure(request.operation_id(), error)
    331         })?;
    332         revision_proposal_result::<OrderRevisionProposeResult>(request.operation_id(), &view)
    333     }
    334 }
    335 
    336 impl OperationService<OrderRevisionAcceptRequest> for OrderOperationService<'_> {
    337     type Result = OrderRevisionAcceptResult;
    338 
    339     fn execute(
    340         &self,
    341         request: OperationRequest<OrderRevisionAcceptRequest>,
    342     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    343         let args = OrderRevisionDecisionArgs {
    344             key: required_order_key(&request)?,
    345             revision_id: required_string_input(&request, "revision_id")?,
    346             decision: OrderRevisionDecisionArg::Accept,
    347             reason: None,
    348             idempotency_key: request
    349                 .context
    350                 .idempotency_key
    351                 .clone()
    352                 .or_else(|| string_input(&request, "idempotency_key")),
    353         };
    354         if request.context.requires_approval_token() {
    355             return Err(OperationAdapterError::approval_required(
    356                 request.operation_id(),
    357             ));
    358         }
    359 
    360         let mut config = self.config.clone();
    361         if request.context.dry_run {
    362             config.output.dry_run = true;
    363         }
    364         let view = crate::runtime::order::revision_decide(&config, &args).map_err(|error| {
    365             OperationAdapterError::runtime_failure(request.operation_id(), error)
    366         })?;
    367         revision_decision_result::<OrderRevisionAcceptResult>(request.operation_id(), &view)
    368     }
    369 }
    370 
    371 impl OperationService<OrderRevisionDeclineRequest> for OrderOperationService<'_> {
    372     type Result = OrderRevisionDeclineResult;
    373 
    374     fn execute(
    375         &self,
    376         request: OperationRequest<OrderRevisionDeclineRequest>,
    377     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    378         let reason = string_input(&request, "reason")
    379             .map(|reason| reason.trim().to_owned())
    380             .filter(|reason| !reason.is_empty())
    381             .ok_or_else(|| {
    382                 invalid_input(
    383                     request.operation_id(),
    384                     "missing required `reason` input".to_owned(),
    385                 )
    386             })?;
    387         let args = OrderRevisionDecisionArgs {
    388             key: required_order_key(&request)?,
    389             revision_id: required_string_input(&request, "revision_id")?,
    390             decision: OrderRevisionDecisionArg::Decline,
    391             reason: Some(reason),
    392             idempotency_key: request
    393                 .context
    394                 .idempotency_key
    395                 .clone()
    396                 .or_else(|| string_input(&request, "idempotency_key")),
    397         };
    398         if request.context.requires_approval_token() {
    399             return Err(OperationAdapterError::approval_required(
    400                 request.operation_id(),
    401             ));
    402         }
    403 
    404         let mut config = self.config.clone();
    405         if request.context.dry_run {
    406             config.output.dry_run = true;
    407         }
    408         let view = crate::runtime::order::revision_decide(&config, &args).map_err(|error| {
    409             OperationAdapterError::runtime_failure(request.operation_id(), error)
    410         })?;
    411         revision_decision_result::<OrderRevisionDeclineResult>(request.operation_id(), &view)
    412     }
    413 }
    414 
    415 impl OperationService<OrderStatusGetRequest> for OrderOperationService<'_> {
    416     type Result = OrderStatusGetResult;
    417 
    418     fn execute(
    419         &self,
    420         request: OperationRequest<OrderStatusGetRequest>,
    421     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    422         let args = OrderStatusArgs {
    423             key: required_order_key(&request)?,
    424         };
    425         let view = crate::runtime::order::status(self.config, &args).map_err(|error| {
    426             OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
    427         })?;
    428         status_result::<OrderStatusGetResult>(request.operation_id(), &view)
    429     }
    430 }
    431 
    432 impl OperationService<OrderEventListRequest> for OrderOperationService<'_> {
    433     type Result = OrderEventListResult;
    434 
    435     fn execute(
    436         &self,
    437         request: OperationRequest<OrderEventListRequest>,
    438     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    439         let order_id = string_input(&request, "order_id");
    440         let view = crate::runtime::order::event_list(self.config, order_id.as_deref()).map_err(
    441             |error| OperationAdapterError::runtime_failure(request.operation_id(), error),
    442         )?;
    443         event_list_result::<OrderEventListResult>(request.operation_id(), &view)
    444     }
    445 }
    446 
    447 impl OperationService<OrderEventWatchRequest> for OrderOperationService<'_> {
    448     type Result = OrderEventWatchResult;
    449 
    450     fn execute(
    451         &self,
    452         request: OperationRequest<OrderEventWatchRequest>,
    453     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    454         let order_id = required_order_key(&request)?;
    455         let action = format!("radroots order status get {order_id}");
    456         Err(OperationAdapterError::not_implemented_with_detail(
    457             request.operation_id(),
    458             ORDER_EVENT_WATCH_DEFERRED_REASON.to_owned(),
    459             json!({
    460                 "state": "not_implemented",
    461                 "order_id": order_id,
    462                 "reason": ORDER_EVENT_WATCH_DEFERRED_REASON,
    463                 "actions": [action],
    464             }),
    465         ))
    466     }
    467 }
    468 
    469 fn decision_result<R>(
    470     operation_id: &str,
    471     view: &OrderDecisionView,
    472 ) -> Result<OperationResult<R>, OperationAdapterError>
    473 where
    474     R: OperationResultData,
    475 {
    476     match view.disposition() {
    477         CommandDisposition::Success => serialized_target_result::<R, _>(view),
    478         CommandDisposition::ValidationFailed => {
    479             let message = view.reason.clone().unwrap_or_else(|| {
    480                 format!(
    481                     "order decision failed validation with state `{}`",
    482                     view.state
    483                 )
    484             });
    485             Err(OperationAdapterError::validation_failed_with_detail(
    486                 operation_id,
    487                 message,
    488                 order_decision_error_detail(view),
    489             ))
    490         }
    491         disposition => {
    492             let message = view
    493                 .reason
    494                 .clone()
    495                 .unwrap_or_else(|| format!("order decision finished with state `{}`", view.state));
    496             if disposition == CommandDisposition::ExternalUnavailable {
    497                 let detail = order_decision_error_detail(view);
    498                 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() {
    499                     Err(OperationAdapterError::network_unavailable_with_detail(
    500                         operation_id,
    501                         message,
    502                         detail,
    503                     ))
    504                 } else {
    505                     Err(OperationAdapterError::operation_unavailable_with_detail(
    506                         operation_id,
    507                         message,
    508                         detail,
    509                     ))
    510                 }
    511             } else if disposition == CommandDisposition::Unconfigured {
    512                 Err(OperationAdapterError::operation_unavailable_with_detail(
    513                     operation_id,
    514                     message,
    515                     order_decision_error_detail(view),
    516                 ))
    517             } else if disposition == CommandDisposition::NotFound {
    518                 Err(OperationAdapterError::not_found_with_detail(
    519                     operation_id,
    520                     message,
    521                     order_decision_error_detail(view),
    522                 ))
    523             } else {
    524                 Err(OperationAdapterError::from_command_disposition(
    525                     operation_id,
    526                     disposition,
    527                     message,
    528                 ))
    529             }
    530         }
    531     }
    532 }
    533 
    534 fn order_decision_error_detail(view: &OrderDecisionView) -> Value {
    535     json!({
    536         "state": &view.state,
    537         "order_id": &view.order_id,
    538         "listing_addr": &view.listing_addr,
    539         "listing_event_id": &view.listing_event_id,
    540         "request_event_id": &view.request_event_id,
    541         "root_event_id": &view.root_event_id,
    542         "prev_event_id": &view.prev_event_id,
    543         "event_id": &view.event_id,
    544         "event_kind": view.event_kind,
    545         "inventory": &view.inventory,
    546         "buyer_pubkey": &view.buyer_pubkey,
    547         "seller_pubkey": &view.seller_pubkey,
    548         "decision": &view.decision,
    549         "dry_run": view.dry_run,
    550         "target_relays": &view.target_relays,
    551         "connected_relays": &view.connected_relays,
    552         "acknowledged_relays": &view.acknowledged_relays,
    553         "failed_relays": &view.failed_relays,
    554         "fetched_count": view.fetched_count,
    555         "decoded_count": view.decoded_count,
    556         "skipped_count": view.skipped_count,
    557         "idempotency_key": &view.idempotency_key,
    558         "signer_mode": &view.signer_mode,
    559         "issues": &view.issues,
    560         "actions": &view.actions,
    561     })
    562 }
    563 
    564 fn cancellation_result<R>(
    565     operation_id: &str,
    566     view: &OrderCancellationView,
    567 ) -> Result<OperationResult<R>, OperationAdapterError>
    568 where
    569     R: OperationResultData,
    570 {
    571     match view.disposition() {
    572         CommandDisposition::Success => serialized_target_result::<R, _>(view),
    573         CommandDisposition::ValidationFailed => {
    574             let message = view.reason.clone().unwrap_or_else(|| {
    575                 format!("order cancel failed validation with state `{}`", view.state)
    576             });
    577             Err(OperationAdapterError::validation_failed_with_detail(
    578                 operation_id,
    579                 message,
    580                 order_cancellation_error_detail(view),
    581             ))
    582         }
    583         disposition => {
    584             let message = view
    585                 .reason
    586                 .clone()
    587                 .unwrap_or_else(|| format!("order cancel finished with state `{}`", view.state));
    588             if disposition == CommandDisposition::ExternalUnavailable {
    589                 let detail = order_cancellation_error_detail(view);
    590                 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() {
    591                     Err(OperationAdapterError::network_unavailable_with_detail(
    592                         operation_id,
    593                         message,
    594                         detail,
    595                     ))
    596                 } else {
    597                     Err(OperationAdapterError::operation_unavailable_with_detail(
    598                         operation_id,
    599                         message,
    600                         detail,
    601                     ))
    602                 }
    603             } else if disposition == CommandDisposition::Unconfigured {
    604                 Err(OperationAdapterError::operation_unavailable_with_detail(
    605                     operation_id,
    606                     message,
    607                     order_cancellation_error_detail(view),
    608                 ))
    609             } else {
    610                 Err(OperationAdapterError::from_command_disposition(
    611                     operation_id,
    612                     disposition,
    613                     message,
    614                 ))
    615             }
    616         }
    617     }
    618 }
    619 
    620 fn order_cancellation_error_detail(view: &OrderCancellationView) -> Value {
    621     json!({
    622         "state": &view.state,
    623         "order_id": &view.order_id,
    624         "listing_addr": &view.listing_addr,
    625         "request_event_id": &view.request_event_id,
    626         "decision_event_id": &view.decision_event_id,
    627         "root_event_id": &view.root_event_id,
    628         "prev_event_id": &view.prev_event_id,
    629         "event_id": &view.event_id,
    630         "event_kind": view.event_kind,
    631         "buyer_pubkey": &view.buyer_pubkey,
    632         "seller_pubkey": &view.seller_pubkey,
    633         "cancellation_reason": &view.cancellation_reason,
    634         "dry_run": view.dry_run,
    635         "target_relays": &view.target_relays,
    636         "connected_relays": &view.connected_relays,
    637         "acknowledged_relays": &view.acknowledged_relays,
    638         "failed_relays": &view.failed_relays,
    639         "fetched_count": view.fetched_count,
    640         "decoded_count": view.decoded_count,
    641         "skipped_count": view.skipped_count,
    642         "idempotency_key": &view.idempotency_key,
    643         "signer_mode": &view.signer_mode,
    644         "issues": &view.issues,
    645         "actions": &view.actions,
    646     })
    647 }
    648 
    649 fn revision_proposal_result<R>(
    650     operation_id: &str,
    651     view: &OrderRevisionProposalView,
    652 ) -> Result<OperationResult<R>, OperationAdapterError>
    653 where
    654     R: OperationResultData,
    655 {
    656     match view.disposition() {
    657         CommandDisposition::Success => serialized_target_result::<R, _>(view),
    658         CommandDisposition::ValidationFailed => {
    659             let message = view.reason.clone().unwrap_or_else(|| {
    660                 format!(
    661                     "order revision propose failed validation with state `{}`",
    662                     view.state
    663                 )
    664             });
    665             Err(OperationAdapterError::validation_failed_with_detail(
    666                 operation_id,
    667                 message,
    668                 order_revision_proposal_error_detail(view),
    669             ))
    670         }
    671         disposition => {
    672             let message = view.reason.clone().unwrap_or_else(|| {
    673                 format!(
    674                     "order revision propose finished with state `{}`",
    675                     view.state
    676                 )
    677             });
    678             if disposition == CommandDisposition::ExternalUnavailable {
    679                 let detail = order_revision_proposal_error_detail(view);
    680                 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() {
    681                     Err(OperationAdapterError::network_unavailable_with_detail(
    682                         operation_id,
    683                         message,
    684                         detail,
    685                     ))
    686                 } else {
    687                     Err(OperationAdapterError::operation_unavailable_with_detail(
    688                         operation_id,
    689                         message,
    690                         detail,
    691                     ))
    692                 }
    693             } else if disposition == CommandDisposition::Unconfigured {
    694                 Err(OperationAdapterError::operation_unavailable_with_detail(
    695                     operation_id,
    696                     message,
    697                     order_revision_proposal_error_detail(view),
    698                 ))
    699             } else {
    700                 Err(OperationAdapterError::from_command_disposition(
    701                     operation_id,
    702                     disposition,
    703                     message,
    704                 ))
    705             }
    706         }
    707     }
    708 }
    709 
    710 fn order_revision_proposal_error_detail(view: &OrderRevisionProposalView) -> Value {
    711     json!({
    712         "state": &view.state,
    713         "order_id": &view.order_id,
    714         "revision_id": &view.revision_id,
    715         "listing_addr": &view.listing_addr,
    716         "request_event_id": &view.request_event_id,
    717         "decision_event_id": &view.decision_event_id,
    718         "root_event_id": &view.root_event_id,
    719         "prev_event_id": &view.prev_event_id,
    720         "event_id": &view.event_id,
    721         "event_kind": view.event_kind,
    722         "items": &view.items,
    723         "economics": &view.economics,
    724         "inventory": &view.inventory,
    725         "buyer_pubkey": &view.buyer_pubkey,
    726         "seller_pubkey": &view.seller_pubkey,
    727         "dry_run": view.dry_run,
    728         "target_relays": &view.target_relays,
    729         "connected_relays": &view.connected_relays,
    730         "acknowledged_relays": &view.acknowledged_relays,
    731         "failed_relays": &view.failed_relays,
    732         "fetched_count": view.fetched_count,
    733         "decoded_count": view.decoded_count,
    734         "skipped_count": view.skipped_count,
    735         "idempotency_key": &view.idempotency_key,
    736         "signer_mode": &view.signer_mode,
    737         "issues": &view.issues,
    738         "actions": &view.actions,
    739     })
    740 }
    741 
    742 fn revision_decision_result<R>(
    743     operation_id: &str,
    744     view: &OrderRevisionDecisionView,
    745 ) -> Result<OperationResult<R>, OperationAdapterError>
    746 where
    747     R: OperationResultData,
    748 {
    749     match view.disposition() {
    750         CommandDisposition::Success => serialized_target_result::<R, _>(view),
    751         CommandDisposition::ValidationFailed => {
    752             let message = view.reason.clone().unwrap_or_else(|| {
    753                 format!(
    754                     "order revision {} failed validation with state `{}`",
    755                     view.decision.as_deref().unwrap_or("decision"),
    756                     view.state
    757                 )
    758             });
    759             Err(OperationAdapterError::validation_failed_with_detail(
    760                 operation_id,
    761                 message,
    762                 order_revision_decision_error_detail(view),
    763             ))
    764         }
    765         disposition => {
    766             let message = view.reason.clone().unwrap_or_else(|| {
    767                 format!(
    768                     "order revision {} finished with state `{}`",
    769                     view.decision.as_deref().unwrap_or("decision"),
    770                     view.state
    771                 )
    772             });
    773             if disposition == CommandDisposition::ExternalUnavailable {
    774                 let detail = order_revision_decision_error_detail(view);
    775                 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() {
    776                     Err(OperationAdapterError::network_unavailable_with_detail(
    777                         operation_id,
    778                         message,
    779                         detail,
    780                     ))
    781                 } else {
    782                     Err(OperationAdapterError::operation_unavailable_with_detail(
    783                         operation_id,
    784                         message,
    785                         detail,
    786                     ))
    787                 }
    788             } else if disposition == CommandDisposition::Unconfigured {
    789                 Err(OperationAdapterError::operation_unavailable_with_detail(
    790                     operation_id,
    791                     message,
    792                     order_revision_decision_error_detail(view),
    793                 ))
    794             } else {
    795                 Err(OperationAdapterError::from_command_disposition(
    796                     operation_id,
    797                     disposition,
    798                     message,
    799                 ))
    800             }
    801         }
    802     }
    803 }
    804 
    805 fn order_revision_decision_error_detail(view: &OrderRevisionDecisionView) -> Value {
    806     json!({
    807         "state": &view.state,
    808         "order_id": &view.order_id,
    809         "revision_id": &view.revision_id,
    810         "decision": &view.decision,
    811         "listing_addr": &view.listing_addr,
    812         "request_event_id": &view.request_event_id,
    813         "decision_event_id": &view.decision_event_id,
    814         "agreement_event_id": &view.agreement_event_id,
    815         "root_event_id": &view.root_event_id,
    816         "prev_event_id": &view.prev_event_id,
    817         "event_id": &view.event_id,
    818         "event_kind": view.event_kind,
    819         "economics": &view.economics,
    820         "inventory": &view.inventory,
    821         "buyer_pubkey": &view.buyer_pubkey,
    822         "seller_pubkey": &view.seller_pubkey,
    823         "dry_run": view.dry_run,
    824         "target_relays": &view.target_relays,
    825         "connected_relays": &view.connected_relays,
    826         "acknowledged_relays": &view.acknowledged_relays,
    827         "failed_relays": &view.failed_relays,
    828         "fetched_count": view.fetched_count,
    829         "decoded_count": view.decoded_count,
    830         "skipped_count": view.skipped_count,
    831         "idempotency_key": &view.idempotency_key,
    832         "signer_mode": &view.signer_mode,
    833         "issues": &view.issues,
    834         "actions": &view.actions,
    835     })
    836 }
    837 
    838 fn status_result<R>(
    839     operation_id: &str,
    840     view: &OrderStatusView,
    841 ) -> Result<OperationResult<R>, OperationAdapterError>
    842 where
    843     R: OperationResultData,
    844 {
    845     match view.disposition() {
    846         CommandDisposition::Success => serialized_target_result::<R, _>(view),
    847         disposition => {
    848             let message = view
    849                 .reason
    850                 .clone()
    851                 .unwrap_or_else(|| format!("order status finished with state `{}`", view.state));
    852             if disposition == CommandDisposition::ExternalUnavailable {
    853                 let detail = order_status_error_detail(view);
    854                 if !view.failed_relays.is_empty() && view.connected_relays.is_empty() {
    855                     Err(OperationAdapterError::network_unavailable_with_detail(
    856                         operation_id,
    857                         message,
    858                         detail,
    859                     ))
    860                 } else {
    861                     Err(OperationAdapterError::operation_unavailable_with_detail(
    862                         operation_id,
    863                         message,
    864                         detail,
    865                     ))
    866                 }
    867             } else if disposition == CommandDisposition::Unconfigured {
    868                 Err(OperationAdapterError::operation_unavailable_with_detail(
    869                     operation_id,
    870                     message,
    871                     order_status_error_detail(view),
    872                 ))
    873             } else {
    874                 Err(OperationAdapterError::from_command_disposition(
    875                     operation_id,
    876                     disposition,
    877                     message,
    878                 ))
    879             }
    880         }
    881     }
    882 }
    883 
    884 fn order_status_error_detail(view: &OrderStatusView) -> Value {
    885     json!({
    886         "state": &view.state,
    887         "order_id": &view.order_id,
    888         "request_event_id": &view.request_event_id,
    889         "decision_event_id": &view.decision_event_id,
    890         "agreement_event_id": &view.agreement_event_id,
    891         "listing_event_id": &view.listing_event_id,
    892         "listing_addr": &view.listing_addr,
    893         "buyer_pubkey": &view.buyer_pubkey,
    894         "seller_pubkey": &view.seller_pubkey,
    895         "last_event_id": &view.last_event_id,
    896         "revision": &view.revision,
    897         "inventory": &view.inventory,
    898         "lifecycle": &view.lifecycle,
    899         "reducer_issues": &view.reducer_issues,
    900         "target_relays": &view.target_relays,
    901         "connected_relays": &view.connected_relays,
    902         "failed_relays": &view.failed_relays,
    903         "fetched_count": view.fetched_count,
    904         "decoded_count": view.decoded_count,
    905         "skipped_count": view.skipped_count,
    906         "actions": &view.actions,
    907     })
    908 }
    909 
    910 fn serialized_target_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
    911 where
    912     R: OperationResultData,
    913     T: Serialize,
    914 {
    915     OperationResult::new(R::from_serializable(value)?)
    916 }
    917 
    918 fn submit_result<R>(
    919     operation_id: &str,
    920     view: &OrderSubmitView,
    921 ) -> Result<OperationResult<R>, OperationAdapterError>
    922 where
    923     R: OperationResultData,
    924 {
    925     match view.disposition() {
    926         CommandDisposition::Success => serialized_target_result::<R, _>(view),
    927         disposition => {
    928             let message = view
    929                 .reason
    930                 .clone()
    931                 .unwrap_or_else(|| format!("order submit finished with state `{}`", view.state));
    932             let detail = order_submit_error_detail(view);
    933             match disposition {
    934                 CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail(
    935                     operation_id,
    936                     message,
    937                     detail,
    938                 )),
    939                 CommandDisposition::ValidationFailed => {
    940                     Err(OperationAdapterError::validation_failed_with_detail(
    941                         operation_id,
    942                         message,
    943                         detail,
    944                     ))
    945                 }
    946                 CommandDisposition::Unconfigured => {
    947                     Err(OperationAdapterError::operation_unavailable_with_detail(
    948                         operation_id,
    949                         message,
    950                         detail,
    951                     ))
    952                 }
    953                 CommandDisposition::ExternalUnavailable => {
    954                     if !view.failed_relays.is_empty() && view.connected_relays.is_empty() {
    955                         Err(OperationAdapterError::network_unavailable_with_detail(
    956                             operation_id,
    957                             message,
    958                             detail,
    959                         ))
    960                     } else {
    961                         Err(OperationAdapterError::operation_unavailable_with_detail(
    962                             operation_id,
    963                             message,
    964                             detail,
    965                         ))
    966                     }
    967                 }
    968                 _ => Err(OperationAdapterError::from_command_disposition(
    969                     operation_id,
    970                     disposition,
    971                     message,
    972                 )),
    973             }
    974         }
    975     }
    976 }
    977 
    978 fn order_app_record_export_result<R>(
    979     operation_id: &str,
    980     view: &OrderAppRecordExportView,
    981 ) -> Result<OperationResult<R>, OperationAdapterError>
    982 where
    983     R: OperationResultData,
    984 {
    985     match view.disposition() {
    986         CommandDisposition::Success => serialized_target_result::<R, _>(view),
    987         CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail(
    988             operation_id,
    989             view.reason.clone().unwrap_or_else(|| {
    990                 format!(
    991                     "app-authored local order record `{}` was not found",
    992                     view.record_id
    993                 )
    994             }),
    995             serde_json::to_value(view).unwrap_or(Value::Null),
    996         )),
    997         CommandDisposition::ValidationFailed => {
    998             Err(OperationAdapterError::validation_failed_with_detail(
    999                 operation_id,
   1000                 view.reason.clone().unwrap_or_else(|| {
   1001                     format!(
   1002                         "app-authored local order record `{}` cannot be exported",
   1003                         view.record_id
   1004                     )
   1005                 }),
   1006                 serde_json::to_value(view).unwrap_or(Value::Null),
   1007             ))
   1008         }
   1009         disposition => Err(OperationAdapterError::from_command_disposition(
   1010             operation_id,
   1011             disposition,
   1012             view.reason.clone().unwrap_or_else(|| {
   1013                 format!(
   1014                     "app-authored local order record export finished with state `{}`",
   1015                     view.state
   1016                 )
   1017             }),
   1018         )),
   1019     }
   1020 }
   1021 
   1022 fn order_rebind_result<R>(
   1023     operation_id: &str,
   1024     view: &OrderRebindView,
   1025 ) -> Result<OperationResult<R>, OperationAdapterError>
   1026 where
   1027     R: OperationResultData,
   1028 {
   1029     match view.disposition() {
   1030         CommandDisposition::Success => serialized_target_result::<R, _>(view),
   1031         CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail(
   1032             operation_id,
   1033             view.reason
   1034                 .clone()
   1035                 .unwrap_or_else(|| format!("order draft `{}` was not found", view.lookup)),
   1036             order_rebind_error_detail(view),
   1037         )),
   1038         CommandDisposition::ValidationFailed => {
   1039             Err(OperationAdapterError::validation_failed_with_detail(
   1040                 operation_id,
   1041                 view.reason.clone().unwrap_or_else(|| {
   1042                     format!("order rebind finished with state `{}`", view.state)
   1043                 }),
   1044                 order_rebind_error_detail(view),
   1045             ))
   1046         }
   1047         disposition => Err(OperationAdapterError::from_command_disposition(
   1048             operation_id,
   1049             disposition,
   1050             view.reason
   1051                 .clone()
   1052                 .unwrap_or_else(|| format!("order rebind finished with state `{}`", view.state)),
   1053         )),
   1054     }
   1055 }
   1056 
   1057 fn order_rebind_error_detail(view: &OrderRebindView) -> Value {
   1058     json!({
   1059         "state": &view.state,
   1060         "source": &view.source,
   1061         "lookup": &view.lookup,
   1062         "file": &view.file,
   1063         "dry_run": view.dry_run,
   1064         "from_order_id": &view.from_order_id,
   1065         "to_order_id": &view.to_order_id,
   1066         "order_id_changed": view.order_id_changed,
   1067         "from_buyer_account_id": &view.from_buyer_account_id,
   1068         "from_buyer_pubkey": &view.from_buyer_pubkey,
   1069         "from_buyer_actor_source": &view.from_buyer_actor_source,
   1070         "to_buyer_account_id": &view.to_buyer_account_id,
   1071         "to_buyer_pubkey": &view.to_buyer_pubkey,
   1072         "to_buyer_actor_source": &view.to_buyer_actor_source,
   1073         "buyer_pubkey_changed": view.buyer_pubkey_changed,
   1074         "existing_request_check": &view.existing_request_check,
   1075         "existing_request_event_ids": &view.existing_request_event_ids,
   1076         "actions": &view.actions,
   1077     })
   1078 }
   1079 
   1080 fn order_submit_error_detail(view: &OrderSubmitView) -> Value {
   1081     json!({
   1082         "state": &view.state,
   1083         "source": &view.source,
   1084         "order_id": &view.order_id,
   1085         "file": &view.file,
   1086         "listing_lookup": &view.listing_lookup,
   1087         "listing_addr": &view.listing_addr,
   1088         "listing_event_id": &view.listing_event_id,
   1089         "listing_relays": &view.listing_relays,
   1090         "buyer_account_id": &view.buyer_account_id,
   1091         "buyer_pubkey": &view.buyer_pubkey,
   1092         "seller_pubkey": &view.seller_pubkey,
   1093         "event_id": &view.event_id,
   1094         "event_kind": view.event_kind,
   1095         "dry_run": view.dry_run,
   1096         "deduplicated": view.deduplicated,
   1097         "target_relays": &view.target_relays,
   1098         "connected_relays": &view.connected_relays,
   1099         "acknowledged_relays": &view.acknowledged_relays,
   1100         "failed_relays": &view.failed_relays,
   1101         "idempotency_key": &view.idempotency_key,
   1102         "signer_mode": &view.signer_mode,
   1103         "issues": &view.issues,
   1104         "actions": &view.actions,
   1105     })
   1106 }
   1107 
   1108 fn event_list_result<R>(
   1109     operation_id: &str,
   1110     view: &crate::view::runtime::OrderEventListView,
   1111 ) -> Result<OperationResult<R>, OperationAdapterError>
   1112 where
   1113     R: OperationResultData,
   1114 {
   1115     match view.disposition() {
   1116         CommandDisposition::Success => serialized_target_result::<R, _>(view),
   1117         disposition => {
   1118             let message = view.reason.clone().unwrap_or_else(|| {
   1119                 format!("order event list finished with state `{}`", view.state)
   1120             });
   1121             if disposition == CommandDisposition::ExternalUnavailable {
   1122                 let detail = order_event_list_error_detail(view);
   1123                 Err(OperationAdapterError::network_unavailable_with_detail(
   1124                     operation_id,
   1125                     message,
   1126                     detail,
   1127                 ))
   1128             } else if disposition == CommandDisposition::Unconfigured {
   1129                 Err(OperationAdapterError::operation_unavailable_with_detail(
   1130                     operation_id,
   1131                     message,
   1132                     order_event_list_error_detail(view),
   1133                 ))
   1134             } else {
   1135                 Err(OperationAdapterError::from_command_disposition(
   1136                     operation_id,
   1137                     disposition,
   1138                     message,
   1139                 ))
   1140             }
   1141         }
   1142     }
   1143 }
   1144 
   1145 fn order_event_list_error_detail(view: &crate::view::runtime::OrderEventListView) -> Value {
   1146     json!({
   1147         "state": &view.state,
   1148         "seller_pubkey": &view.seller_pubkey,
   1149         "target_relays": &view.target_relays,
   1150         "connected_relays": &view.connected_relays,
   1151         "failed_relays": &view.failed_relays,
   1152         "fetched_count": view.fetched_count,
   1153         "decoded_count": view.decoded_count,
   1154         "skipped_count": view.skipped_count,
   1155         "count": view.count,
   1156         "actions": &view.actions,
   1157     })
   1158 }
   1159 
   1160 fn required_order_key<P>(request: &OperationRequest<P>) -> Result<String, OperationAdapterError>
   1161 where
   1162     P: OperationRequestPayload + OperationRequestData,
   1163 {
   1164     string_input(request, "order_id")
   1165         .or_else(|| string_input(request, "key"))
   1166         .ok_or_else(|| {
   1167             invalid_input(
   1168                 request.operation_id(),
   1169                 "missing required `order_id` input".to_owned(),
   1170             )
   1171         })
   1172 }
   1173 
   1174 fn required_string_input<P>(
   1175     request: &OperationRequest<P>,
   1176     key: &str,
   1177 ) -> Result<String, OperationAdapterError>
   1178 where
   1179     P: OperationRequestPayload + OperationRequestData,
   1180 {
   1181     string_input(request, key)
   1182         .map(|value| value.trim().to_owned())
   1183         .filter(|value| !value.is_empty())
   1184         .ok_or_else(|| {
   1185             invalid_input(
   1186                 request.operation_id(),
   1187                 format!("missing required `{key}` input"),
   1188             )
   1189         })
   1190 }
   1191 
   1192 fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
   1193 where
   1194     P: OperationRequestPayload + OperationRequestData,
   1195 {
   1196     request
   1197         .payload
   1198         .input()
   1199         .get(key)
   1200         .and_then(Value::as_str)
   1201         .map(str::to_owned)
   1202 }
   1203 
   1204 fn optional_path_input<P>(request: &OperationRequest<P>, key: &str) -> Option<PathBuf>
   1205 where
   1206     P: OperationRequestPayload + OperationRequestData,
   1207 {
   1208     string_input(request, key).map(PathBuf::from)
   1209 }
   1210 
   1211 fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool>
   1212 where
   1213     P: OperationRequestPayload + OperationRequestData,
   1214 {
   1215     request.payload.input().get(key).and_then(Value::as_bool)
   1216 }
   1217 
   1218 fn u32_input<P>(request: &OperationRequest<P>, key: &str) -> Option<u32>
   1219 where
   1220     P: OperationRequestPayload + OperationRequestData,
   1221 {
   1222     request
   1223         .payload
   1224         .input()
   1225         .get(key)
   1226         .and_then(Value::as_u64)
   1227         .and_then(|value| u32::try_from(value).ok())
   1228 }
   1229 
   1230 fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> {
   1231     result.map_err(|error| OperationAdapterError::Runtime(error.to_string()))
   1232 }
   1233 
   1234 fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
   1235     OperationAdapterError::InvalidInput {
   1236         operation_id: operation_id.to_owned(),
   1237         message,
   1238     }
   1239 }
   1240 
   1241 #[cfg(test)]
   1242 mod tests {
   1243     use std::path::{Path, PathBuf};
   1244 
   1245     use radroots_runtime_paths::RadrootsMigrationReport;
   1246     use radroots_secret_vault::RadrootsSecretBackend;
   1247     use serde_json::{Map, Value};
   1248     use tempfile::tempdir;
   1249 
   1250     use super::{OrderOperationService, decision_result};
   1251     use crate::ops::{
   1252         OperationAdapter, OperationContext, OperationData, OperationRequest, OrderAcceptRequest,
   1253         OrderAcceptResult, OrderCancelRequest, OrderDeclineRequest, OrderDeclineResult,
   1254         OrderEventListRequest, OrderEventWatchRequest, OrderGetRequest, OrderListRequest,
   1255         OrderRevisionAcceptRequest, OrderRevisionDeclineRequest, OrderRevisionProposeRequest,
   1256         OrderStatusGetRequest, OrderSubmitRequest,
   1257     };
   1258     use crate::runtime::config::{
   1259         AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
   1260         LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
   1261         PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
   1262         RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
   1263         SignerConfig, Verbosity,
   1264     };
   1265     use crate::view::runtime::OrderDecisionView;
   1266 
   1267     #[test]
   1268     fn order_service_get_and_list_preserve_order_truth() {
   1269         let dir = tempdir().expect("tempdir");
   1270         let config = sample_config(dir.path());
   1271         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1272         let get = OperationRequest::new(
   1273             OperationContext::default(),
   1274             OrderGetRequest::from_data(data(&[("order_id", "ord_missing")])),
   1275         )
   1276         .expect("order get request");
   1277         let get_envelope = service
   1278             .execute(get)
   1279             .expect("order get result")
   1280             .to_envelope(OperationContext::default().envelope_context("req_order_get"))
   1281             .expect("order get envelope");
   1282 
   1283         assert_eq!(get_envelope.operation_id, "order.get");
   1284         assert_eq!(get_envelope.result["state"], "missing");
   1285         assert_eq!(get_envelope.result["actions"][0], "radroots order list");
   1286         assert_eq!(get_envelope.result["actions"][1], "radroots basket create");
   1287 
   1288         let list = OperationRequest::new(OperationContext::default(), OrderListRequest::default())
   1289             .expect("order list request");
   1290         let list_envelope = service
   1291             .execute(list)
   1292             .expect("order list result")
   1293             .to_envelope(OperationContext::default().envelope_context("req_order_list"))
   1294             .expect("order list envelope");
   1295         assert_eq!(list_envelope.operation_id, "order.list");
   1296         assert_eq!(list_envelope.result["state"], "empty");
   1297         assert_eq!(list_envelope.result["actions"][0], "radroots basket create");
   1298     }
   1299 
   1300     #[test]
   1301     fn order_submit_requires_approval_token() {
   1302         let dir = tempdir().expect("tempdir");
   1303         let config = sample_config(dir.path());
   1304         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1305         let submit = OperationRequest::new(
   1306             OperationContext::default(),
   1307             OrderSubmitRequest::from_data(data(&[("order_id", "ord_missing")])),
   1308         )
   1309         .expect("order submit request");
   1310         let error = service.execute(submit).expect_err("approval required");
   1311 
   1312         assert!(format!("{error}").contains("approval_token"));
   1313         assert_eq!(error.to_output_error().code, "approval_required");
   1314         assert_eq!(error.to_output_error().exit_code, 6);
   1315     }
   1316 
   1317     #[test]
   1318     fn order_submit_with_approval_returns_not_found_for_missing_order() {
   1319         let dir = tempdir().expect("tempdir");
   1320         let config = sample_config(dir.path());
   1321         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1322         let mut context = OperationContext::default();
   1323         context.approval_token = Some("approve_test".to_owned());
   1324         let submit = OperationRequest::new(
   1325             context.clone(),
   1326             OrderSubmitRequest::from_data(data(&[("order_id", "ord_missing")])),
   1327         )
   1328         .expect("order submit request");
   1329         let error = service.execute(submit).expect_err("missing order error");
   1330         let output_error = error.to_output_error();
   1331 
   1332         assert_eq!(output_error.code, "not_found");
   1333         assert_eq!(output_error.exit_code, 4);
   1334         assert!(output_error.message.contains("ord_missing"));
   1335         let envelope = crate::out::envelope::OutputEnvelope::failure(
   1336             "order.submit",
   1337             output_error,
   1338             context.envelope_context("req_order_submit"),
   1339         );
   1340         let detail = envelope.errors[0].detail.as_ref().expect("submit detail");
   1341         assert_eq!(detail["state"], "missing");
   1342         assert_eq!(detail["order_id"], "ord_missing");
   1343         assert_eq!(detail["actions"][0], "radroots order list");
   1344         assert_eq!(detail["actions"][1], "radroots basket create");
   1345         assert_eq!(
   1346             envelope.next_actions[0].command.as_deref(),
   1347             Some("radroots order list")
   1348         );
   1349         assert_eq!(
   1350             envelope.next_actions[1].command.as_deref(),
   1351             Some("radroots basket create")
   1352         );
   1353     }
   1354 
   1355     #[test]
   1356     fn order_accept_requires_approval_token() {
   1357         let dir = tempdir().expect("tempdir");
   1358         let config = sample_config(dir.path());
   1359         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1360         let accept = OperationRequest::new(
   1361             OperationContext::default(),
   1362             OrderAcceptRequest::from_data(data(&[("order_id", "ord_pending")])),
   1363         )
   1364         .expect("order accept request");
   1365         let error = service.execute(accept).expect_err("approval required");
   1366 
   1367         assert_eq!(error.to_output_error().code, "approval_required");
   1368     }
   1369 
   1370     #[test]
   1371     fn order_accept_unconfigured_preserves_decision_detail() {
   1372         let dir = tempdir().expect("tempdir");
   1373         let config = sample_config(dir.path());
   1374         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1375         let mut context = OperationContext::default();
   1376         context.dry_run = true;
   1377         let accept = OperationRequest::new(
   1378             context,
   1379             OrderAcceptRequest::from_data(data(&[("order_id", "ord_pending")])),
   1380         )
   1381         .expect("order accept request");
   1382         let error = service
   1383             .execute(accept)
   1384             .expect_err("order accept unconfigured");
   1385         let output_error = error.to_output_error();
   1386         let detail = output_error.detail.as_ref().expect("decision detail");
   1387 
   1388         assert_eq!(output_error.code, "operation_unavailable");
   1389         assert_eq!(detail["state"], "unconfigured");
   1390         assert_eq!(detail["order_id"], "ord_pending");
   1391         assert_eq!(detail["decision"], "accepted");
   1392         assert!(detail["target_relays"].as_array().unwrap().is_empty());
   1393     }
   1394 
   1395     #[test]
   1396     fn order_decision_already_decided_maps_to_validation_failure() {
   1397         let view = already_decided_view();
   1398         let error = match decision_result::<OrderAcceptResult>("order.accept", &view) {
   1399             Ok(_) => panic!("already decided view should fail validation"),
   1400             Err(error) => error,
   1401         };
   1402         let output_error = error.to_output_error();
   1403 
   1404         assert_eq!(output_error.code, "validation_failed");
   1405         assert_eq!(output_error.exit_code, 10);
   1406         let detail = output_error.detail.expect("validation detail");
   1407         assert_eq!(detail["state"], "already_decided");
   1408         assert_eq!(detail["operation_id"], "order.accept");
   1409         assert_eq!(detail["listing_event_id"], "l".repeat(64));
   1410         assert_eq!(detail["event_id"], "d".repeat(64));
   1411         assert_eq!(detail["event_kind"], 3423);
   1412         assert_eq!(detail["idempotency_key"], "idem_test");
   1413         assert_eq!(detail["signer_mode"], "local");
   1414         assert_eq!(detail["actions"][0], "radroots order status get ord_test");
   1415     }
   1416 
   1417     #[test]
   1418     fn order_decision_invalid_maps_to_validation_failure() {
   1419         let view = invalid_decision_view();
   1420         let error = match decision_result::<OrderDeclineResult>("order.decline", &view) {
   1421             Ok(_) => panic!("invalid view should fail validation"),
   1422             Err(error) => error,
   1423         };
   1424         let output_error = error.to_output_error();
   1425 
   1426         assert_eq!(output_error.code, "validation_failed");
   1427         assert_eq!(output_error.exit_code, 10);
   1428         assert_eq!(
   1429             output_error.message,
   1430             "active order events for `ord_test` failed reducer validation"
   1431         );
   1432         let detail = output_error.detail.expect("validation detail");
   1433         assert_eq!(detail["state"], "invalid");
   1434         assert_eq!(detail["operation_id"], "order.decline");
   1435         assert_eq!(detail["listing_event_id"], "l".repeat(64));
   1436         assert_eq!(detail["event_id"], Value::Null);
   1437         assert_eq!(detail["event_kind"], Value::Null);
   1438         assert_eq!(detail["idempotency_key"], "idem_test");
   1439         assert_eq!(detail["signer_mode"], "local");
   1440     }
   1441 
   1442     #[test]
   1443     fn order_decline_requires_reason_before_approval() {
   1444         let dir = tempdir().expect("tempdir");
   1445         let config = sample_config(dir.path());
   1446         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1447         let decline = OperationRequest::new(
   1448             OperationContext::default(),
   1449             OrderDeclineRequest::from_data(data(&[("order_id", "ord_pending")])),
   1450         )
   1451         .expect("order decline request");
   1452         let error = service.execute(decline).expect_err("reason required");
   1453         let output_error = error.to_output_error();
   1454 
   1455         assert_eq!(output_error.code, "invalid_input");
   1456         assert!(output_error.message.contains("reason"));
   1457     }
   1458 
   1459     #[test]
   1460     fn order_decline_rejects_blank_reason_before_approval() {
   1461         let dir = tempdir().expect("tempdir");
   1462         let config = sample_config(dir.path());
   1463         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1464         let decline = OperationRequest::new(
   1465             OperationContext::default(),
   1466             OrderDeclineRequest::from_data(data(&[("order_id", "ord_pending"), ("reason", " ")])),
   1467         )
   1468         .expect("order decline request");
   1469         let error = service.execute(decline).expect_err("reason required");
   1470         let output_error = error.to_output_error();
   1471 
   1472         assert_eq!(output_error.code, "invalid_input");
   1473         assert!(output_error.message.contains("reason"));
   1474     }
   1475 
   1476     #[test]
   1477     fn order_cancel_requires_reason_before_approval() {
   1478         let dir = tempdir().expect("tempdir");
   1479         let config = sample_config(dir.path());
   1480         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1481         let cancel = OperationRequest::new(
   1482             OperationContext::default(),
   1483             OrderCancelRequest::from_data(data(&[("order_id", "ord_pending")])),
   1484         )
   1485         .expect("order cancel request");
   1486         let error = service.execute(cancel).expect_err("reason required");
   1487         let output_error = error.to_output_error();
   1488 
   1489         assert_eq!(output_error.code, "invalid_input");
   1490         assert!(output_error.message.contains("reason"));
   1491     }
   1492 
   1493     #[test]
   1494     fn order_cancel_requires_approval_token() {
   1495         let dir = tempdir().expect("tempdir");
   1496         let config = sample_config(dir.path());
   1497         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1498         let cancel = OperationRequest::new(
   1499             OperationContext::default(),
   1500             OrderCancelRequest::from_data(data(&[
   1501                 ("order_id", "ord_pending"),
   1502                 ("reason", "changed plans"),
   1503             ])),
   1504         )
   1505         .expect("order cancel request");
   1506         let error = service.execute(cancel).expect_err("approval required");
   1507 
   1508         assert_eq!(error.to_output_error().code, "approval_required");
   1509     }
   1510 
   1511     #[test]
   1512     fn order_revision_propose_requires_reason_before_approval() {
   1513         let dir = tempdir().expect("tempdir");
   1514         let config = sample_config(dir.path());
   1515         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1516         let revision = OperationRequest::new(
   1517             OperationContext::default(),
   1518             OrderRevisionProposeRequest::from_data(data(&[("order_id", "ord_pending")])),
   1519         )
   1520         .expect("order revision request");
   1521         let error = service.execute(revision).expect_err("reason required");
   1522         let output_error = error.to_output_error();
   1523 
   1524         assert_eq!(output_error.code, "invalid_input");
   1525         assert!(output_error.message.contains("reason"));
   1526     }
   1527 
   1528     #[test]
   1529     fn order_revision_propose_requires_approval_token() {
   1530         let dir = tempdir().expect("tempdir");
   1531         let config = sample_config(dir.path());
   1532         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1533         let mut input = data(&[
   1534             ("order_id", "ord_pending"),
   1535             ("reason", "update count"),
   1536             ("bin_id", "bin-1"),
   1537         ]);
   1538         input.insert("bin_count".to_owned(), Value::from(3));
   1539         let revision = OperationRequest::new(
   1540             OperationContext::default(),
   1541             OrderRevisionProposeRequest::from_data(input),
   1542         )
   1543         .expect("order revision request");
   1544         let error = service.execute(revision).expect_err("approval required");
   1545 
   1546         assert_eq!(error.to_output_error().code, "approval_required");
   1547     }
   1548 
   1549     #[test]
   1550     fn order_revision_accept_requires_approval_token() {
   1551         let dir = tempdir().expect("tempdir");
   1552         let config = sample_config(dir.path());
   1553         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1554         let revision = OperationRequest::new(
   1555             OperationContext::default(),
   1556             OrderRevisionAcceptRequest::from_data(data(&[
   1557                 ("order_id", "ord_pending"),
   1558                 ("revision_id", "rev_pending"),
   1559             ])),
   1560         )
   1561         .expect("order revision accept request");
   1562         let error = service.execute(revision).expect_err("approval required");
   1563 
   1564         assert_eq!(error.to_output_error().code, "approval_required");
   1565     }
   1566 
   1567     #[test]
   1568     fn order_revision_decline_requires_reason_before_approval() {
   1569         let dir = tempdir().expect("tempdir");
   1570         let config = sample_config(dir.path());
   1571         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1572         let revision = OperationRequest::new(
   1573             OperationContext::default(),
   1574             OrderRevisionDeclineRequest::from_data(data(&[
   1575                 ("order_id", "ord_pending"),
   1576                 ("revision_id", "rev_pending"),
   1577             ])),
   1578         )
   1579         .expect("order revision decline request");
   1580         let error = service.execute(revision).expect_err("reason required");
   1581         let output_error = error.to_output_error();
   1582 
   1583         assert_eq!(output_error.code, "invalid_input");
   1584         assert!(output_error.message.contains("reason"));
   1585     }
   1586 
   1587     #[test]
   1588     fn order_revision_decline_requires_approval_token() {
   1589         let dir = tempdir().expect("tempdir");
   1590         let config = sample_config(dir.path());
   1591         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1592         let revision = OperationRequest::new(
   1593             OperationContext::default(),
   1594             OrderRevisionDeclineRequest::from_data(data(&[
   1595                 ("order_id", "ord_pending"),
   1596                 ("revision_id", "rev_pending"),
   1597                 ("reason", "keep original order"),
   1598             ])),
   1599         )
   1600         .expect("order revision decline request");
   1601         let error = service.execute(revision).expect_err("approval required");
   1602 
   1603         assert_eq!(error.to_output_error().code, "approval_required");
   1604     }
   1605 
   1606     #[test]
   1607     fn order_status_get_uses_local_sdk_projection_without_relay() {
   1608         let dir = tempdir().expect("tempdir");
   1609         let config = sample_config(dir.path());
   1610         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1611         let status = OperationRequest::new(
   1612             OperationContext::default(),
   1613             OrderStatusGetRequest::from_data(data(&[("order_id", "ord_pending")])),
   1614         )
   1615         .expect("order status request");
   1616         let envelope = service
   1617             .execute(status)
   1618             .expect("status result")
   1619             .to_envelope(OperationContext::default().envelope_context("req_order_status"))
   1620             .expect("status envelope");
   1621 
   1622         assert_eq!(envelope.operation_id, "order.status.get");
   1623         assert_eq!(envelope.result["state"], "missing");
   1624         assert_eq!(envelope.result["source"], "SDK local order projection");
   1625         assert_eq!(
   1626             envelope.result["actor_context_source"],
   1627             "sdk_local_projection"
   1628         );
   1629         assert_eq!(envelope.result["order_id"], "ord_pending");
   1630         assert_eq!(envelope.result["fetched_count"], 0);
   1631         assert_eq!(envelope.result["decoded_count"], 0);
   1632         assert_eq!(envelope.result["skipped_count"], 0);
   1633         assert!(envelope.next_actions.is_empty());
   1634     }
   1635 
   1636     #[test]
   1637     fn order_event_list_requires_relay_configuration() {
   1638         let dir = tempdir().expect("tempdir");
   1639         let config = sample_config(dir.path());
   1640         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1641         let context = OperationContext::default();
   1642         let request = OperationRequest::new(context.clone(), OrderEventListRequest::default())
   1643             .expect("order event list request");
   1644         let output_error = service
   1645             .execute(request)
   1646             .expect_err("order event list unconfigured")
   1647             .to_output_error();
   1648         let envelope = crate::out::envelope::OutputEnvelope::failure(
   1649             "order.event.list",
   1650             output_error,
   1651             context.envelope_context("req_order_event_list"),
   1652         );
   1653 
   1654         assert_eq!(envelope.errors[0].code, "operation_unavailable");
   1655         assert_eq!(envelope.errors[0].exit_code, 3);
   1656         assert!(envelope.errors[0].message.contains("configured relay"));
   1657         assert_eq!(
   1658             envelope.errors[0].detail.as_ref().unwrap()["actions"][0],
   1659             "radroots --relay wss://relay.example.com order event list"
   1660         );
   1661         assert_eq!(
   1662             envelope.next_actions[0].command.as_deref(),
   1663             Some("radroots --relay wss://relay.example.com order event list")
   1664         );
   1665     }
   1666 
   1667     #[test]
   1668     fn order_event_list_requires_seller_account_with_account_action() {
   1669         let dir = tempdir().expect("tempdir");
   1670         let mut config = sample_config(dir.path());
   1671         config.relay.urls = vec!["ws://127.0.0.1:9".to_owned()];
   1672         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1673         let context = OperationContext::default();
   1674         let request = OperationRequest::new(context.clone(), OrderEventListRequest::default())
   1675             .expect("order event list request");
   1676         let output_error = service
   1677             .execute(request)
   1678             .expect_err("order event list missing account")
   1679             .to_output_error();
   1680         let envelope = crate::out::envelope::OutputEnvelope::failure(
   1681             "order.event.list",
   1682             output_error,
   1683             context.envelope_context("req_order_event_list"),
   1684         );
   1685 
   1686         assert_eq!(envelope.errors[0].code, "operation_unavailable");
   1687         assert!(
   1688             envelope.errors[0]
   1689                 .message
   1690                 .contains("selected seller account")
   1691         );
   1692         assert_eq!(
   1693             envelope.errors[0].detail.as_ref().unwrap()["actions"][0],
   1694             "radroots account create"
   1695         );
   1696         assert_eq!(
   1697             envelope.next_actions[0].command.as_deref(),
   1698             Some("radroots account create")
   1699         );
   1700     }
   1701 
   1702     #[test]
   1703     fn order_event_watch_returns_deferred_error_with_target_action() {
   1704         let dir = tempdir().expect("tempdir");
   1705         let config = sample_config(dir.path());
   1706         let service = OperationAdapter::new(OrderOperationService::new(&config));
   1707         let request = OperationRequest::new(
   1708             OperationContext::default(),
   1709             OrderEventWatchRequest::from_data(data(&[("order_id", "ord_missing")])),
   1710         )
   1711         .expect("order event watch request");
   1712         let error = service
   1713             .execute(request)
   1714             .expect_err("order event watch deferred");
   1715         let envelope = crate::out::envelope::OutputEnvelope::failure(
   1716             "order.event.watch",
   1717             error.to_output_error(),
   1718             OperationContext::default().envelope_context("req_order_watch"),
   1719         );
   1720 
   1721         assert_eq!(envelope.operation_id, "order.event.watch");
   1722         assert!(envelope.result.is_null());
   1723         assert_eq!(envelope.errors[0].code, "not_implemented");
   1724         assert_eq!(
   1725             envelope.errors[0].detail.as_ref().unwrap()["state"],
   1726             "not_implemented"
   1727         );
   1728         assert_eq!(
   1729             envelope.errors[0].detail.as_ref().unwrap()["order_id"],
   1730             "ord_missing"
   1731         );
   1732         assert_eq!(
   1733             envelope.next_actions[0].command.as_deref(),
   1734             Some("radroots order status get ord_missing")
   1735         );
   1736     }
   1737 
   1738     fn sample_config(root: &Path) -> RuntimeConfig {
   1739         let data = root.join("data");
   1740         let logs = root.join("logs");
   1741         let secrets = root.join("secrets");
   1742         RuntimeConfig {
   1743             output: OutputConfig {
   1744                 format: OutputFormat::Human,
   1745                 verbosity: Verbosity::Normal,
   1746                 color: true,
   1747                 dry_run: false,
   1748             },
   1749             interaction: InteractionConfig {
   1750                 input_enabled: true,
   1751                 assume_yes: false,
   1752                 stdin_tty: false,
   1753                 stdout_tty: false,
   1754                 prompts_allowed: false,
   1755                 confirmations_allowed: false,
   1756             },
   1757             paths: PathsConfig {
   1758                 profile: "interactive_user".into(),
   1759                 profile_source: "test".into(),
   1760                 allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
   1761                 root_source: "test".into(),
   1762                 repo_local_root: None,
   1763                 repo_local_root_source: None,
   1764                 subordinate_path_override_source: "runtime_config".into(),
   1765                 app_namespace: "apps/cli".into(),
   1766                 shared_accounts_namespace: "shared/accounts".into(),
   1767                 shared_identities_namespace: "shared/identities".into(),
   1768                 app_config_path: root.join("config/apps/cli/config.toml"),
   1769                 workspace_config_path: None,
   1770                 app_data_root: data.join("apps/cli"),
   1771                 app_logs_root: logs.join("apps/cli"),
   1772                 shared_accounts_data_root: data.join("shared/accounts"),
   1773                 shared_accounts_secrets_root: secrets.join("shared/accounts"),
   1774                 default_identity_path: secrets.join("shared/identities/default.json"),
   1775             },
   1776             migration: MigrationConfig {
   1777                 report: RadrootsMigrationReport::empty(),
   1778             },
   1779             logging: LoggingConfig {
   1780                 filter: "info".into(),
   1781                 directory: None,
   1782                 stdout: false,
   1783             },
   1784             account: AccountConfig {
   1785                 selector: None,
   1786                 store_path: data.join("shared/accounts/store.json"),
   1787                 secrets_dir: secrets.join("shared/accounts"),
   1788                 secret_backend: RadrootsSecretBackend::EncryptedFile,
   1789                 secret_fallback: None,
   1790             },
   1791             account_secret_contract: AccountSecretContractConfig {
   1792                 default_backend: "host_vault".into(),
   1793                 default_fallback: Some("encrypted_file".into()),
   1794                 allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
   1795                 host_vault_policy: Some("desktop".into()),
   1796                 uses_protected_store: true,
   1797             },
   1798             identity: IdentityConfig {
   1799                 path: secrets.join("shared/identities/default.json"),
   1800             },
   1801             signer: SignerConfig {
   1802                 backend: SignerBackend::Local,
   1803             },
   1804             publish: PublishConfig {
   1805                 transport: PublishTransport::DirectNostrRelay,
   1806                 source: PublishTransportSource::Defaults,
   1807                 radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
   1808             },
   1809             relay: RelayConfig {
   1810                 urls: Vec::new(),
   1811                 publish_policy: RelayPublishPolicy::Any,
   1812                 source: RelayConfigSource::Defaults,
   1813             },
   1814             local: LocalConfig {
   1815                 root: data.join("apps/cli/replica"),
   1816                 replica_db_path: data.join("apps/cli/replica/replica.sqlite"),
   1817                 backups_dir: data.join("apps/cli/replica/backups"),
   1818                 exports_dir: data.join("apps/cli/replica/exports"),
   1819             },
   1820             myc: MycConfig {
   1821                 executable: PathBuf::from("myc"),
   1822                 status_timeout_ms: 2_000,
   1823             },
   1824             hyf: HyfConfig {
   1825                 enabled: false,
   1826                 executable: PathBuf::from("hyfd"),
   1827             },
   1828             rpc: RpcConfig {
   1829                 url: "http://127.0.0.1:7070".into(),
   1830             },
   1831             rhi: crate::runtime::config::RhiConfig {
   1832                 trusted_worker_pubkeys: Vec::new(),
   1833             },
   1834             capability_bindings: Vec::new(),
   1835         }
   1836     }
   1837 
   1838     fn data(entries: &[(&str, &str)]) -> OperationData {
   1839         entries
   1840             .iter()
   1841             .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
   1842             .collect::<Map<String, Value>>()
   1843     }
   1844 
   1845     fn already_decided_view() -> OrderDecisionView {
   1846         OrderDecisionView {
   1847             state: "already_decided".to_owned(),
   1848             source: "test".to_owned(),
   1849             order_id: "ord_test".to_owned(),
   1850             listing_addr: Some("30402:seller:listing".to_owned()),
   1851             buyer_pubkey: Some("b".repeat(64)),
   1852             seller_pubkey: Some("s".repeat(64)),
   1853             decision: "accepted".to_owned(),
   1854             request_event_id: Some("r".repeat(64)),
   1855             listing_event_id: Some("l".repeat(64)),
   1856             root_event_id: Some("r".repeat(64)),
   1857             prev_event_id: Some("r".repeat(64)),
   1858             event_id: Some("d".repeat(64)),
   1859             event_kind: Some(3423),
   1860             inventory: None,
   1861             dry_run: false,
   1862             target_relays: vec!["ws://relay.test".to_owned()],
   1863             connected_relays: vec!["ws://relay.test".to_owned()],
   1864             acknowledged_relays: Vec::new(),
   1865             failed_relays: Vec::new(),
   1866             fetched_count: 2,
   1867             decoded_count: 2,
   1868             skipped_count: 0,
   1869             idempotency_key: Some("idem_test".to_owned()),
   1870             signer_mode: Some("local".to_owned()),
   1871             reason: Some(
   1872                 "order accept refused because order `ord_test` already has a visible `accepted` seller decision"
   1873                     .to_owned(),
   1874             ),
   1875             issues: Vec::new(),
   1876             actions: vec!["radroots order status get ord_test".to_owned()],
   1877         }
   1878     }
   1879 
   1880     fn invalid_decision_view() -> OrderDecisionView {
   1881         let mut view = already_decided_view();
   1882         view.state = "invalid".to_owned();
   1883         view.decision = "declined".to_owned();
   1884         view.event_id = None;
   1885         view.event_kind = None;
   1886         view.reason =
   1887             Some("active order events for `ord_test` failed reducer validation".to_owned());
   1888         view
   1889     }
   1890 }