cli

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

commit bac1ea987aaf4aa2a46c14fe26b433fdb63a612c
parent 11c706f8f2453d61cbb91d0c1078d106cef06ebe
Author: triesap <tyson@radroots.org>
Date:   Tue,  5 May 2026 18:32:31 +0000

cli: expose payment status axis

- add structured payment status output beside lifecycle state
- source cancellation and receipt reasons from lifecycle payloads only
- pass empty payment inputs until proposal 6 write commands fetch events
- cover nested CLI status and command tests

Diffstat:
MCargo.lock | 2++
Msrc/domain/runtime.rs | 37++++++++++++++++++++++++++++++++++++-
Msrc/runtime/order.rs | 389+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
3 files changed, 400 insertions(+), 28 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1941,11 +1941,13 @@ dependencies = [ name = "radroots_trade" version = "0.1.0-alpha.2" dependencies = [ + "hex", "radroots_core", "radroots_events", "radroots_events_codec", "serde", "serde_json", + "sha2", "thiserror 1.0.69", "ts-rs", ] diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs @@ -2,10 +2,11 @@ use std::process::ExitCode; +use radroots_core::{RadrootsCoreCurrency, RadrootsCoreDecimal}; use radroots_events::farm::RadrootsFarm; use radroots_events::listing::RadrootsListingLocation; use radroots_events::profile::RadrootsProfile; -use radroots_events::trade::RadrootsTradeOrderEconomics; +use radroots_events::trade::{RadrootsTradeOrderEconomics, RadrootsTradePaymentMethod}; use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord; use serde::Serialize; @@ -1665,6 +1666,8 @@ pub struct OrderStatusView { pub fulfillment: Option<OrderStatusFulfillmentView>, #[serde(skip_serializing_if = "Option::is_none")] pub lifecycle: Option<OrderStatusLifecycleView>, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment: Option<OrderStatusPaymentView>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub reducer_issues: Vec<OrderIssueView>, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -1722,6 +1725,38 @@ pub struct OrderStatusFulfillmentView { } #[derive(Debug, Clone, Serialize)] +pub struct OrderStatusPaymentView { + pub state: String, + pub settlement_state: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_event_id: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub settlement_event_id: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub agreement_event_id: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub quote_id: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub quote_version: Option<u32>, + #[serde(skip_serializing_if = "Option::is_none")] + pub economics_digest: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub amount: Option<RadrootsCoreDecimal>, + #[serde(skip_serializing_if = "Option::is_none")] + pub currency: Option<RadrootsCoreCurrency>, + #[serde(skip_serializing_if = "Option::is_none")] + pub method: Option<RadrootsTradePaymentMethod>, + #[serde(skip_serializing_if = "Option::is_none")] + pub reference: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub paid_at: Option<u64>, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option<String>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub issues: Vec<OrderIssueView>, +} + +#[derive(Debug, Clone, Serialize)] pub struct OrderStatusLifecycleView { pub phase: String, #[serde(default)] diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -58,13 +58,16 @@ use radroots_replica_db_schema::trade_product::{ use radroots_sql_core::SqliteExecutor; use radroots_trade::order::{ RadrootsActiveOrderCancellationRecord, RadrootsActiveOrderDecisionRecord, - RadrootsActiveOrderFulfillmentRecord, RadrootsActiveOrderReceiptRecord, - RadrootsActiveOrderReducerIssue, RadrootsActiveOrderRequestRecord, - RadrootsActiveOrderRevisionDecisionRecord, RadrootsActiveOrderRevisionProposalRecord, - RadrootsActiveOrderStatus, RadrootsListingInventoryAccountingIssue, - RadrootsListingInventoryAccountingProjection, RadrootsListingInventoryBinAvailability, - canonicalize_active_order_decision_for_signer, canonicalize_active_order_request_for_signer, - reduce_active_order_events, reduce_listing_inventory_accounting, + RadrootsActiveOrderFulfillmentRecord, RadrootsActiveOrderPaymentProjection, + RadrootsActiveOrderPaymentRecord, RadrootsActiveOrderPaymentState, + RadrootsActiveOrderReceiptRecord, RadrootsActiveOrderReducerIssue, + RadrootsActiveOrderRequestRecord, RadrootsActiveOrderRevisionDecisionRecord, + RadrootsActiveOrderRevisionProposalRecord, RadrootsActiveOrderSettlementRecord, + RadrootsActiveOrderSettlementState, RadrootsActiveOrderStatus, + RadrootsListingInventoryAccountingIssue, RadrootsListingInventoryAccountingProjection, + RadrootsListingInventoryBinAvailability, canonicalize_active_order_decision_for_signer, + canonicalize_active_order_request_for_signer, reduce_active_order_events, + reduce_listing_inventory_accounting, }; use serde::{Deserialize, Serialize}; @@ -74,8 +77,8 @@ use crate::domain::runtime::{ OrderInventoryView, OrderIssueView, OrderListView, OrderNewView, OrderReceiptView, OrderRevisionDecisionView, OrderRevisionProposalView, OrderStatusFulfillmentView, OrderStatusLifecycleCancellationView, OrderStatusLifecycleReceiptView, - OrderStatusLifecycleView, OrderStatusRevisionView, OrderStatusView, OrderSubmitView, - OrderSummaryView, OrderWatchView, RelayFailureView, + OrderStatusLifecycleView, OrderStatusPaymentView, OrderStatusRevisionView, OrderStatusView, + OrderSubmitView, OrderSummaryView, OrderWatchView, RelayFailureView, }; use crate::runtime::RuntimeError; use crate::runtime::accounts; @@ -1501,6 +1504,7 @@ pub fn status( inventory: None, fulfillment: None, lifecycle: None, + payment: None, reducer_issues: Vec::new(), target_relays: Vec::new(), connected_relays: Vec::new(), @@ -1538,6 +1542,7 @@ pub fn status( inventory: None, fulfillment: None, lifecycle: None, + payment: None, reducer_issues: Vec::new(), target_relays, connected_relays: Vec::new(), @@ -1724,6 +1729,8 @@ fn order_status_reduction_from_receipt_with_context( fulfillments, cancellations, receipts, + Vec::<RadrootsActiveOrderPaymentRecord>::new(), + Vec::<RadrootsActiveOrderSettlementRecord>::new(), ); let fulfillment_event_id = projection.fulfillment_event_id.clone(); let fulfillment_status = projection.fulfillment_status; @@ -1759,6 +1766,15 @@ fn order_status_reduction_from_receipt_with_context( .find(|record| &record.event_id == event_id) .map(|record| record.prev_event_id.clone()) }); + let cancellation_reason = projection + .cancellation_event_id + .as_ref() + .and_then(|event_id| { + cancellation_records + .iter() + .find(|record| &record.event_id == event_id) + .map(|record| record.payload.reason.clone()) + }); let receipt_root_event_id = projection.receipt_event_id.as_ref().and_then(|event_id| { receipt_records .iter() @@ -1819,8 +1835,9 @@ fn order_status_reduction_from_receipt_with_context( projection.cancellation_event_id.clone(), cancellation_root_event_id, cancellation_prev_event_id, - projection.settlement_pending, - projection.settlement_reason.clone(), + cancellation_reason, + false, + None, projection.receipt_event_id.clone(), receipt_root_event_id, receipt_prev_event_id, @@ -1839,6 +1856,10 @@ fn order_status_reduction_from_receipt_with_context( &revision_proposal_records, &revision_decision_records, ); + let payment = Some(order_status_payment_view( + projection.payment, + reducer_issues.as_slice(), + )); let view = OrderStatusView { state, @@ -1857,6 +1878,7 @@ fn order_status_reduction_from_receipt_with_context( inventory, fulfillment, lifecycle: Some(lifecycle), + payment, reducer_issues, target_relays, connected_relays, @@ -2475,6 +2497,26 @@ fn active_order_status_state(status: &RadrootsActiveOrderStatus) -> &'static str } } +fn active_order_payment_state(status: &RadrootsActiveOrderPaymentState) -> &'static str { + match status { + RadrootsActiveOrderPaymentState::NotRecorded => "not_recorded", + RadrootsActiveOrderPaymentState::Recorded => "recorded", + RadrootsActiveOrderPaymentState::Settled => "settled", + RadrootsActiveOrderPaymentState::Rejected => "rejected", + RadrootsActiveOrderPaymentState::Invalid => "invalid", + } +} + +fn active_order_settlement_state(status: &RadrootsActiveOrderSettlementState) -> &'static str { + match status { + RadrootsActiveOrderSettlementState::NotRequired => "not_required", + RadrootsActiveOrderSettlementState::Pending => "pending", + RadrootsActiveOrderSettlementState::Accepted => "accepted", + RadrootsActiveOrderSettlementState::Rejected => "rejected", + RadrootsActiveOrderSettlementState::Invalid => "invalid", + } +} + fn active_order_status_reason( status: &RadrootsActiveOrderStatus, order_id: &str, @@ -2631,6 +2673,29 @@ fn order_status_fulfillment_view( }) } +fn order_status_payment_view( + projection: RadrootsActiveOrderPaymentProjection, + reducer_issues: &[OrderIssueView], +) -> OrderStatusPaymentView { + OrderStatusPaymentView { + state: active_order_payment_state(&projection.state).to_owned(), + settlement_state: active_order_settlement_state(&projection.settlement_state).to_owned(), + payment_event_id: projection.payment_event_id, + settlement_event_id: projection.settlement_event_id, + agreement_event_id: projection.agreement_event_id, + quote_id: projection.quote_id, + quote_version: projection.quote_version, + economics_digest: projection.economics_digest, + amount: projection.amount, + currency: projection.currency, + method: projection.method, + reference: projection.reference, + paid_at: projection.paid_at, + reason: projection.reason, + issues: reducer_issues.to_vec(), + } +} + fn order_status_lifecycle_view( status: &RadrootsActiveOrderStatus, request_event_id: Option<String>, @@ -2639,6 +2704,7 @@ fn order_status_lifecycle_view( cancellation_event_id: Option<String>, cancellation_root_event_id: Option<String>, cancellation_prev_event_id: Option<String>, + cancellation_reason: Option<String>, settlement_required: bool, settlement_reason: Option<String>, receipt_event_id: Option<String>, @@ -2664,7 +2730,7 @@ fn order_status_lifecycle_view( .clone() .or(request_event_id.clone()), prev_event_id: cancellation_prev_event_id.clone(), - reason: settlement_reason.clone(), + reason: cancellation_reason.clone(), }); let receipt_view = receipt_event_id.as_ref().map(|event_id| { let (received, issue, received_at) = receipt.clone().unwrap_or((false, None, None)); @@ -3424,6 +3490,284 @@ fn active_order_reducer_issue_view(issue_value: RadrootsActiveOrderReducerIssue) "active order reducer reported receipt previous mismatch", vec![event_id], ), + RadrootsActiveOrderReducerIssue::PaymentWithoutAcceptedAgreement { event_id } => { + issue_with_events( + "payment_without_accepted_agreement", + "payment_event_id", + "active order reducer reported payment without accepted agreement", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::PaymentPayloadInvalid { event_id } => issue_with_events( + "invalid_payment_payload", + "payment_payload", + "active order reducer reported invalid payment payload", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentOrderIdMismatch { event_id } => issue_with_events( + "payment_order_id_mismatch", + "order_id", + "active order reducer reported payment order id mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentAuthorMismatch { event_id } => issue_with_events( + "payment_author_mismatch", + "buyer_pubkey", + "active order reducer reported payment author mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentCounterpartyMismatch { event_id } => { + issue_with_events( + "payment_counterparty_mismatch", + "seller_pubkey", + "active order reducer reported payment counterparty mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::PaymentBuyerMismatch { event_id } => issue_with_events( + "payment_buyer_mismatch", + "buyer_pubkey", + "active order reducer reported payment buyer mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentSellerMismatch { event_id } => issue_with_events( + "payment_seller_mismatch", + "seller_pubkey", + "active order reducer reported payment seller mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentListingAddressInvalid { event_id } => { + issue_with_events( + "invalid_payment_listing_address", + "listing_addr", + "active order reducer reported invalid payment listing address", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::PaymentListingMismatch { event_id } => issue_with_events( + "payment_listing_mismatch", + "listing_addr", + "active order reducer reported payment listing mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentRootMismatch { event_id } => issue_with_events( + "payment_root_mismatch", + "root_event_id", + "active order reducer reported payment root mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentPreviousMismatch { event_id } => issue_with_events( + "payment_previous_mismatch", + "prev_event_id", + "active order reducer reported payment previous mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentAgreementMismatch { event_id } => { + issue_with_events( + "payment_agreement_mismatch", + "agreement_event_id", + "active order reducer reported payment agreement mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::PaymentQuoteMismatch { event_id } => issue_with_events( + "payment_quote_mismatch", + "quote_id", + "active order reducer reported payment quote mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentQuoteVersionMismatch { event_id } => { + issue_with_events( + "payment_quote_version_mismatch", + "quote_version", + "active order reducer reported payment quote version mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::PaymentEconomicsDigestMismatch { event_id } => { + issue_with_events( + "payment_economics_digest_mismatch", + "economics_digest", + "active order reducer reported payment economics digest mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::PaymentAmountMismatch { event_id } => issue_with_events( + "payment_amount_mismatch", + "amount", + "active order reducer reported payment amount mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentCurrencyMismatch { event_id } => issue_with_events( + "payment_currency_mismatch", + "currency", + "active order reducer reported payment currency mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::PaymentAfterCancellation { event_id } => { + issue_with_events( + "payment_after_cancellation", + "payment_event_id", + "active order reducer reported payment after cancellation", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::RevisionAfterPayment { event_id } => issue_with_events( + "revision_after_payment", + "revision_event_id", + "active order reducer reported revision after payment", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::DuplicatePayments { event_ids } => issue_with_events( + "duplicate_payments", + "payment_event_id", + "active order reducer reported duplicate payment events", + event_ids, + ), + RadrootsActiveOrderReducerIssue::SettlementWithoutValidPayment { event_id } => { + issue_with_events( + "settlement_without_valid_payment", + "settlement_event_id", + "active order reducer reported settlement without valid payment", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementPayloadInvalid { event_id } => { + issue_with_events( + "invalid_settlement_payload", + "settlement_payload", + "active order reducer reported invalid settlement payload", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementOrderIdMismatch { event_id } => { + issue_with_events( + "settlement_order_id_mismatch", + "order_id", + "active order reducer reported settlement order id mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementAuthorMismatch { event_id } => { + issue_with_events( + "settlement_author_mismatch", + "seller_pubkey", + "active order reducer reported settlement author mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementCounterpartyMismatch { event_id } => { + issue_with_events( + "settlement_counterparty_mismatch", + "buyer_pubkey", + "active order reducer reported settlement counterparty mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementBuyerMismatch { event_id } => issue_with_events( + "settlement_buyer_mismatch", + "buyer_pubkey", + "active order reducer reported settlement buyer mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::SettlementSellerMismatch { event_id } => { + issue_with_events( + "settlement_seller_mismatch", + "seller_pubkey", + "active order reducer reported settlement seller mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementListingAddressInvalid { event_id } => { + issue_with_events( + "invalid_settlement_listing_address", + "listing_addr", + "active order reducer reported invalid settlement listing address", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementListingMismatch { event_id } => { + issue_with_events( + "settlement_listing_mismatch", + "listing_addr", + "active order reducer reported settlement listing mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementRootMismatch { event_id } => issue_with_events( + "settlement_root_mismatch", + "root_event_id", + "active order reducer reported settlement root mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::SettlementPreviousMismatch { event_id } => { + issue_with_events( + "settlement_previous_mismatch", + "prev_event_id", + "active order reducer reported settlement previous mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementPaymentEventMismatch { event_id } => { + issue_with_events( + "settlement_payment_event_mismatch", + "payment_event_id", + "active order reducer reported settlement payment event mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementAgreementMismatch { event_id } => { + issue_with_events( + "settlement_agreement_mismatch", + "agreement_event_id", + "active order reducer reported settlement agreement mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementQuoteMismatch { event_id } => issue_with_events( + "settlement_quote_mismatch", + "quote_id", + "active order reducer reported settlement quote mismatch", + vec![event_id], + ), + RadrootsActiveOrderReducerIssue::SettlementQuoteVersionMismatch { event_id } => { + issue_with_events( + "settlement_quote_version_mismatch", + "quote_version", + "active order reducer reported settlement quote version mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementEconomicsDigestMismatch { event_id } => { + issue_with_events( + "settlement_economics_digest_mismatch", + "economics_digest", + "active order reducer reported settlement economics digest mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementAmountMismatch { event_id } => { + issue_with_events( + "settlement_amount_mismatch", + "amount", + "active order reducer reported settlement amount mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::SettlementCurrencyMismatch { event_id } => { + issue_with_events( + "settlement_currency_mismatch", + "currency", + "active order reducer reported settlement currency mismatch", + vec![event_id], + ) + } + RadrootsActiveOrderReducerIssue::DuplicateSettlements { event_ids } => issue_with_events( + "duplicate_settlements", + "settlement_event_id", + "active order reducer reported duplicate settlement events", + event_ids, + ), RadrootsActiveOrderReducerIssue::ForkedLifecycle { event_ids } => issue_with_events( "forked_lifecycle", "event_id", @@ -10546,11 +10890,8 @@ mod tests { lifecycle.prev_event_id.as_deref(), Some(request_event_id.as_str()) ); - assert_eq!(lifecycle.settlement_required, true); - assert_eq!( - lifecycle.settlement_reason.as_deref(), - Some("buyer cancelled") - ); + assert_eq!(lifecycle.settlement_required, false); + assert_eq!(lifecycle.settlement_reason, None); assert_eq!(cancellation.event_id, cancellation_event_id); assert_eq!( cancellation.root_event_id.as_deref(), @@ -10627,11 +10968,8 @@ mod tests { lifecycle.prev_event_id.as_deref(), Some(decision_event_id.as_str()) ); - assert_eq!(lifecycle.settlement_required, true); - assert_eq!( - lifecycle.settlement_reason.as_deref(), - Some("buyer cannot collect") - ); + assert_eq!(lifecycle.settlement_required, false); + assert_eq!(lifecycle.settlement_reason, None); assert!(view.reducer_issues.is_empty()); } @@ -11192,11 +11530,8 @@ mod tests { lifecycle.event_id.as_deref(), Some(receipt_event_id.as_str()) ); - assert_eq!(lifecycle.settlement_required, true); - assert_eq!( - lifecycle.settlement_reason.as_deref(), - Some("damaged items") - ); + assert_eq!(lifecycle.settlement_required, false); + assert_eq!(lifecycle.settlement_reason, None); assert_eq!(receipt.received, false); assert_eq!(receipt.issue.as_deref(), Some("damaged items")); assert_eq!(receipt.received_at, Some(1_777_665_600));