app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

runtime.rs (813570B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 use std::fmt;
      3 use std::fs;
      4 use std::path::PathBuf;
      5 use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
      6 use std::time::{Duration as StdDuration, SystemTime, UNIX_EPOCH};
      7 
      8 use chrono::{DateTime, Duration, Utc};
      9 use radroots_app_core::{
     10     AppBuildIdentity, AppDesktopRuntimePaths, AppRuntimeCapture, AppRuntimeMode,
     11     AppRuntimePathsError, AppRuntimeSnapshot, AppSdkConfig, AppSdkDiagnostics,
     12     AppSdkFarmPublishRequest, AppSdkLifecycleState, AppSdkListingPublishRequest,
     13     AppSdkOrderCancellationRequest, AppSdkOrderDecisionRequest, AppSdkOrderRevisionDecisionRequest,
     14     AppSdkOrderRevisionProposalRequest, AppSdkOrderSubmitRequest, AppSdkProjectionLifecycleState,
     15     AppSdkRelayUrlPolicy, AppSdkRuntime, AppSdkRuntimeError, AppSdkRuntimeIssue,
     16     AppSdkRuntimeStatus, AppSdkStoragePaths, AppSdkWorkflowReceipt, AppSharedAccountsPaths,
     17     PackDayExportWriteError, prepare_pack_day_export_bundle_at_data_root,
     18     shared_local_events_database_path_from_shared_accounts, write_prepared_pack_day_export_bundle,
     19 };
     20 use radroots_app_remote_signer::{
     21     RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession,
     22 };
     23 use radroots_app_sqlite::{
     24     APP_ACTIVITY_CONTEXT_LIMIT, AppLocalInteropImportReport, AppSdkMigrationReceiptInput,
     25     AppSdkMigrationReceiptSourceKind, AppSdkMigrationState, AppSqliteError, AppSqliteStore,
     26     BuyerOrderLocalEventExport, BuyerOrderLocalEventLine, BuyerRepeatDemandApplyOutcome,
     27     DatabaseTarget, SelectedBuyerOrderScope, SellerOrderDecisionExport, StoredPendingSyncOperation,
     28     StoredRelayIngestCursor, StoredSyncConflict, derive_farm_rules_readiness,
     29     projected_order_id_from_trade_request,
     30 };
     31 use radroots_app_state::{
     32     APP_STATE_FILE_NAME, AppShellProjection, AppStateCommand, AppStatePersistenceRepository,
     33     AppStateStore, AppStateStoreError, BuyerBrowseScreenProjection, BuyerCartScreenProjection,
     34     BuyerOrdersScreenProjection, BuyerSearchScreenProjection, BuyerSearchScreenQueryState,
     35     FarmSetupFlowStage, FarmWorkspaceReadinessProjection, HomeRoute, OrdersScreenProjection,
     36     PackDayBatchPrintRequest, PackDayExportRequest, PackDayHostHandoffRequest, PackDayPrintRequest,
     37     PackDayScreenProjection, PersistedAppState, PersonalWorkspaceProjection,
     38     ProductsScreenProjection, ProductsScreenQueryState, derive_product_publish_blockers,
     39     derive_sync_projection,
     40 };
     41 use radroots_app_sync::{
     42     AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload,
     43     AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload, AppOrderDecisionPublishPayload,
     44     AppOrderRequestItemPayload, AppOrderRequestPublishPayload,
     45     AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload,
     46     AppPublishContext, AppPublishPayload, AppPublishedOperationReceipt,
     47     AppRelayIngestScopeFreshness, AppSyncProjection, AppSyncRequest, AppSyncResult,
     48     AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, SyncCheckpointStatus,
     49     SyncConflictSeverity, SyncTrigger,
     50 };
     51 #[cfg(test)]
     52 use radroots_app_sync::{PendingSyncOperation, SyncAggregateRef, SyncOperationKind};
     53 use radroots_app_view::{
     54     ActiveSurface, AppActivityContext, AppActivityKind, AppIdentityProjection, AppStartupGate,
     55     BuyerCartLineProjection, BuyerCartProjection, BuyerCartReplaceConfirmationProjection,
     56     BuyerContext, BuyerOrderDetailProjection, BuyerOrderReviewDraft, BuyerOrderStatus,
     57     BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmProfileRecord, FarmReadiness,
     58     FarmRulesProjection, FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection,
     59     FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrderStatus,
     60     OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayBatchPrintStatus,
     61     PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind,
     62     PackDayHostHandoffStatus, PackDayPrintKind, PackDayPrintStatus, PackDayProjection,
     63     PackDayScreenQueryState, PersonalSection, PickupLocationRecord, ProductEditorDraft, ProductId,
     64     ProductStatus, ProductsFilter, ProductsListProjection, ProductsSort,
     65     ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, ReminderId,
     66     ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface,
     67     ReminderUrgency, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection,
     68     TodayAgendaProjection,
     69 };
     70 use radroots_core::{
     71     RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
     72     RadrootsCoreQuantityPrice, RadrootsCoreUnit,
     73 };
     74 use radroots_events::{
     75     ids::{
     76         RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress,
     77         RadrootsOrderId, RadrootsOrderRevisionId, RadrootsPublicKey,
     78     },
     79     kinds::{
     80         KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION,
     81         KIND_ORDER_REQUEST, KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL,
     82         KIND_PROFILE,
     83     },
     84 };
     85 use radroots_events_codec::order::order_event_context_from_tags;
     86 use radroots_identity::{RadrootsIdentity, RadrootsIdentityId};
     87 use radroots_local_events::{
     88     BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT,
     89     BUYER_ORDER_REQUEST_ACTOR_SOURCE_UNRESOLVED_APP, BUYER_ORDER_REQUEST_DOCUMENT_KIND,
     90     BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalEventRecordInput,
     91     LocalEventRecordUpdate, LocalEventsStore, LocalRecordFamily, LocalRecordStatus,
     92     PublishOutboxStatus, RelayDeliveryEvidence, RelayDeliveryFailure, SourceRuntime,
     93     buyer_order_request_local_work_record_id, validate_buyer_order_request_local_work_payload,
     94 };
     95 use radroots_nostr::prelude::{
     96     RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrOutput,
     97     RadrootsNostrTimestamp, radroots_nostr_kind, radroots_nostr_parse_pubkey,
     98 };
     99 use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager;
    100 use radroots_sdk::protocol::events::{
    101     RadrootsNostrEvent as SdkRadrootsNostrEvent, RadrootsNostrEventPtr,
    102 };
    103 use radroots_sdk::protocol::farm::{RadrootsFarm, RadrootsFarmRef};
    104 use radroots_sdk::protocol::listing::{
    105     RadrootsListing, RadrootsListingAvailability, RadrootsListingBin,
    106     RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct,
    107     RadrootsListingStatus,
    108 };
    109 use radroots_sdk::protocol::order::{
    110     RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
    111     RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem,
    112     RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome,
    113     RadrootsOrderRevisionProposal,
    114 };
    115 use radroots_sdk::{
    116     FARM_PUBLISH_OPERATION_KIND, LISTING_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND,
    117     ORDER_DECISION_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND,
    118     ORDER_REVISION_PROPOSAL_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND,
    119 };
    120 use radroots_sql_core::SqliteExecutor;
    121 use radroots_trade::listing::parse_public_listing_address;
    122 use radroots_trade::order::{
    123     RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderReductionInputs,
    124     RadrootsOrderRequestRecord, RadrootsOrderRevisionDecisionRecord,
    125     RadrootsOrderRevisionProposalRecord, RadrootsOrderStatus, reduce_order_events,
    126 };
    127 use serde_json::json;
    128 use thiserror::Error;
    129 use tokio::runtime::Builder as TokioRuntimeBuilder;
    130 use tracing::error;
    131 use uuid::Uuid;
    132 
    133 use crate::accounts::{
    134     DesktopAccountsBootstrapError, DesktopAccountsCommandError, DesktopAccountsProjectionError,
    135     DesktopLocalIdentityImportRequest, bootstrap_desktop_accounts, generate_local_account,
    136     identity_projection_from_manager, import_local_account, remove_selected_local_key,
    137     reset_local_device_state, select_active_surface, select_local_account,
    138 };
    139 use crate::pack_day_host_handoff::{
    140     PackDayHostHandoffCommandPlan, PackDayHostHandoffError, plan_pack_day_host_handoff,
    141 };
    142 use crate::pack_day_print::{
    143     PackDayBatchPrintCommandPlan, PackDayBatchPrintError, PackDayPrintCommandPlan,
    144     PackDayPrintError, cleanup_prepared_customer_label_asset_root,
    145     cleanup_prepared_customer_label_assets_for_export_instance, plan_pack_day_batch_print,
    146     plan_pack_day_print,
    147 };
    148 use crate::remote_signer::{
    149     DesktopRemoteSignerError, DesktopRemoteSignerPaths, activate_pending_session,
    150     apply_remote_signer_custody, clear_pending_session, load_pending_session, purge_all_state,
    151     reconcile_startup, store_pending_session,
    152 };
    153 
    154 const APP_DATABASE_FILE_NAME: &str = "app.sqlite3";
    155 const SYNC_TRANSPORT_UNAVAILABLE_MESSAGE: &str = "remote sync transport is not configured";
    156 const APP_SYNC_PUBLISH_USES_SDK_RUNTIME_MESSAGE: &str = "app sync publish work uses AppSdkRuntime";
    157 const APP_DIRECT_RELAY_SYNC_TIMEOUT_MS: u64 = 2_000;
    158 const APP_DIRECT_RELAY_CONNECT_TIMEOUT: StdDuration = StdDuration::from_secs(10);
    159 const APP_DIRECT_RELAY_INGEST_LIMIT: usize = 1_000;
    160 const APP_DIRECT_RELAY_INGEST_MAX_PAGES: usize = 5;
    161 const APP_DIRECT_RELAY_INGEST_SCOPE_KEY: &str = "direct_relay_ingest";
    162 const APP_DIRECT_RELAY_INGEST_STALE_AFTER_SECONDS: i64 = 900;
    163 const APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE: u32 = 250;
    164 const APP_DIRECT_RELAY_INGEST_KINDS: &[u16] = &[
    165     KIND_PROFILE as u16,
    166     KIND_FARM as u16,
    167     KIND_LISTING as u16,
    168     KIND_LISTING_DRAFT as u16,
    169     KIND_ORDER_REQUEST as u16,
    170     KIND_ORDER_DECISION as u16,
    171     KIND_ORDER_REVISION_PROPOSAL as u16,
    172     KIND_ORDER_REVISION_DECISION as u16,
    173     KIND_ORDER_CANCELLATION as u16,
    174 ];
    175 
    176 #[derive(Debug, Default)]
    177 struct UnavailableAppSyncTransport;
    178 
    179 impl AppSyncTransport for UnavailableAppSyncTransport {
    180     fn sync(&mut self, _request: AppSyncRequest) -> Result<AppSyncResult, AppSyncTransportError> {
    181         Err(AppSyncTransportError::unavailable(
    182             SYNC_TRANSPORT_UNAVAILABLE_MESSAGE,
    183         ))
    184     }
    185 
    186     fn supports_empty_sync_request(&self) -> bool {
    187         false
    188     }
    189 }
    190 
    191 fn default_sync_transport() -> Box<dyn AppSyncTransport + Send> {
    192     Box::new(UnavailableAppSyncTransport)
    193 }
    194 
    195 #[derive(Debug, Clone)]
    196 struct AppDirectRelayFetchReceipt {
    197     target_relays: Vec<String>,
    198     connected_relays: Vec<String>,
    199     failed_relays: Vec<RelayDeliveryFailure>,
    200     fetched_relays: Vec<AppDirectRelayFetchedRelay>,
    201     event_observed_relays: BTreeMap<String, Vec<String>>,
    202     events: Vec<RadrootsNostrEvent>,
    203 }
    204 
    205 #[derive(Debug, Clone)]
    206 struct AppDirectRelayFetchedRelay {
    207     relay_url: String,
    208     last_event_created_at_unix_seconds: Option<i64>,
    209 }
    210 
    211 #[derive(Clone, Debug, Eq, PartialEq)]
    212 pub enum AppSellerOrderDecisionCommand {
    213     Accept,
    214     Decline { reason: String },
    215 }
    216 
    217 #[derive(Clone, Debug, Eq, PartialEq)]
    218 struct ResolvedAppSellerOrderRequest {
    219     request_event: SdkRadrootsNostrEvent,
    220     request_event_id: String,
    221     request_author_pubkey: String,
    222     listing_event_id: Option<String>,
    223     payload: RadrootsOrderRequest,
    224 }
    225 
    226 #[derive(Clone, Debug, Eq, PartialEq)]
    227 struct ResolvedAppOrderDecisionEvidence {
    228     event_id: String,
    229     payload: RadrootsOrderDecision,
    230 }
    231 
    232 #[derive(Clone, Debug, Eq, PartialEq)]
    233 struct ResolvedAppOrderRevisionProposalEvidence {
    234     event_id: String,
    235     payload: RadrootsOrderRevisionProposal,
    236 }
    237 
    238 #[derive(Clone, Debug, Eq, PartialEq)]
    239 struct ResolvedAppOrderRevisionDecisionEvidence {
    240     event_id: String,
    241     payload: RadrootsOrderRevisionDecision,
    242 }
    243 
    244 #[derive(Clone, Debug, Eq, PartialEq)]
    245 struct ResolvedAppOrderLifecycleEvidence {
    246     evidence_events: Vec<SdkRadrootsNostrEvent>,
    247     request_event_id: String,
    248     status: RadrootsOrderStatus,
    249     agreement_event_id: Option<String>,
    250     last_event_id: Option<String>,
    251     decision: Option<ResolvedAppOrderDecisionEvidence>,
    252     revision_proposals: Vec<ResolvedAppOrderRevisionProposalEvidence>,
    253     revision_decisions: Vec<ResolvedAppOrderRevisionDecisionEvidence>,
    254     cancellation_event_id: Option<String>,
    255 }
    256 
    257 #[derive(Clone, Debug, Default, Eq, PartialEq)]
    258 struct AppActiveOrderEvidenceBuckets {
    259     requests: Vec<RadrootsOrderRequestRecord>,
    260     decisions: Vec<RadrootsOrderDecisionRecord>,
    261     revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>,
    262     revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>,
    263     cancellations: Vec<RadrootsOrderCancellationRecord>,
    264 }
    265 
    266 #[derive(Debug, Default)]
    267 struct AppDirectRelayIngestReport {
    268     local_import: AppLocalInteropImportReport,
    269     freshness_changed: bool,
    270 }
    271 
    272 #[derive(Debug, Error)]
    273 enum AppDirectRelayIngestError {
    274     #[error(transparent)]
    275     Sqlite(#[from] AppSqliteError),
    276     #[error(transparent)]
    277     Transport(#[from] AppSyncTransportError),
    278 }
    279 
    280 #[derive(Clone)]
    281 struct ConfiguredRelayAppSyncTransport {
    282     relay_urls: Vec<String>,
    283 }
    284 
    285 impl ConfiguredRelayAppSyncTransport {
    286     fn new(_accounts_manager: RadrootsNostrAccountsManager, nostr_relay_urls: Vec<String>) -> Self {
    287         Self {
    288             relay_urls: nostr_relay_urls,
    289         }
    290     }
    291 
    292     #[cfg(test)]
    293     fn with_relay_urls(
    294         _accounts_manager: RadrootsNostrAccountsManager,
    295         relay_urls: Vec<String>,
    296     ) -> Self {
    297         Self { relay_urls }
    298     }
    299 
    300     fn sync_with_sdk(
    301         &self,
    302         request: AppSyncRequest,
    303     ) -> Result<AppSyncResult, AppSyncTransportError> {
    304         let run_started_at = current_utc_timestamp();
    305         let _relay_urls = normalized_app_sync_relay_urls(&self.relay_urls)?;
    306 
    307         if !request.pending_operations.is_empty() {
    308             return Err(AppSyncTransportError::failed(
    309                 APP_SYNC_PUBLISH_USES_SDK_RUNTIME_MESSAGE,
    310             ));
    311         }
    312 
    313         Ok(AppSyncResult {
    314             run_status: radroots_app_sync::AppSyncRunStatus::Succeeded,
    315             checkpoint: SyncCheckpointStatus::current(
    316                 Some(run_started_at),
    317                 current_utc_timestamp(),
    318                 request.checkpoint.last_remote_cursor.clone(),
    319             ),
    320             pushed_operation_count: 0,
    321             pulled_record_count: 0,
    322             conflicts: request.known_conflicts,
    323             published_receipts: Vec::new(),
    324         })
    325     }
    326 }
    327 
    328 fn signing_identity_for_publish_payload(
    329     accounts_manager: &RadrootsNostrAccountsManager,
    330     publish_payload: &AppPublishPayload,
    331 ) -> Result<RadrootsIdentity, AppSyncTransportError> {
    332     let context = publish_payload_context(publish_payload);
    333     let account_id = RadrootsIdentityId::parse(context.account_id.trim()).map_err(|error| {
    334         AppSyncTransportError::failed(format!(
    335             "pending app publish work has invalid account context: {error}"
    336         ))
    337     })?;
    338     let record = accounts_manager
    339         .list_accounts()
    340         .map_err(|error| AppSyncTransportError::failed(error.to_string()))?
    341         .into_iter()
    342         .find(|record| record.account_id == account_id)
    343         .ok_or_else(|| {
    344             AppSyncTransportError::unavailable(format!(
    345                 "publish account is not configured locally: {account_id}"
    346             ))
    347         })?;
    348     let identity = accounts_manager
    349         .get_signing_identity(&account_id)
    350         .map_err(|error| AppSyncTransportError::failed(error.to_string()))?
    351         .ok_or_else(|| {
    352             AppSyncTransportError::unavailable(format!(
    353                 "publish account is not backed by a local signing key: {account_id}"
    354             ))
    355         })?;
    356     if identity.public_key_hex() != record.public_identity.public_key_hex {
    357         return Err(AppSyncTransportError::failed(
    358             "publish account signing key does not match account context",
    359         ));
    360     }
    361     Ok(identity)
    362 }
    363 
    364 fn publish_payload_context(publish_payload: &AppPublishPayload) -> &AppPublishContext {
    365     match publish_payload {
    366         AppPublishPayload::FarmProfile(payload) => &payload.context,
    367         AppPublishPayload::Listing(payload) => &payload.context,
    368         AppPublishPayload::OrderRequest(payload) => &payload.context,
    369         AppPublishPayload::OrderDecision(payload) => &payload.context,
    370         AppPublishPayload::OrderRevisionProposal(payload) => &payload.context,
    371         AppPublishPayload::OrderRevisionDecision(payload) => &payload.context,
    372         AppPublishPayload::OrderCancellation(payload) => &payload.context,
    373     }
    374 }
    375 
    376 impl AppSyncTransport for ConfiguredRelayAppSyncTransport {
    377     fn sync(&mut self, request: AppSyncRequest) -> Result<AppSyncResult, AppSyncTransportError> {
    378         self.sync_with_sdk(request)
    379     }
    380 }
    381 
    382 #[derive(Clone, Debug)]
    383 pub struct DesktopAppRuntime {
    384     state: Arc<Mutex<DesktopAppRuntimeState>>,
    385     sdk_runtime: Arc<Mutex<Option<AppSdkRuntime>>>,
    386 }
    387 
    388 impl DesktopAppRuntime {
    389     pub fn bootstrap(nostr_relay_urls: Vec<String>, runtime_snapshot: AppRuntimeSnapshot) -> Self {
    390         let paths = match AppDesktopRuntimePaths::current_desktop() {
    391             Ok(paths) => paths,
    392             Err(error) => {
    393                 return Self::from_state(DesktopAppRuntimeState::degraded_with_snapshot(
    394                     error.into(),
    395                     runtime_snapshot,
    396                 ));
    397             }
    398         };
    399 
    400         Self::bootstrap_from_paths_with_snapshot(paths, nostr_relay_urls, runtime_snapshot)
    401     }
    402 
    403     pub fn bootstrap_with_paths(
    404         paths: AppDesktopRuntimePaths,
    405         nostr_relay_urls: Vec<String>,
    406     ) -> Self {
    407         let runtime_snapshot = default_runtime_snapshot();
    408         Self::bootstrap_from_paths_with_snapshot(paths, nostr_relay_urls, runtime_snapshot)
    409     }
    410 
    411     fn bootstrap_from_paths_with_snapshot(
    412         paths: AppDesktopRuntimePaths,
    413         nostr_relay_urls: Vec<String>,
    414         runtime_snapshot: AppRuntimeSnapshot,
    415     ) -> Self {
    416         let state = match DesktopAppRuntimeState::bootstrap_from_paths(
    417             paths.clone(),
    418             nostr_relay_urls.clone(),
    419             runtime_snapshot.clone(),
    420         ) {
    421             Ok(state) => state,
    422             Err(error) => {
    423                 return Self::from_state(DesktopAppRuntimeState::degraded_with_snapshot(
    424                     error,
    425                     runtime_snapshot,
    426                 ));
    427             }
    428         };
    429 
    430         match start_desktop_sdk_runtime(&paths, nostr_relay_urls) {
    431             Ok(sdk_runtime) => {
    432                 let runtime = Self::from_state_with_sdk_runtime(state, sdk_runtime);
    433                 let _ = runtime.wait_for_sdk_startup(StdDuration::from_secs(5));
    434                 if let Err(error) = runtime.retry_pending_personal_order_coordination() {
    435                     error!(
    436                         target: "buyer_order",
    437                         event = "buyer_order.coordination_bootstrap_retry_failed",
    438                         error = %error,
    439                         "failed to retry pending buyer order coordination during bootstrap"
    440                     );
    441                 }
    442                 runtime
    443             }
    444             Err(error) => {
    445                 let mut state = state;
    446                 state.startup_issue = Some(error.to_string());
    447                 Self::from_state(state)
    448             }
    449         }
    450     }
    451 
    452     pub fn summary(&self) -> DesktopAppRuntimeSummary {
    453         let sdk_status = self.sdk_status_summary();
    454         let state = self.lock_state();
    455         let sync_status = DesktopAppSyncStatusSummary {
    456             account_id: state
    457                 .state_store
    458                 .identity_projection()
    459                 .selected_account
    460                 .as_ref()
    461                 .map(|account| account.account.account_id.clone()),
    462             projection: state.state_store.sync_projection().clone(),
    463             pending_write_count: state.selected_account_pending_sync_write_count,
    464             conflicts: state.selected_account_sync_conflicts.clone(),
    465         };
    466 
    467         DesktopAppRuntimeSummary {
    468             shell_projection: state.state_store.shell_projection().clone(),
    469             settings_account_projection: state.state_store.settings_account_projection(),
    470             startup_gate: state.state_store.startup_gate(),
    471             logged_out_startup: state.state_store.logged_out_startup_projection().clone(),
    472             home_route: state.state_store.home_route(),
    473             personal_projection: state.state_store.personal_projection().clone(),
    474             farm_setup_projection: state.state_store.farm_setup_projection().clone(),
    475             farm_rules_projection: state.state_store.farm_rules_projection().clone(),
    476             farm_readiness_projection: state.state_store.farm_readiness_projection().clone(),
    477             today_projection: state.state_store.today_projection().clone(),
    478             products_projection: state.state_store.products_projection().clone(),
    479             orders_projection: state.state_store.orders_projection().clone(),
    480             pack_day_projection: state.state_store.pack_day_projection().clone(),
    481             reminder_log: state.state_store.reminder_log_projection().clone(),
    482             runtime_metadata: state.runtime_metadata.clone(),
    483             sync_status,
    484             startup_issue: state.startup_issue.clone(),
    485             sdk_status,
    486         }
    487     }
    488 
    489     pub fn nostr_relay_urls(&self) -> Vec<String> {
    490         self.lock_state().nostr_relay_urls.clone()
    491     }
    492 
    493     pub fn sdk_status(&self) -> Option<AppSdkRuntimeStatus> {
    494         self.sdk_runtime
    495             .lock()
    496             .unwrap_or_else(|poisoned| poisoned.into_inner())
    497             .as_ref()
    498             .map(AppSdkRuntime::status)
    499     }
    500 
    501     pub fn sdk_status_summary(&self) -> Option<DesktopAppSdkStatusSummary> {
    502         self.sdk_status()
    503             .as_ref()
    504             .map(DesktopAppSdkStatusSummary::from_status)
    505     }
    506 
    507     pub fn wait_for_sdk_startup(&self, timeout: StdDuration) -> Option<AppSdkRuntimeStatus> {
    508         self.sdk_runtime
    509             .lock()
    510             .unwrap_or_else(|poisoned| poisoned.into_inner())
    511             .as_ref()
    512             .map(|runtime| runtime.wait_for_startup(timeout))
    513     }
    514 
    515     pub fn shutdown_sdk_runtime(&self) -> Result<bool, AppSdkRuntimeError> {
    516         let mut sdk_runtime = self
    517             .sdk_runtime
    518             .lock()
    519             .unwrap_or_else(|poisoned| poisoned.into_inner());
    520         let Some(runtime) = sdk_runtime.take() else {
    521             return Ok(false);
    522         };
    523         runtime.shutdown()?;
    524         Ok(true)
    525     }
    526 
    527     pub fn sdk_diagnostics(&self) -> Result<Option<AppSdkDiagnostics>, AppSdkRuntimeError> {
    528         let sdk_runtime = self
    529             .sdk_runtime
    530             .lock()
    531             .unwrap_or_else(|poisoned| poisoned.into_inner());
    532         let Some(runtime) = sdk_runtime.as_ref() else {
    533             return Ok(None);
    534         };
    535         runtime.diagnostics().map(Some)
    536     }
    537 
    538     pub fn sdk_diagnostics_summary(&self) -> Option<DesktopAppSdkDiagnosticsSummary> {
    539         let sdk_runtime = self
    540             .sdk_runtime
    541             .lock()
    542             .unwrap_or_else(|poisoned| poisoned.into_inner());
    543         let runtime = sdk_runtime.as_ref()?;
    544         let status = runtime.status();
    545         match runtime.diagnostics() {
    546             Ok(diagnostics) => Some(DesktopAppSdkDiagnosticsSummary {
    547                 status: DesktopAppSdkStatusSummary::from_status(&diagnostics.runtime),
    548                 state: DesktopAppSdkDiagnosticsState::Ready(
    549                     DesktopAppSdkReadyDiagnosticsSummary::from_diagnostics(&diagnostics),
    550                 ),
    551             }),
    552             Err(error) => {
    553                 let issue = desktop_app_sdk_issue_from_runtime_error(&error);
    554                 let mut status = DesktopAppSdkStatusSummary::from_status(&status);
    555                 status.last_issue = Some(issue.clone());
    556                 Some(DesktopAppSdkDiagnosticsSummary {
    557                     status,
    558                     state: DesktopAppSdkDiagnosticsState::Blocked(issue),
    559                 })
    560             }
    561         }
    562     }
    563 
    564     pub fn selected_settings_section(&self) -> SettingsSection {
    565         self.lock_state()
    566             .state_store
    567             .shell_projection()
    568             .settings
    569             .selected_section
    570     }
    571 
    572     pub fn sync_settings_section(&self, section: SettingsSection) -> bool {
    573         self.lock_state_mut()
    574             .state_store
    575             .apply_in_memory(AppStateCommand::select_settings_section(section))
    576     }
    577 
    578     pub fn show_startup_identity_choice(&self) -> bool {
    579         self.lock_state_mut()
    580             .state_store
    581             .apply_in_memory(AppStateCommand::show_startup_identity_choice())
    582     }
    583 
    584     pub fn begin_generate_key_startup(&self) -> bool {
    585         self.lock_state_mut()
    586             .state_store
    587             .apply_in_memory(AppStateCommand::begin_generate_key_startup())
    588     }
    589 
    590     pub fn show_startup_signer_entry(&self) -> bool {
    591         self.lock_state_mut()
    592             .state_store
    593             .apply_in_memory(AppStateCommand::show_startup_signer_entry())
    594     }
    595 
    596     pub fn set_startup_signer_source_input(&self, source_input: &str) -> bool {
    597         self.lock_state_mut().state_store.apply_in_memory(
    598             AppStateCommand::set_startup_signer_source_input(source_input),
    599         )
    600     }
    601 
    602     pub fn load_startup_pending_remote_signer_session(
    603         &self,
    604     ) -> Result<Option<RadrootsAppRemoteSignerPendingSession>, DesktopAppRuntimeCommandError> {
    605         self.lock_state()
    606             .load_startup_pending_remote_signer_session()
    607     }
    608 
    609     pub fn store_startup_pending_remote_signer_session(
    610         &self,
    611         pending: &RadrootsAppRemoteSignerPendingSession,
    612     ) -> Result<bool, DesktopAppRuntimeCommandError> {
    613         self.lock_state_mut()
    614             .store_startup_pending_remote_signer_session(pending)
    615     }
    616 
    617     pub fn clear_startup_pending_remote_signer_session(
    618         &self,
    619     ) -> Result<bool, DesktopAppRuntimeCommandError> {
    620         self.lock_state_mut()
    621             .clear_startup_pending_remote_signer_session()
    622     }
    623 
    624     pub fn activate_startup_approved_remote_signer_session(
    625         &self,
    626         pending: &RadrootsAppRemoteSignerPendingSession,
    627         approved: &RadrootsAppRemoteSignerApprovedSession,
    628     ) -> Result<bool, DesktopAppRuntimeCommandError> {
    629         self.lock_state_mut()
    630             .activate_startup_approved_remote_signer_session(pending, approved)
    631     }
    632 
    633     pub fn select_settings_section(&self, section: SettingsSection) -> bool {
    634         let changed = self.sync_settings_section(section);
    635 
    636         if changed {
    637             let _ = self.record_activity(AppActivityKind::SettingsSectionSelected { section });
    638         }
    639 
    640         changed
    641     }
    642 
    643     pub fn select_home(&self) -> bool {
    644         let mut state = self.lock_state_mut();
    645         let selected_section = match state.state_store.startup_gate() {
    646             AppStartupGate::Farmer => ShellSection::Farmer(FarmerSection::Today),
    647             AppStartupGate::Blocked | AppStartupGate::SetupRequired => ShellSection::Home,
    648             AppStartupGate::Personal => ShellSection::Personal(PersonalSection::Browse),
    649         };
    650 
    651         let section_changed = state
    652             .state_store
    653             .apply_in_memory(AppStateCommand::SelectSection(selected_section));
    654         let editor_changed = state.close_product_editor();
    655 
    656         section_changed || editor_changed
    657     }
    658 
    659     pub fn select_account(&self) -> bool {
    660         let mut state = self.lock_state_mut();
    661         let section_changed = state
    662             .state_store
    663             .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Account));
    664         let editor_changed = state.close_product_editor();
    665 
    666         section_changed || editor_changed
    667     }
    668 
    669     pub fn select_personal_section(
    670         &self,
    671         section: PersonalSection,
    672     ) -> Result<bool, AppSqliteError> {
    673         self.lock_state_mut().select_personal_section(section)
    674     }
    675 
    676     pub fn select_farmer_section(&self, section: FarmerSection) -> bool {
    677         self.lock_state_mut().select_farmer_section(section)
    678     }
    679 
    680     pub fn open_personal_product_detail(
    681         &self,
    682         section: PersonalSection,
    683         product_id: ProductId,
    684     ) -> Result<bool, AppSqliteError> {
    685         self.lock_state_mut()
    686             .open_personal_product_detail(section, product_id)
    687     }
    688 
    689     pub fn close_personal_product_detail(&self, section: PersonalSection) -> bool {
    690         self.lock_state_mut().close_personal_product_detail(section)
    691     }
    692 
    693     pub fn increase_personal_product_quantity(&self, section: PersonalSection) -> bool {
    694         self.lock_state_mut()
    695             .adjust_personal_product_quantity(section, 1)
    696     }
    697 
    698     pub fn decrease_personal_product_quantity(&self, section: PersonalSection) -> bool {
    699         self.lock_state_mut()
    700             .adjust_personal_product_quantity(section, -1)
    701     }
    702 
    703     pub fn add_personal_product_to_cart(
    704         &self,
    705         section: PersonalSection,
    706         replace_existing: bool,
    707     ) -> Result<bool, AppSqliteError> {
    708         self.lock_state_mut()
    709             .add_personal_product_to_cart(section, replace_existing)
    710     }
    711 
    712     pub fn clear_personal_cart_replace_confirmation(&self) -> bool {
    713         self.lock_state_mut()
    714             .clear_personal_cart_replace_confirmation()
    715     }
    716 
    717     pub fn remove_personal_cart_line(&self, product_id: ProductId) -> Result<bool, AppSqliteError> {
    718         self.lock_state_mut().remove_personal_cart_line(product_id)
    719     }
    720 
    721     pub fn save_personal_order_review_draft(
    722         &self,
    723         draft: BuyerOrderReviewDraft,
    724     ) -> Result<bool, AppSqliteError> {
    725         self.lock_state_mut()
    726             .save_personal_order_review_draft(draft)
    727     }
    728 
    729     pub fn place_personal_order(&self) -> Result<bool, AppSqliteError> {
    730         self.lock_state_mut().place_personal_order()
    731     }
    732 
    733     pub fn retry_pending_personal_order_coordination(&self) -> Result<bool, AppSqliteError> {
    734         self.lock_state_mut()
    735             .retry_pending_personal_order_coordination()
    736     }
    737 
    738     pub fn open_personal_order_detail(&self, order_id: OrderId) -> Result<bool, AppSqliteError> {
    739         self.lock_state_mut().open_personal_order_detail(order_id)
    740     }
    741 
    742     pub fn repeat_personal_order(
    743         &self,
    744         order_id: OrderId,
    745         replace_existing: bool,
    746     ) -> Result<bool, AppSqliteError> {
    747         self.lock_state_mut()
    748             .repeat_personal_order(order_id, replace_existing)
    749     }
    750 
    751     pub fn set_personal_search_query(&self, search_query: &str) -> Result<bool, AppSqliteError> {
    752         self.lock_state_mut()
    753             .set_personal_search_query(search_query)
    754     }
    755 
    756     pub fn set_personal_search_fulfillment_method(
    757         &self,
    758         method: FarmOrderMethod,
    759         enabled: bool,
    760     ) -> Result<bool, AppSqliteError> {
    761         self.lock_state_mut()
    762             .set_personal_search_fulfillment_method(method, enabled)
    763     }
    764 
    765     pub fn set_products_search_query(&self, search_query: &str) -> Result<bool, AppSqliteError> {
    766         self.lock_state_mut()
    767             .set_products_search_query(search_query)
    768     }
    769 
    770     pub fn select_products_filter(&self, filter: ProductsFilter) -> Result<bool, AppSqliteError> {
    771         self.lock_state_mut().select_products_filter(filter)
    772     }
    773 
    774     pub fn select_products_sort(&self, sort: ProductsSort) -> Result<bool, AppSqliteError> {
    775         self.lock_state_mut().select_products_sort(sort)
    776     }
    777 
    778     pub fn open_products_filter(&self, filter: ProductsFilter) -> Result<bool, AppSqliteError> {
    779         self.lock_state_mut().open_products_filter(filter)
    780     }
    781 
    782     pub fn select_orders_filter(&self, filter: OrdersFilter) -> Result<bool, AppSqliteError> {
    783         self.lock_state_mut().select_orders_filter(filter)
    784     }
    785 
    786     pub fn open_orders(&self) -> Result<bool, AppSqliteError> {
    787         self.lock_state_mut().open_orders()
    788     }
    789 
    790     pub fn open_orders_fulfillment_window(
    791         &self,
    792         fulfillment_window_id: FulfillmentWindowId,
    793     ) -> Result<bool, AppSqliteError> {
    794         self.lock_state_mut()
    795             .open_orders_fulfillment_window(fulfillment_window_id)
    796     }
    797 
    798     pub fn open_order_detail(&self, order_id: OrderId) -> Result<bool, AppSqliteError> {
    799         self.lock_state_mut().open_order_detail(order_id)
    800     }
    801 
    802     pub fn prepare_order_accept(
    803         &self,
    804         order_id: OrderId,
    805     ) -> Result<AppOrderDecisionPublishPayload, AppSqliteError> {
    806         self.lock_state_mut()
    807             .prepare_seller_order_decision(order_id, AppSellerOrderDecisionCommand::Accept)
    808     }
    809 
    810     pub fn prepare_order_decline(
    811         &self,
    812         order_id: OrderId,
    813         reason: &str,
    814     ) -> Result<AppOrderDecisionPublishPayload, AppSqliteError> {
    815         self.lock_state_mut().prepare_seller_order_decision(
    816             order_id,
    817             AppSellerOrderDecisionCommand::Decline {
    818                 reason: reason.to_owned(),
    819             },
    820         )
    821     }
    822 
    823     pub fn publish_order_accept(&self, order_id: OrderId) -> Result<bool, AppSqliteError> {
    824         self.lock_state_mut()
    825             .publish_seller_order_decision(order_id, AppSellerOrderDecisionCommand::Accept)
    826     }
    827 
    828     pub fn publish_order_decline(
    829         &self,
    830         order_id: OrderId,
    831         reason: &str,
    832     ) -> Result<bool, AppSqliteError> {
    833         self.lock_state_mut().publish_seller_order_decision(
    834             order_id,
    835             AppSellerOrderDecisionCommand::Decline {
    836                 reason: reason.to_owned(),
    837             },
    838         )
    839     }
    840 
    841     pub fn publish_order_revision_proposal(
    842         &self,
    843         order_id: OrderId,
    844         items: Vec<RadrootsOrderItem>,
    845         economics: RadrootsOrderEconomics,
    846         reason: &str,
    847     ) -> Result<bool, AppSqliteError> {
    848         self.lock_state_mut()
    849             .publish_seller_order_revision_proposal(order_id, items, economics, reason)
    850     }
    851 
    852     pub fn publish_buyer_order_cancel(&self, order_id: OrderId) -> Result<bool, AppSqliteError> {
    853         self.lock_state_mut()
    854             .publish_buyer_order_cancellation(order_id)
    855     }
    856 
    857     pub fn publish_buyer_order_revision_accept(
    858         &self,
    859         order_id: OrderId,
    860     ) -> Result<bool, AppSqliteError> {
    861         self.lock_state_mut()
    862             .publish_buyer_order_revision_accept(order_id)
    863     }
    864 
    865     pub fn publish_buyer_order_revision_decline(
    866         &self,
    867         order_id: OrderId,
    868     ) -> Result<bool, AppSqliteError> {
    869         self.lock_state_mut()
    870             .publish_buyer_order_revision_decline(order_id)
    871     }
    872 
    873     pub fn open_pack_day(
    874         &self,
    875         fulfillment_window_id: Option<FulfillmentWindowId>,
    876     ) -> Result<bool, AppSqliteError> {
    877         self.lock_state_mut().open_pack_day(fulfillment_window_id)
    878     }
    879 
    880     pub fn export_pack_day(&self) -> Result<bool, DesktopAppRuntimeCommandError> {
    881         self.lock_state_mut().export_pack_day()
    882     }
    883 
    884     pub fn prepare_pack_day_host_handoff(
    885         &self,
    886         kind: PackDayHostHandoffKind,
    887     ) -> Result<
    888         Option<(PackDayHostHandoffRequest, PackDayHostHandoffCommandPlan)>,
    889         DesktopAppRuntimeCommandError,
    890     > {
    891         self.lock_state_mut().prepare_pack_day_host_handoff(kind)
    892     }
    893 
    894     pub fn finish_pack_day_host_handoff(
    895         &self,
    896         request: PackDayHostHandoffRequest,
    897         result: Result<(), PackDayHostHandoffError>,
    898     ) -> Result<bool, DesktopAppRuntimeCommandError> {
    899         self.lock_state_mut()
    900             .finish_pack_day_host_handoff(request, result)
    901     }
    902 
    903     pub fn prepare_pack_day_print(
    904         &self,
    905         kind: PackDayPrintKind,
    906     ) -> Result<Option<(PackDayPrintRequest, PackDayPrintCommandPlan)>, DesktopAppRuntimeCommandError>
    907     {
    908         self.lock_state_mut().prepare_pack_day_print(kind)
    909     }
    910 
    911     pub fn finish_pack_day_print(
    912         &self,
    913         request: PackDayPrintRequest,
    914         result: Result<(), PackDayPrintError>,
    915     ) -> Result<bool, DesktopAppRuntimeCommandError> {
    916         self.lock_state_mut().finish_pack_day_print(request, result)
    917     }
    918 
    919     pub fn prepare_pack_day_batch_print(
    920         &self,
    921     ) -> Result<
    922         Option<(PackDayBatchPrintRequest, PackDayBatchPrintCommandPlan)>,
    923         DesktopAppRuntimeCommandError,
    924     > {
    925         self.lock_state_mut().prepare_pack_day_batch_print()
    926     }
    927 
    928     pub fn finish_pack_day_batch_print(
    929         &self,
    930         request: PackDayBatchPrintRequest,
    931         result: Result<(), PackDayBatchPrintError>,
    932     ) -> Result<bool, DesktopAppRuntimeCommandError> {
    933         self.lock_state_mut()
    934             .finish_pack_day_batch_print(request, result)
    935     }
    936 
    937     pub fn update_product_stock(
    938         &self,
    939         product_id: ProductId,
    940         stock_quantity: u32,
    941     ) -> Result<bool, DesktopAppRuntimeProductStockUpdateError> {
    942         self.lock_state_mut()
    943             .update_product_stock(product_id, stock_quantity)
    944     }
    945 
    946     pub fn open_new_product_editor(&self) -> Result<bool, AppSqliteError> {
    947         self.lock_state_mut().open_new_product_editor()
    948     }
    949 
    950     pub fn open_existing_product_editor(
    951         &self,
    952         product_id: ProductId,
    953     ) -> Result<bool, AppSqliteError> {
    954         self.lock_state_mut()
    955             .open_existing_product_editor(product_id)
    956     }
    957 
    958     pub fn save_product_editor_draft(
    959         &self,
    960         draft: ProductEditorDraft,
    961     ) -> Result<bool, DesktopAppRuntimeProductEditorSaveError> {
    962         self.lock_state_mut().save_product_editor_draft(draft)
    963     }
    964 
    965     pub fn close_product_editor(&self) -> bool {
    966         self.lock_state_mut().close_product_editor()
    967     }
    968 
    969     pub fn set_settings_preference(&self, preference: SettingsPreference, enabled: bool) -> bool {
    970         let changed = self.lock_state_mut().state_store.apply_in_memory(
    971             AppStateCommand::SetSettingsPreference {
    972                 preference,
    973                 enabled,
    974             },
    975         );
    976 
    977         if changed {
    978             let _ = self.record_activity(AppActivityKind::SettingsPreferenceUpdated {
    979                 preference,
    980                 enabled,
    981             });
    982         }
    983 
    984         changed
    985     }
    986 
    987     pub fn generate_local_account(
    988         &self,
    989         label: Option<String>,
    990     ) -> Result<bool, DesktopAppRuntimeCommandError> {
    991         self.lock_state_mut().generate_local_account(label)
    992     }
    993 
    994     pub fn import_local_account(
    995         &self,
    996         request: DesktopLocalIdentityImportRequest,
    997     ) -> Result<bool, DesktopAppRuntimeCommandError> {
    998         self.lock_state_mut().import_local_account(&request)
    999     }
   1000 
   1001     pub fn select_local_account(
   1002         &self,
   1003         account_id: &str,
   1004     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   1005         self.lock_state_mut().select_local_account(account_id)
   1006     }
   1007 
   1008     pub fn select_active_surface(
   1009         &self,
   1010         active_surface: ActiveSurface,
   1011     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   1012         self.lock_state_mut().select_active_surface(active_surface)
   1013     }
   1014 
   1015     pub fn remove_selected_local_key(&self) -> Result<bool, DesktopAppRuntimeCommandError> {
   1016         self.lock_state_mut().remove_selected_local_key()
   1017     }
   1018 
   1019     pub fn reset_local_device_state(&self) -> Result<bool, DesktopAppRuntimeCommandError> {
   1020         self.lock_state_mut().reset_local_device_state()
   1021     }
   1022 
   1023     #[allow(dead_code)]
   1024     pub fn replace_today_agenda(&self, projection: TodayAgendaProjection) -> bool {
   1025         self.lock_state_mut()
   1026             .state_store
   1027             .apply_in_memory(AppStateCommand::replace_today_agenda(projection))
   1028     }
   1029 
   1030     pub fn select_farm_setup_flow_stage(&self, stage: FarmSetupFlowStage) -> bool {
   1031         self.lock_state_mut()
   1032             .state_store
   1033             .apply_in_memory(AppStateCommand::select_farm_setup_flow_stage(stage))
   1034     }
   1035 
   1036     pub fn save_farm_setup_draft(
   1037         &self,
   1038         draft: FarmSetupDraft,
   1039     ) -> Result<FarmSetupProjection, DesktopAppRuntimeFarmSetupError> {
   1040         self.lock_state_mut().save_farm_setup_draft(draft)
   1041     }
   1042 
   1043     pub fn finish_farm_setup(
   1044         &self,
   1045     ) -> Result<FarmSetupProjection, DesktopAppRuntimeFarmSetupError> {
   1046         self.lock_state_mut().finish_farm_setup()
   1047     }
   1048 
   1049     pub fn load_farm_rules_projection(
   1050         &self,
   1051     ) -> Result<FarmRulesProjection, DesktopAppRuntimeFarmRulesError> {
   1052         self.lock_state().load_farm_rules_projection()
   1053     }
   1054 
   1055     pub fn save_farm_rules_projection(
   1056         &self,
   1057         projection: FarmRulesProjection,
   1058     ) -> Result<FarmRulesProjection, DesktopAppRuntimeFarmRulesError> {
   1059         self.lock_state_mut().save_farm_rules_projection(projection)
   1060     }
   1061 
   1062     pub fn sync_on_app_launch(&self) -> Result<bool, AppSqliteError> {
   1063         self.lock_state_mut().attempt_sync(SyncTrigger::AppLaunch)
   1064     }
   1065 
   1066     pub fn sync_on_foreground_resume(&self) -> Result<bool, AppSqliteError> {
   1067         self.lock_state_mut().sync_on_foreground_resume()
   1068     }
   1069 
   1070     pub fn sync_on_manual_refresh(&self) -> Result<bool, AppSqliteError> {
   1071         self.lock_state_mut()
   1072             .attempt_sync(SyncTrigger::ManualRefresh)
   1073     }
   1074 
   1075     pub fn refresh_shared_local_events(
   1076         &self,
   1077     ) -> Result<AppLocalInteropImportReport, AppSqliteError> {
   1078         let mut state = self.lock_state_mut();
   1079         let report = state.import_shared_local_events()?;
   1080         let _ = state.refresh_selected_account_context_after_local_events()?;
   1081         Ok(report)
   1082     }
   1083 
   1084     pub fn resolve_sync_conflict(
   1085         &self,
   1086         conflict_id: &str,
   1087         resolution: radroots_app_sync::SyncConflictResolutionStatus,
   1088     ) -> Result<bool, AppSqliteError> {
   1089         self.lock_state_mut()
   1090             .resolve_sync_conflict(conflict_id, resolution)
   1091     }
   1092 
   1093     pub fn acknowledge_reminder(&self, reminder_id: ReminderId) -> Result<bool, AppSqliteError> {
   1094         self.lock_state_mut().acknowledge_reminder(reminder_id)
   1095     }
   1096 
   1097     pub fn record_home_opened(&self) -> bool {
   1098         self.record_activity(AppActivityKind::HomeOpened)
   1099     }
   1100 
   1101     pub fn record_settings_opened(&self, section: SettingsSection) -> bool {
   1102         self.record_activity(AppActivityKind::SettingsOpened { section })
   1103     }
   1104 
   1105     pub fn activity_context(
   1106         &self,
   1107         limit: Option<usize>,
   1108     ) -> Result<AppActivityContext, DesktopAppRuntimeActivityContextError> {
   1109         let state = self.lock_state();
   1110         let store = state
   1111             .sqlite_store
   1112             .as_ref()
   1113             .ok_or(DesktopAppRuntimeActivityContextError::RuntimeUnavailable)?;
   1114 
   1115         store
   1116             .load_activity_context(limit.unwrap_or(APP_ACTIVITY_CONTEXT_LIMIT))
   1117             .map_err(DesktopAppRuntimeActivityContextError::from)
   1118     }
   1119 
   1120     fn from_state(state: DesktopAppRuntimeState) -> Self {
   1121         let sdk_runtime = Arc::new(Mutex::new(None));
   1122         Self {
   1123             state: Arc::new(Mutex::new(state)),
   1124             sdk_runtime,
   1125         }
   1126     }
   1127 
   1128     fn from_state_with_sdk_runtime(
   1129         mut state: DesktopAppRuntimeState,
   1130         sdk_runtime: AppSdkRuntime,
   1131     ) -> Self {
   1132         let sdk_runtime = Arc::new(Mutex::new(Some(sdk_runtime)));
   1133         state.sdk_runtime = Some(Arc::clone(&sdk_runtime));
   1134         Self {
   1135             state: Arc::new(Mutex::new(state)),
   1136             sdk_runtime,
   1137         }
   1138     }
   1139 
   1140     fn lock_state(&self) -> MutexGuard<'_, DesktopAppRuntimeState> {
   1141         self.state.lock().unwrap_or_else(PoisonError::into_inner)
   1142     }
   1143 
   1144     fn lock_state_mut(&self) -> MutexGuard<'_, DesktopAppRuntimeState> {
   1145         self.state.lock().unwrap_or_else(PoisonError::into_inner)
   1146     }
   1147 
   1148     fn record_activity(&self, kind: AppActivityKind) -> bool {
   1149         let result = self.lock_state().record_activity(kind.clone());
   1150         if let Err(error) = result {
   1151             error!(
   1152                 target: "activity",
   1153                 event = "activity.record_failed",
   1154                 activity_kind = kind.storage_key(),
   1155                 error = %error,
   1156                 "failed to record activity event"
   1157             );
   1158             return false;
   1159         }
   1160 
   1161         true
   1162     }
   1163 }
   1164 
   1165 fn default_runtime_snapshot() -> AppRuntimeSnapshot {
   1166     AppRuntimeSnapshot::from_capture(
   1167         AppBuildIdentity {
   1168             package_name: env!("CARGO_PKG_NAME").to_owned(),
   1169             package_version: env!("CARGO_PKG_VERSION").to_owned(),
   1170             build_profile: option_env!("PROFILE").unwrap_or("debug").to_owned(),
   1171             target_triple: option_env!("TARGET").unwrap_or("unknown-target").to_owned(),
   1172             projection_source: "rust".to_owned(),
   1173             git_commit: None,
   1174         },
   1175         AppRuntimeMode::Development,
   1176         AppRuntimeCapture {
   1177             host_locale: "en_US.UTF-8".to_owned(),
   1178             operating_system: "macos".to_owned(),
   1179             run_id: "runtime-summary-test-run".to_owned(),
   1180         },
   1181     )
   1182 }
   1183 
   1184 fn start_desktop_sdk_runtime(
   1185     paths: &AppDesktopRuntimePaths,
   1186     nostr_relay_urls: Vec<String>,
   1187 ) -> Result<AppSdkRuntime, AppSdkRuntimeError> {
   1188     AppSdkRuntime::start(AppSdkConfig::from_desktop_paths(paths, nostr_relay_urls))
   1189 }
   1190 
   1191 fn sdk_storage_path_pair(paths: Option<&AppSdkStoragePaths>) -> (Option<PathBuf>, Option<PathBuf>) {
   1192     paths
   1193         .map(|paths| {
   1194             (
   1195                 Some(paths.event_store_path.clone()),
   1196                 Some(paths.outbox_path.clone()),
   1197             )
   1198         })
   1199         .unwrap_or((None, None))
   1200 }
   1201 
   1202 fn desktop_app_sdk_issue_from_runtime_error(
   1203     error: &AppSdkRuntimeError,
   1204 ) -> DesktopAppSdkIssueSummary {
   1205     match error {
   1206         AppSdkRuntimeError::CommandFailed(issue) => DesktopAppSdkIssueSummary::from_issue(issue),
   1207         AppSdkRuntimeError::CommandQueueCapacityZero => DesktopAppSdkIssueSummary::runtime(
   1208             "sdk_command_queue_capacity_zero",
   1209             false,
   1210             ["review_runtime_configuration"],
   1211         ),
   1212         AppSdkRuntimeError::WorkerSpawn(_) => {
   1213             DesktopAppSdkIssueSummary::runtime("sdk_worker_spawn_failed", true, ["retry_startup"])
   1214         }
   1215         AppSdkRuntimeError::CommandQueueFull => DesktopAppSdkIssueSummary::runtime(
   1216             "sdk_command_queue_full",
   1217             true,
   1218             ["retry_status_refresh"],
   1219         ),
   1220         AppSdkRuntimeError::CommandQueueClosed => {
   1221             DesktopAppSdkIssueSummary::runtime("sdk_command_queue_closed", true, ["retry_startup"])
   1222         }
   1223         AppSdkRuntimeError::CommandResponseClosed => DesktopAppSdkIssueSummary::runtime(
   1224             "sdk_command_response_closed",
   1225             true,
   1226             ["retry_status_refresh"],
   1227         ),
   1228         AppSdkRuntimeError::ShutdownAck => {
   1229             DesktopAppSdkIssueSummary::runtime("sdk_shutdown_ack_failed", true, ["retry_startup"])
   1230         }
   1231         AppSdkRuntimeError::WorkerJoin => {
   1232             DesktopAppSdkIssueSummary::runtime("sdk_worker_join_failed", true, ["retry_startup"])
   1233         }
   1234     }
   1235 }
   1236 
   1237 #[derive(Clone, Debug, Default, Eq, PartialEq)]
   1238 pub struct DesktopAppSyncStatusSummary {
   1239     pub account_id: Option<String>,
   1240     pub projection: AppSyncProjection,
   1241     pub pending_write_count: usize,
   1242     pub conflicts: Vec<DesktopAppSyncConflictSummary>,
   1243 }
   1244 
   1245 impl DesktopAppSyncStatusSummary {
   1246     pub const fn is_enabled(&self) -> bool {
   1247         self.account_id.is_some()
   1248     }
   1249 }
   1250 
   1251 #[derive(Clone, Debug, Eq, PartialEq)]
   1252 pub struct DesktopAppSyncConflictSummary {
   1253     pub conflict_id: String,
   1254     pub conflict: radroots_app_sync::SyncConflict,
   1255 }
   1256 
   1257 #[derive(Clone, Debug, Eq, PartialEq)]
   1258 pub struct DesktopAppSdkStatusSummary {
   1259     pub lifecycle_state: AppSdkLifecycleState,
   1260     pub projection_lifecycle_state: AppSdkProjectionLifecycleState,
   1261     pub projection_lifecycle_reason: Option<String>,
   1262     pub storage_root: PathBuf,
   1263     pub event_store_path: Option<PathBuf>,
   1264     pub outbox_path: Option<PathBuf>,
   1265     pub relay_target_count: usize,
   1266     pub relay_url_policy: AppSdkRelayUrlPolicy,
   1267     pub last_issue: Option<DesktopAppSdkIssueSummary>,
   1268 }
   1269 
   1270 impl DesktopAppSdkStatusSummary {
   1271     fn from_status(status: &AppSdkRuntimeStatus) -> Self {
   1272         let (event_store_path, outbox_path) = sdk_storage_path_pair(status.storage_paths.as_ref());
   1273         Self {
   1274             lifecycle_state: status.state,
   1275             projection_lifecycle_state: status.projection_lifecycle.state,
   1276             projection_lifecycle_reason: status.projection_lifecycle.reason.clone(),
   1277             storage_root: status.storage_root.clone(),
   1278             event_store_path,
   1279             outbox_path,
   1280             relay_target_count: status.relay_urls.len(),
   1281             relay_url_policy: status.relay_url_policy,
   1282             last_issue: status
   1283                 .last_issue
   1284                 .as_ref()
   1285                 .map(DesktopAppSdkIssueSummary::from_issue),
   1286         }
   1287     }
   1288 }
   1289 
   1290 #[derive(Clone, Debug, Eq, PartialEq)]
   1291 pub struct DesktopAppSdkDiagnosticsSummary {
   1292     pub status: DesktopAppSdkStatusSummary,
   1293     pub state: DesktopAppSdkDiagnosticsState,
   1294 }
   1295 
   1296 #[derive(Clone, Debug, Eq, PartialEq)]
   1297 pub enum DesktopAppSdkDiagnosticsState {
   1298     Ready(DesktopAppSdkReadyDiagnosticsSummary),
   1299     Blocked(DesktopAppSdkIssueSummary),
   1300 }
   1301 
   1302 #[derive(Clone, Debug, Eq, PartialEq)]
   1303 pub struct DesktopAppSdkReadyDiagnosticsSummary {
   1304     pub storage_kind: String,
   1305     pub event_store_total_events: i64,
   1306     pub outbox_total_events: i64,
   1307     pub outbox_pending_events: i64,
   1308     pub outbox_failed_terminal_events: i64,
   1309     pub integrity_event_store_ok: bool,
   1310     pub integrity_outbox_ok: bool,
   1311     pub sync_source: String,
   1312     pub sync_observed_at_ms: i64,
   1313     pub sync_relay_target_count: usize,
   1314 }
   1315 
   1316 impl DesktopAppSdkReadyDiagnosticsSummary {
   1317     fn from_diagnostics(diagnostics: &AppSdkDiagnostics) -> Self {
   1318         Self {
   1319             storage_kind: diagnostics.storage.storage_kind.clone(),
   1320             event_store_total_events: diagnostics.storage.event_store.total_events,
   1321             outbox_total_events: diagnostics.storage.outbox.total_events,
   1322             outbox_pending_events: diagnostics.storage.outbox.pending_events,
   1323             outbox_failed_terminal_events: diagnostics.storage.outbox.failed_terminal_events,
   1324             integrity_event_store_ok: diagnostics.integrity.event_store_ok,
   1325             integrity_outbox_ok: diagnostics.integrity.outbox_ok,
   1326             sync_source: diagnostics.sync.source.clone(),
   1327             sync_observed_at_ms: diagnostics.sync.observed_at_ms,
   1328             sync_relay_target_count: diagnostics.sync.relay_targets.configured_count,
   1329         }
   1330     }
   1331 
   1332     pub const fn integrity_ok(&self) -> bool {
   1333         self.integrity_event_store_ok && self.integrity_outbox_ok
   1334     }
   1335 }
   1336 
   1337 #[derive(Clone, Debug, Eq, PartialEq)]
   1338 pub struct DesktopAppSdkIssueSummary {
   1339     pub code: String,
   1340     pub class: String,
   1341     pub retryable: bool,
   1342     pub recovery_actions: Vec<String>,
   1343 }
   1344 
   1345 impl DesktopAppSdkIssueSummary {
   1346     fn from_issue(issue: &AppSdkRuntimeIssue) -> Self {
   1347         Self {
   1348             code: issue.code.clone(),
   1349             class: issue.class.clone(),
   1350             retryable: issue.retryable,
   1351             recovery_actions: issue.recovery_actions.clone(),
   1352         }
   1353     }
   1354 
   1355     fn runtime(
   1356         code: impl Into<String>,
   1357         retryable: bool,
   1358         recovery_actions: impl IntoIterator<Item = &'static str>,
   1359     ) -> Self {
   1360         Self {
   1361             code: code.into(),
   1362             class: "runtime".to_owned(),
   1363             retryable,
   1364             recovery_actions: recovery_actions.into_iter().map(str::to_owned).collect(),
   1365         }
   1366     }
   1367 }
   1368 
   1369 #[derive(Clone, Debug, Eq, PartialEq)]
   1370 pub struct DesktopAppRuntimeMetadataSummary {
   1371     pub snapshot: AppRuntimeSnapshot,
   1372     pub data_root: Option<PathBuf>,
   1373     pub logs_root: Option<PathBuf>,
   1374     pub database_path: Option<PathBuf>,
   1375     pub database_schema_version: Option<u32>,
   1376 }
   1377 
   1378 impl DesktopAppRuntimeMetadataSummary {
   1379     fn available(
   1380         snapshot: AppRuntimeSnapshot,
   1381         paths: &AppDesktopRuntimePaths,
   1382         database_path: PathBuf,
   1383         database_schema_version: u32,
   1384     ) -> Self {
   1385         Self {
   1386             snapshot,
   1387             data_root: Some(paths.app.data.clone()),
   1388             logs_root: Some(paths.app.logs.clone()),
   1389             database_path: Some(database_path),
   1390             database_schema_version: Some(database_schema_version),
   1391         }
   1392     }
   1393 
   1394     fn unavailable(snapshot: AppRuntimeSnapshot) -> Self {
   1395         Self {
   1396             snapshot,
   1397             data_root: None,
   1398             logs_root: None,
   1399             database_path: None,
   1400             database_schema_version: None,
   1401         }
   1402     }
   1403 }
   1404 
   1405 impl Default for DesktopAppRuntimeMetadataSummary {
   1406     fn default() -> Self {
   1407         Self::unavailable(default_runtime_snapshot())
   1408     }
   1409 }
   1410 
   1411 #[derive(Clone, Debug)]
   1412 pub struct DesktopAppRuntimeSummary {
   1413     pub shell_projection: AppShellProjection,
   1414     pub settings_account_projection: SettingsAccountProjection,
   1415     pub startup_gate: AppStartupGate,
   1416     pub logged_out_startup: LoggedOutStartupProjection,
   1417     pub home_route: HomeRoute,
   1418     pub personal_projection: PersonalWorkspaceProjection,
   1419     pub farm_setup_projection: FarmSetupProjection,
   1420     pub farm_rules_projection: FarmRulesProjection,
   1421     pub farm_readiness_projection: FarmWorkspaceReadinessProjection,
   1422     pub today_projection: TodayAgendaProjection,
   1423     pub products_projection: ProductsScreenProjection,
   1424     pub orders_projection: OrdersScreenProjection,
   1425     pub pack_day_projection: PackDayScreenProjection,
   1426     pub reminder_log: ReminderLogProjection,
   1427     pub runtime_metadata: DesktopAppRuntimeMetadataSummary,
   1428     pub sync_status: DesktopAppSyncStatusSummary,
   1429     pub startup_issue: Option<String>,
   1430     pub sdk_status: Option<DesktopAppSdkStatusSummary>,
   1431 }
   1432 
   1433 #[derive(Debug, Error)]
   1434 pub enum DesktopAppRuntimeActivityContextError {
   1435     #[error("desktop runtime activity context is unavailable while the runtime is degraded")]
   1436     RuntimeUnavailable,
   1437     #[error(transparent)]
   1438     Sqlite(#[from] AppSqliteError),
   1439 }
   1440 
   1441 #[derive(Clone, Debug, Default)]
   1442 struct DesktopSelectedAccountContext {
   1443     personal_projection: PersonalWorkspaceProjection,
   1444     farm_setup_projection: FarmSetupProjection,
   1445     farm_rules_projection: FarmRulesProjection,
   1446     today_projection: TodayAgendaProjection,
   1447     products_query: ProductsScreenQueryState,
   1448     products_list: ProductsListProjection,
   1449     orders_query: OrdersScreenQueryState,
   1450     orders_list: OrdersListProjection,
   1451     orders_reminders: ReminderFeedProjection,
   1452     order_detail: Option<OrderDetailProjection>,
   1453     pack_day_query: PackDayScreenQueryState,
   1454     pack_day_projection: PackDayProjection,
   1455     product_editor_draft: Option<(ProductId, ProductEditorDraft)>,
   1456     reminder_log: ReminderLogProjection,
   1457 }
   1458 
   1459 #[derive(Clone, Debug, Default)]
   1460 struct DesktopSellerReminderContext {
   1461     today_feed: ReminderFeedProjection,
   1462     orders_feed: ReminderFeedProjection,
   1463     pack_day_feed: ReminderFeedProjection,
   1464     due_soon_count: u32,
   1465     reminder_log: ReminderLogProjection,
   1466 }
   1467 
   1468 #[derive(Clone, Debug, Default)]
   1469 struct DesktopReminderSyncTruth {
   1470     checkpoint: SyncCheckpointStatus,
   1471     pending_write_count: usize,
   1472     unresolved_conflict_count: usize,
   1473     blocking_conflict_count: usize,
   1474 }
   1475 
   1476 #[derive(Clone, Debug, Default, Eq, PartialEq)]
   1477 struct DesktopSelectedAccountSyncContext {
   1478     projection: AppSyncProjection,
   1479     relay_ingest: AppRelayIngestScopeFreshness,
   1480     pending_write_count: usize,
   1481     conflicts: Vec<DesktopAppSyncConflictSummary>,
   1482 }
   1483 
   1484 #[derive(Clone, Debug)]
   1485 struct DesktopPreparedSyncRequest {
   1486     account_id: String,
   1487     checkpoint: SyncCheckpointStatus,
   1488     conflicts: Vec<StoredSyncConflict>,
   1489     pending_operations: Vec<StoredPendingSyncOperation>,
   1490 }
   1491 
   1492 struct DesktopAppRuntimeState {
   1493     state_store: AppStateStore<AppStatePersistenceRepository>,
   1494     nostr_relay_urls: Vec<String>,
   1495     shared_accounts_paths: Option<AppSharedAccountsPaths>,
   1496     remote_signer_paths: Option<DesktopRemoteSignerPaths>,
   1497     accounts_manager: Option<RadrootsNostrAccountsManager>,
   1498     sqlite_store: Option<AppSqliteStore>,
   1499     sdk_runtime: Option<Arc<Mutex<Option<AppSdkRuntime>>>>,
   1500     sync_transport: Box<dyn AppSyncTransport + Send>,
   1501     runtime_metadata: DesktopAppRuntimeMetadataSummary,
   1502     selected_account_pending_sync_write_count: usize,
   1503     selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness,
   1504     selected_account_sync_conflicts: Vec<DesktopAppSyncConflictSummary>,
   1505     startup_issue: Option<String>,
   1506 }
   1507 
   1508 impl fmt::Debug for DesktopAppRuntimeState {
   1509     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
   1510         formatter
   1511             .debug_struct("DesktopAppRuntimeState")
   1512             .field("state_store", &self.state_store)
   1513             .field(
   1514                 "shared_accounts_paths",
   1515                 &self.shared_accounts_paths.as_ref().map(|_| "available"),
   1516             )
   1517             .field(
   1518                 "remote_signer_paths",
   1519                 &self.remote_signer_paths.as_ref().map(|_| "available"),
   1520             )
   1521             .field(
   1522                 "accounts_manager",
   1523                 &self.accounts_manager.as_ref().map(|_| "available"),
   1524             )
   1525             .field(
   1526                 "sqlite_store",
   1527                 &self.sqlite_store.as_ref().map(|_| "available"),
   1528             )
   1529             .field(
   1530                 "sdk_runtime",
   1531                 &self.sdk_runtime.as_ref().map(|_| "available"),
   1532             )
   1533             .field("sync_transport", &"configured")
   1534             .field("runtime_metadata", &self.runtime_metadata)
   1535             .field(
   1536                 "selected_account_pending_sync_write_count",
   1537                 &self.selected_account_pending_sync_write_count,
   1538             )
   1539             .field(
   1540                 "selected_account_relay_ingest_freshness",
   1541                 &self.selected_account_relay_ingest_freshness,
   1542             )
   1543             .field(
   1544                 "selected_account_sync_conflicts",
   1545                 &self.selected_account_sync_conflicts,
   1546             )
   1547             .field("startup_issue", &self.startup_issue)
   1548             .finish()
   1549     }
   1550 }
   1551 
   1552 impl DesktopAppRuntimeState {
   1553     fn bootstrap_from_paths(
   1554         paths: AppDesktopRuntimePaths,
   1555         nostr_relay_urls: Vec<String>,
   1556         runtime_snapshot: AppRuntimeSnapshot,
   1557     ) -> Result<Self, DesktopAppRuntimeBootstrapError> {
   1558         if let Err(error) = cleanup_prepared_customer_label_asset_root() {
   1559             error!(
   1560                 target: "pack_day",
   1561                 event = "pack_day.print_prepared_asset_bootstrap_sweep_failed",
   1562                 error = %error,
   1563                 "failed to sweep prepared pack day print assets during bootstrap"
   1564             );
   1565         }
   1566         let database_path = paths.app.data.join(APP_DATABASE_FILE_NAME);
   1567         let sqlite_store = AppSqliteStore::open(DatabaseTarget::Path(database_path.clone()))?;
   1568         let shared_local_events_database_path = paths.shared_local_events_database_path()?;
   1569         let _ = sqlite_store
   1570             .import_shared_local_events_from_path(shared_local_events_database_path.as_path())?;
   1571         let database_schema_version = sqlite_store.schema_version()?;
   1572         let mut state_store = AppStateStore::load(AppStatePersistenceRepository::file_backed(
   1573             paths.app.data.join(APP_STATE_FILE_NAME),
   1574         ))?;
   1575         let continuity_state = state_store.persisted_state().clone();
   1576         let remote_signer_paths = DesktopRemoteSignerPaths::from_runtime_paths(&paths);
   1577         let accounts_bootstrap = bootstrap_desktop_accounts(&paths.shared_accounts, &sqlite_store)?;
   1578         if let Some(accounts_manager) = accounts_bootstrap.accounts_manager.as_ref() {
   1579             reconcile_startup(accounts_manager, &remote_signer_paths)?;
   1580         }
   1581         let identity_projection = apply_remote_signer_custody(
   1582             identity_projection_from_manager(
   1583                 accounts_bootstrap
   1584                     .accounts_manager
   1585                     .as_ref()
   1586                     .expect("desktop bootstrap always returns an accounts manager"),
   1587                 &sqlite_store,
   1588             )?,
   1589             &remote_signer_paths,
   1590         )?;
   1591         let selected_account_context =
   1592             load_selected_account_context(&sqlite_store, &identity_projection, &continuity_state)?;
   1593         let selected_account_sync_context = load_selected_account_sync_context(
   1594             &sqlite_store,
   1595             &identity_projection,
   1596             &nostr_relay_urls,
   1597         )?;
   1598         let _ = state_store.apply_in_memory(AppStateCommand::replace_identity_projection(
   1599             identity_projection.clone(),
   1600         ));
   1601         if identity_projection.startup_gate() == AppStartupGate::SetupRequired
   1602             && load_pending_session(&remote_signer_paths)?.is_some()
   1603         {
   1604             let _ = state_store.apply_in_memory(AppStateCommand::show_startup_signer_entry());
   1605         }
   1606         let pending_sync_write_count = selected_account_sync_context.pending_write_count;
   1607         let selected_account_relay_ingest_freshness =
   1608             selected_account_sync_context.relay_ingest.clone();
   1609         let selected_account_sync_conflicts = selected_account_sync_context.conflicts;
   1610         let _ = state_store.apply_in_memory(AppStateCommand::replace_sync_projection(
   1611             selected_account_sync_context.projection,
   1612         ));
   1613         let sync_transport: Box<dyn AppSyncTransport + Send> =
   1614             match accounts_bootstrap.accounts_manager.as_ref() {
   1615                 Some(accounts_manager) => Box::new(ConfiguredRelayAppSyncTransport::new(
   1616                     accounts_manager.clone(),
   1617                     nostr_relay_urls.clone(),
   1618                 )),
   1619                 None => default_sync_transport(),
   1620             };
   1621         let mut state = Self {
   1622             state_store,
   1623             nostr_relay_urls,
   1624             shared_accounts_paths: Some(paths.shared_accounts.clone()),
   1625             remote_signer_paths: Some(remote_signer_paths),
   1626             accounts_manager: accounts_bootstrap.accounts_manager,
   1627             sqlite_store: Some(sqlite_store),
   1628             sdk_runtime: None,
   1629             sync_transport,
   1630             runtime_metadata: DesktopAppRuntimeMetadataSummary::available(
   1631                 runtime_snapshot,
   1632                 &paths,
   1633                 database_path,
   1634                 database_schema_version,
   1635             ),
   1636             selected_account_pending_sync_write_count: pending_sync_write_count,
   1637             selected_account_relay_ingest_freshness,
   1638             selected_account_sync_conflicts,
   1639             startup_issue: None,
   1640         };
   1641         let _ = state.apply_selected_account_context(&selected_account_context);
   1642 
   1643         Ok(state)
   1644     }
   1645 
   1646     #[cfg(test)]
   1647     fn degraded(error: DesktopAppRuntimeBootstrapError) -> Self {
   1648         Self::degraded_with_snapshot(error, default_runtime_snapshot())
   1649     }
   1650 
   1651     fn degraded_with_snapshot(
   1652         error: DesktopAppRuntimeBootstrapError,
   1653         runtime_snapshot: AppRuntimeSnapshot,
   1654     ) -> Self {
   1655         Self {
   1656             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
   1657                 .expect("in-memory state store should load"),
   1658             nostr_relay_urls: Vec::new(),
   1659             shared_accounts_paths: None,
   1660             remote_signer_paths: None,
   1661             accounts_manager: None,
   1662             sqlite_store: None,
   1663             sdk_runtime: None,
   1664             sync_transport: default_sync_transport(),
   1665             runtime_metadata: DesktopAppRuntimeMetadataSummary::unavailable(runtime_snapshot),
   1666             selected_account_pending_sync_write_count: 0,
   1667             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
   1668             selected_account_sync_conflicts: Vec::new(),
   1669             startup_issue: Some(error.to_string()),
   1670         }
   1671     }
   1672 
   1673     fn generate_local_account(
   1674         &mut self,
   1675         label: Option<String>,
   1676     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   1677         let projection = {
   1678             let accounts_manager = self.accounts_manager()?;
   1679             let sqlite_store = self.sqlite_store()?;
   1680             generate_local_account(accounts_manager, sqlite_store, label)?
   1681         };
   1682 
   1683         self.replace_identity_projection(projection)
   1684     }
   1685 
   1686     fn import_local_account(
   1687         &mut self,
   1688         request: &DesktopLocalIdentityImportRequest,
   1689     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   1690         let projection = {
   1691             let accounts_manager = self.accounts_manager()?;
   1692             let sqlite_store = self.sqlite_store()?;
   1693             import_local_account(accounts_manager, sqlite_store, request)?
   1694         };
   1695 
   1696         self.replace_identity_projection(projection)
   1697     }
   1698 
   1699     fn select_local_account(
   1700         &mut self,
   1701         account_id: &str,
   1702     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   1703         let projection = {
   1704             let accounts_manager = self.accounts_manager()?;
   1705             let sqlite_store = self.sqlite_store()?;
   1706             select_local_account(accounts_manager, sqlite_store, account_id)?
   1707         };
   1708 
   1709         self.replace_identity_projection(projection)
   1710     }
   1711 
   1712     fn select_active_surface(
   1713         &mut self,
   1714         active_surface: ActiveSurface,
   1715     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   1716         let projection = {
   1717             let accounts_manager = self.accounts_manager()?;
   1718             let sqlite_store = self.sqlite_store()?;
   1719             select_active_surface(accounts_manager, sqlite_store, active_surface)?
   1720         };
   1721 
   1722         let identity_changed = self.replace_identity_projection(projection)?;
   1723         let shell_changed = self
   1724             .state_store
   1725             .apply_in_memory(AppStateCommand::select_active_surface(active_surface));
   1726 
   1727         Ok(identity_changed || shell_changed)
   1728     }
   1729 
   1730     fn remove_selected_local_key(&mut self) -> Result<bool, DesktopAppRuntimeCommandError> {
   1731         let projection = {
   1732             let accounts_manager = self.accounts_manager()?;
   1733             let sqlite_store = self.sqlite_store()?;
   1734             remove_selected_local_key(accounts_manager, sqlite_store)?
   1735         };
   1736 
   1737         self.replace_identity_projection(projection)
   1738     }
   1739 
   1740     fn reset_local_device_state(&mut self) -> Result<bool, DesktopAppRuntimeCommandError> {
   1741         if let Some(paths) = self.remote_signer_paths.as_ref() {
   1742             purge_all_state(paths)?;
   1743         }
   1744         let projection = {
   1745             let accounts_manager = self.accounts_manager()?;
   1746             let sqlite_store = self.sqlite_store()?;
   1747             let shared_accounts_paths = self.shared_accounts_paths()?;
   1748             reset_local_device_state(accounts_manager, sqlite_store, shared_accounts_paths)?
   1749         };
   1750 
   1751         self.replace_identity_projection(projection)
   1752     }
   1753 
   1754     fn record_activity(&self, kind: AppActivityKind) -> Result<(), AppSqliteError> {
   1755         match self.sqlite_store.as_ref() {
   1756             Some(store) => store.record_activity_event(&kind),
   1757             None => Ok(()),
   1758         }
   1759     }
   1760 
   1761     fn select_farmer_section(&mut self, section: FarmerSection) -> bool {
   1762         match section {
   1763             FarmerSection::Today => {
   1764                 let section_changed =
   1765                     self.state_store
   1766                         .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Farmer(
   1767                             FarmerSection::Today,
   1768                         )));
   1769                 let editor_changed = self.close_product_editor();
   1770 
   1771                 section_changed || editor_changed
   1772             }
   1773             FarmerSection::Products if self.has_saved_farm() => {
   1774                 self.state_store
   1775                     .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Farmer(
   1776                         FarmerSection::Products,
   1777                     )))
   1778             }
   1779             FarmerSection::Orders if self.has_saved_farm() => {
   1780                 let section_changed =
   1781                     self.state_store
   1782                         .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Farmer(
   1783                             FarmerSection::Orders,
   1784                         )));
   1785                 let detail_changed = self
   1786                     .state_store
   1787                     .apply_in_memory(AppStateCommand::replace_order_detail(None));
   1788                 let editor_changed = self.close_product_editor();
   1789 
   1790                 section_changed || detail_changed || editor_changed
   1791             }
   1792             FarmerSection::PackDay if self.has_saved_farm() && self.has_pack_day_context() => {
   1793                 let section_changed =
   1794                     self.state_store
   1795                         .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Farmer(
   1796                             FarmerSection::PackDay,
   1797                         )));
   1798                 let editor_changed = self.close_product_editor();
   1799 
   1800                 section_changed || editor_changed
   1801             }
   1802             FarmerSection::Products
   1803             | FarmerSection::Orders
   1804             | FarmerSection::PackDay
   1805             | FarmerSection::Farm => false,
   1806         }
   1807     }
   1808 
   1809     fn select_personal_section(
   1810         &mut self,
   1811         section: PersonalSection,
   1812     ) -> Result<bool, AppSqliteError> {
   1813         let freshness_changed = if section == PersonalSection::Browse {
   1814             self.refresh_personal_browse_navigation()?
   1815         } else {
   1816             false
   1817         };
   1818         let section_changed = self.apply_personal_section_selection(section);
   1819 
   1820         Ok(freshness_changed || section_changed)
   1821     }
   1822 
   1823     fn apply_personal_section_selection(&mut self, section: PersonalSection) -> bool {
   1824         let section_changed = self
   1825             .state_store
   1826             .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Personal(
   1827                 section,
   1828             )));
   1829         let editor_changed = self.close_product_editor();
   1830 
   1831         section_changed || editor_changed
   1832     }
   1833 
   1834     fn refresh_personal_browse_navigation(&mut self) -> Result<bool, AppSqliteError> {
   1835         let report = self.import_shared_local_events()?;
   1836         let local_changed = report.imported_records > 0 || report.skipped_records > 0;
   1837         let context_changed = self.refresh_selected_account_context_after_local_events()?;
   1838 
   1839         Ok(local_changed || context_changed)
   1840     }
   1841 
   1842     fn open_personal_product_detail(
   1843         &mut self,
   1844         section: PersonalSection,
   1845         product_id: ProductId,
   1846     ) -> Result<bool, AppSqliteError> {
   1847         let should_refresh_before_lookup =
   1848             matches!(section, PersonalSection::Browse | PersonalSection::Search);
   1849         let freshness_changed = if should_refresh_before_lookup {
   1850             self.refresh_personal_browse_navigation()?
   1851         } else {
   1852             false
   1853         };
   1854         let section_changed = if should_refresh_before_lookup {
   1855             self.apply_personal_section_selection(section)
   1856         } else {
   1857             false
   1858         };
   1859         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   1860             return Ok(freshness_changed || section_changed);
   1861         };
   1862         let Some(detail) = sqlite_store.load_buyer_product_detail(product_id)? else {
   1863             return Ok(freshness_changed || section_changed);
   1864         };
   1865 
   1866         let detail_changed = self.set_personal_product_detail(section, Some(detail));
   1867 
   1868         Ok(freshness_changed || section_changed || detail_changed)
   1869     }
   1870 
   1871     fn close_personal_product_detail(&mut self, section: PersonalSection) -> bool {
   1872         self.set_personal_product_detail(section, None)
   1873     }
   1874 
   1875     fn adjust_personal_product_quantity(&mut self, section: PersonalSection, delta: i32) -> bool {
   1876         self.mutate_personal_projection(|projection| {
   1877             let Some(detail) = personal_detail_mut(projection, section) else {
   1878                 return false;
   1879             };
   1880             let next_quantity = if delta.is_negative() {
   1881                 detail
   1882                     .selected_quantity
   1883                     .saturating_sub(delta.unsigned_abs())
   1884             } else {
   1885                 detail.selected_quantity.saturating_add(delta as u32)
   1886             };
   1887 
   1888             if next_quantity == 0 || next_quantity == detail.selected_quantity {
   1889                 return false;
   1890             }
   1891 
   1892             detail.selected_quantity = next_quantity;
   1893             true
   1894         })
   1895     }
   1896 
   1897     fn add_personal_product_to_cart(
   1898         &mut self,
   1899         section: PersonalSection,
   1900         replace_existing: bool,
   1901     ) -> Result<bool, AppSqliteError> {
   1902         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   1903             return Ok(false);
   1904         };
   1905         let Some(detail) =
   1906             personal_detail(self.state_store.personal_projection(), section).cloned()
   1907         else {
   1908             return Ok(false);
   1909         };
   1910         let buyer_context = self.state_store.identity_projection().buyer_context();
   1911         let current_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
   1912 
   1913         if !replace_existing
   1914             && !current_cart.is_empty()
   1915             && current_cart.farm_id != Some(detail.listing.farm_id)
   1916         {
   1917             let current_farm_display_name = current_cart
   1918                 .farm_display_name
   1919                 .clone()
   1920                 .or_else(|| {
   1921                     current_cart
   1922                         .lines
   1923                         .first()
   1924                         .map(|line| line.farm_display_name.clone())
   1925                 })
   1926                 .ok_or(AppSqliteError::InvalidProjection {
   1927                     reason: "buyer cart farm display name is missing",
   1928                 })?;
   1929             let replace_confirmation = BuyerCartReplaceConfirmationProjection {
   1930                 current_farm_display_name,
   1931                 incoming_farm_display_name: detail.listing.farm_display_name.clone(),
   1932             };
   1933 
   1934             return Ok(self.mutate_personal_projection(|projection| {
   1935                 let cart = &mut projection.cart.cart;
   1936                 if cart.replace_confirmation.as_ref() == Some(&replace_confirmation) {
   1937                     return false;
   1938                 }
   1939 
   1940                 cart.replace_confirmation = Some(replace_confirmation);
   1941                 true
   1942             }));
   1943         }
   1944 
   1945         let next_cart = next_buyer_cart_for_detail(current_cart, &detail, replace_existing)?;
   1946         sqlite_store.replace_buyer_cart(&buyer_context, &next_cart)?;
   1947         let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
   1948         let refreshed_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
   1949         let cart_changed = self.mutate_personal_projection(|projection| {
   1950             let mut changed = false;
   1951             if projection.cart.cart != refreshed_cart {
   1952                 projection.cart.cart = refreshed_cart.clone();
   1953                 changed = true;
   1954             }
   1955             if projection.cart.order_review != refreshed_order_review {
   1956                 projection.cart.order_review = refreshed_order_review.clone();
   1957                 changed = true;
   1958             }
   1959             changed
   1960         });
   1961         let section_changed = self.select_personal_section(PersonalSection::Cart)?;
   1962 
   1963         Ok(cart_changed || section_changed)
   1964     }
   1965 
   1966     fn clear_personal_cart_replace_confirmation(&mut self) -> bool {
   1967         self.mutate_personal_projection(|projection| {
   1968             if projection.cart.cart.replace_confirmation.is_none() {
   1969                 return false;
   1970             }
   1971 
   1972             projection.cart.cart.replace_confirmation = None;
   1973             true
   1974         })
   1975     }
   1976 
   1977     fn remove_personal_cart_line(&mut self, product_id: ProductId) -> Result<bool, AppSqliteError> {
   1978         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   1979             return Ok(false);
   1980         };
   1981         let buyer_context = self.state_store.identity_projection().buyer_context();
   1982         let current_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
   1983         let Some(next_cart) = next_buyer_cart_after_removing_line(current_cart, product_id)? else {
   1984             return Ok(false);
   1985         };
   1986 
   1987         if next_cart.lines.is_empty() {
   1988             sqlite_store.clear_buyer_cart(&buyer_context)?;
   1989         } else {
   1990             sqlite_store.replace_buyer_cart(&buyer_context, &next_cart)?;
   1991         }
   1992 
   1993         let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
   1994         let refreshed_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
   1995 
   1996         Ok(self.refresh_personal_cart_and_order_review(refreshed_cart, refreshed_order_review))
   1997     }
   1998 
   1999     fn save_personal_order_review_draft(
   2000         &mut self,
   2001         draft: BuyerOrderReviewDraft,
   2002     ) -> Result<bool, AppSqliteError> {
   2003         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2004             return Ok(false);
   2005         };
   2006         let buyer_context = self.state_store.identity_projection().buyer_context();
   2007         sqlite_store.save_buyer_order_review_draft(&buyer_context, &draft)?;
   2008         let refreshed_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
   2009 
   2010         Ok(self.mutate_personal_projection(|projection| {
   2011             if projection.cart.order_review == refreshed_order_review {
   2012                 return false;
   2013             }
   2014 
   2015             projection.cart.order_review = refreshed_order_review;
   2016             true
   2017         }))
   2018     }
   2019 
   2020     fn place_personal_order(&mut self) -> Result<bool, AppSqliteError> {
   2021         let buyer_context = self.state_store.identity_projection().buyer_context();
   2022         if matches!(buyer_context, BuyerContext::Guest) {
   2023             return Err(AppSqliteError::InvalidProjection {
   2024                 reason: "buyer order review requires a selected account",
   2025             });
   2026         }
   2027         let (refreshed_cart, refreshed_order_review, refreshed_orders, order_detail, order_export) = {
   2028             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2029                 return Ok(false);
   2030             };
   2031             let order_id = sqlite_store.place_buyer_order(&buyer_context)?;
   2032             let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
   2033             let refreshed_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
   2034             let refreshed_orders = sqlite_store.load_buyer_orders(&buyer_context)?;
   2035             if !refreshed_orders
   2036                 .rows
   2037                 .iter()
   2038                 .any(|row| row.order_id == order_id)
   2039             {
   2040                 return Err(AppSqliteError::InvalidProjection {
   2041                     reason: "buyer order write did not surface in buyer order history",
   2042                 });
   2043             }
   2044             let Some(order_detail) =
   2045                 sqlite_store.load_buyer_order_detail(&buyer_context, order_id)?
   2046             else {
   2047                 return Err(AppSqliteError::InvalidProjection {
   2048                     reason: "buyer order write did not surface in buyer order detail",
   2049                 });
   2050             };
   2051             let Some(order_export) =
   2052                 sqlite_store.load_buyer_order_local_event_export(&buyer_context, order_id)?
   2053             else {
   2054                 return Err(AppSqliteError::InvalidProjection {
   2055                     reason: "buyer order write did not surface in buyer order local event export",
   2056                 });
   2057             };
   2058             (
   2059                 refreshed_cart,
   2060                 refreshed_order_review,
   2061                 refreshed_orders,
   2062                 order_detail,
   2063                 order_export,
   2064             )
   2065         };
   2066         let personal_changed = self.mutate_personal_projection(|projection| {
   2067             let mut changed = false;
   2068             if projection.cart.cart != refreshed_cart {
   2069                 projection.cart.cart = refreshed_cart.clone();
   2070                 changed = true;
   2071             }
   2072             if projection.cart.order_review != refreshed_order_review {
   2073                 projection.cart.order_review = refreshed_order_review.clone();
   2074                 changed = true;
   2075             }
   2076             if projection.orders.list != refreshed_orders {
   2077                 projection.orders.list = refreshed_orders.clone();
   2078                 changed = true;
   2079             }
   2080             if projection.orders.detail.as_ref() != Some(&order_detail) {
   2081                 projection.orders.detail = Some(order_detail.clone());
   2082                 changed = true;
   2083             }
   2084             if !projection.orders.has_recoverable_coordination {
   2085                 projection.orders.has_recoverable_coordination = true;
   2086                 changed = true;
   2087             }
   2088 
   2089             changed
   2090         });
   2091         let section_changed = self.select_personal_section(PersonalSection::Orders)?;
   2092         let order_local_work = {
   2093             let sqlite_store =
   2094                 self.sqlite_store
   2095                     .as_ref()
   2096                     .ok_or_else(|| AppSqliteError::InvalidProjection {
   2097                         reason: "sqlite store became unavailable during buyer order placement",
   2098                     })?;
   2099             self.append_app_buyer_order_request_local_work_record(
   2100                 sqlite_store,
   2101                 &buyer_context,
   2102                 &order_export,
   2103             )?
   2104         };
   2105         let pending_changed = if matches!(buyer_context, BuyerContext::Account(_)) {
   2106             self.enqueue_selected_account_order_sync_operation(
   2107                 &buyer_context,
   2108                 &order_export,
   2109                 order_local_work.as_ref(),
   2110             )?
   2111         } else {
   2112             false
   2113         };
   2114         let coordination_changed =
   2115             self.refresh_personal_orders_coordination_retry_state(&buyer_context)?;
   2116 
   2117         Ok(personal_changed || section_changed || pending_changed || coordination_changed)
   2118     }
   2119 
   2120     fn retry_pending_personal_order_coordination(&mut self) -> Result<bool, AppSqliteError> {
   2121         let buyer_context = self.state_store.identity_projection().buyer_context();
   2122         let records = {
   2123             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2124                 return Ok(false);
   2125             };
   2126             sqlite_store.load_recoverable_buyer_order_coordination_records(&buyer_context)?
   2127         };
   2128         if records.is_empty() {
   2129             return self.refresh_personal_orders_coordination_retry_state(&buyer_context);
   2130         }
   2131         let mut changed = false;
   2132         let mut refreshed_order_id = None;
   2133 
   2134         for record in records {
   2135             let order_export = {
   2136                 let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2137                     return Ok(changed);
   2138                 };
   2139                 let Some(order_export) = sqlite_store
   2140                     .load_buyer_order_local_event_export(&buyer_context, record.order_id)?
   2141                 else {
   2142                     changed |= sqlite_store.mark_buyer_order_coordination_failed(
   2143                         &buyer_context,
   2144                         record.order_id,
   2145                         "buyer order local event export is unavailable",
   2146                     )?;
   2147                     continue;
   2148                 };
   2149                 order_export
   2150             };
   2151 
   2152             let order_local_work = {
   2153                 let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2154                     return Ok(changed);
   2155                 };
   2156                 self.append_app_buyer_order_request_local_work_record(
   2157                     sqlite_store,
   2158                     &buyer_context,
   2159                     &order_export,
   2160                 )?
   2161             };
   2162             if let Some(order_local_work) = order_local_work.as_ref()
   2163                 && matches!(buyer_context, BuyerContext::Account(_))
   2164             {
   2165                 let _ = self.enqueue_selected_account_order_sync_operation(
   2166                     &buyer_context,
   2167                     &order_export,
   2168                     Some(order_local_work),
   2169                 )?;
   2170             }
   2171             if order_local_work.is_some() {
   2172                 refreshed_order_id.get_or_insert(record.order_id);
   2173                 changed = true;
   2174             }
   2175         }
   2176 
   2177         if changed {
   2178             changed |=
   2179                 self.refresh_personal_orders_projection(&buyer_context, refreshed_order_id)?;
   2180         }
   2181 
   2182         Ok(changed)
   2183     }
   2184 
   2185     fn refresh_personal_orders_coordination_retry_state(
   2186         &mut self,
   2187         buyer_context: &BuyerContext,
   2188     ) -> Result<bool, AppSqliteError> {
   2189         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2190             return Ok(false);
   2191         };
   2192         let has_recoverable_coordination = !sqlite_store
   2193             .load_recoverable_buyer_order_coordination_records(buyer_context)?
   2194             .is_empty();
   2195         Ok(self.mutate_personal_projection(|projection| {
   2196             if projection.orders.has_recoverable_coordination == has_recoverable_coordination {
   2197                 false
   2198             } else {
   2199                 projection.orders.has_recoverable_coordination = has_recoverable_coordination;
   2200                 true
   2201             }
   2202         }))
   2203     }
   2204 
   2205     fn refresh_personal_orders_projection(
   2206         &mut self,
   2207         buyer_context: &BuyerContext,
   2208         preferred_order_id: Option<OrderId>,
   2209     ) -> Result<bool, AppSqliteError> {
   2210         let current_detail_order_id = self
   2211             .state_store
   2212             .personal_projection()
   2213             .orders
   2214             .detail
   2215             .as_ref()
   2216             .map(|detail| detail.order_id);
   2217         let buyer_order_scope = selected_buyer_order_scope(self.state_store.identity_projection());
   2218         let (
   2219             refreshed_cart,
   2220             refreshed_order_review,
   2221             refreshed_orders,
   2222             refreshed_order_detail,
   2223             has_recoverable_coordination,
   2224         ) = {
   2225             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2226                 return Ok(false);
   2227             };
   2228             let refreshed_cart = sqlite_store.load_buyer_cart(buyer_context)?;
   2229             let refreshed_order_review = sqlite_store.load_buyer_order_review(buyer_context)?;
   2230             let refreshed_orders = sqlite_store.load_buyer_orders_for_scope(&buyer_order_scope)?;
   2231             let has_recoverable_coordination = !sqlite_store
   2232                 .load_recoverable_buyer_order_coordination_records(buyer_context)?
   2233                 .is_empty();
   2234             let detail_order_id = current_detail_order_id
   2235                 .filter(|order_id| {
   2236                     refreshed_orders
   2237                         .rows
   2238                         .iter()
   2239                         .any(|row| row.order_id == *order_id)
   2240                 })
   2241                 .or_else(|| {
   2242                     preferred_order_id.filter(|order_id| {
   2243                         refreshed_orders
   2244                             .rows
   2245                             .iter()
   2246                             .any(|row| row.order_id == *order_id)
   2247                     })
   2248                 });
   2249             let refreshed_order_detail = match detail_order_id {
   2250                 Some(order_id) => {
   2251                     sqlite_store.load_buyer_order_detail_for_scope(&buyer_order_scope, order_id)?
   2252                 }
   2253                 None => None,
   2254             };
   2255             (
   2256                 refreshed_cart,
   2257                 refreshed_order_review,
   2258                 refreshed_orders,
   2259                 refreshed_order_detail,
   2260                 has_recoverable_coordination,
   2261             )
   2262         };
   2263 
   2264         Ok(self.mutate_personal_projection(|projection| {
   2265             let mut changed = false;
   2266             if projection.cart.cart != refreshed_cart {
   2267                 projection.cart.cart = refreshed_cart.clone();
   2268                 changed = true;
   2269             }
   2270             if projection.cart.order_review != refreshed_order_review {
   2271                 projection.cart.order_review = refreshed_order_review.clone();
   2272                 changed = true;
   2273             }
   2274             if projection.orders.list != refreshed_orders {
   2275                 projection.orders.list = refreshed_orders.clone();
   2276                 changed = true;
   2277             }
   2278             if projection.orders.detail != refreshed_order_detail {
   2279                 projection.orders.detail = refreshed_order_detail.clone();
   2280                 changed = true;
   2281             }
   2282             if projection.orders.has_recoverable_coordination != has_recoverable_coordination {
   2283                 projection.orders.has_recoverable_coordination = has_recoverable_coordination;
   2284                 changed = true;
   2285             }
   2286 
   2287             changed
   2288         }))
   2289     }
   2290 
   2291     fn open_personal_order_detail(&mut self, order_id: OrderId) -> Result<bool, AppSqliteError> {
   2292         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2293             return Ok(false);
   2294         };
   2295         let buyer_order_scope = selected_buyer_order_scope(self.state_store.identity_projection());
   2296         let Some(order_detail) =
   2297             sqlite_store.load_buyer_order_detail_for_scope(&buyer_order_scope, order_id)?
   2298         else {
   2299             return Ok(false);
   2300         };
   2301 
   2302         let detail_changed = self.set_personal_order_detail(Some(order_detail));
   2303         let section_changed = self.select_personal_section(PersonalSection::Orders)?;
   2304 
   2305         Ok(detail_changed || section_changed)
   2306     }
   2307 
   2308     fn repeat_personal_order(
   2309         &mut self,
   2310         order_id: OrderId,
   2311         replace_existing: bool,
   2312     ) -> Result<bool, AppSqliteError> {
   2313         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2314             return Ok(false);
   2315         };
   2316         let buyer_context = self.state_store.identity_projection().buyer_context();
   2317         let buyer_order_scope = selected_buyer_order_scope(self.state_store.identity_projection());
   2318 
   2319         match sqlite_store.apply_buyer_repeat_demand_from_scope_to_cart(
   2320             &buyer_order_scope,
   2321             &buyer_context,
   2322             order_id,
   2323             replace_existing,
   2324         )? {
   2325             BuyerRepeatDemandApplyOutcome::Applied => {
   2326                 let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
   2327                 let refreshed_order_review =
   2328                     sqlite_store.load_buyer_order_review(&buyer_context)?;
   2329                 let refreshed_orders =
   2330                     sqlite_store.load_buyer_orders_for_scope(&buyer_order_scope)?;
   2331                 let refreshed_detail =
   2332                     sqlite_store.load_buyer_order_detail_for_scope(&buyer_order_scope, order_id)?;
   2333                 let personal_changed = self.mutate_personal_projection(|projection| {
   2334                     let mut changed = false;
   2335                     if projection.cart.cart != refreshed_cart {
   2336                         projection.cart.cart = refreshed_cart.clone();
   2337                         changed = true;
   2338                     }
   2339                     if projection.cart.order_review != refreshed_order_review {
   2340                         projection.cart.order_review = refreshed_order_review.clone();
   2341                         changed = true;
   2342                     }
   2343                     if projection.orders.list != refreshed_orders {
   2344                         projection.orders.list = refreshed_orders.clone();
   2345                         changed = true;
   2346                     }
   2347                     if projection.orders.detail != refreshed_detail {
   2348                         projection.orders.detail = refreshed_detail.clone();
   2349                         changed = true;
   2350                     }
   2351 
   2352                     changed
   2353                 });
   2354                 let section_changed = self.select_personal_section(PersonalSection::Cart)?;
   2355 
   2356                 Ok(personal_changed || section_changed)
   2357             }
   2358             BuyerRepeatDemandApplyOutcome::ConfirmationRequired(replace_confirmation) => Ok(self
   2359                 .mutate_personal_projection(|projection| {
   2360                     let cart = &mut projection.cart.cart;
   2361                     if cart.replace_confirmation.as_ref() == Some(&replace_confirmation) {
   2362                         return false;
   2363                     }
   2364 
   2365                     cart.replace_confirmation = Some(replace_confirmation);
   2366                     true
   2367                 })),
   2368             BuyerRepeatDemandApplyOutcome::Unavailable => Ok(false),
   2369         }
   2370     }
   2371 
   2372     fn set_personal_search_query(&mut self, search_query: &str) -> Result<bool, AppSqliteError> {
   2373         let query = self.state_store.personal_projection().search.query.clone();
   2374         if query.search_query == search_query {
   2375             return self.replace_personal_search_query(query);
   2376         }
   2377 
   2378         self.replace_personal_search_query(BuyerSearchScreenQueryState::new(
   2379             search_query,
   2380             query.fulfillment_methods,
   2381         ))
   2382     }
   2383 
   2384     fn set_personal_search_fulfillment_method(
   2385         &mut self,
   2386         method: FarmOrderMethod,
   2387         enabled: bool,
   2388     ) -> Result<bool, AppSqliteError> {
   2389         let mut query = self.state_store.personal_projection().search.query.clone();
   2390         let changed = if enabled {
   2391             query.fulfillment_methods.insert(method)
   2392         } else {
   2393             query.fulfillment_methods.remove(&method)
   2394         };
   2395 
   2396         if !changed {
   2397             return Ok(false);
   2398         }
   2399 
   2400         self.replace_personal_search_query(query)
   2401     }
   2402 
   2403     fn set_products_search_query(&mut self, search_query: &str) -> Result<bool, AppSqliteError> {
   2404         let query = self.state_store.products_projection().query.clone();
   2405         if query.search_query == search_query {
   2406             return Ok(false);
   2407         }
   2408 
   2409         self.replace_products_query(ProductsScreenQueryState::new(
   2410             search_query,
   2411             query.filter,
   2412             query.sort,
   2413         ))
   2414     }
   2415 
   2416     fn select_products_filter(&mut self, filter: ProductsFilter) -> Result<bool, AppSqliteError> {
   2417         let query = self.state_store.products_projection().query.clone();
   2418         if query.filter == filter {
   2419             return Ok(false);
   2420         }
   2421 
   2422         self.replace_products_query(ProductsScreenQueryState::new(
   2423             query.search_query,
   2424             filter,
   2425             query.sort,
   2426         ))
   2427     }
   2428 
   2429     fn select_products_sort(&mut self, sort: ProductsSort) -> Result<bool, AppSqliteError> {
   2430         let query = self.state_store.products_projection().query.clone();
   2431         if query.sort == sort {
   2432             return Ok(false);
   2433         }
   2434 
   2435         self.replace_products_query(ProductsScreenQueryState::new(
   2436             query.search_query,
   2437             query.filter,
   2438             sort,
   2439         ))
   2440     }
   2441 
   2442     fn open_products_filter(&mut self, filter: ProductsFilter) -> Result<bool, AppSqliteError> {
   2443         if !self.state_store.farm_setup_projection().has_saved_farm() {
   2444             return Ok(false);
   2445         }
   2446 
   2447         let filter_changed = self.select_products_filter(filter)?;
   2448         let section_changed = self.select_farmer_section(FarmerSection::Products);
   2449 
   2450         Ok(filter_changed || section_changed)
   2451     }
   2452 
   2453     fn select_orders_filter(&mut self, filter: OrdersFilter) -> Result<bool, AppSqliteError> {
   2454         if !self.has_saved_farm() {
   2455             return Ok(false);
   2456         }
   2457 
   2458         let query = self.state_store.orders_projection().query.clone();
   2459         if query.filter == filter {
   2460             return Ok(false);
   2461         }
   2462 
   2463         self.replace_orders_query(OrdersScreenQueryState {
   2464             filter,
   2465             fulfillment_window_id: query.fulfillment_window_id,
   2466         })
   2467     }
   2468 
   2469     fn open_orders(&mut self) -> Result<bool, AppSqliteError> {
   2470         if !self.has_saved_farm() {
   2471             return Ok(false);
   2472         }
   2473 
   2474         self.open_orders_query(OrdersScreenQueryState::default())
   2475     }
   2476 
   2477     fn open_orders_fulfillment_window(
   2478         &mut self,
   2479         fulfillment_window_id: FulfillmentWindowId,
   2480     ) -> Result<bool, AppSqliteError> {
   2481         if !self.has_saved_farm() {
   2482             return Ok(false);
   2483         }
   2484 
   2485         self.open_orders_query(OrdersScreenQueryState {
   2486             filter: OrdersFilter::All,
   2487             fulfillment_window_id: Some(fulfillment_window_id),
   2488         })
   2489     }
   2490 
   2491     fn open_orders_query(&mut self, query: OrdersScreenQueryState) -> Result<bool, AppSqliteError> {
   2492         let query_changed = self.replace_orders_query(query)?;
   2493         let section_changed = self
   2494             .state_store
   2495             .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Farmer(
   2496                 FarmerSection::Orders,
   2497             )));
   2498         let editor_changed = self.close_product_editor();
   2499 
   2500         Ok(query_changed || section_changed || editor_changed)
   2501     }
   2502 
   2503     fn open_order_detail(&mut self, order_id: OrderId) -> Result<bool, AppSqliteError> {
   2504         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2505             return Ok(false);
   2506         };
   2507         let Some(farm_id) = self.selected_farm_id() else {
   2508             return Ok(false);
   2509         };
   2510         let Some(_) = sqlite_store.load_order_detail(farm_id, order_id)? else {
   2511             return Ok(false);
   2512         };
   2513         let continuity_state = self.continuity_state_with_order_detail(Some(order_id));
   2514         let selected_account_context = load_selected_account_context(
   2515             sqlite_store,
   2516             self.state_store.identity_projection(),
   2517             &continuity_state,
   2518         )?;
   2519         let detail_changed = self.apply_selected_account_context(&selected_account_context);
   2520         let section_changed = self
   2521             .state_store
   2522             .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Farmer(
   2523                 FarmerSection::Orders,
   2524             )));
   2525         let editor_changed = self.close_product_editor();
   2526 
   2527         Ok(detail_changed || section_changed || editor_changed)
   2528     }
   2529 
   2530     fn prepare_seller_order_decision(
   2531         &mut self,
   2532         order_id: OrderId,
   2533         command: AppSellerOrderDecisionCommand,
   2534     ) -> Result<AppOrderDecisionPublishPayload, AppSqliteError> {
   2535         let _ = self.import_shared_local_events()?;
   2536         let relay_urls = normalized_app_sync_relay_urls(&self.nostr_relay_urls).map_err(|_| {
   2537             AppSqliteError::InvalidProjection {
   2538                 reason: "seller order decision requires valid configured relays",
   2539             }
   2540         })?;
   2541         if relay_urls.is_empty() {
   2542             return Err(AppSqliteError::InvalidProjection {
   2543                 reason: "seller order decision requires configured relays",
   2544             });
   2545         }
   2546         self.refresh_configured_relay_state_before_order_lifecycle()?;
   2547         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2548             return Err(AppSqliteError::InvalidProjection {
   2549                 reason: "seller order decision requires local state",
   2550             });
   2551         };
   2552         let Some(farm_id) = self.selected_farm_id() else {
   2553             return Err(AppSqliteError::InvalidProjection {
   2554                 reason: "seller order decision requires a selected farm",
   2555             });
   2556         };
   2557         let Some(selected_account) = self
   2558             .state_store
   2559             .identity_projection()
   2560             .selected_account
   2561             .as_ref()
   2562         else {
   2563             return Err(AppSqliteError::InvalidProjection {
   2564                 reason: "seller order decision requires a selected seller account",
   2565             });
   2566         };
   2567         let account_id = selected_account.account.account_id.clone();
   2568         let seller_pubkey = self.local_events_owner_pubkey(selected_account).ok_or(
   2569             AppSqliteError::InvalidProjection {
   2570                 reason: "seller order decision requires a selected seller public key",
   2571             },
   2572         )?;
   2573         let request = self.resolve_seller_order_request_evidence(order_id)?;
   2574         if request.payload.seller_pubkey.trim() != seller_pubkey.as_str() {
   2575             return Err(AppSqliteError::InvalidProjection {
   2576                 reason: "seller order decision seller account does not match order seller",
   2577             });
   2578         }
   2579         let listing_address = parse_public_listing_address(request.payload.listing_addr.as_str())
   2580             .map_err(|_| AppSqliteError::InvalidProjection {
   2581             reason: "seller order decision listing address is invalid",
   2582         })?;
   2583         if listing_address.seller_pubkey.as_str() != seller_pubkey.as_str() {
   2584             return Err(AppSqliteError::InvalidProjection {
   2585                 reason: "seller order decision listing address is outside seller authority",
   2586             });
   2587         }
   2588         let Some(order_export) =
   2589             sqlite_store.load_seller_order_decision_export(farm_id, order_id)?
   2590         else {
   2591             return Err(AppSqliteError::InvalidProjection {
   2592                 reason: "seller order decision requires a visible seller order",
   2593             });
   2594         };
   2595         if order_export.status != OrderStatus::NeedsAction {
   2596             return Err(AppSqliteError::InvalidProjection {
   2597                 reason: "seller order decision requires an undecided order",
   2598             });
   2599         }
   2600 
   2601         let decision = match command {
   2602             AppSellerOrderDecisionCommand::Accept => AppOrderDecisionPayload::Accepted {
   2603                 inventory_commitments: seller_order_inventory_commitments(&order_export)?,
   2604             },
   2605             AppSellerOrderDecisionCommand::Decline { reason } => {
   2606                 let reason = reason.trim();
   2607                 if reason.is_empty() {
   2608                     return Err(AppSqliteError::InvalidProjection {
   2609                         reason: "seller order decline requires a non-empty reason",
   2610                     });
   2611                 }
   2612                 AppOrderDecisionPayload::Declined {
   2613                     reason: reason.to_owned(),
   2614                 }
   2615             }
   2616         };
   2617         let payload = AppOrderDecisionPublishPayload {
   2618             context: AppPublishContext::new(account_id, "seller_order_decision"),
   2619             app_order_id: order_id,
   2620             farm_id,
   2621             trade_order_id: request.payload.order_id.to_string(),
   2622             request_event_id: request.request_event_id,
   2623             listing_event_id: request.listing_event_id,
   2624             listing_addr: request.payload.listing_addr.to_string(),
   2625             buyer_pubkey: request.payload.buyer_pubkey.to_string(),
   2626             seller_pubkey: request.payload.seller_pubkey.to_string(),
   2627             decision,
   2628         };
   2629         AppPublishPayload::OrderDecision(payload.clone())
   2630             .validate()
   2631             .map_err(|_| AppSqliteError::InvalidProjection {
   2632                 reason: "seller order decision publish payload is invalid",
   2633             })?;
   2634 
   2635         Ok(payload)
   2636     }
   2637 
   2638     fn refresh_configured_relay_state_before_order_lifecycle(
   2639         &mut self,
   2640     ) -> Result<(), AppSqliteError> {
   2641         match self.ingest_configured_relay_events() {
   2642             Ok(report) => {
   2643                 if report.freshness_changed
   2644                     || report.local_import.imported_records > 0
   2645                     || report.local_import.skipped_records > 0
   2646                 {
   2647                     let _ = self.refresh_selected_account_context_after_local_events()?;
   2648                 }
   2649                 Ok(())
   2650             }
   2651             Err(AppDirectRelayIngestError::Sqlite(error)) => Err(error),
   2652             Err(AppDirectRelayIngestError::Transport(_)) => {
   2653                 Err(AppSqliteError::InvalidProjection {
   2654                     reason: "order lifecycle publish requires fresh configured relay state",
   2655                 })
   2656             }
   2657         }
   2658     }
   2659 
   2660     fn publish_seller_order_decision(
   2661         &mut self,
   2662         order_id: OrderId,
   2663         command: AppSellerOrderDecisionCommand,
   2664     ) -> Result<bool, AppSqliteError> {
   2665         let payload = self.prepare_seller_order_decision(order_id, command)?;
   2666         let source_record_id = order_decision_sdk_source_record_id(&payload);
   2667         self.enqueue_order_decision_payload_via_sdk(
   2668             &payload,
   2669             AppSdkMigrationReceiptSourceKind::LocalOutbox,
   2670             source_record_id.as_str(),
   2671         )?;
   2672         let _ = self.refresh_selected_account_sync()?;
   2673         Ok(true)
   2674     }
   2675 
   2676     fn prepare_seller_order_revision_proposal(
   2677         &mut self,
   2678         order_id: OrderId,
   2679         items: Vec<RadrootsOrderItem>,
   2680         economics: RadrootsOrderEconomics,
   2681         reason: &str,
   2682     ) -> Result<AppOrderRevisionProposalPublishPayload, AppSqliteError> {
   2683         let _ = self.import_shared_local_events()?;
   2684         let relay_urls = normalized_app_sync_relay_urls(&self.nostr_relay_urls).map_err(|_| {
   2685             AppSqliteError::InvalidProjection {
   2686                 reason: "seller order revision requires valid configured relays",
   2687             }
   2688         })?;
   2689         if relay_urls.is_empty() {
   2690             return Err(AppSqliteError::InvalidProjection {
   2691                 reason: "seller order revision requires configured relays",
   2692             });
   2693         }
   2694         self.refresh_configured_relay_state_before_order_lifecycle()?;
   2695         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2696             return Err(AppSqliteError::InvalidProjection {
   2697                 reason: "seller order revision requires local state",
   2698             });
   2699         };
   2700         let Some(farm_id) = self.selected_farm_id() else {
   2701             return Err(AppSqliteError::InvalidProjection {
   2702                 reason: "seller order revision requires a selected farm",
   2703             });
   2704         };
   2705         let Some(selected_account) = self
   2706             .state_store
   2707             .identity_projection()
   2708             .selected_account
   2709             .as_ref()
   2710         else {
   2711             return Err(AppSqliteError::InvalidProjection {
   2712                 reason: "seller order revision requires a selected seller account",
   2713             });
   2714         };
   2715         let account_id = selected_account.account.account_id.clone();
   2716         let seller_pubkey = self.local_events_owner_pubkey(selected_account).ok_or(
   2717             AppSqliteError::InvalidProjection {
   2718                 reason: "seller order revision requires a selected seller public key",
   2719             },
   2720         )?;
   2721         let request = self.resolve_seller_order_request_evidence(order_id)?;
   2722         if request.payload.seller_pubkey.trim() != seller_pubkey.as_str() {
   2723             return Err(AppSqliteError::InvalidProjection {
   2724                 reason: "seller order revision seller account does not match order seller",
   2725             });
   2726         }
   2727         let listing_address = parse_public_listing_address(request.payload.listing_addr.as_str())
   2728             .map_err(|_| AppSqliteError::InvalidProjection {
   2729             reason: "seller order revision listing address is invalid",
   2730         })?;
   2731         if listing_address.seller_pubkey.as_str() != seller_pubkey.as_str() {
   2732             return Err(AppSqliteError::InvalidProjection {
   2733                 reason: "seller order revision listing address is outside seller authority",
   2734             });
   2735         }
   2736         let lifecycle = self.resolve_order_lifecycle_evidence(&request)?;
   2737         if lifecycle.decision.is_some() || lifecycle.status != RadrootsOrderStatus::Requested {
   2738             return Err(AppSqliteError::InvalidProjection {
   2739                 reason: "seller order revision requires an undecided order",
   2740             });
   2741         }
   2742         if lifecycle.cancellation_event_id.is_some() {
   2743             return Err(AppSqliteError::InvalidProjection {
   2744                 reason: "seller order revision requires an undecided order",
   2745             });
   2746         }
   2747         let Some(order_detail) = sqlite_store.load_order_detail(farm_id, order_id)? else {
   2748             return Err(AppSqliteError::InvalidProjection {
   2749                 reason: "seller order revision requires a visible seller order",
   2750             });
   2751         };
   2752         if order_detail.status != OrderStatus::NeedsAction {
   2753             return Err(AppSqliteError::InvalidProjection {
   2754                 reason: "seller order revision requires an undecided order",
   2755             });
   2756         }
   2757         if active_order_pending_revision_proposal(&lifecycle).is_some() {
   2758             return Err(AppSqliteError::InvalidProjection {
   2759                 reason: "seller order revision requires no pending revision proposal",
   2760             });
   2761         }
   2762         let reason = reason.trim();
   2763         if reason.is_empty() {
   2764             return Err(AppSqliteError::InvalidProjection {
   2765                 reason: "seller order revision requires a non-empty reason",
   2766             });
   2767         }
   2768         let payload = AppOrderRevisionProposalPublishPayload {
   2769             context: AppPublishContext::new(account_id, "seller_order_revision_proposal"),
   2770             app_order_id: order_id,
   2771             farm_id,
   2772             trade_order_id: request.payload.order_id.to_string(),
   2773             request_event_id: request.request_event_id,
   2774             prev_event_id: lifecycle.request_event_id,
   2775             revision_id: format!("app-revision-{}", d_tag_from_uuid(Uuid::now_v7())),
   2776             listing_addr: request.payload.listing_addr.to_string(),
   2777             buyer_pubkey: request.payload.buyer_pubkey.to_string(),
   2778             seller_pubkey: request.payload.seller_pubkey.to_string(),
   2779             items,
   2780             economics,
   2781             reason: reason.to_owned(),
   2782         };
   2783         AppPublishPayload::OrderRevisionProposal(payload.clone())
   2784             .validate()
   2785             .map_err(|_| AppSqliteError::InvalidProjection {
   2786                 reason: "seller order revision publish payload is invalid",
   2787             })?;
   2788         Ok(payload)
   2789     }
   2790 
   2791     fn publish_seller_order_revision_proposal(
   2792         &mut self,
   2793         order_id: OrderId,
   2794         items: Vec<RadrootsOrderItem>,
   2795         economics: RadrootsOrderEconomics,
   2796         reason: &str,
   2797     ) -> Result<bool, AppSqliteError> {
   2798         let payload =
   2799             self.prepare_seller_order_revision_proposal(order_id, items, economics, reason)?;
   2800         let source_record_id = order_revision_proposal_sdk_source_record_id(&payload);
   2801         self.enqueue_order_revision_proposal_payload_via_sdk(
   2802             &payload,
   2803             AppSdkMigrationReceiptSourceKind::LocalOutbox,
   2804             source_record_id.as_str(),
   2805         )?;
   2806         let _ = self.refresh_selected_account_sync()?;
   2807         Ok(true)
   2808     }
   2809 
   2810     fn prepare_buyer_order_revision_decision(
   2811         &mut self,
   2812         order_id: OrderId,
   2813         decision: RadrootsOrderRevisionOutcome,
   2814     ) -> Result<AppOrderRevisionDecisionPublishPayload, AppSqliteError> {
   2815         let _ = self.import_shared_local_events()?;
   2816         let relay_urls = normalized_app_sync_relay_urls(&self.nostr_relay_urls).map_err(|_| {
   2817             AppSqliteError::InvalidProjection {
   2818                 reason: "buyer order revision requires valid configured relays",
   2819             }
   2820         })?;
   2821         if relay_urls.is_empty() {
   2822             return Err(AppSqliteError::InvalidProjection {
   2823                 reason: "buyer order revision requires configured relays",
   2824             });
   2825         }
   2826         self.refresh_configured_relay_state_before_order_lifecycle()?;
   2827         let buyer_context = self.state_store.identity_projection().buyer_context();
   2828         let BuyerContext::Account(account_id) = &buyer_context else {
   2829             return Err(AppSqliteError::InvalidProjection {
   2830                 reason: "buyer order revision requires a selected buyer account",
   2831             });
   2832         };
   2833         let Some(selected_account) = self.selected_buyer_account(&buyer_context) else {
   2834             return Err(AppSqliteError::InvalidProjection {
   2835                 reason: "buyer order revision requires a selected buyer account",
   2836             });
   2837         };
   2838         let buyer_pubkey = self.local_events_owner_pubkey(selected_account).ok_or(
   2839             AppSqliteError::InvalidProjection {
   2840                 reason: "buyer order revision requires a selected buyer public key",
   2841             },
   2842         )?;
   2843         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2844             return Err(AppSqliteError::InvalidProjection {
   2845                 reason: "buyer order revision requires local state",
   2846             });
   2847         };
   2848         let buyer_order_scope = selected_buyer_order_scope(self.state_store.identity_projection());
   2849         let Some(detail) =
   2850             sqlite_store.load_buyer_order_detail_for_scope(&buyer_order_scope, order_id)?
   2851         else {
   2852             return Err(AppSqliteError::InvalidProjection {
   2853                 reason: "buyer order revision requires a visible buyer order",
   2854             });
   2855         };
   2856         if matches!(
   2857             detail.status,
   2858             BuyerOrderStatus::Ready | BuyerOrderStatus::Completed | BuyerOrderStatus::Declined
   2859         ) {
   2860             return Err(AppSqliteError::InvalidProjection {
   2861                 reason: "buyer order revision requires an active negotiated order",
   2862             });
   2863         }
   2864         let request = self.resolve_seller_order_request_evidence(order_id)?;
   2865         if request.payload.buyer_pubkey.trim() != buyer_pubkey.as_str() {
   2866             return Err(AppSqliteError::InvalidProjection {
   2867                 reason: "buyer order revision buyer account does not match order buyer",
   2868             });
   2869         }
   2870         let lifecycle = self.resolve_order_lifecycle_evidence(&request)?;
   2871         if lifecycle.decision.is_some() || lifecycle.status != RadrootsOrderStatus::Requested {
   2872             return Err(AppSqliteError::InvalidProjection {
   2873                 reason: "buyer order revision requires active pre-agreement negotiation",
   2874             });
   2875         }
   2876         if lifecycle.cancellation_event_id.is_some() {
   2877             return Err(AppSqliteError::InvalidProjection {
   2878                 reason: "buyer order revision requires active pre-agreement negotiation",
   2879             });
   2880         }
   2881         let Some(proposal) = active_order_pending_revision_proposal(&lifecycle) else {
   2882             return Err(AppSqliteError::InvalidProjection {
   2883                 reason: "buyer order revision requires a pending seller proposal",
   2884             });
   2885         };
   2886         let payload = AppOrderRevisionDecisionPublishPayload {
   2887             context: AppPublishContext::new(account_id.clone(), "buyer_order_revision_decision"),
   2888             app_order_id: order_id,
   2889             farm_id: detail.farm_id,
   2890             trade_order_id: request.payload.order_id.to_string(),
   2891             request_event_id: request.request_event_id,
   2892             prev_event_id: proposal.event_id.clone(),
   2893             revision_id: proposal.payload.revision_id.to_string(),
   2894             listing_addr: request.payload.listing_addr.to_string(),
   2895             buyer_pubkey: request.payload.buyer_pubkey.to_string(),
   2896             seller_pubkey: request.payload.seller_pubkey.to_string(),
   2897             decision,
   2898         };
   2899         AppPublishPayload::OrderRevisionDecision(payload.clone())
   2900             .validate()
   2901             .map_err(|_| AppSqliteError::InvalidProjection {
   2902                 reason: "buyer order revision publish payload is invalid",
   2903             })?;
   2904         Ok(payload)
   2905     }
   2906 
   2907     fn publish_buyer_order_revision_accept(
   2908         &mut self,
   2909         order_id: OrderId,
   2910     ) -> Result<bool, AppSqliteError> {
   2911         self.publish_buyer_order_revision_decision(order_id, RadrootsOrderRevisionOutcome::Accepted)
   2912     }
   2913 
   2914     fn publish_buyer_order_revision_decline(
   2915         &mut self,
   2916         order_id: OrderId,
   2917     ) -> Result<bool, AppSqliteError> {
   2918         self.publish_buyer_order_revision_decision(
   2919             order_id,
   2920             RadrootsOrderRevisionOutcome::Declined {
   2921                 reason: "buyer kept order as placed".to_owned(),
   2922             },
   2923         )
   2924     }
   2925 
   2926     fn publish_buyer_order_revision_decision(
   2927         &mut self,
   2928         order_id: OrderId,
   2929         decision: RadrootsOrderRevisionOutcome,
   2930     ) -> Result<bool, AppSqliteError> {
   2931         let payload = self.prepare_buyer_order_revision_decision(order_id, decision)?;
   2932         let source_record_id = order_revision_decision_sdk_source_record_id(&payload);
   2933         self.enqueue_order_revision_decision_payload_via_sdk(
   2934             &payload,
   2935             AppSdkMigrationReceiptSourceKind::LocalOutbox,
   2936             source_record_id.as_str(),
   2937         )?;
   2938         let _ = self.refresh_selected_account_sync()?;
   2939         Ok(true)
   2940     }
   2941 
   2942     fn prepare_buyer_order_cancellation(
   2943         &mut self,
   2944         order_id: OrderId,
   2945     ) -> Result<AppOrderCancellationPublishPayload, AppSqliteError> {
   2946         let _ = self.import_shared_local_events()?;
   2947         let relay_urls = normalized_app_sync_relay_urls(&self.nostr_relay_urls).map_err(|_| {
   2948             AppSqliteError::InvalidProjection {
   2949                 reason: "buyer order cancellation requires valid configured relays",
   2950             }
   2951         })?;
   2952         if relay_urls.is_empty() {
   2953             return Err(AppSqliteError::InvalidProjection {
   2954                 reason: "buyer order cancellation requires configured relays",
   2955             });
   2956         }
   2957         self.refresh_configured_relay_state_before_order_lifecycle()?;
   2958         let buyer_context = self.state_store.identity_projection().buyer_context();
   2959         let BuyerContext::Account(account_id) = &buyer_context else {
   2960             return Err(AppSqliteError::InvalidProjection {
   2961                 reason: "buyer order cancellation requires a selected buyer account",
   2962             });
   2963         };
   2964         let Some(selected_account) = self.selected_buyer_account(&buyer_context) else {
   2965             return Err(AppSqliteError::InvalidProjection {
   2966                 reason: "buyer order cancellation requires a selected buyer account",
   2967             });
   2968         };
   2969         let buyer_pubkey = self.local_events_owner_pubkey(selected_account).ok_or(
   2970             AppSqliteError::InvalidProjection {
   2971                 reason: "buyer order cancellation requires a selected buyer public key",
   2972             },
   2973         )?;
   2974         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   2975             return Err(AppSqliteError::InvalidProjection {
   2976                 reason: "buyer order cancellation requires local state",
   2977             });
   2978         };
   2979         let buyer_order_scope = selected_buyer_order_scope(self.state_store.identity_projection());
   2980         let Some(detail) =
   2981             sqlite_store.load_buyer_order_detail_for_scope(&buyer_order_scope, order_id)?
   2982         else {
   2983             return Err(AppSqliteError::InvalidProjection {
   2984                 reason: "buyer order cancellation requires a visible buyer order",
   2985             });
   2986         };
   2987         if !matches!(detail.status, BuyerOrderStatus::Placed) {
   2988             return Err(AppSqliteError::InvalidProjection {
   2989                 reason: "buyer order cancellation requires an open pre-agreement order",
   2990             });
   2991         }
   2992         let request = self.resolve_seller_order_request_evidence(order_id)?;
   2993         if request.payload.buyer_pubkey.trim() != buyer_pubkey.as_str() {
   2994             return Err(AppSqliteError::InvalidProjection {
   2995                 reason: "buyer order cancellation buyer account does not match order buyer",
   2996             });
   2997         }
   2998         let lifecycle = self.resolve_order_lifecycle_evidence(&request)?;
   2999         if lifecycle.cancellation_event_id.is_some() {
   3000             return Err(AppSqliteError::InvalidProjection {
   3001                 reason: "buyer order cancellation requires an open pre-agreement order",
   3002             });
   3003         }
   3004         let prev_event_id = match lifecycle.status {
   3005             RadrootsOrderStatus::Requested => {
   3006                 if active_order_pending_revision_proposal(&lifecycle).is_some() {
   3007                     return Err(AppSqliteError::InvalidProjection {
   3008                         reason: "buyer order cancellation requires no pending seller proposal",
   3009                     });
   3010                 }
   3011                 request.request_event_id.clone()
   3012             }
   3013             RadrootsOrderStatus::Accepted => {
   3014                 return Err(AppSqliteError::InvalidProjection {
   3015                     reason: "buyer order cancellation requires an open pre-agreement order",
   3016                 });
   3017             }
   3018             RadrootsOrderStatus::Missing
   3019             | RadrootsOrderStatus::Declined
   3020             | RadrootsOrderStatus::Cancelled
   3021             | RadrootsOrderStatus::Invalid => {
   3022                 return Err(AppSqliteError::InvalidProjection {
   3023                     reason: "buyer order cancellation requires an open pre-agreement order",
   3024                 });
   3025             }
   3026         };
   3027         let payload = AppOrderCancellationPublishPayload {
   3028             context: AppPublishContext::new(account_id.clone(), "buyer_order_cancellation"),
   3029             app_order_id: order_id,
   3030             farm_id: detail.farm_id,
   3031             trade_order_id: request.payload.order_id.to_string(),
   3032             request_event_id: request.request_event_id,
   3033             prev_event_id,
   3034             listing_addr: request.payload.listing_addr.to_string(),
   3035             buyer_pubkey: request.payload.buyer_pubkey.to_string(),
   3036             seller_pubkey: request.payload.seller_pubkey.to_string(),
   3037             reason: "buyer cancelled order".to_owned(),
   3038         };
   3039         AppPublishPayload::OrderCancellation(payload.clone())
   3040             .validate()
   3041             .map_err(|_| AppSqliteError::InvalidProjection {
   3042                 reason: "buyer order cancellation publish payload is invalid",
   3043             })?;
   3044         Ok(payload)
   3045     }
   3046 
   3047     fn publish_buyer_order_cancellation(
   3048         &mut self,
   3049         order_id: OrderId,
   3050     ) -> Result<bool, AppSqliteError> {
   3051         let payload = self.prepare_buyer_order_cancellation(order_id)?;
   3052         let source_record_id = order_cancellation_sdk_source_record_id(&payload);
   3053         self.enqueue_order_cancellation_payload_via_sdk(
   3054             &payload,
   3055             AppSdkMigrationReceiptSourceKind::LocalOutbox,
   3056             source_record_id.as_str(),
   3057         )?;
   3058         let _ = self.refresh_selected_account_sync()?;
   3059         Ok(true)
   3060     }
   3061 
   3062     fn open_pack_day(
   3063         &mut self,
   3064         fulfillment_window_id: Option<FulfillmentWindowId>,
   3065     ) -> Result<bool, AppSqliteError> {
   3066         if !self.has_saved_farm() {
   3067             return Ok(false);
   3068         }
   3069 
   3070         let query = PackDayScreenQueryState {
   3071             fulfillment_window_id,
   3072         };
   3073         let query_changed = self.replace_pack_day_query(query)?;
   3074         if !self.has_pack_day_context() {
   3075             return Ok(false);
   3076         }
   3077         let section_changed = self
   3078             .state_store
   3079             .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Farmer(
   3080                 FarmerSection::PackDay,
   3081             )));
   3082         let editor_changed = self.close_product_editor();
   3083 
   3084         Ok(query_changed || section_changed || editor_changed)
   3085     }
   3086 
   3087     fn export_pack_day(&mut self) -> Result<bool, DesktopAppRuntimeCommandError> {
   3088         let Some(farm_id) = self.selected_farm_id() else {
   3089             return Ok(false);
   3090         };
   3091         let previous_export_instance_id = self.current_pack_day_export_instance_id();
   3092         let Some(fulfillment_window_id) = self
   3093             .state_store
   3094             .pack_day_projection()
   3095             .projection
   3096             .fulfillment_window
   3097             .as_ref()
   3098             .map(|window| window.fulfillment_window_id)
   3099         else {
   3100             return Ok(false);
   3101         };
   3102         let Some(data_root) = self.runtime_metadata.data_root.clone() else {
   3103             return Err(self.command_unavailable_error());
   3104         };
   3105 
   3106         let source = {
   3107             let sqlite_store = self.sqlite_store()?;
   3108             sqlite_store.load_pack_day_output_source(farm_id, fulfillment_window_id)?
   3109         };
   3110         let Some(source) = source else {
   3111             return Ok(false);
   3112         };
   3113         if source.is_empty() {
   3114             return Ok(false);
   3115         }
   3116 
   3117         let request = PackDayExportRequest::for_fulfillment_window(
   3118             source.fulfillment_window.fulfillment_window_id,
   3119         );
   3120         let _ = self
   3121             .state_store
   3122             .apply_in_memory(AppStateCommand::begin_pack_day_export(request.clone()));
   3123         self.cleanup_prepared_pack_day_print_assets_if_export_changed(
   3124             previous_export_instance_id,
   3125             "export_reset",
   3126         );
   3127         let prepared =
   3128             prepare_pack_day_export_bundle_at_data_root(data_root.as_path(), &source, Utc::now());
   3129 
   3130         match write_prepared_pack_day_export_bundle(&prepared) {
   3131             Ok(()) => {
   3132                 let _ = self
   3133                     .state_store
   3134                     .apply_in_memory(AppStateCommand::succeed_pack_day_export(
   3135                         request,
   3136                         prepared.bundle,
   3137                     ));
   3138                 Ok(true)
   3139             }
   3140             Err(error) => {
   3141                 let _ = self
   3142                     .state_store
   3143                     .apply_in_memory(AppStateCommand::fail_pack_day_export(
   3144                         request,
   3145                         error.to_string(),
   3146                     ));
   3147                 Err(error.into())
   3148             }
   3149         }
   3150     }
   3151 
   3152     fn prepare_pack_day_host_handoff(
   3153         &mut self,
   3154         kind: PackDayHostHandoffKind,
   3155     ) -> Result<
   3156         Option<(PackDayHostHandoffRequest, PackDayHostHandoffCommandPlan)>,
   3157         DesktopAppRuntimeCommandError,
   3158     > {
   3159         if self.state_store.pack_day_projection().host_handoff.status
   3160             == PackDayHostHandoffStatus::Running
   3161             || self.state_store.pack_day_projection().print.status == PackDayPrintStatus::Running
   3162             || self.state_store.pack_day_projection().batch_print.status
   3163                 == PackDayBatchPrintStatus::Running
   3164         {
   3165             return Ok(None);
   3166         }
   3167 
   3168         let Some(bundle) = self.current_pack_day_export_bundle() else {
   3169             return Ok(None);
   3170         };
   3171         let request = PackDayHostHandoffRequest::for_bundle(kind, &bundle);
   3172         let _ = self
   3173             .state_store
   3174             .apply_in_memory(AppStateCommand::begin_pack_day_host_handoff(
   3175                 request.clone(),
   3176             ));
   3177 
   3178         match plan_pack_day_host_handoff(&bundle, kind) {
   3179             Ok(plan) => Ok(Some((request, plan))),
   3180             Err(error) => {
   3181                 let _ =
   3182                     self.state_store
   3183                         .apply_in_memory(AppStateCommand::fail_pack_day_host_handoff(
   3184                             request,
   3185                             error.to_string(),
   3186                         ));
   3187                 Err(error.into())
   3188             }
   3189         }
   3190     }
   3191 
   3192     fn prepare_pack_day_print(
   3193         &mut self,
   3194         kind: PackDayPrintKind,
   3195     ) -> Result<Option<(PackDayPrintRequest, PackDayPrintCommandPlan)>, DesktopAppRuntimeCommandError>
   3196     {
   3197         if self.state_store.pack_day_projection().print.status == PackDayPrintStatus::Running
   3198             || self.state_store.pack_day_projection().host_handoff.status
   3199                 == PackDayHostHandoffStatus::Running
   3200             || self.state_store.pack_day_projection().batch_print.status
   3201                 == PackDayBatchPrintStatus::Running
   3202         {
   3203             return Ok(None);
   3204         }
   3205 
   3206         let Some(bundle) = self.current_pack_day_export_bundle() else {
   3207             return Ok(None);
   3208         };
   3209         let request = PackDayPrintRequest::for_bundle(kind, &bundle);
   3210         let _ = self
   3211             .state_store
   3212             .apply_in_memory(AppStateCommand::begin_pack_day_print(request.clone()));
   3213 
   3214         match plan_pack_day_print(&bundle, kind) {
   3215             Ok(plan) => Ok(Some((request, plan))),
   3216             Err(error) => {
   3217                 let failure_command = match error.failure_kind() {
   3218                     Some(failure) => {
   3219                         AppStateCommand::fail_pack_day_print_with_kind(request, failure)
   3220                     }
   3221                     None => AppStateCommand::fail_pack_day_print(request),
   3222                 };
   3223                 let _ = self.state_store.apply_in_memory(failure_command);
   3224                 Err(error.into())
   3225             }
   3226         }
   3227     }
   3228 
   3229     fn prepare_pack_day_batch_print(
   3230         &mut self,
   3231     ) -> Result<
   3232         Option<(PackDayBatchPrintRequest, PackDayBatchPrintCommandPlan)>,
   3233         DesktopAppRuntimeCommandError,
   3234     > {
   3235         if self.state_store.pack_day_projection().batch_print.status
   3236             == PackDayBatchPrintStatus::Running
   3237             || self.state_store.pack_day_projection().print.status == PackDayPrintStatus::Running
   3238             || self.state_store.pack_day_projection().host_handoff.status
   3239                 == PackDayHostHandoffStatus::Running
   3240         {
   3241             return Ok(None);
   3242         }
   3243 
   3244         let Some(bundle) = self.current_pack_day_export_bundle() else {
   3245             return Ok(None);
   3246         };
   3247         let request = PackDayBatchPrintRequest::for_bundle(&bundle);
   3248         let _ = self
   3249             .state_store
   3250             .apply_in_memory(AppStateCommand::begin_pack_day_batch_print(request.clone()));
   3251 
   3252         match plan_pack_day_batch_print(&bundle, &request) {
   3253             Ok(plan) => Ok(Some((request, plan))),
   3254             Err(error) => {
   3255                 let _ =
   3256                     self.state_store
   3257                         .apply_in_memory(AppStateCommand::fail_pack_day_batch_print(
   3258                             request,
   3259                             error.failed_artifact(),
   3260                             error.failure_kind(),
   3261                         ));
   3262                 Err(error.into())
   3263             }
   3264         }
   3265     }
   3266 
   3267     fn finish_pack_day_batch_print(
   3268         &mut self,
   3269         request: PackDayBatchPrintRequest,
   3270         result: Result<(), PackDayBatchPrintError>,
   3271     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   3272         if !self.current_pack_day_batch_print_request_matches(&request) {
   3273             return Ok(false);
   3274         }
   3275 
   3276         let cleanup_export_instance_id = request.export_instance_id;
   3277 
   3278         match result {
   3279             Ok(()) => {
   3280                 let changed = self
   3281                     .state_store
   3282                     .apply_in_memory(AppStateCommand::succeed_pack_day_batch_print(request));
   3283                 self.cleanup_prepared_pack_day_print_assets_for_export_instance(
   3284                     cleanup_export_instance_id,
   3285                     "batch_print_completion",
   3286                 );
   3287                 Ok(changed)
   3288             }
   3289             Err(error) => {
   3290                 let _ =
   3291                     self.state_store
   3292                         .apply_in_memory(AppStateCommand::fail_pack_day_batch_print(
   3293                             request,
   3294                             error.failed_artifact(),
   3295                             error.failure_kind(),
   3296                         ));
   3297                 self.cleanup_prepared_pack_day_print_assets_for_export_instance(
   3298                     cleanup_export_instance_id,
   3299                     "batch_print_completion",
   3300                 );
   3301                 Err(error.into())
   3302             }
   3303         }
   3304     }
   3305 
   3306     fn finish_pack_day_print(
   3307         &mut self,
   3308         request: PackDayPrintRequest,
   3309         result: Result<(), PackDayPrintError>,
   3310     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   3311         if !self.current_pack_day_print_request_matches(&request) {
   3312             return Ok(false);
   3313         }
   3314 
   3315         let cleanup_export_instance_id = (request.kind == PackDayPrintKind::PrintCustomerLabels)
   3316             .then_some(request.export_instance_id);
   3317 
   3318         match result {
   3319             Ok(()) => {
   3320                 let changed = self
   3321                     .state_store
   3322                     .apply_in_memory(AppStateCommand::succeed_pack_day_print(request));
   3323                 if let Some(export_instance_id) = cleanup_export_instance_id {
   3324                     self.cleanup_prepared_pack_day_print_assets_for_export_instance(
   3325                         export_instance_id,
   3326                         "print_completion",
   3327                     );
   3328                 }
   3329                 Ok(changed)
   3330             }
   3331             Err(error) => {
   3332                 let failure_command = match error.failure_kind() {
   3333                     Some(failure) => {
   3334                         AppStateCommand::fail_pack_day_print_with_kind(request, failure)
   3335                     }
   3336                     None => AppStateCommand::fail_pack_day_print(request),
   3337                 };
   3338                 let _ = self.state_store.apply_in_memory(failure_command);
   3339                 if let Some(export_instance_id) = cleanup_export_instance_id {
   3340                     self.cleanup_prepared_pack_day_print_assets_for_export_instance(
   3341                         export_instance_id,
   3342                         "print_completion",
   3343                     );
   3344                 }
   3345                 Err(error.into())
   3346             }
   3347         }
   3348     }
   3349 
   3350     fn finish_pack_day_host_handoff(
   3351         &mut self,
   3352         request: PackDayHostHandoffRequest,
   3353         result: Result<(), PackDayHostHandoffError>,
   3354     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   3355         if !self.current_pack_day_host_handoff_request_matches(&request) {
   3356             return Ok(false);
   3357         }
   3358 
   3359         match result {
   3360             Ok(()) => Ok(self
   3361                 .state_store
   3362                 .apply_in_memory(AppStateCommand::succeed_pack_day_host_handoff(request))),
   3363             Err(error) => {
   3364                 let _ =
   3365                     self.state_store
   3366                         .apply_in_memory(AppStateCommand::fail_pack_day_host_handoff(
   3367                             request,
   3368                             error.to_string(),
   3369                         ));
   3370                 Err(error.into())
   3371             }
   3372         }
   3373     }
   3374 
   3375     fn update_product_stock(
   3376         &mut self,
   3377         product_id: ProductId,
   3378         stock_quantity: u32,
   3379     ) -> Result<bool, DesktopAppRuntimeProductStockUpdateError> {
   3380         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3381             return Ok(false);
   3382         };
   3383         let Some(_) = self.selected_farm_id() else {
   3384             return Ok(false);
   3385         };
   3386         if self
   3387             .state_store
   3388             .identity_projection()
   3389             .selected_account
   3390             .is_none()
   3391         {
   3392             return Ok(false);
   3393         }
   3394 
   3395         let updated = sqlite_store.update_product_stock(product_id, stock_quantity)?;
   3396 
   3397         let continuity_state =
   3398             self.continuity_state_with_order_detail(self.selected_order_detail_id());
   3399         let selected_account_context = load_selected_account_context(
   3400             sqlite_store,
   3401             self.state_store.identity_projection(),
   3402             &continuity_state,
   3403         )?;
   3404         let context_changed = self.apply_selected_account_context(&selected_account_context);
   3405         let publish_changed = self.enqueue_selected_account_product_publish_operation(
   3406             product_id,
   3407             "update_product_stock",
   3408             None,
   3409         )?;
   3410 
   3411         Ok(updated || context_changed || publish_changed)
   3412     }
   3413 
   3414     fn open_new_product_editor(&mut self) -> Result<bool, AppSqliteError> {
   3415         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3416             return Ok(false);
   3417         };
   3418         let Some(farm_id) = self.selected_farm_id() else {
   3419             return Ok(false);
   3420         };
   3421 
   3422         let product_id = sqlite_store.create_product_draft(farm_id)?;
   3423         let Some(draft) = sqlite_store.load_product_editor_draft(product_id)? else {
   3424             return Ok(false);
   3425         };
   3426         let continuity_state = self.continuity_state();
   3427         let selected_account_context = load_selected_account_context(
   3428             sqlite_store,
   3429             self.state_store.identity_projection(),
   3430             &continuity_state,
   3431         )?;
   3432         let context_changed = self.apply_selected_account_context(&selected_account_context);
   3433         let section_changed = self.select_farmer_section(FarmerSection::Products);
   3434         let editor_changed =
   3435             self.state_store
   3436                 .apply_in_memory(AppStateCommand::open_existing_product_editor(
   3437                     product_id, draft,
   3438                 ));
   3439 
   3440         Ok(context_changed || section_changed || editor_changed)
   3441     }
   3442 
   3443     fn open_existing_product_editor(
   3444         &mut self,
   3445         product_id: ProductId,
   3446     ) -> Result<bool, AppSqliteError> {
   3447         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3448             return Ok(false);
   3449         };
   3450         let Some(draft) = sqlite_store.load_product_editor_draft(product_id)? else {
   3451             return Ok(false);
   3452         };
   3453         let section_changed = self.select_farmer_section(FarmerSection::Products);
   3454         let editor_changed =
   3455             self.state_store
   3456                 .apply_in_memory(AppStateCommand::open_existing_product_editor(
   3457                     product_id, draft,
   3458                 ));
   3459 
   3460         Ok(section_changed || editor_changed)
   3461     }
   3462 
   3463     fn save_product_editor_draft(
   3464         &mut self,
   3465         draft: ProductEditorDraft,
   3466     ) -> Result<bool, DesktopAppRuntimeProductEditorSaveError> {
   3467         let Some(product_id) = self.selected_product_editor_id() else {
   3468             return Ok(false);
   3469         };
   3470 
   3471         let saved = {
   3472             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3473                 return Ok(false);
   3474             };
   3475             sqlite_store.save_product_editor_draft(product_id, &draft)?
   3476         };
   3477         if !saved {
   3478             return Ok(false);
   3479         }
   3480 
   3481         let selected_account_context = {
   3482             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3483                 return Ok(false);
   3484             };
   3485             let continuity_state = self.continuity_state();
   3486             load_selected_account_context(
   3487                 sqlite_store,
   3488                 self.state_store.identity_projection(),
   3489                 &continuity_state,
   3490             )?
   3491         };
   3492         let reloaded_draft = {
   3493             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3494                 return Ok(false);
   3495             };
   3496             sqlite_store
   3497                 .load_product_editor_draft(product_id)?
   3498                 .unwrap_or(draft)
   3499         };
   3500         let context_changed = self.apply_selected_account_context(&selected_account_context);
   3501         let draft_payload = reloaded_draft.clone();
   3502         let editor_changed =
   3503             self.state_store
   3504                 .apply_in_memory(AppStateCommand::replace_product_editor_draft(
   3505                     reloaded_draft,
   3506                 ));
   3507         let source_local_event_id =
   3508             self.append_app_listing_local_work_record(product_id, &draft_payload)?;
   3509         let pending_changed = self.enqueue_selected_account_product_publish_operation(
   3510             product_id,
   3511             "save_product_editor_draft",
   3512             source_local_event_id.as_deref(),
   3513         )?;
   3514 
   3515         Ok(saved
   3516             || context_changed
   3517             || editor_changed
   3518             || source_local_event_id.is_some()
   3519             || pending_changed)
   3520     }
   3521 
   3522     fn close_product_editor(&mut self) -> bool {
   3523         self.state_store
   3524             .apply_in_memory(AppStateCommand::close_product_editor())
   3525     }
   3526 
   3527     fn save_farm_setup_draft(
   3528         &mut self,
   3529         draft: FarmSetupDraft,
   3530     ) -> Result<FarmSetupProjection, DesktopAppRuntimeFarmSetupError> {
   3531         let account_id = self.selected_account_id()?;
   3532         let sqlite_store = self.sqlite_store_for_farm_setup()?;
   3533         let projection = FarmSetupProjection::from_draft(draft);
   3534         sqlite_store.save_farm_setup(account_id.as_str(), &projection)?;
   3535 
   3536         let selected_account_context = self.refresh_selected_account_context()?;
   3537         self.apply_selected_account_context(&selected_account_context);
   3538 
   3539         Ok(selected_account_context.farm_setup_projection)
   3540     }
   3541 
   3542     fn finish_farm_setup(
   3543         &mut self,
   3544     ) -> Result<FarmSetupProjection, DesktopAppRuntimeFarmSetupError> {
   3545         let account = self.selected_account_for_farm_setup()?.clone();
   3546         let sqlite_store = self.sqlite_store_for_farm_setup()?;
   3547         let draft = self.state_store.farm_setup_projection().draft.clone();
   3548 
   3549         if !draft.blockers().is_empty() {
   3550             return Err(DesktopAppRuntimeFarmSetupError::IncompleteDraft);
   3551         }
   3552 
   3553         let saved_farm = FarmSummary {
   3554             farm_id: account
   3555                 .farmer_activation
   3556                 .farm_id
   3557                 .unwrap_or_else(FarmId::new),
   3558             display_name: draft.farm_name.trim().to_owned(),
   3559             readiness: FarmReadiness::Incomplete,
   3560         };
   3561         let projection = FarmSetupProjection::new(draft, Some(saved_farm.clone()));
   3562 
   3563         sqlite_store.save_farm_summary(&saved_farm)?;
   3564         sqlite_store.save_farm_setup(account.account.account_id.as_str(), &projection)?;
   3565         let source_local_event_id =
   3566             self.append_app_farm_local_work_record(&account, &projection, &saved_farm)?;
   3567 
   3568         let selected_account_context = self.refresh_selected_account_context()?;
   3569         self.apply_selected_account_context(&selected_account_context);
   3570         let _ = self.enqueue_selected_account_farm_publish_operation(
   3571             saved_farm.farm_id,
   3572             saved_farm.display_name.as_str(),
   3573             Some(saved_farm.readiness),
   3574             "finish_farm_setup",
   3575             source_local_event_id.as_deref(),
   3576         )?;
   3577 
   3578         Ok(selected_account_context.farm_setup_projection)
   3579     }
   3580 
   3581     fn load_farm_rules_projection(
   3582         &self,
   3583     ) -> Result<FarmRulesProjection, DesktopAppRuntimeFarmRulesError> {
   3584         let farm_id = self
   3585             .selected_farm_id()
   3586             .ok_or(DesktopAppRuntimeFarmRulesError::FarmRequired)?;
   3587         let fallback_profile = self.fallback_farm_profile(farm_id);
   3588         let projection = self
   3589             .sqlite_store_for_farm_rules()?
   3590             .load_farm_rules(farm_id)
   3591             .map(|projection| {
   3592                 prepare_loaded_farm_rules_projection(projection, &fallback_profile)
   3593             })?;
   3594 
   3595         Ok(projection)
   3596     }
   3597 
   3598     fn save_farm_rules_projection(
   3599         &mut self,
   3600         projection: FarmRulesProjection,
   3601     ) -> Result<FarmRulesProjection, DesktopAppRuntimeFarmRulesError> {
   3602         let account_id = self.selected_account_id_for_farm_rules()?;
   3603         let farm_id = self
   3604             .selected_farm_id()
   3605             .ok_or(DesktopAppRuntimeFarmRulesError::FarmRequired)?;
   3606         let fallback_profile = self.fallback_farm_profile(farm_id);
   3607         let normalized = normalize_farm_rules_projection(projection, &fallback_profile);
   3608         let saved_projection = {
   3609             let sqlite_store = self.sqlite_store_for_farm_rules()?;
   3610             sqlite_store.save_farm_rules(&normalized)?;
   3611 
   3612             let mut refreshed = sqlite_store.load_farm_rules(farm_id)?;
   3613             refreshed = prepare_loaded_farm_rules_projection(refreshed, &fallback_profile);
   3614 
   3615             let saved_farm = FarmSummary {
   3616                 farm_id,
   3617                 display_name: refreshed
   3618                     .farm_profile
   3619                     .as_ref()
   3620                     .map(|profile| profile.display_name.clone())
   3621                     .unwrap_or_default(),
   3622                 readiness: if refreshed.is_ready() {
   3623                     FarmReadiness::Ready
   3624                 } else {
   3625                     FarmReadiness::Incomplete
   3626                 },
   3627             };
   3628             let mut farm_setup_projection = self.state_store.farm_setup_projection().clone();
   3629             farm_setup_projection.draft.farm_name = saved_farm.display_name.clone();
   3630             farm_setup_projection.saved_farm = Some(saved_farm.clone());
   3631 
   3632             sqlite_store.save_farm_summary(&saved_farm)?;
   3633             sqlite_store.save_farm_setup(account_id.as_str(), &farm_setup_projection)?;
   3634 
   3635             refreshed
   3636         };
   3637 
   3638         let selected_account_context = {
   3639             let sqlite_store = self.sqlite_store_for_farm_rules()?;
   3640             let continuity_state = self.continuity_state();
   3641             load_selected_account_context(
   3642                 sqlite_store,
   3643                 self.state_store.identity_projection(),
   3644                 &continuity_state,
   3645             )?
   3646         };
   3647         self.apply_selected_account_context(&selected_account_context);
   3648         let display_name = saved_projection
   3649             .farm_profile
   3650             .as_ref()
   3651             .map(|profile| profile.display_name.as_str())
   3652             .unwrap_or_default();
   3653         let readiness = if saved_projection.is_ready() {
   3654             FarmReadiness::Ready
   3655         } else {
   3656             FarmReadiness::Incomplete
   3657         };
   3658         let _ = self.enqueue_selected_account_farm_publish_operation(
   3659             farm_id,
   3660             display_name,
   3661             Some(readiness),
   3662             "save_farm_rules_projection",
   3663             None,
   3664         )?;
   3665 
   3666         Ok(saved_projection)
   3667     }
   3668 
   3669     fn replace_identity_projection(
   3670         &mut self,
   3671         projection: AppIdentityProjection,
   3672     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   3673         let projection = self.decorate_identity_projection(projection)?;
   3674         let _ = self.import_shared_local_events()?;
   3675         let continuity_state = self.continuity_state();
   3676         let selected_account_context =
   3677             load_selected_account_context(self.sqlite_store()?, &projection, &continuity_state)?;
   3678         let selected_account_sync_context = load_selected_account_sync_context(
   3679             self.sqlite_store()?,
   3680             &projection,
   3681             &self.nostr_relay_urls,
   3682         )?;
   3683         let identity_changed = self
   3684             .state_store
   3685             .apply_in_memory(AppStateCommand::replace_identity_projection(projection));
   3686         let context_changed = self.apply_selected_account_context(&selected_account_context);
   3687         let sync_changed = self.apply_selected_account_sync_context(&selected_account_sync_context);
   3688         let editor_changed = self.close_product_editor();
   3689 
   3690         Ok(identity_changed || context_changed || sync_changed || editor_changed)
   3691     }
   3692 
   3693     fn refresh_selected_account_context(
   3694         &self,
   3695     ) -> Result<DesktopSelectedAccountContext, DesktopAppRuntimeFarmSetupError> {
   3696         let _ = self.import_shared_local_events()?;
   3697         let continuity_state = self.continuity_state();
   3698         Ok(load_selected_account_context(
   3699             self.sqlite_store_for_farm_setup()?,
   3700             self.state_store.identity_projection(),
   3701             &continuity_state,
   3702         )?)
   3703     }
   3704 
   3705     fn apply_selected_account_context(&mut self, context: &DesktopSelectedAccountContext) -> bool {
   3706         self.apply_selected_account_context_with_options(context, true)
   3707     }
   3708 
   3709     fn apply_selected_account_seller_context(
   3710         &mut self,
   3711         context: &DesktopSelectedAccountContext,
   3712     ) -> bool {
   3713         self.apply_selected_account_context_with_options(context, false)
   3714     }
   3715 
   3716     fn apply_selected_account_context_with_options(
   3717         &mut self,
   3718         context: &DesktopSelectedAccountContext,
   3719         include_personal: bool,
   3720     ) -> bool {
   3721         let previous_export_instance_id = self.current_pack_day_export_instance_id();
   3722         let personal_changed = if include_personal {
   3723             self.state_store
   3724                 .apply_in_memory(AppStateCommand::replace_personal_projection(
   3725                     context.personal_projection.clone(),
   3726                 ))
   3727         } else {
   3728             false
   3729         };
   3730         let farm_setup_changed =
   3731             self.state_store
   3732                 .apply_in_memory(AppStateCommand::replace_farm_setup_projection(
   3733                     context.farm_setup_projection.clone(),
   3734                 ));
   3735         let farm_rules_changed =
   3736             self.state_store
   3737                 .apply_in_memory(AppStateCommand::replace_farm_rules_projection(
   3738                     context.farm_rules_projection.clone(),
   3739                 ));
   3740         let today_changed =
   3741             self.state_store
   3742                 .apply_in_memory(AppStateCommand::replace_today_agenda(
   3743                     context.today_projection.clone(),
   3744                 ));
   3745         let products_query_changed =
   3746             self.state_store
   3747                 .apply_in_memory(AppStateCommand::set_products_search_query(
   3748                     context.products_query.search_query.clone(),
   3749                 ))
   3750                 || self
   3751                     .state_store
   3752                     .apply_in_memory(AppStateCommand::select_products_filter(
   3753                         context.products_query.filter,
   3754                     ))
   3755                 || self
   3756                     .state_store
   3757                     .apply_in_memory(AppStateCommand::select_products_sort(
   3758                         context.products_query.sort,
   3759                     ));
   3760         let products_changed =
   3761             self.state_store
   3762                 .apply_in_memory(AppStateCommand::replace_products_list(
   3763                     context.products_list.clone(),
   3764                 ));
   3765         let orders_query_changed =
   3766             self.state_store
   3767                 .apply_in_memory(AppStateCommand::select_orders_filter(
   3768                     context.orders_query.filter,
   3769                 ))
   3770                 || self.state_store.apply_in_memory(
   3771                     AppStateCommand::select_orders_fulfillment_window(
   3772                         context.orders_query.fulfillment_window_id,
   3773                     ),
   3774                 );
   3775         let orders_changed =
   3776             self.state_store
   3777                 .apply_in_memory(AppStateCommand::replace_orders_list(
   3778                     context.orders_list.clone(),
   3779                 ));
   3780         let orders_reminders_changed =
   3781             self.state_store
   3782                 .apply_in_memory(AppStateCommand::replace_orders_reminders(
   3783                     context.orders_reminders.clone(),
   3784                 ));
   3785         let reminder_log_changed =
   3786             self.state_store
   3787                 .apply_in_memory(AppStateCommand::replace_reminder_log(
   3788                     context.reminder_log.clone(),
   3789                 ));
   3790         let order_detail_changed =
   3791             self.state_store
   3792                 .apply_in_memory(AppStateCommand::replace_order_detail(
   3793                     context.order_detail.clone(),
   3794                 ));
   3795         let pack_day_changed =
   3796             self.state_store
   3797                 .apply_in_memory(AppStateCommand::replace_pack_day_projection(
   3798                     context.pack_day_projection.clone(),
   3799                 ));
   3800         let pack_day_query_changed =
   3801             self.state_store
   3802                 .apply_in_memory(AppStateCommand::set_pack_day_fulfillment_window(
   3803                     context.pack_day_query.fulfillment_window_id,
   3804                 ));
   3805         let editor_changed =
   3806             if let Some((product_id, draft)) = context.product_editor_draft.as_ref() {
   3807                 self.state_store
   3808                     .apply_in_memory(AppStateCommand::open_existing_product_editor(
   3809                         *product_id,
   3810                         draft.clone(),
   3811                     ))
   3812             } else {
   3813                 self.close_product_editor()
   3814             };
   3815         let shell_changed = self.sync_truthful_farmer_section();
   3816         self.cleanup_prepared_pack_day_print_assets_if_export_changed(
   3817             previous_export_instance_id,
   3818             "context_refresh",
   3819         );
   3820 
   3821         personal_changed
   3822             || farm_setup_changed
   3823             || farm_rules_changed
   3824             || today_changed
   3825             || products_query_changed
   3826             || products_changed
   3827             || orders_query_changed
   3828             || orders_changed
   3829             || orders_reminders_changed
   3830             || reminder_log_changed
   3831             || order_detail_changed
   3832             || pack_day_query_changed
   3833             || pack_day_changed
   3834             || editor_changed
   3835             || shell_changed
   3836     }
   3837 
   3838     fn refresh_selected_account_sync_context(
   3839         &self,
   3840     ) -> Result<DesktopSelectedAccountSyncContext, AppSqliteError> {
   3841         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3842             return Ok(DesktopSelectedAccountSyncContext::default());
   3843         };
   3844 
   3845         load_selected_account_sync_context(
   3846             sqlite_store,
   3847             self.state_store.identity_projection(),
   3848             &self.nostr_relay_urls,
   3849         )
   3850     }
   3851 
   3852     fn apply_selected_account_sync_context(
   3853         &mut self,
   3854         context: &DesktopSelectedAccountSyncContext,
   3855     ) -> bool {
   3856         let projection_changed =
   3857             self.state_store
   3858                 .apply_in_memory(AppStateCommand::replace_sync_projection(
   3859                     context.projection.clone(),
   3860                 ));
   3861         let pending_changed =
   3862             self.selected_account_pending_sync_write_count != context.pending_write_count;
   3863         let relay_ingest_changed =
   3864             self.selected_account_relay_ingest_freshness != context.relay_ingest;
   3865         let conflicts_changed = self.selected_account_sync_conflicts != context.conflicts;
   3866 
   3867         self.selected_account_pending_sync_write_count = context.pending_write_count;
   3868         self.selected_account_relay_ingest_freshness = context.relay_ingest.clone();
   3869         self.selected_account_sync_conflicts = context.conflicts.clone();
   3870 
   3871         projection_changed || pending_changed || relay_ingest_changed || conflicts_changed
   3872     }
   3873 
   3874     fn refresh_selected_account_sync(&mut self) -> Result<bool, AppSqliteError> {
   3875         let context = self.refresh_selected_account_sync_context()?;
   3876         let sync_changed = self.apply_selected_account_sync_context(&context);
   3877         let selected_account_changed = match self.sqlite_store.as_ref() {
   3878             Some(sqlite_store) => {
   3879                 let continuity_state = self.continuity_state();
   3880                 let selected_account_context = load_selected_account_context(
   3881                     sqlite_store,
   3882                     self.state_store.identity_projection(),
   3883                     &continuity_state,
   3884                 )?;
   3885                 self.apply_selected_account_seller_context(&selected_account_context)
   3886             }
   3887             None => false,
   3888         };
   3889 
   3890         Ok(sync_changed || selected_account_changed)
   3891     }
   3892 
   3893     fn resolve_sync_conflict(
   3894         &mut self,
   3895         conflict_id: &str,
   3896         resolution: radroots_app_sync::SyncConflictResolutionStatus,
   3897     ) -> Result<bool, AppSqliteError> {
   3898         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3899             return Ok(false);
   3900         };
   3901         let Some(selected_account) = self
   3902             .state_store
   3903             .identity_projection()
   3904             .selected_account
   3905             .as_ref()
   3906         else {
   3907             return Ok(false);
   3908         };
   3909         let account_id = selected_account.account.account_id.as_str();
   3910         let stored_conflicts = sqlite_store.load_sync_conflicts(account_id)?;
   3911         let Some(stored_conflict) = stored_conflicts
   3912             .iter()
   3913             .find(|stored| stored.conflict_id == conflict_id)
   3914         else {
   3915             return Ok(false);
   3916         };
   3917         if !stored_conflict.conflict.is_unresolved() {
   3918             return Ok(false);
   3919         }
   3920         if matches!(
   3921             (stored_conflict.conflict.severity, resolution,),
   3922             (
   3923                 SyncConflictSeverity::Blocking,
   3924                 radroots_app_sync::SyncConflictResolutionStatus::Dismissed,
   3925             )
   3926         ) {
   3927             return Ok(false);
   3928         }
   3929 
   3930         if !sqlite_store.resolve_sync_conflict(
   3931             account_id,
   3932             conflict_id,
   3933             resolution,
   3934             current_utc_timestamp().as_str(),
   3935         )? {
   3936             return Ok(false);
   3937         }
   3938 
   3939         self.refresh_selected_account_sync()
   3940     }
   3941 
   3942     fn acknowledge_reminder(&mut self, reminder_id: ReminderId) -> Result<bool, AppSqliteError> {
   3943         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   3944             return Ok(false);
   3945         };
   3946         let Some(selected_account) = self
   3947             .state_store
   3948             .identity_projection()
   3949             .selected_account
   3950             .as_ref()
   3951         else {
   3952             return Ok(false);
   3953         };
   3954         let Some(farm_id) = self.selected_farm_id() else {
   3955             return Ok(false);
   3956         };
   3957         let account_id = selected_account.account.account_id.clone();
   3958         let mut schedule = sqlite_store.load_reminder_schedule(account_id.as_str(), farm_id)?;
   3959         let Some(reminder) = schedule
   3960             .items
   3961             .iter_mut()
   3962             .find(|item| item.reminder_id == reminder_id)
   3963         else {
   3964             return Ok(false);
   3965         };
   3966         if matches!(
   3967             reminder.delivery_state,
   3968             ReminderDeliveryState::Acknowledged | ReminderDeliveryState::Resolved
   3969         ) {
   3970             return Ok(false);
   3971         }
   3972 
   3973         reminder.delivery_state = ReminderDeliveryState::Acknowledged;
   3974         let reminder_log_entry =
   3975             build_reminder_log_entry(reminder, ReminderDeliveryState::Acknowledged);
   3976         sqlite_store.apply_reminder_schedule_update(
   3977             account_id.as_str(),
   3978             farm_id,
   3979             &schedule,
   3980             &[reminder_log_entry],
   3981         )?;
   3982 
   3983         let continuity_state = self.continuity_state();
   3984         let selected_account_context = load_selected_account_context_with_options(
   3985             sqlite_store,
   3986             self.state_store.identity_projection(),
   3987             &continuity_state,
   3988             false,
   3989         )?;
   3990 
   3991         let _ = self.apply_selected_account_context(&selected_account_context);
   3992 
   3993         Ok(true)
   3994     }
   3995 
   3996     fn attempt_sync(&mut self, trigger: SyncTrigger) -> Result<bool, AppSqliteError> {
   3997         let Some(prepared) = self.prepare_sync_request(trigger)? else {
   3998             return Ok(false);
   3999         };
   4000 
   4001         let started_at = current_utc_timestamp();
   4002         let syncing_checkpoint = SyncCheckpointStatus::syncing(
   4003             started_at.clone(),
   4004             prepared.checkpoint.last_remote_cursor.clone(),
   4005         );
   4006         {
   4007             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   4008                 return Ok(false);
   4009             };
   4010             sqlite_store.save_sync_checkpoint(prepared.account_id.as_str(), &syncing_checkpoint)?;
   4011         }
   4012 
   4013         let mut changed = self.refresh_selected_account_sync()?;
   4014         let request = AppSyncRequest {
   4015             trigger,
   4016             checkpoint: prepared.checkpoint.clone(),
   4017             pending_operations: prepared
   4018                 .pending_operations
   4019                 .iter()
   4020                 .map(|stored| stored.operation.clone())
   4021                 .collect(),
   4022             known_conflicts: prepared
   4023                 .conflicts
   4024                 .iter()
   4025                 .map(|stored| stored.conflict.clone())
   4026                 .collect(),
   4027         };
   4028 
   4029         match self.run_sync_transport_or_relay_only(request, started_at.as_str()) {
   4030             Ok(mut result) => {
   4031                 let relay_context_changed =
   4032                     self.ingest_configured_relay_events_for_sync(Some(&mut result), &started_at)?;
   4033                 changed |= self.apply_sync_result(
   4034                     prepared.account_id.as_str(),
   4035                     &prepared.pending_operations,
   4036                     &result,
   4037                 )?;
   4038                 if relay_context_changed {
   4039                     changed |= self.refresh_selected_account_context_after_local_events()?;
   4040                 }
   4041             }
   4042             Err(error) => {
   4043                 changed |= self.apply_sync_transport_error(
   4044                     prepared.account_id.as_str(),
   4045                     &prepared.checkpoint,
   4046                     &prepared.pending_operations,
   4047                     started_at.as_str(),
   4048                     error,
   4049                 )?;
   4050                 let relay_context_changed =
   4051                     self.ingest_configured_relay_events_for_sync(None, &started_at)?;
   4052                 if relay_context_changed {
   4053                     changed |= self.refresh_selected_account_sync()?;
   4054                     changed |= self.refresh_selected_account_context_after_local_events()?;
   4055                 }
   4056             }
   4057         }
   4058 
   4059         Ok(changed)
   4060     }
   4061 
   4062     fn run_sync_transport_or_relay_only(
   4063         &mut self,
   4064         request: AppSyncRequest,
   4065         started_at: &str,
   4066     ) -> Result<AppSyncResult, AppSyncTransportError> {
   4067         if request.pending_operations.is_empty()
   4068             && self.has_configured_relay_ingest()
   4069             && !self.sync_transport.supports_empty_sync_request()
   4070         {
   4071             return Ok(AppSyncResult {
   4072                 run_status: AppSyncRunStatus::Succeeded,
   4073                 checkpoint: SyncCheckpointStatus::current(
   4074                     Some(started_at.to_owned()),
   4075                     current_utc_timestamp(),
   4076                     request.checkpoint.last_remote_cursor.clone(),
   4077                 ),
   4078                 pushed_operation_count: 0,
   4079                 pulled_record_count: 0,
   4080                 conflicts: request.known_conflicts,
   4081                 published_receipts: Vec::new(),
   4082             });
   4083         }
   4084 
   4085         self.sync_transport.sync(request)
   4086     }
   4087 
   4088     fn has_configured_relay_ingest(&self) -> bool {
   4089         self.nostr_relay_urls
   4090             .iter()
   4091             .any(|relay_url| !relay_url.trim().is_empty())
   4092     }
   4093 
   4094     fn ingest_configured_relay_events(
   4095         &self,
   4096     ) -> Result<AppDirectRelayIngestReport, AppDirectRelayIngestError> {
   4097         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   4098             return Ok(AppDirectRelayIngestReport::default());
   4099         };
   4100         let relay_urls = normalized_app_relay_ingest_urls(&self.nostr_relay_urls)?;
   4101         if relay_urls.is_empty() {
   4102             return Ok(AppDirectRelayIngestReport::default());
   4103         }
   4104         let started_at = current_utc_timestamp();
   4105         let started_unix_seconds = current_runtime_time_seconds()?;
   4106         let cursors = sqlite_store
   4107             .load_relay_ingest_cursors(APP_DIRECT_RELAY_INGEST_SCOPE_KEY, &relay_urls)?;
   4108         let receipt = fetch_app_events_from_relays_windowed(cursors.as_slice())?;
   4109         let completed_at = current_utc_timestamp();
   4110         let completed_unix_seconds = current_runtime_time_seconds()?;
   4111         self.record_relay_ingest_freshness(
   4112             &receipt,
   4113             started_at.as_str(),
   4114             started_unix_seconds,
   4115             completed_at.as_str(),
   4116             completed_unix_seconds,
   4117         )?;
   4118         if receipt.connected_relays.is_empty() {
   4119             return Err(AppSyncTransportError::unavailable(format!(
   4120                 "direct relay app ingest connection failed: {}",
   4121                 summarize_app_relay_failures(&receipt.failed_relays)
   4122             ))
   4123             .into());
   4124         }
   4125         if receipt.events.is_empty() {
   4126             return Ok(AppDirectRelayIngestReport {
   4127                 local_import: AppLocalInteropImportReport::default(),
   4128                 freshness_changed: true,
   4129             });
   4130         }
   4131         let records = direct_relay_event_records(&receipt, current_runtime_time_ms()?)?;
   4132         let local_import = sqlite_store
   4133             .import_local_event_records(records.as_slice())
   4134             .map_err(AppDirectRelayIngestError::from)?;
   4135         Ok(AppDirectRelayIngestReport {
   4136             local_import,
   4137             freshness_changed: true,
   4138         })
   4139     }
   4140 
   4141     fn record_relay_ingest_freshness(
   4142         &self,
   4143         receipt: &AppDirectRelayFetchReceipt,
   4144         started_at: &str,
   4145         started_unix_seconds: i64,
   4146         completed_at: &str,
   4147         completed_unix_seconds: i64,
   4148     ) -> Result<(), AppSqliteError> {
   4149         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   4150             return Ok(());
   4151         };
   4152         for relay in &receipt.fetched_relays {
   4153             let cursor_since_unix_seconds = relay
   4154                 .last_event_created_at_unix_seconds
   4155                 .map_or(started_unix_seconds, |last_event_created_at| {
   4156                     started_unix_seconds.max(last_event_created_at)
   4157                 });
   4158             sqlite_store.record_relay_ingest_success(
   4159                 APP_DIRECT_RELAY_INGEST_SCOPE_KEY,
   4160                 relay.relay_url.as_str(),
   4161                 cursor_since_unix_seconds,
   4162                 relay.last_event_created_at_unix_seconds,
   4163                 started_at,
   4164                 started_unix_seconds,
   4165                 completed_at,
   4166                 completed_unix_seconds,
   4167             )?;
   4168         }
   4169         for failure in &receipt.failed_relays {
   4170             sqlite_store.record_relay_ingest_failure(
   4171                 APP_DIRECT_RELAY_INGEST_SCOPE_KEY,
   4172                 failure.relay_url.as_str(),
   4173                 started_at,
   4174                 started_unix_seconds,
   4175                 completed_at,
   4176                 completed_unix_seconds,
   4177                 failure.error.as_str(),
   4178             )?;
   4179         }
   4180 
   4181         Ok(())
   4182     }
   4183 
   4184     fn ingest_configured_relay_events_for_sync(
   4185         &mut self,
   4186         mut result: Option<&mut AppSyncResult>,
   4187         started_at: &str,
   4188     ) -> Result<bool, AppSqliteError> {
   4189         if !self.has_configured_relay_ingest() {
   4190             return Ok(false);
   4191         }
   4192         match self.ingest_configured_relay_events() {
   4193             Ok(report) => {
   4194                 if let Some(result) = result.as_mut() {
   4195                     result.pulled_record_count = result
   4196                         .pulled_record_count
   4197                         .saturating_add(report.local_import.scanned_records as usize);
   4198                 }
   4199                 Ok(report.freshness_changed
   4200                     || report.local_import.imported_records > 0
   4201                     || report.local_import.skipped_records > 0)
   4202             }
   4203             Err(AppDirectRelayIngestError::Sqlite(error)) => Err(error),
   4204             Err(AppDirectRelayIngestError::Transport(error)) => {
   4205                 if let Some(result) = result.as_mut() {
   4206                     result.run_status = AppSyncRunStatus::Failed;
   4207                     result.checkpoint = SyncCheckpointStatus::failed(
   4208                         Some(started_at.to_owned()),
   4209                         Some(current_utc_timestamp()),
   4210                         result.checkpoint.last_remote_cursor.clone(),
   4211                         error.to_string(),
   4212                     );
   4213                 }
   4214                 Ok(true)
   4215             }
   4216         }
   4217     }
   4218 
   4219     fn prepare_sync_request(
   4220         &self,
   4221         trigger: SyncTrigger,
   4222     ) -> Result<Option<DesktopPreparedSyncRequest>, AppSqliteError> {
   4223         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   4224             return Ok(None);
   4225         };
   4226         let Some(selected_account) = self
   4227             .state_store
   4228             .identity_projection()
   4229             .selected_account
   4230             .as_ref()
   4231         else {
   4232             return Ok(None);
   4233         };
   4234 
   4235         let account_id = selected_account.account.account_id.clone();
   4236         let checkpoint = sqlite_store.load_sync_checkpoint(account_id.as_str())?;
   4237         let conflicts = sqlite_store.load_sync_conflicts(account_id.as_str())?;
   4238         let pending_operations = sqlite_store.load_pending_sync_operations(account_id.as_str())?;
   4239 
   4240         if conflicts.iter().any(|stored| {
   4241             stored.conflict.is_unresolved()
   4242                 && matches!(stored.conflict.severity, SyncConflictSeverity::Blocking)
   4243         }) {
   4244             return Ok(None);
   4245         }
   4246 
   4247         if !matches!(trigger, SyncTrigger::ManualRefresh)
   4248             && !self.has_configured_relay_ingest()
   4249             && !self.has_sync_eligible_runtime_state(&checkpoint, &conflicts, &pending_operations)
   4250         {
   4251             return Ok(None);
   4252         }
   4253 
   4254         Ok(Some(DesktopPreparedSyncRequest {
   4255             account_id,
   4256             checkpoint,
   4257             conflicts,
   4258             pending_operations,
   4259         }))
   4260     }
   4261 
   4262     fn has_sync_eligible_runtime_state(
   4263         &self,
   4264         checkpoint: &SyncCheckpointStatus,
   4265         conflicts: &[StoredSyncConflict],
   4266         pending_operations: &[StoredPendingSyncOperation],
   4267     ) -> bool {
   4268         !pending_operations.is_empty()
   4269             || !conflicts.is_empty()
   4270             || *checkpoint != SyncCheckpointStatus::never_synced()
   4271             || self.selected_farm_id().is_some()
   4272             || !self
   4273                 .state_store
   4274                 .personal_projection()
   4275                 .orders
   4276                 .list
   4277                 .rows
   4278                 .is_empty()
   4279             || !self.state_store.orders_projection().list.rows.is_empty()
   4280             || !self.state_store.products_projection().list.rows.is_empty()
   4281     }
   4282 
   4283     fn apply_sync_result(
   4284         &mut self,
   4285         account_id: &str,
   4286         pending_operations: &[StoredPendingSyncOperation],
   4287         result: &AppSyncResult,
   4288     ) -> Result<bool, AppSqliteError> {
   4289         self.record_published_sync_receipts(result.published_receipts.as_slice())?;
   4290         let receipt_import_changed = if result.published_receipts.is_empty() {
   4291             false
   4292         } else {
   4293             let report = self.import_shared_local_events()?;
   4294             report.imported_records > 0 || report.skipped_records > 0
   4295         };
   4296         {
   4297             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   4298                 return Ok(false);
   4299             };
   4300             sqlite_store.save_sync_checkpoint(account_id, &result.checkpoint)?;
   4301             sqlite_store.replace_sync_conflicts(account_id, &result.conflicts)?;
   4302 
   4303             for pending in pending_operations
   4304                 .iter()
   4305                 .take(result.pushed_operation_count)
   4306             {
   4307                 let _ = sqlite_store
   4308                     .dequeue_pending_sync_operation(account_id, pending.operation_id.as_str())?;
   4309             }
   4310             if result.run_status == AppSyncRunStatus::Failed {
   4311                 let retry_available_at = result
   4312                     .checkpoint
   4313                     .last_sync_completed_at
   4314                     .clone()
   4315                     .unwrap_or_else(current_utc_timestamp);
   4316                 let last_error_message = result.checkpoint.last_error_message.as_deref();
   4317                 for pending in pending_operations
   4318                     .iter()
   4319                     .skip(result.pushed_operation_count)
   4320                 {
   4321                     let _ = sqlite_store.update_pending_sync_operation_retry(
   4322                         account_id,
   4323                         pending.operation_id.as_str(),
   4324                         retry_available_at.as_str(),
   4325                         pending.operation.attempt_count.saturating_add(1),
   4326                         last_error_message,
   4327                     )?;
   4328                 }
   4329             }
   4330         }
   4331 
   4332         let sync_changed = self.refresh_selected_account_sync()?;
   4333         Ok(receipt_import_changed || sync_changed)
   4334     }
   4335 
   4336     fn apply_sync_transport_error(
   4337         &mut self,
   4338         account_id: &str,
   4339         previous_checkpoint: &SyncCheckpointStatus,
   4340         pending_operations: &[StoredPendingSyncOperation],
   4341         started_at: &str,
   4342         error: AppSyncTransportError,
   4343     ) -> Result<bool, AppSqliteError> {
   4344         let error_message = error.to_string();
   4345         let failed_checkpoint = SyncCheckpointStatus::failed(
   4346             Some(started_at.to_owned()),
   4347             previous_checkpoint.last_sync_completed_at.clone(),
   4348             previous_checkpoint.last_remote_cursor.clone(),
   4349             error_message.clone(),
   4350         );
   4351         {
   4352             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   4353                 return Ok(false);
   4354             };
   4355             sqlite_store.save_sync_checkpoint(account_id, &failed_checkpoint)?;
   4356 
   4357             for pending in pending_operations {
   4358                 let _ = sqlite_store.update_pending_sync_operation_retry(
   4359                     account_id,
   4360                     pending.operation_id.as_str(),
   4361                     started_at,
   4362                     pending.operation.attempt_count.saturating_add(1),
   4363                     Some(error_message.as_str()),
   4364                 )?;
   4365             }
   4366         }
   4367 
   4368         self.refresh_selected_account_sync()
   4369     }
   4370 
   4371     #[cfg(test)]
   4372     fn enqueue_selected_account_sync_operations(
   4373         &mut self,
   4374         operations: Vec<PendingSyncOperation>,
   4375     ) -> Result<bool, AppSqliteError> {
   4376         if operations.is_empty() {
   4377             return Ok(false);
   4378         }
   4379 
   4380         let Some(selected_account) = self
   4381             .state_store
   4382             .identity_projection()
   4383             .selected_account
   4384             .as_ref()
   4385         else {
   4386             return Ok(false);
   4387         };
   4388         {
   4389             let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   4390                 return Ok(false);
   4391             };
   4392             for operation in &operations {
   4393                 let _ = sqlite_store.enqueue_pending_sync_operation(
   4394                     selected_account.account.account_id.as_str(),
   4395                     operation,
   4396                 )?;
   4397             }
   4398         }
   4399 
   4400         self.refresh_selected_account_sync()
   4401     }
   4402 
   4403     fn enqueue_selected_account_order_sync_operation(
   4404         &mut self,
   4405         buyer_context: &BuyerContext,
   4406         order: &BuyerOrderLocalEventExport,
   4407         local_work: Option<&AppOrderLocalWorkPublishSource>,
   4408     ) -> Result<bool, AppSqliteError> {
   4409         let Some(payload) = self.order_request_publish_payload(buyer_context, order, local_work)?
   4410         else {
   4411             return self.refresh_selected_account_sync();
   4412         };
   4413 
   4414         let source_record_id = payload
   4415             .context
   4416             .source_local_event_id
   4417             .clone()
   4418             .unwrap_or_else(|| format!("app:order_request:{}", payload.order_id));
   4419         self.enqueue_order_request_payload_via_sdk(
   4420             &payload,
   4421             AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
   4422             source_record_id.as_str(),
   4423         )?;
   4424         self.refresh_selected_account_sync()
   4425     }
   4426 
   4427     fn enqueue_selected_account_farm_publish_operation(
   4428         &mut self,
   4429         farm_id: FarmId,
   4430         display_name: &str,
   4431         readiness: Option<FarmReadiness>,
   4432         source: &str,
   4433         source_local_event_id: Option<&str>,
   4434     ) -> Result<bool, AppSqliteError> {
   4435         let Some(payload) = self.farm_profile_publish_payload(
   4436             farm_id,
   4437             display_name,
   4438             readiness,
   4439             source,
   4440             source_local_event_id,
   4441         )?
   4442         else {
   4443             return self.refresh_selected_account_sync();
   4444         };
   4445 
   4446         let (source_kind, source_record_id) = farm_publish_source_record(
   4447             farm_id,
   4448             source,
   4449             payload.context.source_local_event_id.as_deref(),
   4450         );
   4451         self.enqueue_farm_profile_payload_via_sdk(
   4452             &payload,
   4453             source_kind,
   4454             source_record_id.as_str(),
   4455         )?;
   4456         self.refresh_selected_account_sync()
   4457     }
   4458 
   4459     fn enqueue_selected_account_product_publish_operation(
   4460         &mut self,
   4461         product_id: ProductId,
   4462         source: &str,
   4463         source_local_event_id: Option<&str>,
   4464     ) -> Result<bool, DesktopAppRuntimeProductPublishError> {
   4465         let Some(payload) =
   4466             self.product_publish_payload(product_id, source, source_local_event_id)?
   4467         else {
   4468             return self
   4469                 .refresh_selected_account_sync()
   4470                 .map_err(DesktopAppRuntimeProductPublishError::from);
   4471         };
   4472 
   4473         let (source_kind, source_record_id) = listing_publish_source_record(
   4474             product_id,
   4475             source,
   4476             payload.context.source_local_event_id.as_deref(),
   4477         );
   4478         self.enqueue_listing_payload_via_sdk(&payload, source_kind, source_record_id.as_str())?;
   4479         let _ = self.refresh_selected_account_sync()?;
   4480         Ok(true)
   4481     }
   4482 
   4483     fn farm_profile_publish_payload(
   4484         &self,
   4485         farm_id: FarmId,
   4486         display_name: &str,
   4487         readiness: Option<FarmReadiness>,
   4488         source: &str,
   4489         source_local_event_id: Option<&str>,
   4490     ) -> Result<Option<AppFarmProfilePublishPayload>, AppSqliteError> {
   4491         let Some(selected_account) = self
   4492             .state_store
   4493             .identity_projection()
   4494             .selected_account
   4495             .as_ref()
   4496         else {
   4497             return Ok(None);
   4498         };
   4499         let mut context = AppPublishContext::new(
   4500             selected_account.account.account_id.clone(),
   4501             source.to_owned(),
   4502         );
   4503         if let Some(source_local_event_id) = source_local_event_id {
   4504             context = context.with_source_local_event_id(source_local_event_id.to_owned());
   4505         }
   4506         let payload = AppFarmProfilePublishPayload {
   4507             context,
   4508             farm_id,
   4509             display_name: display_name.trim().to_owned(),
   4510             readiness,
   4511         };
   4512         if AppPublishPayload::FarmProfile(payload.clone())
   4513             .validate()
   4514             .is_err()
   4515         {
   4516             return Ok(None);
   4517         }
   4518 
   4519         Ok(Some(payload))
   4520     }
   4521 
   4522     fn product_publish_payload(
   4523         &self,
   4524         product_id: ProductId,
   4525         source: &str,
   4526         source_local_event_id: Option<&str>,
   4527     ) -> Result<Option<AppListingPublishPayload>, AppSqliteError> {
   4528         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   4529             return Ok(None);
   4530         };
   4531         let Some(selected_account) = self
   4532             .state_store
   4533             .identity_projection()
   4534             .selected_account
   4535             .as_ref()
   4536         else {
   4537             return Ok(None);
   4538         };
   4539         let Some(draft) = sqlite_store.load_product_editor_draft(product_id)? else {
   4540             return Ok(None);
   4541         };
   4542         if !product_status_needs_relay_publish(draft.status) {
   4543             return Ok(None);
   4544         }
   4545         let farm_rules = self.state_store.farm_rules_projection();
   4546         if !derive_product_publish_blockers(
   4547             &draft,
   4548             self.state_store.farm_readiness_projection(),
   4549             farm_rules,
   4550         )
   4551         .is_empty()
   4552         {
   4553             return Ok(None);
   4554         }
   4555         let Some(farm_id) = self.selected_farm_id() else {
   4556             return Ok(None);
   4557         };
   4558         let Some(farm_pubkey) = self.local_events_owner_pubkey(selected_account) else {
   4559             return Ok(None);
   4560         };
   4561         let farm_setup = self.state_store.farm_setup_projection();
   4562         let (availability_starts_at, availability_ends_at) =
   4563             listing_availability_window_times(&draft, farm_rules);
   4564         let listing_d_tag = d_tag_from_uuid(product_id.as_uuid());
   4565         let mut context = AppPublishContext::new(
   4566             selected_account.account.account_id.clone(),
   4567             source.to_owned(),
   4568         );
   4569         if let Some(source_local_event_id) = source_local_event_id {
   4570             context = context.with_source_local_event_id(source_local_event_id.to_owned());
   4571         }
   4572         let payload = AppListingPublishPayload {
   4573             context,
   4574             product_id,
   4575             listing_d_tag: Some(listing_d_tag),
   4576             farm_id: Some(farm_id),
   4577             farm_pubkey: Some(farm_pubkey),
   4578             farm_d_tag: Some(d_tag_from_uuid(farm_id.as_uuid())),
   4579             title: draft.title.trim().to_owned(),
   4580             subtitle: non_empty_string(draft.subtitle.as_str()),
   4581             category: non_empty_string(draft.category.as_str()),
   4582             unit_label: draft.unit_label.trim().to_owned(),
   4583             price_minor_units: draft.price_minor_units,
   4584             price_currency: draft.price_currency.trim().to_uppercase(),
   4585             stock_quantity: draft.stock_quantity,
   4586             availability_window_id: draft.availability_window_id,
   4587             availability_starts_at,
   4588             availability_ends_at,
   4589             fulfillment_method: listing_fulfillment_method(&draft, farm_setup, farm_rules),
   4590             fulfillment_location: listing_fulfillment_location(&draft, farm_setup, farm_rules),
   4591             status: draft.status,
   4592         };
   4593         if AppPublishPayload::Listing(payload.clone())
   4594             .validate()
   4595             .is_err()
   4596         {
   4597             return Ok(None);
   4598         }
   4599 
   4600         Ok(Some(payload))
   4601     }
   4602 
   4603     fn order_request_publish_payload(
   4604         &self,
   4605         buyer_context: &BuyerContext,
   4606         order: &BuyerOrderLocalEventExport,
   4607         local_work: Option<&AppOrderLocalWorkPublishSource>,
   4608     ) -> Result<Option<AppOrderRequestPublishPayload>, AppSqliteError> {
   4609         let Some(local_work) = local_work else {
   4610             return Ok(None);
   4611         };
   4612         let Some(buyer_account) = self.selected_buyer_account(buyer_context) else {
   4613             return Ok(None);
   4614         };
   4615         let buyer_pubkey = self.local_events_owner_pubkey(buyer_account);
   4616         let export = AppBuyerOrderRequestExport::from_order(order, buyer_pubkey.as_deref())?;
   4617         if !export.is_supported() {
   4618             return Ok(None);
   4619         }
   4620         let Some((currency_code, total_minor_units)) = order_currency_and_total(order)? else {
   4621             return Ok(None);
   4622         };
   4623         let context = AppPublishContext::new(
   4624             buyer_account.account.account_id.clone(),
   4625             "place_personal_order",
   4626         )
   4627         .with_source_local_event_id(local_work.record_id.clone());
   4628         let payload = AppOrderRequestPublishPayload {
   4629             context,
   4630             order_id: order.order_id,
   4631             farm_id: order.farm_id,
   4632             status: Some(order.status.clone()),
   4633             order_document_json: Some(local_work.payload.clone()),
   4634             listing_addr: export.listing_addr,
   4635             listing_event_id: export.listing_event_id,
   4636             listing_relays: export.listing_relays,
   4637             buyer_pubkey: export.buyer_pubkey,
   4638             seller_pubkey: export.seller_pubkey,
   4639             items: order
   4640                 .lines
   4641                 .iter()
   4642                 .map(|line| AppOrderRequestItemPayload {
   4643                     product_id: line.product_id,
   4644                     quantity: line.quantity,
   4645                 })
   4646                 .collect(),
   4647             currency_code: Some(currency_code),
   4648             total_minor_units: Some(total_minor_units),
   4649             note: non_empty_string(order.buyer_order_note.as_str()),
   4650         };
   4651         if AppPublishPayload::OrderRequest(payload.clone())
   4652             .validate()
   4653             .is_err()
   4654         {
   4655             return Ok(None);
   4656         }
   4657 
   4658         Ok(Some(payload))
   4659     }
   4660 
   4661     fn enqueue_farm_profile_payload_via_sdk(
   4662         &self,
   4663         payload: &AppFarmProfilePublishPayload,
   4664         source_kind: AppSdkMigrationReceiptSourceKind,
   4665         source_record_id: &str,
   4666     ) -> Result<(), AppSqliteError> {
   4667         let operation_kind = FARM_PUBLISH_OPERATION_KIND;
   4668         let actor_pubkey = self
   4669             .local_signing_identity_for_publish_payload(&AppPublishPayload::FarmProfile(
   4670                 payload.clone(),
   4671             ))
   4672             .and_then(|identity| {
   4673                 let actor_pubkey = identity.public_key_hex();
   4674                 let request = AppSdkFarmPublishRequest {
   4675                     actor_account_id: payload.context.account_id.clone(),
   4676                     actor_pubkey: actor_pubkey.clone(),
   4677                     signer_keys: identity.into_keys(),
   4678                     farm: farm_profile_publish_payload_to_sdk_farm(payload),
   4679                     target_relays: normalized_app_sync_relay_urls(&self.nostr_relay_urls)?,
   4680                     relay_url_policy: sdk_relay_url_policy_for_targets(&self.nostr_relay_urls),
   4681                     idempotency_key: Some(sdk_idempotency_key(source_record_id)),
   4682                 };
   4683                 self.enqueue_app_sdk_farm_publish(request)
   4684                     .map(|receipt| (actor_pubkey, receipt))
   4685                     .map_err(sync_transport_error_from_sdk_runtime_error)
   4686             });
   4687         match actor_pubkey {
   4688             Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success(
   4689                 source_kind,
   4690                 source_record_id,
   4691                 operation_kind,
   4692                 actor_pubkey.as_str(),
   4693                 &receipt,
   4694             ),
   4695             Err(error) => self.record_app_sdk_migration_failure(
   4696                 source_kind,
   4697                 source_record_id,
   4698                 operation_kind,
   4699                 None,
   4700                 sync_transport_error_detail_json(&error),
   4701             ),
   4702         }
   4703     }
   4704 
   4705     fn enqueue_listing_payload_via_sdk(
   4706         &self,
   4707         payload: &AppListingPublishPayload,
   4708         source_kind: AppSdkMigrationReceiptSourceKind,
   4709         source_record_id: &str,
   4710     ) -> Result<(), DesktopAppRuntimeProductPublishError> {
   4711         let operation_kind = LISTING_PUBLISH_OPERATION_KIND;
   4712         let actor_pubkey = self
   4713             .local_signing_identity_for_publish_payload(&AppPublishPayload::Listing(
   4714                 payload.clone(),
   4715             ))
   4716             .and_then(|identity| {
   4717                 let actor_pubkey = identity.public_key_hex();
   4718                 let request = AppSdkListingPublishRequest {
   4719                     actor_account_id: payload.context.account_id.clone(),
   4720                     actor_pubkey: actor_pubkey.clone(),
   4721                     signer_keys: identity.into_keys(),
   4722                     listing: listing_publish_payload_to_sdk_listing(payload)?,
   4723                     target_relays: normalized_app_sync_relay_urls(&self.nostr_relay_urls)?,
   4724                     relay_url_policy: sdk_relay_url_policy_for_targets(&self.nostr_relay_urls),
   4725                     idempotency_key: Some(sdk_idempotency_key(source_record_id)),
   4726                 };
   4727                 self.enqueue_app_sdk_listing_publish(request)
   4728                     .map(|receipt| (actor_pubkey, receipt))
   4729                     .map_err(sync_transport_error_from_sdk_runtime_error)
   4730             });
   4731         match actor_pubkey {
   4732             Ok((actor_pubkey, receipt)) => self
   4733                 .record_app_sdk_migration_success(
   4734                     source_kind,
   4735                     source_record_id,
   4736                     operation_kind,
   4737                     actor_pubkey.as_str(),
   4738                     &receipt,
   4739                 )
   4740                 .map_err(DesktopAppRuntimeProductPublishError::from),
   4741             Err(error) => {
   4742                 self.record_app_sdk_migration_failure(
   4743                     source_kind,
   4744                     source_record_id,
   4745                     operation_kind,
   4746                     None,
   4747                     sync_transport_error_detail_json(&error),
   4748                 )?;
   4749                 Err(DesktopAppRuntimeProductPublishError::ListingPublishSdkEnqueueFailed)
   4750             }
   4751         }
   4752     }
   4753 
   4754     fn enqueue_order_request_payload_via_sdk(
   4755         &self,
   4756         payload: &AppOrderRequestPublishPayload,
   4757         source_kind: AppSdkMigrationReceiptSourceKind,
   4758         source_record_id: &str,
   4759     ) -> Result<(), AppSqliteError> {
   4760         let operation_kind = ORDER_SUBMIT_OPERATION_KIND;
   4761         let actor_pubkey = self
   4762             .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderRequest(
   4763                 payload.clone(),
   4764             ))
   4765             .and_then(|identity| {
   4766                 let actor_pubkey = identity.public_key_hex();
   4767                 let target_relays =
   4768                     order_request_sdk_target_relays(payload, self.nostr_relay_urls.as_slice())?;
   4769                 let request = AppSdkOrderSubmitRequest {
   4770                     actor_account_id: payload.context.account_id.clone(),
   4771                     actor_pubkey: actor_pubkey.clone(),
   4772                     signer_keys: identity.into_keys(),
   4773                     listing_event: order_request_sdk_listing_event_ptr(payload)?,
   4774                     order: order_request_publish_payload_to_sdk_order(payload)?,
   4775                     relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()),
   4776                     target_relays,
   4777                     idempotency_key: Some(sdk_idempotency_key(source_record_id)),
   4778                 };
   4779                 self.enqueue_app_sdk_order_submit(request)
   4780                     .map(|receipt| (actor_pubkey, receipt))
   4781                     .map_err(sync_transport_error_from_sdk_runtime_error)
   4782             });
   4783         match actor_pubkey {
   4784             Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success(
   4785                 source_kind,
   4786                 source_record_id,
   4787                 operation_kind,
   4788                 actor_pubkey.as_str(),
   4789                 &receipt,
   4790             ),
   4791             Err(error) => self.record_app_sdk_migration_failure(
   4792                 source_kind,
   4793                 source_record_id,
   4794                 operation_kind,
   4795                 None,
   4796                 sync_transport_error_detail_json(&error),
   4797             ),
   4798         }
   4799     }
   4800 
   4801     fn enqueue_order_decision_payload_via_sdk(
   4802         &self,
   4803         payload: &AppOrderDecisionPublishPayload,
   4804         source_kind: AppSdkMigrationReceiptSourceKind,
   4805         source_record_id: &str,
   4806     ) -> Result<(), AppSqliteError> {
   4807         let operation_kind = ORDER_DECISION_OPERATION_KIND;
   4808         let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?;
   4809         let actor_pubkey = self
   4810             .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderDecision(
   4811                 payload.clone(),
   4812             ))
   4813             .and_then(|identity| {
   4814                 let actor_pubkey = identity.public_key_hex();
   4815                 let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?;
   4816                 let request = AppSdkOrderDecisionRequest {
   4817                     actor_account_id: payload.context.account_id.clone(),
   4818                     actor_pubkey: actor_pubkey.clone(),
   4819                     signer_keys: identity.into_keys(),
   4820                     request_event: request_evidence.request_event,
   4821                     request_event_ptr: order_decision_sdk_request_event_ptr(
   4822                         payload,
   4823                         target_relays.as_slice(),
   4824                     )?,
   4825                     decision: order_decision_publish_payload_to_sdk_decision(payload)?,
   4826                     relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()),
   4827                     target_relays,
   4828                     idempotency_key: Some(sdk_idempotency_key(source_record_id)),
   4829                 };
   4830                 self.enqueue_app_sdk_order_decision(request)
   4831                     .map(|receipt| (actor_pubkey, receipt))
   4832                     .map_err(sync_transport_error_from_sdk_runtime_error)
   4833             });
   4834         match actor_pubkey {
   4835             Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success(
   4836                 source_kind,
   4837                 source_record_id,
   4838                 operation_kind,
   4839                 actor_pubkey.as_str(),
   4840                 &receipt,
   4841             ),
   4842             Err(error) => self.record_app_sdk_migration_failure(
   4843                 source_kind,
   4844                 source_record_id,
   4845                 operation_kind,
   4846                 None,
   4847                 sync_transport_error_detail_json(&error),
   4848             ),
   4849         }
   4850     }
   4851 
   4852     fn enqueue_order_revision_proposal_payload_via_sdk(
   4853         &self,
   4854         payload: &AppOrderRevisionProposalPublishPayload,
   4855         source_kind: AppSdkMigrationReceiptSourceKind,
   4856         source_record_id: &str,
   4857     ) -> Result<(), AppSqliteError> {
   4858         let operation_kind = ORDER_REVISION_PROPOSAL_OPERATION_KIND;
   4859         let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?;
   4860         let lifecycle = self.resolve_order_lifecycle_evidence(&request_evidence)?;
   4861         let actor_pubkey = self
   4862             .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderRevisionProposal(
   4863                 payload.clone(),
   4864             ))
   4865             .and_then(|identity| {
   4866                 let actor_pubkey = identity.public_key_hex();
   4867                 let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?;
   4868                 let request = AppSdkOrderRevisionProposalRequest {
   4869                     actor_account_id: payload.context.account_id.clone(),
   4870                     actor_pubkey: actor_pubkey.clone(),
   4871                     signer_keys: identity.into_keys(),
   4872                     evidence_events: lifecycle.evidence_events,
   4873                     root_event: order_lifecycle_sdk_event_ptr(
   4874                         payload.request_event_id.as_str(),
   4875                         target_relays.as_slice(),
   4876                         "order revision proposal requires request event id",
   4877                     )?,
   4878                     previous_event: order_lifecycle_sdk_event_ptr(
   4879                         payload.prev_event_id.as_str(),
   4880                         target_relays.as_slice(),
   4881                         "order revision proposal requires previous event id",
   4882                     )?,
   4883                     proposal: order_revision_proposal_publish_payload_to_sdk_revision(payload)?,
   4884                     relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()),
   4885                     target_relays,
   4886                     idempotency_key: Some(sdk_idempotency_key(source_record_id)),
   4887                 };
   4888                 self.enqueue_app_sdk_order_revision_proposal(request)
   4889                     .map(|receipt| (actor_pubkey, receipt))
   4890                     .map_err(sync_transport_error_from_sdk_runtime_error)
   4891             });
   4892         match actor_pubkey {
   4893             Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success(
   4894                 source_kind,
   4895                 source_record_id,
   4896                 operation_kind,
   4897                 actor_pubkey.as_str(),
   4898                 &receipt,
   4899             ),
   4900             Err(error) => self.record_app_sdk_migration_failure(
   4901                 source_kind,
   4902                 source_record_id,
   4903                 operation_kind,
   4904                 None,
   4905                 sync_transport_error_detail_json(&error),
   4906             ),
   4907         }
   4908     }
   4909 
   4910     fn enqueue_order_revision_decision_payload_via_sdk(
   4911         &self,
   4912         payload: &AppOrderRevisionDecisionPublishPayload,
   4913         source_kind: AppSdkMigrationReceiptSourceKind,
   4914         source_record_id: &str,
   4915     ) -> Result<(), AppSqliteError> {
   4916         let operation_kind = ORDER_REVISION_DECISION_OPERATION_KIND;
   4917         let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?;
   4918         let lifecycle = self.resolve_order_lifecycle_evidence(&request_evidence)?;
   4919         let actor_pubkey = self
   4920             .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderRevisionDecision(
   4921                 payload.clone(),
   4922             ))
   4923             .and_then(|identity| {
   4924                 let actor_pubkey = identity.public_key_hex();
   4925                 let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?;
   4926                 let request = AppSdkOrderRevisionDecisionRequest {
   4927                     actor_account_id: payload.context.account_id.clone(),
   4928                     actor_pubkey: actor_pubkey.clone(),
   4929                     signer_keys: identity.into_keys(),
   4930                     evidence_events: lifecycle.evidence_events,
   4931                     root_event: order_lifecycle_sdk_event_ptr(
   4932                         payload.request_event_id.as_str(),
   4933                         target_relays.as_slice(),
   4934                         "order revision decision requires request event id",
   4935                     )?,
   4936                     previous_event: order_lifecycle_sdk_event_ptr(
   4937                         payload.prev_event_id.as_str(),
   4938                         target_relays.as_slice(),
   4939                         "order revision decision requires previous event id",
   4940                     )?,
   4941                     decision: order_revision_decision_publish_payload_to_sdk_revision_decision(
   4942                         payload,
   4943                     )?,
   4944                     relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()),
   4945                     target_relays,
   4946                     idempotency_key: Some(sdk_idempotency_key(source_record_id)),
   4947                 };
   4948                 self.enqueue_app_sdk_order_revision_decision(request)
   4949                     .map(|receipt| (actor_pubkey, receipt))
   4950                     .map_err(sync_transport_error_from_sdk_runtime_error)
   4951             });
   4952         match actor_pubkey {
   4953             Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success(
   4954                 source_kind,
   4955                 source_record_id,
   4956                 operation_kind,
   4957                 actor_pubkey.as_str(),
   4958                 &receipt,
   4959             ),
   4960             Err(error) => self.record_app_sdk_migration_failure(
   4961                 source_kind,
   4962                 source_record_id,
   4963                 operation_kind,
   4964                 None,
   4965                 sync_transport_error_detail_json(&error),
   4966             ),
   4967         }
   4968     }
   4969 
   4970     fn enqueue_order_cancellation_payload_via_sdk(
   4971         &self,
   4972         payload: &AppOrderCancellationPublishPayload,
   4973         source_kind: AppSdkMigrationReceiptSourceKind,
   4974         source_record_id: &str,
   4975     ) -> Result<(), AppSqliteError> {
   4976         let operation_kind = ORDER_CANCELLATION_OPERATION_KIND;
   4977         let request_evidence = self.resolve_seller_order_request_evidence(payload.app_order_id)?;
   4978         let lifecycle = self.resolve_order_lifecycle_evidence(&request_evidence)?;
   4979         let actor_pubkey = self
   4980             .local_signing_identity_for_publish_payload(&AppPublishPayload::OrderCancellation(
   4981                 payload.clone(),
   4982             ))
   4983             .and_then(|identity| {
   4984                 let actor_pubkey = identity.public_key_hex();
   4985                 let target_relays = normalized_app_sync_relay_urls(&self.nostr_relay_urls)?;
   4986                 let request = AppSdkOrderCancellationRequest {
   4987                     actor_account_id: payload.context.account_id.clone(),
   4988                     actor_pubkey: actor_pubkey.clone(),
   4989                     signer_keys: identity.into_keys(),
   4990                     evidence_events: lifecycle.evidence_events,
   4991                     root_event: order_lifecycle_sdk_event_ptr(
   4992                         payload.request_event_id.as_str(),
   4993                         target_relays.as_slice(),
   4994                         "order cancellation requires request event id",
   4995                     )?,
   4996                     previous_event: order_lifecycle_sdk_event_ptr(
   4997                         payload.prev_event_id.as_str(),
   4998                         target_relays.as_slice(),
   4999                         "order cancellation requires previous event id",
   5000                     )?,
   5001                     cancellation: order_cancellation_publish_payload_to_sdk_cancellation(payload)?,
   5002                     relay_url_policy: sdk_relay_url_policy_for_targets(target_relays.as_slice()),
   5003                     target_relays,
   5004                     idempotency_key: Some(sdk_idempotency_key(source_record_id)),
   5005                 };
   5006                 self.enqueue_app_sdk_order_cancellation(request)
   5007                     .map(|receipt| (actor_pubkey, receipt))
   5008                     .map_err(sync_transport_error_from_sdk_runtime_error)
   5009             });
   5010         match actor_pubkey {
   5011             Ok((actor_pubkey, receipt)) => self.record_app_sdk_migration_success(
   5012                 source_kind,
   5013                 source_record_id,
   5014                 operation_kind,
   5015                 actor_pubkey.as_str(),
   5016                 &receipt,
   5017             ),
   5018             Err(error) => self.record_app_sdk_migration_failure(
   5019                 source_kind,
   5020                 source_record_id,
   5021                 operation_kind,
   5022                 None,
   5023                 sync_transport_error_detail_json(&error),
   5024             ),
   5025         }
   5026     }
   5027 
   5028     fn local_signing_identity_for_publish_payload(
   5029         &self,
   5030         payload: &AppPublishPayload,
   5031     ) -> Result<RadrootsIdentity, AppSyncTransportError> {
   5032         let accounts_manager = self.accounts_manager.as_ref().ok_or_else(|| {
   5033             AppSyncTransportError::unavailable("app account manager is not configured")
   5034         })?;
   5035         signing_identity_for_publish_payload(accounts_manager, payload)
   5036     }
   5037 
   5038     fn enqueue_app_sdk_farm_publish(
   5039         &self,
   5040         request: AppSdkFarmPublishRequest,
   5041     ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> {
   5042         self.with_app_sdk_runtime(|runtime| runtime.enqueue_farm_publish(request))
   5043     }
   5044 
   5045     fn enqueue_app_sdk_listing_publish(
   5046         &self,
   5047         request: AppSdkListingPublishRequest,
   5048     ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> {
   5049         self.with_app_sdk_runtime(|runtime| runtime.enqueue_listing_publish(request))
   5050     }
   5051 
   5052     fn enqueue_app_sdk_order_submit(
   5053         &self,
   5054         request: AppSdkOrderSubmitRequest,
   5055     ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> {
   5056         self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_submit(request))
   5057     }
   5058 
   5059     fn enqueue_app_sdk_order_decision(
   5060         &self,
   5061         request: AppSdkOrderDecisionRequest,
   5062     ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> {
   5063         self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_decision(request))
   5064     }
   5065 
   5066     fn enqueue_app_sdk_order_revision_proposal(
   5067         &self,
   5068         request: AppSdkOrderRevisionProposalRequest,
   5069     ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> {
   5070         self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_revision_proposal(request))
   5071     }
   5072 
   5073     fn enqueue_app_sdk_order_revision_decision(
   5074         &self,
   5075         request: AppSdkOrderRevisionDecisionRequest,
   5076     ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> {
   5077         self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_revision_decision(request))
   5078     }
   5079 
   5080     fn enqueue_app_sdk_order_cancellation(
   5081         &self,
   5082         request: AppSdkOrderCancellationRequest,
   5083     ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> {
   5084         self.with_app_sdk_runtime(|runtime| runtime.enqueue_order_cancellation(request))
   5085     }
   5086 
   5087     fn with_app_sdk_runtime<T>(
   5088         &self,
   5089         command: impl FnOnce(&AppSdkRuntime) -> Result<T, AppSdkRuntimeError>,
   5090     ) -> Result<T, AppSdkRuntimeError> {
   5091         let Some(handle) = self.sdk_runtime.as_ref() else {
   5092             return Err(sdk_runtime_unavailable_error());
   5093         };
   5094         let sdk_runtime = handle.lock().unwrap_or_else(PoisonError::into_inner);
   5095         let Some(runtime) = sdk_runtime.as_ref() else {
   5096             return Err(sdk_runtime_unavailable_error());
   5097         };
   5098         command(runtime)
   5099     }
   5100 
   5101     fn record_app_sdk_migration_success(
   5102         &self,
   5103         source_kind: AppSdkMigrationReceiptSourceKind,
   5104         source_record_id: &str,
   5105         operation_kind: &str,
   5106         actor_pubkey: &str,
   5107         receipt: &AppSdkWorkflowReceipt,
   5108     ) -> Result<(), AppSqliteError> {
   5109         let detail_json = json!({
   5110             "operation_kind": receipt.operation_kind,
   5111             "expected_event_id": receipt.expected_event_id,
   5112             "signed_event_id": receipt.signed_event_id,
   5113             "outbox_operation_id": receipt.outbox_operation_id,
   5114             "outbox_event_id": receipt.outbox_event_id,
   5115             "state": receipt.state,
   5116         });
   5117         self.record_app_sdk_migration_receipt(AppSdkMigrationReceiptInput {
   5118             source_kind,
   5119             source_record_id: source_record_id.to_owned(),
   5120             sdk_operation_kind: operation_kind.to_owned(),
   5121             sdk_outbox_event_ids: vec![receipt.outbox_event_id.to_string()],
   5122             expected_event_id: Some(receipt.expected_event_id.clone()),
   5123             actor_pubkey: Some(actor_pubkey.to_owned()),
   5124             idempotency_digest_prefix: receipt.idempotency_digest_prefix.clone(),
   5125             migration_state: AppSdkMigrationState::Enqueued,
   5126             recorded_at: current_utc_timestamp(),
   5127             detail_json,
   5128         })
   5129     }
   5130 
   5131     fn record_app_sdk_migration_failure(
   5132         &self,
   5133         source_kind: AppSdkMigrationReceiptSourceKind,
   5134         source_record_id: &str,
   5135         operation_kind: &str,
   5136         actor_pubkey: Option<&str>,
   5137         detail_json: serde_json::Value,
   5138     ) -> Result<(), AppSqliteError> {
   5139         self.record_app_sdk_migration_receipt(AppSdkMigrationReceiptInput {
   5140             source_kind,
   5141             source_record_id: source_record_id.to_owned(),
   5142             sdk_operation_kind: operation_kind.to_owned(),
   5143             sdk_outbox_event_ids: Vec::new(),
   5144             expected_event_id: None,
   5145             actor_pubkey: actor_pubkey.map(str::to_owned),
   5146             idempotency_digest_prefix: None,
   5147             migration_state: AppSdkMigrationState::Failed,
   5148             recorded_at: current_utc_timestamp(),
   5149             detail_json,
   5150         })
   5151     }
   5152 
   5153     fn record_app_sdk_migration_receipt(
   5154         &self,
   5155         input: AppSdkMigrationReceiptInput,
   5156     ) -> Result<(), AppSqliteError> {
   5157         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   5158             return Ok(());
   5159         };
   5160         let _ = sqlite_store
   5161             .sdk_migration_receipt_repository()
   5162             .record_receipt(&input)?;
   5163         Ok(())
   5164     }
   5165 
   5166     fn selected_account_id(&self) -> Result<String, DesktopAppRuntimeFarmSetupError> {
   5167         self.selected_account_for_farm_setup()
   5168             .map(|account| account.account.account_id.clone())
   5169     }
   5170 
   5171     fn selected_account_id_for_farm_rules(
   5172         &self,
   5173     ) -> Result<String, DesktopAppRuntimeFarmRulesError> {
   5174         self.selected_account_for_farm_rules()
   5175             .map(|account| account.account.account_id.clone())
   5176     }
   5177 
   5178     fn selected_account_for_farm_setup(
   5179         &self,
   5180     ) -> Result<&radroots_app_view::SelectedAccountProjection, DesktopAppRuntimeFarmSetupError>
   5181     {
   5182         self.state_store
   5183             .identity_projection()
   5184             .selected_account
   5185             .as_ref()
   5186             .ok_or(DesktopAppRuntimeFarmSetupError::AccountRequired)
   5187     }
   5188 
   5189     fn selected_account_for_farm_rules(
   5190         &self,
   5191     ) -> Result<&radroots_app_view::SelectedAccountProjection, DesktopAppRuntimeFarmRulesError>
   5192     {
   5193         self.state_store
   5194             .identity_projection()
   5195             .selected_account
   5196             .as_ref()
   5197             .ok_or(DesktopAppRuntimeFarmRulesError::AccountRequired)
   5198     }
   5199 
   5200     fn accounts_manager(
   5201         &self,
   5202     ) -> Result<&RadrootsNostrAccountsManager, DesktopAppRuntimeCommandError> {
   5203         self.accounts_manager
   5204             .as_ref()
   5205             .ok_or_else(|| self.command_unavailable_error())
   5206     }
   5207 
   5208     fn sqlite_store(&self) -> Result<&AppSqliteStore, DesktopAppRuntimeCommandError> {
   5209         self.sqlite_store
   5210             .as_ref()
   5211             .ok_or(DesktopAppRuntimeCommandError::RuntimeUnavailable)
   5212     }
   5213 
   5214     fn sqlite_store_for_farm_setup(
   5215         &self,
   5216     ) -> Result<&AppSqliteStore, DesktopAppRuntimeFarmSetupError> {
   5217         self.sqlite_store
   5218             .as_ref()
   5219             .ok_or(DesktopAppRuntimeFarmSetupError::RuntimeUnavailable)
   5220     }
   5221 
   5222     fn sqlite_store_for_farm_rules(
   5223         &self,
   5224     ) -> Result<&AppSqliteStore, DesktopAppRuntimeFarmRulesError> {
   5225         self.sqlite_store
   5226             .as_ref()
   5227             .ok_or(DesktopAppRuntimeFarmRulesError::RuntimeUnavailable)
   5228     }
   5229 
   5230     fn mutate_personal_projection(
   5231         &mut self,
   5232         mutator: impl FnOnce(&mut PersonalWorkspaceProjection) -> bool,
   5233     ) -> bool {
   5234         let mut projection = self.state_store.personal_projection().clone();
   5235         if !mutator(&mut projection) {
   5236             return false;
   5237         }
   5238 
   5239         self.state_store
   5240             .apply_in_memory(AppStateCommand::replace_personal_projection(projection))
   5241     }
   5242 
   5243     fn set_personal_product_detail(
   5244         &mut self,
   5245         section: PersonalSection,
   5246         detail: Option<BuyerProductDetailProjection>,
   5247     ) -> bool {
   5248         self.mutate_personal_projection(|projection| {
   5249             let current_detail = match section {
   5250                 PersonalSection::Browse => &mut projection.browse.detail,
   5251                 PersonalSection::Search => &mut projection.search.detail,
   5252                 PersonalSection::Cart | PersonalSection::Orders => return false,
   5253             };
   5254             if *current_detail == detail {
   5255                 return false;
   5256             }
   5257 
   5258             *current_detail = detail;
   5259             true
   5260         })
   5261     }
   5262 
   5263     fn set_personal_order_detail(&mut self, detail: Option<BuyerOrderDetailProjection>) -> bool {
   5264         self.mutate_personal_projection(|projection| {
   5265             if projection.orders.detail == detail {
   5266                 return false;
   5267             }
   5268 
   5269             projection.orders.detail = detail;
   5270             true
   5271         })
   5272     }
   5273 
   5274     fn replace_personal_search_query(
   5275         &mut self,
   5276         query: BuyerSearchScreenQueryState,
   5277     ) -> Result<bool, AppSqliteError> {
   5278         let search_listings = self.load_personal_listings_for_query(&query)?;
   5279         let mut personal_projection = self.state_store.personal_projection().clone();
   5280 
   5281         if personal_projection.search.query == query
   5282             && personal_projection.search.listings == search_listings
   5283         {
   5284             return Ok(false);
   5285         }
   5286 
   5287         personal_projection.search.query = query;
   5288         personal_projection.search.listings = search_listings;
   5289 
   5290         Ok(self
   5291             .state_store
   5292             .apply_in_memory(AppStateCommand::replace_personal_projection(
   5293                 personal_projection,
   5294             )))
   5295     }
   5296 
   5297     fn load_personal_listings_for_query(
   5298         &self,
   5299         query: &BuyerSearchScreenQueryState,
   5300     ) -> Result<radroots_app_view::BuyerListingsProjection, AppSqliteError> {
   5301         let _ = self.import_shared_local_events()?;
   5302         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   5303             return Ok(Default::default());
   5304         };
   5305 
   5306         sqlite_store.load_buyer_listings(&query.search_query, &query.fulfillment_methods)
   5307     }
   5308 
   5309     fn refresh_personal_cart_and_order_review(
   5310         &mut self,
   5311         refreshed_cart: BuyerCartProjection,
   5312         refreshed_order_review: radroots_app_view::BuyerOrderReviewProjection,
   5313     ) -> bool {
   5314         self.mutate_personal_projection(|projection| {
   5315             let mut changed = false;
   5316             if projection.cart.cart != refreshed_cart {
   5317                 projection.cart.cart = refreshed_cart.clone();
   5318                 changed = true;
   5319             }
   5320             if projection.cart.order_review != refreshed_order_review {
   5321                 projection.cart.order_review = refreshed_order_review.clone();
   5322                 changed = true;
   5323             }
   5324 
   5325             changed
   5326         })
   5327     }
   5328 
   5329     fn replace_products_query(
   5330         &mut self,
   5331         query: ProductsScreenQueryState,
   5332     ) -> Result<bool, AppSqliteError> {
   5333         let products_list = self.load_products_list_for_query(&query)?;
   5334         let query_changed =
   5335             self.state_store
   5336                 .apply_in_memory(AppStateCommand::set_products_search_query(
   5337                     query.search_query.clone(),
   5338                 ));
   5339         let filter_changed = self
   5340             .state_store
   5341             .apply_in_memory(AppStateCommand::select_products_filter(query.filter));
   5342         let sort_changed = self
   5343             .state_store
   5344             .apply_in_memory(AppStateCommand::select_products_sort(query.sort));
   5345         let list_changed = self
   5346             .state_store
   5347             .apply_in_memory(AppStateCommand::replace_products_list(products_list));
   5348 
   5349         Ok(query_changed || filter_changed || sort_changed || list_changed)
   5350     }
   5351 
   5352     fn selected_farm_id(&self) -> Option<FarmId> {
   5353         selected_farm_id_from_context(
   5354             self.state_store.identity_projection(),
   5355             self.state_store.farm_setup_projection(),
   5356         )
   5357     }
   5358 
   5359     fn has_saved_farm(&self) -> bool {
   5360         self.state_store.farm_setup_projection().has_saved_farm()
   5361     }
   5362 
   5363     fn has_pack_day_context(&self) -> bool {
   5364         self.state_store
   5365             .pack_day_projection()
   5366             .projection
   5367             .fulfillment_window
   5368             .is_some()
   5369     }
   5370 
   5371     fn selected_product_editor_id(&self) -> Option<ProductId> {
   5372         match &self.state_store.products_projection().editor {
   5373             radroots_app_state::ProductEditorState::Open(session) => session.selected_product_id,
   5374             radroots_app_state::ProductEditorState::Closed => None,
   5375         }
   5376     }
   5377 
   5378     fn selected_order_detail_id(&self) -> Option<OrderId> {
   5379         self.state_store
   5380             .orders_projection()
   5381             .detail
   5382             .as_ref()
   5383             .map(|detail| detail.order_id)
   5384     }
   5385 
   5386     fn continuity_state(&self) -> PersistedAppState {
   5387         self.state_store.persisted_state().clone()
   5388     }
   5389 
   5390     fn continuity_state_with_order_detail(&self, order_id: Option<OrderId>) -> PersistedAppState {
   5391         let mut state = self.continuity_state();
   5392         state.seller.order_detail_order_id = order_id;
   5393         state
   5394     }
   5395 
   5396     fn continuity_state_with_orders_query(
   5397         &self,
   5398         query: OrdersScreenQueryState,
   5399         order_id: Option<OrderId>,
   5400     ) -> PersistedAppState {
   5401         let mut state = self.continuity_state();
   5402         state.seller.orders_query = query;
   5403         state.seller.order_detail_order_id = order_id;
   5404         state
   5405     }
   5406 
   5407     fn continuity_state_with_pack_day_query(
   5408         &self,
   5409         query: PackDayScreenQueryState,
   5410     ) -> PersistedAppState {
   5411         let mut state = self.continuity_state();
   5412         state.seller.pack_day_query = query;
   5413         state
   5414     }
   5415 
   5416     fn fallback_farm_profile(&self, farm_id: FarmId) -> FarmProfileRecord {
   5417         fallback_farm_profile_for_projection(farm_id, self.state_store.farm_setup_projection())
   5418     }
   5419 
   5420     fn load_products_list_for_query(
   5421         &self,
   5422         query: &ProductsScreenQueryState,
   5423     ) -> Result<ProductsListProjection, AppSqliteError> {
   5424         let _ = self.import_shared_local_events()?;
   5425         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   5426             return Ok(ProductsListProjection::default());
   5427         };
   5428         let Some(farm_id) = self.selected_farm_id() else {
   5429             return Ok(ProductsListProjection::default());
   5430         };
   5431 
   5432         sqlite_store.load_products(farm_id, &query.search_query, query.filter, query.sort)
   5433     }
   5434 
   5435     fn import_shared_local_events(&self) -> Result<AppLocalInteropImportReport, AppSqliteError> {
   5436         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   5437             return Ok(AppLocalInteropImportReport::default());
   5438         };
   5439         let Some(shared_accounts_paths) = self.shared_accounts_paths.as_ref() else {
   5440             return Ok(AppLocalInteropImportReport::default());
   5441         };
   5442         let Some(database_path) =
   5443             shared_local_events_database_path_from_shared_accounts(shared_accounts_paths)
   5444         else {
   5445             return Ok(AppLocalInteropImportReport::default());
   5446         };
   5447         sqlite_store.import_shared_local_events_from_path(database_path.as_path())
   5448     }
   5449 
   5450     fn resolve_seller_order_request_evidence(
   5451         &self,
   5452         order_id: OrderId,
   5453     ) -> Result<ResolvedAppSellerOrderRequest, AppSqliteError> {
   5454         let mut matched_requests = BTreeMap::new();
   5455         self.collect_seller_order_request_evidence_from_shared_events(
   5456             &order_id,
   5457             &mut matched_requests,
   5458         )?;
   5459         self.collect_seller_order_request_evidence_from_local_interop(
   5460             &order_id,
   5461             &mut matched_requests,
   5462         )?;
   5463 
   5464         if matched_requests.len() > 1 {
   5465             return Err(AppSqliteError::InvalidProjection {
   5466                 reason: "seller order decision found multiple signed order requests",
   5467             });
   5468         }
   5469 
   5470         matched_requests
   5471             .into_values()
   5472             .next()
   5473             .ok_or(AppSqliteError::InvalidProjection {
   5474                 reason: "seller order decision requires signed order request evidence",
   5475             })
   5476     }
   5477 
   5478     fn collect_seller_order_request_evidence_from_shared_events(
   5479         &self,
   5480         order_id: &OrderId,
   5481         matched_requests: &mut BTreeMap<String, ResolvedAppSellerOrderRequest>,
   5482     ) -> Result<(), AppSqliteError> {
   5483         let store = self.open_shared_local_events_store()?;
   5484         let Some(store) = store else {
   5485             return Ok(());
   5486         };
   5487         let mut before = None;
   5488 
   5489         loop {
   5490             let records = match before {
   5491                 Some((before_change_seq, before_seq)) => store
   5492                     .list_records_changed_before(
   5493                         before_change_seq,
   5494                         before_seq,
   5495                         APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE,
   5496                     )
   5497                     .map_err(|source| AppSqliteError::LocalEvents {
   5498                         operation: "load shared order request evidence",
   5499                         source,
   5500                     })?,
   5501                 None => store
   5502                     .list_records_changed_latest(APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE)
   5503                     .map_err(|source| AppSqliteError::LocalEvents {
   5504                         operation: "load shared order request evidence",
   5505                         source,
   5506                     })?,
   5507             };
   5508             if records.is_empty() {
   5509                 break;
   5510             }
   5511             let is_last_page =
   5512                 records.len() < APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE as usize;
   5513             before = records.last().map(|record| (record.change_seq, record.seq));
   5514 
   5515             for record in records {
   5516                 if record.family != LocalRecordFamily::SignedEvent
   5517                     || record.event_kind
   5518                         != Some(i64::from(
   5519                             radroots_sdk::protocol::order::RadrootsOrderEventType::OrderRequested
   5520                                 .kind(),
   5521                         ))
   5522                     || !signed_order_request_evidence_record_is_usable(&record)
   5523                 {
   5524                     continue;
   5525                 }
   5526                 let Some(event) = signed_event_from_local_record(&record)? else {
   5527                     continue;
   5528                 };
   5529                 let Ok(envelope) = radroots_sdk::protocol::order::parse_order_request(&event)
   5530                 else {
   5531                     continue;
   5532                 };
   5533                 insert_seller_order_request_evidence(
   5534                     order_id,
   5535                     &event,
   5536                     envelope.payload,
   5537                     matched_requests,
   5538                 );
   5539             }
   5540 
   5541             if before.is_none() || is_last_page {
   5542                 break;
   5543             }
   5544         }
   5545 
   5546         Ok(())
   5547     }
   5548 
   5549     fn collect_seller_order_request_evidence_from_local_interop(
   5550         &self,
   5551         order_id: &OrderId,
   5552         matched_requests: &mut BTreeMap<String, ResolvedAppSellerOrderRequest>,
   5553     ) -> Result<(), AppSqliteError> {
   5554         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   5555             return Ok(());
   5556         };
   5557         let events = sqlite_store.load_local_interop_signed_events_by_kind(i64::from(
   5558             radroots_sdk::protocol::order::RadrootsOrderEventType::OrderRequested.kind(),
   5559         ))?;
   5560 
   5561         for event in events {
   5562             let Ok(envelope) = radroots_sdk::protocol::order::parse_order_request(&event) else {
   5563                 continue;
   5564             };
   5565             insert_seller_order_request_evidence(
   5566                 order_id,
   5567                 &event,
   5568                 envelope.payload,
   5569                 matched_requests,
   5570             );
   5571         }
   5572 
   5573         Ok(())
   5574     }
   5575 
   5576     fn resolve_order_lifecycle_evidence(
   5577         &self,
   5578         request: &ResolvedAppSellerOrderRequest,
   5579     ) -> Result<ResolvedAppOrderLifecycleEvidence, AppSqliteError> {
   5580         let mut events = self.collect_order_lifecycle_signed_events()?;
   5581         events.sort_by(|left, right| {
   5582             left.created_at
   5583                 .cmp(&right.created_at)
   5584                 .then_with(|| left.id.cmp(&right.id))
   5585         });
   5586 
   5587         let mut evidence_events = vec![request.request_event.clone()];
   5588         let mut buckets = AppActiveOrderEvidenceBuckets::default();
   5589         let request_event_id =
   5590             active_order_event_id(request.request_event_id.as_str(), "request_event_id")?;
   5591         let request_author_pubkey = active_order_pubkey(
   5592             request.request_author_pubkey.as_str(),
   5593             "request_author_pubkey",
   5594         )?;
   5595         buckets.requests.push(RadrootsOrderRequestRecord {
   5596             event_id: request_event_id.clone(),
   5597             author_pubkey: request_author_pubkey,
   5598             payload: request.payload.clone(),
   5599         });
   5600         for event in events {
   5601             if trade_chain_tag_value(&event, "e_root").as_deref()
   5602                 != Some(request.request_event_id.as_str())
   5603             {
   5604                 continue;
   5605             }
   5606             evidence_events.push(event.clone());
   5607             let event_id = active_order_event_id(event.id.as_str(), "event_id")?;
   5608             let author_pubkey = active_order_pubkey(event.author.as_str(), "author_pubkey")?;
   5609             match event.kind {
   5610                 KIND_ORDER_DECISION => {
   5611                     let envelope = radroots_sdk::protocol::order::parse_order_decision(&event)
   5612                         .map_err(|_| AppSqliteError::InvalidProjection {
   5613                             reason: "order lifecycle evidence is invalid",
   5614                         })?;
   5615                     let context = active_order_event_record_context(&event, envelope.message_type)?;
   5616                     buckets.decisions.push(RadrootsOrderDecisionRecord {
   5617                         event_id,
   5618                         author_pubkey,
   5619                         counterparty_pubkey: context.0,
   5620                         root_event_id: context.1,
   5621                         prev_event_id: context.2,
   5622                         payload: envelope.payload,
   5623                     });
   5624                 }
   5625                 KIND_ORDER_REVISION_PROPOSAL => {
   5626                     let Ok(envelope) =
   5627                         radroots_sdk::protocol::order::parse_order_revision_proposal(&event)
   5628                     else {
   5629                         return Err(AppSqliteError::InvalidProjection {
   5630                             reason: "order lifecycle evidence is invalid",
   5631                         });
   5632                     };
   5633                     let context = active_order_event_record_context(&event, envelope.message_type)?;
   5634                     buckets
   5635                         .revision_proposals
   5636                         .push(RadrootsOrderRevisionProposalRecord {
   5637                             event_id,
   5638                             author_pubkey,
   5639                             counterparty_pubkey: context.0,
   5640                             root_event_id: context.1,
   5641                             prev_event_id: context.2,
   5642                             payload: envelope.payload,
   5643                         });
   5644                 }
   5645                 KIND_ORDER_REVISION_DECISION => {
   5646                     let Ok(envelope) =
   5647                         radroots_sdk::protocol::order::parse_order_revision_decision(&event)
   5648                     else {
   5649                         return Err(AppSqliteError::InvalidProjection {
   5650                             reason: "order lifecycle evidence is invalid",
   5651                         });
   5652                     };
   5653                     let context = active_order_event_record_context(&event, envelope.message_type)?;
   5654                     buckets
   5655                         .revision_decisions
   5656                         .push(RadrootsOrderRevisionDecisionRecord {
   5657                             event_id,
   5658                             author_pubkey,
   5659                             counterparty_pubkey: context.0,
   5660                             root_event_id: context.1,
   5661                             prev_event_id: context.2,
   5662                             payload: envelope.payload,
   5663                         });
   5664                 }
   5665                 KIND_ORDER_CANCELLATION => {
   5666                     let Ok(envelope) =
   5667                         radroots_sdk::protocol::order::parse_order_cancellation(&event)
   5668                     else {
   5669                         return Err(AppSqliteError::InvalidProjection {
   5670                             reason: "order lifecycle evidence is invalid",
   5671                         });
   5672                     };
   5673                     let context = active_order_event_record_context(&event, envelope.message_type)?;
   5674                     buckets.cancellations.push(RadrootsOrderCancellationRecord {
   5675                         event_id,
   5676                         author_pubkey,
   5677                         counterparty_pubkey: context.0,
   5678                         root_event_id: context.1,
   5679                         prev_event_id: context.2,
   5680                         payload: envelope.payload,
   5681                     });
   5682                 }
   5683                 _ => {}
   5684             }
   5685         }
   5686 
   5687         let projection = reduce_order_events(
   5688             &request.payload.order_id,
   5689             RadrootsOrderReductionInputs {
   5690                 requests: buckets.requests.clone(),
   5691                 decisions: buckets.decisions.clone(),
   5692                 revision_proposals: buckets.revision_proposals.clone(),
   5693                 revision_decisions: buckets.revision_decisions.clone(),
   5694                 cancellations: buckets.cancellations.clone(),
   5695             },
   5696         );
   5697         if !projection.issues.is_empty() || projection.status == RadrootsOrderStatus::Invalid {
   5698             return Err(AppSqliteError::InvalidProjection {
   5699                 reason: "order lifecycle evidence is invalid",
   5700             });
   5701         }
   5702         if projection.request_event_id.as_ref() != Some(&request_event_id) {
   5703             return Err(AppSqliteError::InvalidProjection {
   5704                 reason: "order lifecycle evidence is invalid",
   5705             });
   5706         }
   5707 
   5708         let decision = projection
   5709             .decision_event_id
   5710             .as_ref()
   5711             .map(|event_id| {
   5712                 buckets
   5713                     .decisions
   5714                     .iter()
   5715                     .find(|decision| decision.event_id == *event_id)
   5716                     .map(|decision| ResolvedAppOrderDecisionEvidence {
   5717                         event_id: decision.event_id.to_string(),
   5718                         payload: decision.payload.clone(),
   5719                     })
   5720                     .ok_or(AppSqliteError::InvalidProjection {
   5721                         reason: "order lifecycle evidence is invalid",
   5722                     })
   5723             })
   5724             .transpose()?;
   5725         Ok(ResolvedAppOrderLifecycleEvidence {
   5726             evidence_events,
   5727             request_event_id: request.request_event_id.clone(),
   5728             status: projection.status,
   5729             agreement_event_id: projection
   5730                 .agreement_event_id
   5731                 .map(|event_id| event_id.to_string()),
   5732             last_event_id: projection
   5733                 .last_event_id
   5734                 .map(|event_id| event_id.to_string()),
   5735             decision,
   5736             revision_proposals: buckets
   5737                 .revision_proposals
   5738                 .into_iter()
   5739                 .map(|proposal| ResolvedAppOrderRevisionProposalEvidence {
   5740                     event_id: proposal.event_id.to_string(),
   5741                     payload: proposal.payload,
   5742                 })
   5743                 .collect(),
   5744             revision_decisions: buckets
   5745                 .revision_decisions
   5746                 .into_iter()
   5747                 .map(|decision| ResolvedAppOrderRevisionDecisionEvidence {
   5748                     event_id: decision.event_id.to_string(),
   5749                     payload: decision.payload,
   5750                 })
   5751                 .collect(),
   5752             cancellation_event_id: projection
   5753                 .cancellation_event_id
   5754                 .map(|event_id| event_id.to_string()),
   5755         })
   5756     }
   5757 
   5758     fn collect_order_lifecycle_signed_events(
   5759         &self,
   5760     ) -> Result<Vec<SdkRadrootsNostrEvent>, AppSqliteError> {
   5761         let mut events = Vec::new();
   5762         let mut seen_event_ids = BTreeSet::new();
   5763         let kinds = [
   5764             KIND_ORDER_DECISION,
   5765             KIND_ORDER_REVISION_PROPOSAL,
   5766             KIND_ORDER_REVISION_DECISION,
   5767             KIND_ORDER_CANCELLATION,
   5768         ];
   5769 
   5770         if let Some(sqlite_store) = self.sqlite_store.as_ref() {
   5771             for kind in kinds {
   5772                 for event in
   5773                     sqlite_store.load_local_interop_signed_events_by_kind(i64::from(kind))?
   5774                 {
   5775                     if seen_event_ids.insert(event.id.clone()) {
   5776                         events.push(event);
   5777                     }
   5778                 }
   5779             }
   5780         }
   5781 
   5782         let Some(store) = self.open_shared_local_events_store()? else {
   5783             return Ok(events);
   5784         };
   5785         let mut before = None;
   5786         loop {
   5787             let records = match before {
   5788                 Some((before_change_seq, before_seq)) => store
   5789                     .list_records_changed_before(
   5790                         before_change_seq,
   5791                         before_seq,
   5792                         APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE,
   5793                     )
   5794                     .map_err(|source| AppSqliteError::LocalEvents {
   5795                         operation: "load shared order lifecycle evidence",
   5796                         source,
   5797                     })?,
   5798                 None => store
   5799                     .list_records_changed_latest(APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE)
   5800                     .map_err(|source| AppSqliteError::LocalEvents {
   5801                         operation: "load shared order lifecycle evidence",
   5802                         source,
   5803                     })?,
   5804             };
   5805             if records.is_empty() {
   5806                 break;
   5807             }
   5808             let is_last_page =
   5809                 records.len() < APP_SELLER_ORDER_DECISION_EVIDENCE_PAGE_SIZE as usize;
   5810             before = records.last().map(|record| (record.change_seq, record.seq));
   5811 
   5812             for record in records {
   5813                 let Some(kind) = record.event_kind else {
   5814                     continue;
   5815                 };
   5816                 if record.family != LocalRecordFamily::SignedEvent
   5817                     || !kinds.contains(&u32::try_from(kind).unwrap_or_default())
   5818                     || !signed_order_request_evidence_record_is_usable(&record)
   5819                 {
   5820                     continue;
   5821                 }
   5822                 let Some(event) = signed_event_from_local_record(&record)? else {
   5823                     continue;
   5824                 };
   5825                 if seen_event_ids.insert(event.id.clone()) {
   5826                     events.push(event);
   5827                 }
   5828             }
   5829 
   5830             if before.is_none() || is_last_page {
   5831                 break;
   5832             }
   5833         }
   5834 
   5835         Ok(events)
   5836     }
   5837 
   5838     fn open_shared_local_events_store(
   5839         &self,
   5840     ) -> Result<Option<LocalEventsStore<SqliteExecutor>>, AppSqliteError> {
   5841         let Some(shared_accounts_paths) = self.shared_accounts_paths.as_ref() else {
   5842             return Ok(None);
   5843         };
   5844         let Some(database_path) =
   5845             shared_local_events_database_path_from_shared_accounts(shared_accounts_paths)
   5846         else {
   5847             return Ok(None);
   5848         };
   5849         if let Some(parent) = database_path.parent() {
   5850             fs::create_dir_all(parent).map_err(|source| AppSqliteError::CreateParentDirectory {
   5851                 path: parent.to_path_buf(),
   5852                 source,
   5853             })?;
   5854         }
   5855         let executor = SqliteExecutor::open(database_path.as_path()).map_err(|source| {
   5856             AppSqliteError::LocalEventsSql {
   5857                 operation: "open shared local events database",
   5858                 source,
   5859             }
   5860         })?;
   5861         let store = LocalEventsStore::new(executor);
   5862         store
   5863             .migrate_up()
   5864             .map_err(|source| AppSqliteError::LocalEventsSql {
   5865                 operation: "migrate shared local events database",
   5866                 source,
   5867             })?;
   5868 
   5869         Ok(Some(store))
   5870     }
   5871 
   5872     fn append_app_farm_local_work_record(
   5873         &self,
   5874         account: &radroots_app_view::SelectedAccountProjection,
   5875         projection: &FarmSetupProjection,
   5876         saved_farm: &FarmSummary,
   5877     ) -> Result<Option<String>, AppSqliteError> {
   5878         let Some(shared_accounts_paths) = self.shared_accounts_paths.as_ref() else {
   5879             return Ok(None);
   5880         };
   5881         let timestamp = current_runtime_time_ms()?;
   5882         let farm_d_tag = d_tag_from_uuid(saved_farm.farm_id.as_uuid());
   5883         let owner_pubkey = self.local_events_owner_pubkey(account);
   5884         let exportability = local_work_exportability(owner_pubkey.as_deref());
   5885         let delivery_method = projection
   5886             .draft
   5887             .order_methods
   5888             .iter()
   5889             .next()
   5890             .map(|method| method.storage_key());
   5891         let payload = json!({
   5892             "record_kind": "farm_config_v1",
   5893             "scope": "app",
   5894             "exportability": exportability,
   5895             "document": {
   5896                 "version": 1,
   5897                 "selection": {
   5898                     "scope": "app",
   5899                     "account": account.account.account_id,
   5900                     "farm_d_tag": farm_d_tag,
   5901                 },
   5902                 "profile": {
   5903                     "name": saved_farm.display_name,
   5904                     "display_name": saved_farm.display_name,
   5905                 },
   5906                 "farm": {
   5907                     "d_tag": farm_d_tag,
   5908                     "name": saved_farm.display_name,
   5909                     "location": {
   5910                         "primary": projection.draft.location_or_service_area,
   5911                     },
   5912                 },
   5913                 "listing_defaults": {
   5914                     "delivery_method": delivery_method,
   5915                     "location": {
   5916                         "primary": projection.draft.location_or_service_area,
   5917                     },
   5918                 },
   5919             },
   5920         });
   5921         let record_id = format!("app:local_work:farm:{farm_d_tag}:{}", Uuid::now_v7());
   5922         let input = LocalEventRecordInput {
   5923             record_id: record_id.clone(),
   5924             family: LocalRecordFamily::LocalWork,
   5925             status: LocalRecordStatus::LocalSaved,
   5926             source_runtime: SourceRuntime::App,
   5927             created_at_ms: timestamp,
   5928             inserted_at_ms: timestamp,
   5929             owner_account_id: Some(account.account.account_id.clone()),
   5930             owner_pubkey,
   5931             farm_id: Some(farm_d_tag),
   5932             listing_addr: None,
   5933             local_work_json: Some(payload.clone()),
   5934             event_id: None,
   5935             event_kind: None,
   5936             event_pubkey: None,
   5937             event_created_at: None,
   5938             event_tags_json: None,
   5939             event_content: None,
   5940             event_sig: None,
   5941             raw_event_json: None,
   5942             outbox_status: PublishOutboxStatus::None,
   5943             relay_set_fingerprint: None,
   5944             relay_delivery_json: None,
   5945         };
   5946 
   5947         self.append_app_local_work_record(shared_accounts_paths, &input)?;
   5948         Ok(Some(record_id))
   5949     }
   5950 
   5951     fn append_app_listing_local_work_record(
   5952         &self,
   5953         product_id: ProductId,
   5954         draft: &ProductEditorDraft,
   5955     ) -> Result<Option<String>, AppSqliteError> {
   5956         let Some(shared_accounts_paths) = self.shared_accounts_paths.as_ref() else {
   5957             return Ok(None);
   5958         };
   5959         let Some(account) = self
   5960             .state_store
   5961             .identity_projection()
   5962             .selected_account
   5963             .as_ref()
   5964         else {
   5965             return Ok(None);
   5966         };
   5967         let Some(farm_id) = self.selected_farm_id() else {
   5968             return Ok(None);
   5969         };
   5970         let timestamp = current_runtime_time_ms()?;
   5971         let farm_d_tag = d_tag_from_uuid(farm_id.as_uuid());
   5972         let listing_d_tag = d_tag_from_uuid(product_id.as_uuid());
   5973         let primary_bin_id = listing_primary_bin_id(listing_d_tag.as_str());
   5974         let owner_pubkey = self.local_events_owner_pubkey(account);
   5975         let listing_addr = owner_pubkey
   5976             .as_ref()
   5977             .map(|pubkey| format!("{KIND_LISTING}:{pubkey}:{listing_d_tag}"));
   5978         let exportability = local_work_exportability(owner_pubkey.as_deref());
   5979         let farm_setup = self.state_store.farm_setup_projection();
   5980         let farm_rules = self.state_store.farm_rules_projection();
   5981         let delivery_method = listing_fulfillment_method(draft, farm_setup, farm_rules);
   5982         let location_primary = listing_fulfillment_location(draft, farm_setup, farm_rules);
   5983         let category = non_empty_string(draft.category.as_str());
   5984         let unit_label = non_empty_string(draft.unit_label.as_str());
   5985         let price_amount = draft.price_minor_units.map(decimal_from_minor_units);
   5986         let available = draft.stock_quantity.map(|value| value.to_string());
   5987         let publish_blockers = derive_product_publish_blockers(
   5988             draft,
   5989             self.state_store.farm_readiness_projection(),
   5990             farm_rules,
   5991         )
   5992         .into_iter()
   5993         .map(|blocker| blocker.storage_key())
   5994         .collect::<Vec<_>>();
   5995         let payload = json!({
   5996             "record_kind": "listing_draft_v1",
   5997             "exportability": exportability,
   5998             "publishability": {
   5999                 "state": if publish_blockers.is_empty() { "publishable" } else { "blocked" },
   6000                 "blockers": publish_blockers,
   6001             },
   6002             "document": {
   6003                 "version": 1,
   6004                 "kind": "listing_draft_v1",
   6005                 "listing": {
   6006                     "d_tag": listing_d_tag,
   6007                     "farm_d_tag": farm_d_tag,
   6008                 },
   6009                 "seller_actor": {
   6010                     "account_id": account.account.account_id,
   6011                     "pubkey": owner_pubkey.as_deref(),
   6012                     "source": "farm_config",
   6013                 },
   6014                 "product": {
   6015                     "key": listing_d_tag,
   6016                     "title": draft.title,
   6017                     "category": category,
   6018                     "summary": draft.subtitle,
   6019                 },
   6020                 "primary_bin": {
   6021                     "bin_id": primary_bin_id,
   6022                     "quantity_amount": "1",
   6023                     "quantity_unit": unit_label,
   6024                     "price_amount": price_amount,
   6025                     "price_currency": draft.price_currency,
   6026                     "price_per_amount": "1",
   6027                     "price_per_unit": unit_label,
   6028                 },
   6029                 "inventory": {
   6030                     "available": available,
   6031                 },
   6032                 "availability": {
   6033                     "kind": "local",
   6034                     "status": draft.status.storage_key(),
   6035                 },
   6036                 "delivery": {
   6037                     "method": delivery_method,
   6038                 },
   6039                 "location": {
   6040                     "primary": location_primary,
   6041                 },
   6042             },
   6043         });
   6044         let record_id = format!("app:local_work:listing:{listing_d_tag}:{}", Uuid::now_v7());
   6045         let input = LocalEventRecordInput {
   6046             record_id: record_id.clone(),
   6047             family: LocalRecordFamily::LocalWork,
   6048             status: LocalRecordStatus::LocalSaved,
   6049             source_runtime: SourceRuntime::App,
   6050             created_at_ms: timestamp,
   6051             inserted_at_ms: timestamp,
   6052             owner_account_id: Some(account.account.account_id.clone()),
   6053             owner_pubkey,
   6054             farm_id: Some(farm_d_tag),
   6055             listing_addr,
   6056             local_work_json: Some(payload.clone()),
   6057             event_id: None,
   6058             event_kind: None,
   6059             event_pubkey: None,
   6060             event_created_at: None,
   6061             event_tags_json: None,
   6062             event_content: None,
   6063             event_sig: None,
   6064             raw_event_json: None,
   6065             outbox_status: PublishOutboxStatus::None,
   6066             relay_set_fingerprint: None,
   6067             relay_delivery_json: None,
   6068         };
   6069 
   6070         self.append_app_local_work_record(shared_accounts_paths, &input)?;
   6071         Ok(Some(record_id))
   6072     }
   6073 
   6074     fn append_app_buyer_order_request_local_work_record(
   6075         &self,
   6076         sqlite_store: &AppSqliteStore,
   6077         buyer_context: &BuyerContext,
   6078         order: &BuyerOrderLocalEventExport,
   6079     ) -> Result<Option<AppOrderLocalWorkPublishSource>, AppSqliteError> {
   6080         let Some(shared_accounts_paths) = self.shared_accounts_paths.as_ref() else {
   6081             return Ok(None);
   6082         };
   6083         let timestamp = current_runtime_time_ms()?;
   6084         let record_id = buyer_order_request_local_work_record_id(
   6085             order.order_id.to_string().as_str(),
   6086         )
   6087         .map_err(|source| AppSqliteError::LocalEvents {
   6088             operation: "build app buyer order request record id",
   6089             source,
   6090         })?;
   6091         let buyer_account = self.selected_buyer_account(buyer_context);
   6092         let owner_account_id = buyer_account.map(|account| account.account.account_id.clone());
   6093         let buyer_pubkey =
   6094             buyer_account.and_then(|account| self.local_events_owner_pubkey(account));
   6095         let export = AppBuyerOrderRequestExport::from_order(order, buyer_pubkey.as_deref())?;
   6096         let payload = buyer_order_request_local_work_payload(
   6097             order,
   6098             buyer_context,
   6099             &record_id,
   6100             &export,
   6101             timestamp,
   6102         );
   6103         if sqlite_store.buyer_order_coordination_is_synced(buyer_context, order.order_id)? {
   6104             return Ok(Some(AppOrderLocalWorkPublishSource { record_id, payload }));
   6105         }
   6106         validate_buyer_order_request_local_work_payload(&payload).map_err(|source| {
   6107             AppSqliteError::LocalEvents {
   6108                 operation: "validate app buyer order request local work payload",
   6109                 source,
   6110             }
   6111         })?;
   6112         let payload_json =
   6113             serde_json::to_string(&payload).map_err(|_| AppSqliteError::InvalidProjection {
   6114                 reason: "buyer order request local work payload must encode",
   6115             })?;
   6116         sqlite_store.prepare_buyer_order_coordination_attempt(
   6117             buyer_context,
   6118             order.order_id,
   6119             record_id.as_str(),
   6120             payload_json.as_str(),
   6121         )?;
   6122         let input = LocalEventRecordInput {
   6123             record_id: record_id.clone(),
   6124             family: LocalRecordFamily::LocalWork,
   6125             status: LocalRecordStatus::LocalSaved,
   6126             source_runtime: SourceRuntime::App,
   6127             created_at_ms: timestamp,
   6128             inserted_at_ms: timestamp,
   6129             owner_account_id,
   6130             owner_pubkey: buyer_pubkey,
   6131             farm_id: export.farm_key.clone(),
   6132             listing_addr: export.listing_addr.clone(),
   6133             local_work_json: Some(payload.clone()),
   6134             event_id: None,
   6135             event_kind: None,
   6136             event_pubkey: None,
   6137             event_created_at: None,
   6138             event_tags_json: None,
   6139             event_content: None,
   6140             event_sig: None,
   6141             raw_event_json: None,
   6142             outbox_status: PublishOutboxStatus::None,
   6143             relay_set_fingerprint: None,
   6144             relay_delivery_json: None,
   6145         };
   6146 
   6147         if let Err(error) = self.append_app_local_work_record(shared_accounts_paths, &input) {
   6148             let failure_message = error.to_string();
   6149             let _ = sqlite_store.mark_buyer_order_coordination_failed(
   6150                 buyer_context,
   6151                 order.order_id,
   6152                 failure_message.as_str(),
   6153             );
   6154             return Err(error);
   6155         }
   6156         sqlite_store.mark_buyer_order_coordination_synced(buyer_context, order.order_id)?;
   6157         Ok(Some(AppOrderLocalWorkPublishSource { record_id, payload }))
   6158     }
   6159 
   6160     fn append_app_local_work_record(
   6161         &self,
   6162         shared_accounts_paths: &AppSharedAccountsPaths,
   6163         input: &LocalEventRecordInput,
   6164     ) -> Result<(), AppSqliteError> {
   6165         let Some(database_path) =
   6166             shared_local_events_database_path_from_shared_accounts(shared_accounts_paths)
   6167         else {
   6168             return Ok(());
   6169         };
   6170         if let Some(parent) = database_path.parent() {
   6171             fs::create_dir_all(parent).map_err(|source| AppSqliteError::CreateParentDirectory {
   6172                 path: parent.to_path_buf(),
   6173                 source,
   6174             })?;
   6175         }
   6176         let executor = SqliteExecutor::open(database_path.as_path()).map_err(|source| {
   6177             AppSqliteError::LocalEventsSql {
   6178                 operation: "open shared local events database",
   6179                 source,
   6180             }
   6181         })?;
   6182         let store = LocalEventsStore::new(executor);
   6183         store
   6184             .migrate_up()
   6185             .map_err(|source| AppSqliteError::LocalEventsSql {
   6186                 operation: "migrate shared local events database",
   6187                 source,
   6188             })?;
   6189         store
   6190             .append_record(input)
   6191             .map_err(|source| AppSqliteError::LocalEvents {
   6192                 operation: "append app local work record",
   6193                 source,
   6194             })?;
   6195         Ok(())
   6196     }
   6197 
   6198     fn record_published_sync_receipts(
   6199         &self,
   6200         receipts: &[AppPublishedOperationReceipt],
   6201     ) -> Result<(), AppSqliteError> {
   6202         if receipts.is_empty() {
   6203             return Ok(());
   6204         }
   6205         let Some(shared_accounts_paths) = self.shared_accounts_paths.as_ref() else {
   6206             return Ok(());
   6207         };
   6208         let Some(database_path) =
   6209             shared_local_events_database_path_from_shared_accounts(shared_accounts_paths)
   6210         else {
   6211             return Ok(());
   6212         };
   6213         if let Some(parent) = database_path.parent() {
   6214             fs::create_dir_all(parent).map_err(|source| AppSqliteError::CreateParentDirectory {
   6215                 path: parent.to_path_buf(),
   6216                 source,
   6217             })?;
   6218         }
   6219         let executor = SqliteExecutor::open(database_path.as_path()).map_err(|source| {
   6220             AppSqliteError::LocalEventsSql {
   6221                 operation: "open shared local events database",
   6222                 source,
   6223             }
   6224         })?;
   6225         let store = LocalEventsStore::new(executor);
   6226         store
   6227             .migrate_up()
   6228             .map_err(|source| AppSqliteError::LocalEventsSql {
   6229                 operation: "migrate shared local events database",
   6230                 source,
   6231             })?;
   6232         let timestamp = current_runtime_time_ms()?;
   6233 
   6234         for receipt in receipts {
   6235             let source_record = receipt
   6236                 .source_local_event_id
   6237                 .as_deref()
   6238                 .map(|source_record_id| {
   6239                     store.get_record(source_record_id).map_err(|source| {
   6240                         AppSqliteError::LocalEvents {
   6241                             operation: "load app publish source record",
   6242                             source,
   6243                         }
   6244                     })
   6245                 })
   6246                 .transpose()?
   6247                 .flatten();
   6248             if source_record
   6249                 .as_ref()
   6250                 .and_then(|record| record.owner_account_id.as_deref())
   6251                 .is_some_and(|owner_account_id| owner_account_id != receipt.source_account_id)
   6252             {
   6253                 return Err(AppSqliteError::InvalidProjection {
   6254                     reason: "published operation source account does not match local event owner",
   6255                 });
   6256             }
   6257             let farm_id = source_record
   6258                 .as_ref()
   6259                 .and_then(|record| record.farm_id.clone())
   6260                 .or_else(|| signed_event_farm_id(receipt));
   6261             let listing_addr = source_record
   6262                 .as_ref()
   6263                 .and_then(|record| record.listing_addr.clone())
   6264                 .or_else(|| receipt.listing_addr.clone())
   6265                 .or_else(|| signed_event_listing_addr(receipt));
   6266             let event_record = LocalEventRecordInput {
   6267                 record_id: format!("app:signed_event:{}", receipt.event_id),
   6268                 family: LocalRecordFamily::SignedEvent,
   6269                 status: LocalRecordStatus::Published,
   6270                 source_runtime: SourceRuntime::App,
   6271                 created_at_ms: i64::from(receipt.event_created_at) * 1_000,
   6272                 inserted_at_ms: timestamp,
   6273                 owner_account_id: Some(receipt.source_account_id.clone()),
   6274                 owner_pubkey: Some(receipt.event_pubkey.clone()),
   6275                 farm_id,
   6276                 listing_addr,
   6277                 local_work_json: None,
   6278                 event_id: Some(receipt.event_id.clone()),
   6279                 event_kind: Some(i64::from(receipt.event_kind)),
   6280                 event_pubkey: Some(receipt.event_pubkey.clone()),
   6281                 event_created_at: Some(i64::from(receipt.event_created_at)),
   6282                 event_tags_json: Some(receipt.event_tags_json.clone()),
   6283                 event_content: Some(receipt.event_content.clone()),
   6284                 event_sig: Some(receipt.event_sig.clone()),
   6285                 raw_event_json: Some(receipt.raw_event_json.clone()),
   6286                 outbox_status: PublishOutboxStatus::Acknowledged,
   6287                 relay_set_fingerprint: Some(receipt.relay_set_fingerprint.clone()),
   6288                 relay_delivery_json: Some(receipt.relay_delivery_json.clone()),
   6289             };
   6290             store
   6291                 .append_record(&event_record)
   6292                 .map_err(|source| AppSqliteError::LocalEvents {
   6293                     operation: "append app published event record",
   6294                     source,
   6295                 })?;
   6296 
   6297             if let Some(source_record_id) = receipt.source_local_event_id.as_deref() {
   6298                 let Some(source_record) = source_record.as_ref() else {
   6299                     continue;
   6300                 };
   6301                 if source_record.family == LocalRecordFamily::LocalWork {
   6302                     continue;
   6303                 }
   6304                 store
   6305                     .update_outbox(&LocalEventRecordUpdate {
   6306                         record_id: source_record_id.to_owned(),
   6307                         status: LocalRecordStatus::Published,
   6308                         outbox_status: PublishOutboxStatus::Acknowledged,
   6309                         relay_set_fingerprint: Some(receipt.relay_set_fingerprint.clone()),
   6310                         relay_delivery_json: Some(receipt.relay_delivery_json.clone()),
   6311                         updated_at_ms: timestamp,
   6312                     })
   6313                     .map_err(|source| AppSqliteError::LocalEvents {
   6314                         operation: "update app publish source evidence",
   6315                         source,
   6316                     })?;
   6317             }
   6318         }
   6319 
   6320         Ok(())
   6321     }
   6322 
   6323     fn local_events_owner_pubkey(
   6324         &self,
   6325         account: &radroots_app_view::SelectedAccountProjection,
   6326     ) -> Option<String> {
   6327         if is_hex_64(account.account.account_id.as_str()) {
   6328             return Some(account.account.account_id.clone());
   6329         }
   6330         self.accounts_manager
   6331             .as_ref()
   6332             .and_then(|manager| {
   6333                 manager
   6334                     .resolve_account_selector(account.account.account_id.as_str())
   6335                     .ok()
   6336             })
   6337             .map(|record| record.public_identity.public_key_hex)
   6338             .filter(|pubkey| is_hex_64(pubkey))
   6339     }
   6340 
   6341     fn selected_buyer_account(
   6342         &self,
   6343         buyer_context: &BuyerContext,
   6344     ) -> Option<&radroots_app_view::SelectedAccountProjection> {
   6345         let BuyerContext::Account(account_id) = buyer_context else {
   6346             return None;
   6347         };
   6348         self.state_store
   6349             .identity_projection()
   6350             .selected_account
   6351             .as_ref()
   6352             .filter(|account| account.account.account_id == *account_id)
   6353     }
   6354 
   6355     fn refresh_selected_account_context_after_local_events(
   6356         &mut self,
   6357     ) -> Result<bool, AppSqliteError> {
   6358         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   6359             return Ok(false);
   6360         };
   6361         let continuity_state = self.continuity_state();
   6362         let identity_projection = self.state_store.identity_projection().clone();
   6363         let selected_account_context =
   6364             load_selected_account_context(sqlite_store, &identity_projection, &continuity_state)?;
   6365 
   6366         Ok(self.apply_selected_account_context(&selected_account_context))
   6367     }
   6368 
   6369     fn sync_on_foreground_resume(&mut self) -> Result<bool, AppSqliteError> {
   6370         let report = self.import_shared_local_events()?;
   6371         let local_changed = report.imported_records > 0 || report.skipped_records > 0;
   6372         let context_changed = self.refresh_selected_account_context_after_local_events()?;
   6373         let coordination_changed = self.retry_pending_personal_order_coordination()?;
   6374         let sync_changed = self.attempt_sync(SyncTrigger::ForegroundResume)?;
   6375 
   6376         Ok(local_changed || context_changed || coordination_changed || sync_changed)
   6377     }
   6378 
   6379     fn replace_orders_query(
   6380         &mut self,
   6381         query: OrdersScreenQueryState,
   6382     ) -> Result<bool, AppSqliteError> {
   6383         let filter_changed = self
   6384             .state_store
   6385             .apply_in_memory(AppStateCommand::select_orders_filter(query.filter));
   6386         let fulfillment_window_changed =
   6387             self.state_store
   6388                 .apply_in_memory(AppStateCommand::select_orders_fulfillment_window(
   6389                     query.fulfillment_window_id,
   6390                 ));
   6391         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   6392             return Ok(filter_changed || fulfillment_window_changed);
   6393         };
   6394         let continuity_state = self.continuity_state_with_orders_query(query, None);
   6395         let selected_account_context = load_selected_account_context(
   6396             sqlite_store,
   6397             self.state_store.identity_projection(),
   6398             &continuity_state,
   6399         )?;
   6400         let context_changed = self.apply_selected_account_context(&selected_account_context);
   6401 
   6402         Ok(filter_changed || fulfillment_window_changed || context_changed)
   6403     }
   6404 
   6405     fn replace_pack_day_query(
   6406         &mut self,
   6407         query: PackDayScreenQueryState,
   6408     ) -> Result<bool, AppSqliteError> {
   6409         let previous_export_instance_id = self.current_pack_day_export_instance_id();
   6410         let fulfillment_window_changed =
   6411             self.state_store
   6412                 .apply_in_memory(AppStateCommand::set_pack_day_fulfillment_window(
   6413                     query.fulfillment_window_id,
   6414                 ));
   6415         let Some(sqlite_store) = self.sqlite_store.as_ref() else {
   6416             return Ok(fulfillment_window_changed);
   6417         };
   6418         let continuity_state = self.continuity_state_with_pack_day_query(query);
   6419         let selected_account_context = load_selected_account_context(
   6420             sqlite_store,
   6421             self.state_store.identity_projection(),
   6422             &continuity_state,
   6423         )?;
   6424         let context_changed = self.apply_selected_account_context(&selected_account_context);
   6425         self.cleanup_prepared_pack_day_print_assets_if_export_changed(
   6426             previous_export_instance_id,
   6427             "query_reset",
   6428         );
   6429 
   6430         Ok(fulfillment_window_changed || context_changed)
   6431     }
   6432     fn sync_truthful_farmer_section(&mut self) -> bool {
   6433         let selected_section = self.state_store.shell_projection().selected_section;
   6434         let should_reset_to_today = match selected_section {
   6435             ShellSection::Farmer(FarmerSection::Today) => false,
   6436             ShellSection::Farmer(FarmerSection::Products | FarmerSection::Orders) => {
   6437                 !self.has_saved_farm()
   6438             }
   6439             ShellSection::Farmer(FarmerSection::PackDay) => {
   6440                 !self.has_saved_farm() || !self.has_pack_day_context()
   6441             }
   6442             ShellSection::Farmer(FarmerSection::Farm) => true,
   6443             ShellSection::Home
   6444             | ShellSection::Account
   6445             | ShellSection::Personal(_)
   6446             | ShellSection::Settings(_) => false,
   6447         };
   6448 
   6449         should_reset_to_today
   6450             && self
   6451                 .state_store
   6452                 .apply_in_memory(AppStateCommand::SelectSection(ShellSection::Farmer(
   6453                     FarmerSection::Today,
   6454                 )))
   6455     }
   6456 
   6457     fn shared_accounts_paths(
   6458         &self,
   6459     ) -> Result<&AppSharedAccountsPaths, DesktopAppRuntimeCommandError> {
   6460         self.shared_accounts_paths
   6461             .as_ref()
   6462             .ok_or(DesktopAppRuntimeCommandError::RuntimeUnavailable)
   6463     }
   6464 
   6465     fn remote_signer_paths(&self) -> Option<&DesktopRemoteSignerPaths> {
   6466         self.remote_signer_paths.as_ref()
   6467     }
   6468 
   6469     fn load_startup_pending_remote_signer_session(
   6470         &self,
   6471     ) -> Result<Option<RadrootsAppRemoteSignerPendingSession>, DesktopAppRuntimeCommandError> {
   6472         let Some(paths) = self.remote_signer_paths() else {
   6473             return Ok(None);
   6474         };
   6475         Ok(load_pending_session(paths)?)
   6476     }
   6477 
   6478     fn store_startup_pending_remote_signer_session(
   6479         &mut self,
   6480         pending: &RadrootsAppRemoteSignerPendingSession,
   6481     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   6482         let Some(paths) = self.remote_signer_paths() else {
   6483             return Err(DesktopAppRuntimeCommandError::RuntimeUnavailable);
   6484         };
   6485         store_pending_session(paths, pending)?;
   6486         Ok(true)
   6487     }
   6488 
   6489     fn clear_startup_pending_remote_signer_session(
   6490         &mut self,
   6491     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   6492         let Some(paths) = self.remote_signer_paths() else {
   6493             return Ok(false);
   6494         };
   6495         clear_pending_session(paths)?;
   6496         Ok(true)
   6497     }
   6498 
   6499     fn activate_startup_approved_remote_signer_session(
   6500         &mut self,
   6501         pending: &RadrootsAppRemoteSignerPendingSession,
   6502         approved: &RadrootsAppRemoteSignerApprovedSession,
   6503     ) -> Result<bool, DesktopAppRuntimeCommandError> {
   6504         let Some(paths) = self.remote_signer_paths() else {
   6505             return Err(DesktopAppRuntimeCommandError::RuntimeUnavailable);
   6506         };
   6507         {
   6508             let accounts_manager = self.accounts_manager()?;
   6509             activate_pending_session(
   6510                 accounts_manager,
   6511                 paths,
   6512                 pending.record.client_account_id(),
   6513                 approved,
   6514             )?;
   6515         }
   6516         let projection = {
   6517             let accounts_manager = self.accounts_manager()?;
   6518             let sqlite_store = self.sqlite_store()?;
   6519             identity_projection_from_manager(accounts_manager, sqlite_store)?
   6520         };
   6521         self.replace_identity_projection(projection)
   6522     }
   6523 
   6524     fn decorate_identity_projection(
   6525         &self,
   6526         projection: AppIdentityProjection,
   6527     ) -> Result<AppIdentityProjection, DesktopAppRuntimeCommandError> {
   6528         let Some(paths) = self.remote_signer_paths() else {
   6529             return Ok(projection);
   6530         };
   6531         Ok(apply_remote_signer_custody(projection, paths)?)
   6532     }
   6533 
   6534     fn command_unavailable_error(&self) -> DesktopAppRuntimeCommandError {
   6535         let _ = self;
   6536         DesktopAppRuntimeCommandError::RuntimeUnavailable
   6537     }
   6538 
   6539     fn current_pack_day_export_bundle(&self) -> Option<PackDayExportBundle> {
   6540         let pack_day = self.state_store.pack_day_projection();
   6541         if pack_day.export.status != PackDayExportStatus::Succeeded {
   6542             return None;
   6543         }
   6544 
   6545         let bundle = pack_day.export.bundle.clone()?;
   6546         let fulfillment_window = pack_day.projection.fulfillment_window.as_ref()?;
   6547         (fulfillment_window.fulfillment_window_id == bundle.fulfillment_window_id).then_some(bundle)
   6548     }
   6549 
   6550     fn current_pack_day_export_instance_id(&self) -> Option<PackDayExportInstanceId> {
   6551         self.current_pack_day_export_bundle()
   6552             .map(|bundle| bundle.export_instance_id)
   6553     }
   6554 
   6555     fn cleanup_prepared_pack_day_print_assets_for_export_instance(
   6556         &self,
   6557         export_instance_id: PackDayExportInstanceId,
   6558         trigger: &'static str,
   6559     ) {
   6560         if let Err(error) =
   6561             cleanup_prepared_customer_label_assets_for_export_instance(export_instance_id)
   6562         {
   6563             error!(
   6564                 target: "pack_day",
   6565                 event = "pack_day.print_prepared_asset_cleanup_failed",
   6566                 trigger,
   6567                 export_instance_id = %export_instance_id,
   6568                 error = %error,
   6569                 "failed to clean prepared pack day print assets"
   6570             );
   6571         }
   6572     }
   6573 
   6574     fn cleanup_prepared_pack_day_print_assets_if_export_changed(
   6575         &self,
   6576         previous_export_instance_id: Option<PackDayExportInstanceId>,
   6577         trigger: &'static str,
   6578     ) {
   6579         let current_export_instance_id = self.current_pack_day_export_instance_id();
   6580         if let Some(export_instance_id) = previous_export_instance_id
   6581             .filter(|export_instance_id| Some(*export_instance_id) != current_export_instance_id)
   6582         {
   6583             self.cleanup_prepared_pack_day_print_assets_for_export_instance(
   6584                 export_instance_id,
   6585                 trigger,
   6586             );
   6587         }
   6588     }
   6589 
   6590     fn current_pack_day_host_handoff_request_matches(
   6591         &self,
   6592         request: &PackDayHostHandoffRequest,
   6593     ) -> bool {
   6594         let pack_day = self.state_store.pack_day_projection();
   6595         pack_day.host_handoff.status == PackDayHostHandoffStatus::Running
   6596             && pack_day.host_handoff.request.as_ref() == Some(request)
   6597     }
   6598 
   6599     fn current_pack_day_print_request_matches(&self, request: &PackDayPrintRequest) -> bool {
   6600         let pack_day = self.state_store.pack_day_projection();
   6601         pack_day.print.status == PackDayPrintStatus::Running
   6602             && pack_day.print.request.as_ref() == Some(request)
   6603     }
   6604 
   6605     fn current_pack_day_batch_print_request_matches(
   6606         &self,
   6607         request: &PackDayBatchPrintRequest,
   6608     ) -> bool {
   6609         let pack_day = self.state_store.pack_day_projection();
   6610         pack_day.batch_print.status == PackDayBatchPrintStatus::Running
   6611             && pack_day.batch_print.request.as_ref() == Some(request)
   6612     }
   6613 }
   6614 
   6615 #[derive(Debug, Error)]
   6616 pub enum DesktopAppRuntimeCommandError {
   6617     #[error("desktop runtime commands are unavailable while the runtime is degraded")]
   6618     RuntimeUnavailable,
   6619     #[error(transparent)]
   6620     Accounts(#[from] DesktopAccountsCommandError),
   6621     #[error(transparent)]
   6622     Projection(#[from] DesktopAccountsProjectionError),
   6623     #[error("remote signer command failed: {0}")]
   6624     RemoteSigner(String),
   6625     #[error(transparent)]
   6626     Sqlite(#[from] AppSqliteError),
   6627     #[error(transparent)]
   6628     PackDayExportWrite(#[from] PackDayExportWriteError),
   6629     #[error(transparent)]
   6630     PackDayHostHandoff(#[from] PackDayHostHandoffError),
   6631     #[error(transparent)]
   6632     PackDayPrint(#[from] PackDayPrintError),
   6633     #[error(transparent)]
   6634     PackDayBatchPrint(#[from] PackDayBatchPrintError),
   6635 }
   6636 
   6637 #[derive(Debug, Error)]
   6638 enum DesktopAppRuntimeProductPublishError {
   6639     #[error(transparent)]
   6640     Sqlite(#[from] AppSqliteError),
   6641     #[error("listing publish could not be queued through the SDK runtime")]
   6642     ListingPublishSdkEnqueueFailed,
   6643 }
   6644 
   6645 #[derive(Debug, Error)]
   6646 pub enum DesktopAppRuntimeProductEditorSaveError {
   6647     #[error(transparent)]
   6648     Sqlite(AppSqliteError),
   6649     #[error(
   6650         "product details were saved, but listing publish could not be queued through the SDK runtime"
   6651     )]
   6652     ListingPublishSdkEnqueueFailed,
   6653 }
   6654 
   6655 impl From<AppSqliteError> for DesktopAppRuntimeProductEditorSaveError {
   6656     fn from(error: AppSqliteError) -> Self {
   6657         Self::Sqlite(error)
   6658     }
   6659 }
   6660 
   6661 impl From<DesktopAppRuntimeProductPublishError> for DesktopAppRuntimeProductEditorSaveError {
   6662     fn from(error: DesktopAppRuntimeProductPublishError) -> Self {
   6663         match error {
   6664             DesktopAppRuntimeProductPublishError::Sqlite(error) => Self::Sqlite(error),
   6665             DesktopAppRuntimeProductPublishError::ListingPublishSdkEnqueueFailed => {
   6666                 Self::ListingPublishSdkEnqueueFailed
   6667             }
   6668         }
   6669     }
   6670 }
   6671 
   6672 impl DesktopAppRuntimeProductEditorSaveError {
   6673     pub fn is_listing_publish_sdk_enqueue_failed(&self) -> bool {
   6674         matches!(self, Self::ListingPublishSdkEnqueueFailed)
   6675     }
   6676 }
   6677 
   6678 #[derive(Debug, Error)]
   6679 pub enum DesktopAppRuntimeProductStockUpdateError {
   6680     #[error(transparent)]
   6681     Sqlite(AppSqliteError),
   6682     #[error("stock was saved, but listing publish could not be queued through the SDK runtime")]
   6683     ListingPublishSdkEnqueueFailed,
   6684 }
   6685 
   6686 impl From<AppSqliteError> for DesktopAppRuntimeProductStockUpdateError {
   6687     fn from(error: AppSqliteError) -> Self {
   6688         Self::Sqlite(error)
   6689     }
   6690 }
   6691 
   6692 impl From<DesktopAppRuntimeProductPublishError> for DesktopAppRuntimeProductStockUpdateError {
   6693     fn from(error: DesktopAppRuntimeProductPublishError) -> Self {
   6694         match error {
   6695             DesktopAppRuntimeProductPublishError::Sqlite(error) => Self::Sqlite(error),
   6696             DesktopAppRuntimeProductPublishError::ListingPublishSdkEnqueueFailed => {
   6697                 Self::ListingPublishSdkEnqueueFailed
   6698             }
   6699         }
   6700     }
   6701 }
   6702 
   6703 impl DesktopAppRuntimeProductStockUpdateError {
   6704     pub fn is_listing_publish_sdk_enqueue_failed(&self) -> bool {
   6705         matches!(self, Self::ListingPublishSdkEnqueueFailed)
   6706     }
   6707 }
   6708 
   6709 impl From<DesktopRemoteSignerError> for DesktopAppRuntimeCommandError {
   6710     fn from(error: DesktopRemoteSignerError) -> Self {
   6711         Self::RemoteSigner(error.to_string())
   6712     }
   6713 }
   6714 
   6715 #[derive(Debug, Error)]
   6716 pub enum DesktopAppRuntimeFarmSetupError {
   6717     #[error("desktop runtime commands are unavailable while the runtime is degraded")]
   6718     RuntimeUnavailable,
   6719     #[error("farm setup requires a selected account")]
   6720     AccountRequired,
   6721     #[error("farm setup is incomplete")]
   6722     IncompleteDraft,
   6723     #[error(transparent)]
   6724     Sqlite(#[from] AppSqliteError),
   6725 }
   6726 
   6727 #[derive(Debug, Error)]
   6728 pub enum DesktopAppRuntimeFarmRulesError {
   6729     #[error("desktop runtime commands are unavailable while the runtime is degraded")]
   6730     RuntimeUnavailable,
   6731     #[error("farm settings require a selected account")]
   6732     AccountRequired,
   6733     #[error("farm settings require a configured farm")]
   6734     FarmRequired,
   6735     #[error(transparent)]
   6736     Sqlite(#[from] AppSqliteError),
   6737 }
   6738 
   6739 #[derive(Debug, Error)]
   6740 enum DesktopAppRuntimeBootstrapError {
   6741     #[error(transparent)]
   6742     RuntimePaths(#[from] AppRuntimePathsError),
   6743     #[error(transparent)]
   6744     Accounts(#[from] DesktopAccountsBootstrapError),
   6745     #[error(transparent)]
   6746     Projection(#[from] DesktopAccountsProjectionError),
   6747     #[error(transparent)]
   6748     RemoteSigner(#[from] DesktopRemoteSignerError),
   6749     #[error(transparent)]
   6750     Sqlite(#[from] AppSqliteError),
   6751     #[error(transparent)]
   6752     State(#[from] AppStateStoreError),
   6753 }
   6754 
   6755 fn current_runtime_time_ms() -> Result<i64, AppSqliteError> {
   6756     let duration = SystemTime::now().duration_since(UNIX_EPOCH).map_err(|_| {
   6757         AppSqliteError::InvalidProjection {
   6758             reason: "current runtime timestamp must be after unix epoch",
   6759         }
   6760     })?;
   6761     i64::try_from(duration.as_millis()).map_err(|_| AppSqliteError::InvalidProjection {
   6762         reason: "current runtime timestamp must fit i64 milliseconds",
   6763     })
   6764 }
   6765 
   6766 fn current_runtime_time_seconds() -> Result<i64, AppSqliteError> {
   6767     let duration = SystemTime::now().duration_since(UNIX_EPOCH).map_err(|_| {
   6768         AppSqliteError::InvalidProjection {
   6769             reason: "current runtime timestamp must be after unix epoch",
   6770         }
   6771     })?;
   6772     i64::try_from(duration.as_secs()).map_err(|_| AppSqliteError::InvalidProjection {
   6773         reason: "current runtime timestamp must fit i64 seconds",
   6774     })
   6775 }
   6776 
   6777 fn normalized_app_sync_relay_urls(
   6778     relay_urls: &[String],
   6779 ) -> Result<Vec<String>, AppSyncTransportError> {
   6780     let normalized = radroots_local_events::normalize_relay_urls(relay_urls).map_err(|error| {
   6781         AppSyncTransportError::failed(format!("invalid direct relay app sync relay url: {error}"))
   6782     })?;
   6783     if normalized.is_empty() {
   6784         return Err(AppSyncTransportError::unavailable(
   6785             "direct relay app sync requires at least one configured relay",
   6786         ));
   6787     }
   6788     Ok(normalized)
   6789 }
   6790 
   6791 fn normalized_app_relay_ingest_urls(relay_urls: &[String]) -> Result<Vec<String>, AppSqliteError> {
   6792     let normalized = radroots_local_events::normalize_relay_urls(relay_urls).map_err(|_| {
   6793         AppSqliteError::InvalidProjection {
   6794             reason: "app relay ingest requires valid relay urls",
   6795         }
   6796     })?;
   6797     Ok(normalized)
   6798 }
   6799 
   6800 fn fetch_app_events_from_relays_windowed(
   6801     cursors: &[StoredRelayIngestCursor],
   6802 ) -> Result<AppDirectRelayFetchReceipt, AppSyncTransportError> {
   6803     let target_relays = cursors
   6804         .iter()
   6805         .map(|cursor| cursor.relay_url.clone())
   6806         .collect::<Vec<_>>();
   6807     let mut merged: Option<AppDirectRelayFetchReceipt> = None;
   6808 
   6809     for cursor in cursors {
   6810         match fetch_app_events_from_single_relay_windowed(cursor) {
   6811             Ok(receipt) => merge_app_direct_relay_fetch_receipt(&mut merged, receipt),
   6812             Err(error) => merge_app_direct_relay_fetch_receipt(
   6813                 &mut merged,
   6814                 AppDirectRelayFetchReceipt {
   6815                     target_relays: vec![cursor.relay_url.clone()],
   6816                     connected_relays: Vec::new(),
   6817                     failed_relays: vec![
   6818                         RelayDeliveryFailure::new(cursor.relay_url.clone(), error.to_string())
   6819                             .map_err(|source| AppSyncTransportError::failed(source.to_string()))?,
   6820                     ],
   6821                     fetched_relays: Vec::new(),
   6822                     event_observed_relays: BTreeMap::new(),
   6823                     events: Vec::new(),
   6824                 },
   6825             ),
   6826         }
   6827     }
   6828 
   6829     Ok(merged.unwrap_or_else(|| AppDirectRelayFetchReceipt {
   6830         target_relays,
   6831         connected_relays: Vec::new(),
   6832         failed_relays: Vec::new(),
   6833         fetched_relays: Vec::new(),
   6834         event_observed_relays: BTreeMap::new(),
   6835         events: Vec::new(),
   6836     }))
   6837 }
   6838 
   6839 fn fetch_app_events_from_single_relay_windowed(
   6840     cursor: &StoredRelayIngestCursor,
   6841 ) -> Result<AppDirectRelayFetchReceipt, AppSyncTransportError> {
   6842     let base_filter = direct_relay_ingest_filter_since(cursor.cursor_since_unix_seconds)?;
   6843     let mut next_filter = base_filter.clone();
   6844     let mut merged: Option<AppDirectRelayFetchReceipt> = None;
   6845 
   6846     for _ in 0..APP_DIRECT_RELAY_INGEST_MAX_PAGES {
   6847         let receipt = fetch_app_events_from_single_relay(cursor.relay_url.as_str(), next_filter)?;
   6848         let page_len = receipt.events.len();
   6849         let oldest_created_at = receipt
   6850             .events
   6851             .iter()
   6852             .map(|event| event.created_at.as_secs())
   6853             .min();
   6854         merge_app_direct_relay_fetch_receipt(&mut merged, receipt);
   6855         if page_len < APP_DIRECT_RELAY_INGEST_LIMIT {
   6856             break;
   6857         }
   6858         let Some(oldest_created_at) = oldest_created_at else {
   6859             break;
   6860         };
   6861         if cursor.cursor_since_unix_seconds.is_some_and(|since| {
   6862             i64::try_from(oldest_created_at).is_ok_and(|oldest| oldest <= since)
   6863         }) || oldest_created_at == 0
   6864         {
   6865             break;
   6866         }
   6867         next_filter = base_filter
   6868             .clone()
   6869             .until(RadrootsNostrTimestamp::from(oldest_created_at - 1))
   6870             .limit(APP_DIRECT_RELAY_INGEST_LIMIT);
   6871     }
   6872 
   6873     Ok(merged.unwrap_or_else(|| AppDirectRelayFetchReceipt {
   6874         target_relays: vec![cursor.relay_url.clone()],
   6875         connected_relays: Vec::new(),
   6876         failed_relays: Vec::new(),
   6877         fetched_relays: Vec::new(),
   6878         event_observed_relays: BTreeMap::new(),
   6879         events: Vec::new(),
   6880     }))
   6881 }
   6882 
   6883 fn fetch_app_events_from_single_relay(
   6884     relay_url: &str,
   6885     filter: RadrootsNostrFilter,
   6886 ) -> Result<AppDirectRelayFetchReceipt, AppSyncTransportError> {
   6887     let runtime = TokioRuntimeBuilder::new_current_thread()
   6888         .enable_all()
   6889         .build()
   6890         .map_err(|error| AppSyncTransportError::failed(error.to_string()))?;
   6891     runtime.block_on(fetch_app_events_from_single_relay_async(relay_url, filter))
   6892 }
   6893 
   6894 async fn fetch_app_events_from_single_relay_async(
   6895     relay_url: &str,
   6896     filter: RadrootsNostrFilter,
   6897 ) -> Result<AppDirectRelayFetchReceipt, AppSyncTransportError> {
   6898     let client = RadrootsNostrClient::new_signerless();
   6899 
   6900     client
   6901         .add_read_relay(relay_url)
   6902         .await
   6903         .map_err(|source| AppSyncTransportError::failed(source.to_string()))?;
   6904 
   6905     let connection_output = client.try_connect(APP_DIRECT_RELAY_CONNECT_TIMEOUT).await;
   6906     let failed_relays = direct_relay_failures_from_output(&connection_output)?;
   6907     if connection_output.success.is_empty() {
   6908         return Err(AppSyncTransportError::unavailable(format!(
   6909             "direct relay app ingest connection failed: {}",
   6910             summarize_app_relay_failures(&failed_relays)
   6911         )));
   6912     }
   6913 
   6914     let events = client
   6915         .fetch_events(
   6916             filter,
   6917             StdDuration::from_millis(APP_DIRECT_RELAY_SYNC_TIMEOUT_MS),
   6918         )
   6919         .await
   6920         .map_err(|source| AppSyncTransportError::failed(source.to_string()))?;
   6921     let last_event_created_at_unix_seconds = events
   6922         .iter()
   6923         .map(|event| relay_event_created_at_unix_seconds_for_fetch(event))
   6924         .collect::<Result<Vec<_>, _>>()?
   6925         .into_iter()
   6926         .max();
   6927     let mut event_observed_relays = BTreeMap::new();
   6928     for event in &events {
   6929         event_observed_relays.insert(event.id.to_hex(), vec![relay_url.to_owned()]);
   6930     }
   6931 
   6932     Ok(AppDirectRelayFetchReceipt {
   6933         target_relays: vec![relay_url.to_owned()],
   6934         connected_relays: connection_output
   6935             .success
   6936             .iter()
   6937             .map(ToString::to_string)
   6938             .collect(),
   6939         failed_relays,
   6940         fetched_relays: vec![AppDirectRelayFetchedRelay {
   6941             relay_url: relay_url.to_owned(),
   6942             last_event_created_at_unix_seconds,
   6943         }],
   6944         event_observed_relays,
   6945         events,
   6946     })
   6947 }
   6948 
   6949 fn direct_relay_ingest_filter() -> RadrootsNostrFilter {
   6950     RadrootsNostrFilter::new()
   6951         .kinds(
   6952             APP_DIRECT_RELAY_INGEST_KINDS
   6953                 .iter()
   6954                 .copied()
   6955                 .map(radroots_nostr_kind),
   6956         )
   6957         .limit(APP_DIRECT_RELAY_INGEST_LIMIT)
   6958 }
   6959 
   6960 fn direct_relay_ingest_filter_since(
   6961     since_unix_seconds: Option<i64>,
   6962 ) -> Result<RadrootsNostrFilter, AppSyncTransportError> {
   6963     let mut filter = direct_relay_ingest_filter();
   6964     if let Some(since_unix_seconds) = since_unix_seconds {
   6965         let since = u64::try_from(since_unix_seconds).map_err(|_| {
   6966             AppSyncTransportError::failed("relay ingest cursor must be non-negative")
   6967         })?;
   6968         filter = filter.since(RadrootsNostrTimestamp::from(since));
   6969     }
   6970     Ok(filter)
   6971 }
   6972 
   6973 fn direct_relay_failures_from_output<T: fmt::Debug>(
   6974     output: &RadrootsNostrOutput<T>,
   6975 ) -> Result<Vec<RelayDeliveryFailure>, AppSyncTransportError> {
   6976     output
   6977         .failed
   6978         .iter()
   6979         .map(|(relay, reason)| {
   6980             RelayDeliveryFailure::new(relay.to_string(), reason.to_string())
   6981                 .map_err(|source| AppSyncTransportError::failed(source.to_string()))
   6982         })
   6983         .collect()
   6984 }
   6985 
   6986 fn summarize_app_relay_failures(failed_relays: &[RelayDeliveryFailure]) -> String {
   6987     if failed_relays.is_empty() {
   6988         return "no relay acknowledged the operation".to_owned();
   6989     }
   6990 
   6991     failed_relays
   6992         .iter()
   6993         .map(|failure| format!("{}: {}", failure.relay_url, failure.error))
   6994         .collect::<Vec<_>>()
   6995         .join("; ")
   6996 }
   6997 
   6998 fn merge_app_direct_relay_fetch_receipt(
   6999     merged: &mut Option<AppDirectRelayFetchReceipt>,
   7000     receipt: AppDirectRelayFetchReceipt,
   7001 ) {
   7002     let Some(existing) = merged.as_mut() else {
   7003         *merged = Some(receipt);
   7004         return;
   7005     };
   7006 
   7007     append_unique_relays(&mut existing.target_relays, receipt.target_relays);
   7008     append_unique_relays(&mut existing.connected_relays, receipt.connected_relays);
   7009     for fetched_relay in receipt.fetched_relays {
   7010         if !existing
   7011             .fetched_relays
   7012             .iter()
   7013             .any(|known| known.relay_url == fetched_relay.relay_url)
   7014         {
   7015             existing.fetched_relays.push(fetched_relay);
   7016         }
   7017     }
   7018     for failure in receipt.failed_relays {
   7019         if !existing
   7020             .failed_relays
   7021             .iter()
   7022             .any(|known| known.relay_url == failure.relay_url && known.error == failure.error)
   7023         {
   7024             existing.failed_relays.push(failure);
   7025         }
   7026     }
   7027     for (event_id, relays) in receipt.event_observed_relays {
   7028         let observed = existing
   7029             .event_observed_relays
   7030             .entry(event_id)
   7031             .or_insert_with(Vec::new);
   7032         append_unique_relays(observed, relays);
   7033     }
   7034     let mut seen_event_ids = existing
   7035         .events
   7036         .iter()
   7037         .map(|event| event.id.to_hex())
   7038         .collect::<BTreeSet<_>>();
   7039     for event in receipt.events {
   7040         if seen_event_ids.insert(event.id.to_hex()) {
   7041             existing.events.push(event);
   7042         }
   7043     }
   7044 }
   7045 
   7046 fn append_unique_relays(target: &mut Vec<String>, relays: Vec<String>) {
   7047     for relay in relays {
   7048         if !target.iter().any(|known| known == &relay) {
   7049             target.push(relay);
   7050         }
   7051     }
   7052 }
   7053 
   7054 fn direct_relay_event_records(
   7055     receipt: &AppDirectRelayFetchReceipt,
   7056     inserted_at_ms: i64,
   7057 ) -> Result<Vec<LocalEventRecord>, AppDirectRelayIngestError> {
   7058     let mut records = Vec::with_capacity(receipt.events.len());
   7059 
   7060     for (index, event) in receipt.events.iter().enumerate() {
   7061         let event_id = event.id.to_hex();
   7062         let observed_relays = receipt
   7063             .event_observed_relays
   7064             .get(event_id.as_str())
   7065             .cloned()
   7066             .unwrap_or_default();
   7067         let delivery_evidence = RelayDeliveryEvidence::observed(
   7068             &receipt.target_relays,
   7069             &receipt.connected_relays,
   7070             observed_relays,
   7071             receipt.failed_relays.clone(),
   7072         )
   7073         .map_err(|source| AppSyncTransportError::failed(source.to_string()))?;
   7074         let relay_set_fingerprint = delivery_evidence.relay_set_fingerprint().ok_or_else(|| {
   7075             AppSyncTransportError::failed("app relay ingest requires a non-empty relay set")
   7076         })?;
   7077         let relay_delivery_json = delivery_evidence
   7078             .to_json_value()
   7079             .map_err(|source| AppSyncTransportError::failed(source.to_string()))?;
   7080         let tags = relay_event_tags(event);
   7081         let kind = relay_event_kind(event);
   7082         let event_pubkey = event.pubkey.to_string();
   7083         let listing_d_tag = relay_event_tag_value(&tags, "d", 1);
   7084         let farm_id = direct_relay_event_farm_id(kind, &tags);
   7085         let listing_addr =
   7086             direct_relay_event_listing_addr(kind, &event_pubkey, listing_d_tag.as_deref());
   7087         let created_at_ms = relay_event_created_at_ms(event)?;
   7088         let local_seq = created_at_ms.saturating_add(i64::try_from(index).map_err(|_| {
   7089             AppSqliteError::InvalidProjection {
   7090                 reason: "app relay ingest sequence must fit i64",
   7091             }
   7092         })?);
   7093         records.push(LocalEventRecord {
   7094             seq: local_seq,
   7095             change_seq: local_seq,
   7096             record_id: format!("app:relay_event:{event_id}"),
   7097             family: LocalRecordFamily::SignedEvent,
   7098             status: LocalRecordStatus::Published,
   7099             source_runtime: direct_relay_event_source_runtime(kind, listing_d_tag.as_deref()),
   7100             created_at_ms,
   7101             inserted_at_ms,
   7102             updated_at_ms: inserted_at_ms,
   7103             owner_account_id: None,
   7104             owner_pubkey: Some(event_pubkey.clone()),
   7105             farm_id,
   7106             listing_addr,
   7107             local_work_json: None,
   7108             event_id: Some(event_id),
   7109             event_kind: Some(i64::from(kind)),
   7110             event_pubkey: Some(event_pubkey),
   7111             event_created_at: Some(relay_event_created_at_i64(event)?),
   7112             event_tags_json: Some(json!(tags)),
   7113             event_content: Some(event.content.clone()),
   7114             event_sig: Some(event.sig.to_string()),
   7115             raw_event_json: Some(relay_raw_event_json(event)?),
   7116             outbox_status: PublishOutboxStatus::None,
   7117             relay_set_fingerprint: Some(relay_set_fingerprint.clone()),
   7118             relay_delivery_json: Some(relay_delivery_json.clone()),
   7119         });
   7120     }
   7121 
   7122     Ok(records)
   7123 }
   7124 
   7125 fn direct_relay_event_farm_id(kind: u16, tags: &[Vec<String>]) -> Option<String> {
   7126     match kind {
   7127         kind if kind == KIND_FARM as u16 => relay_event_tag_value(tags, "d", 1),
   7128         kind if kind == KIND_LISTING as u16 || kind == KIND_LISTING_DRAFT as u16 => {
   7129             relay_event_tag_value(tags, "a", 1).and_then(|address| relay_address_d_tag(&address))
   7130         }
   7131         _ => None,
   7132     }
   7133 }
   7134 
   7135 fn direct_relay_event_listing_addr(
   7136     kind: u16,
   7137     event_pubkey: &str,
   7138     listing_d_tag: Option<&str>,
   7139 ) -> Option<String> {
   7140     match kind {
   7141         kind if kind == KIND_LISTING as u16 || kind == KIND_LISTING_DRAFT as u16 => {
   7142             listing_d_tag.map(|d_tag| format!("{kind}:{event_pubkey}:{d_tag}"))
   7143         }
   7144         _ => None,
   7145     }
   7146 }
   7147 
   7148 fn direct_relay_event_source_runtime(_kind: u16, _d_tag: Option<&str>) -> SourceRuntime {
   7149     SourceRuntime::Network
   7150 }
   7151 
   7152 fn relay_event_kind(event: &RadrootsNostrEvent) -> u16 {
   7153     event.kind.as_u16()
   7154 }
   7155 
   7156 fn relay_event_created_at_i64(event: &RadrootsNostrEvent) -> Result<i64, AppSqliteError> {
   7157     i64::try_from(event.created_at.as_secs()).map_err(|_| AppSqliteError::InvalidProjection {
   7158         reason: "app relay ingest event timestamp must fit i64",
   7159     })
   7160 }
   7161 
   7162 fn relay_event_created_at_unix_seconds_for_fetch(
   7163     event: &RadrootsNostrEvent,
   7164 ) -> Result<i64, AppSyncTransportError> {
   7165     i64::try_from(event.created_at.as_secs())
   7166         .map_err(|_| AppSyncTransportError::failed("app relay ingest event timestamp must fit i64"))
   7167 }
   7168 
   7169 fn relay_event_created_at_ms(event: &RadrootsNostrEvent) -> Result<i64, AppSqliteError> {
   7170     relay_event_created_at_i64(event)?
   7171         .checked_mul(1_000)
   7172         .ok_or(AppSqliteError::InvalidProjection {
   7173             reason: "app relay ingest event timestamp milliseconds must fit i64",
   7174         })
   7175 }
   7176 
   7177 fn relay_event_tags(event: &RadrootsNostrEvent) -> Vec<Vec<String>> {
   7178     event
   7179         .tags
   7180         .iter()
   7181         .map(|tag| tag.as_slice().to_vec())
   7182         .collect()
   7183 }
   7184 
   7185 fn relay_event_tag_value(tags: &[Vec<String>], tag_name: &str, index: usize) -> Option<String> {
   7186     tags.iter().find_map(|tag| {
   7187         (tag.first().map(String::as_str) == Some(tag_name))
   7188             .then(|| tag.get(index))
   7189             .flatten()
   7190             .map(String::as_str)
   7191             .map(str::trim)
   7192             .filter(|value| !value.is_empty())
   7193             .map(str::to_owned)
   7194     })
   7195 }
   7196 
   7197 fn relay_raw_event_json(event: &RadrootsNostrEvent) -> Result<serde_json::Value, AppSqliteError> {
   7198     Ok(json!({
   7199         "id": event.id.to_hex(),
   7200         "pubkey": event.pubkey.to_string(),
   7201         "created_at": relay_event_created_at_i64(event)?,
   7202         "kind": u32::from(event.kind.as_u16()),
   7203         "tags": relay_event_tags(event),
   7204         "content": event.content.clone(),
   7205         "sig": event.sig.to_string(),
   7206     }))
   7207 }
   7208 
   7209 fn relay_address_d_tag(address: &str) -> Option<String> {
   7210     address
   7211         .rsplit(':')
   7212         .next()
   7213         .map(str::trim)
   7214         .filter(|value| !value.is_empty())
   7215         .map(str::to_owned)
   7216 }
   7217 
   7218 fn non_empty_string(value: &str) -> Option<String> {
   7219     let trimmed = value.trim();
   7220     (!trimmed.is_empty()).then(|| trimmed.to_owned())
   7221 }
   7222 
   7223 fn product_status_needs_relay_publish(status: ProductStatus) -> bool {
   7224     !matches!(status, ProductStatus::Draft)
   7225 }
   7226 
   7227 fn listing_primary_bin_id(listing_d_tag: &str) -> String {
   7228     format!("{listing_d_tag}:primary")
   7229 }
   7230 
   7231 fn listing_availability_window_times(
   7232     draft: &ProductEditorDraft,
   7233     farm_rules: &FarmRulesProjection,
   7234 ) -> (Option<String>, Option<String>) {
   7235     draft
   7236         .availability_window_id
   7237         .and_then(|window_id| {
   7238             farm_rules
   7239                 .fulfillment_windows
   7240                 .iter()
   7241                 .find(|window| window.fulfillment_window_id == window_id)
   7242         })
   7243         .map(|window| (Some(window.starts_at.clone()), Some(window.ends_at.clone())))
   7244         .unwrap_or((None, None))
   7245 }
   7246 
   7247 fn listing_fulfillment_method(
   7248     draft: &ProductEditorDraft,
   7249     farm_setup: &FarmSetupProjection,
   7250     farm_rules: &FarmRulesProjection,
   7251 ) -> Option<String> {
   7252     if draft.availability_window_id.is_some_and(|window_id| {
   7253         farm_rules
   7254             .fulfillment_windows
   7255             .iter()
   7256             .any(|window| window.fulfillment_window_id == window_id)
   7257     }) {
   7258         return Some(FarmOrderMethod::Pickup.storage_key().to_owned());
   7259     }
   7260 
   7261     farm_setup
   7262         .draft
   7263         .order_methods
   7264         .iter()
   7265         .next()
   7266         .map(|method| method.storage_key().to_owned())
   7267 }
   7268 
   7269 fn listing_fulfillment_location(
   7270     draft: &ProductEditorDraft,
   7271     farm_setup: &FarmSetupProjection,
   7272     farm_rules: &FarmRulesProjection,
   7273 ) -> Option<String> {
   7274     draft
   7275         .availability_window_id
   7276         .and_then(|window_id| {
   7277             farm_rules
   7278                 .fulfillment_windows
   7279                 .iter()
   7280                 .find(|window| window.fulfillment_window_id == window_id)
   7281         })
   7282         .and_then(|window| {
   7283             farm_rules
   7284                 .pickup_locations
   7285                 .iter()
   7286                 .find(|location| location.pickup_location_id == window.pickup_location_id)
   7287         })
   7288         .and_then(|location| {
   7289             non_empty_string(location.address_line.as_str())
   7290                 .or_else(|| non_empty_string(location.label.as_str()))
   7291         })
   7292         .or_else(|| non_empty_string(farm_setup.draft.location_or_service_area.as_str()))
   7293 }
   7294 
   7295 fn farm_profile_publish_payload_to_sdk_farm(
   7296     payload: &AppFarmProfilePublishPayload,
   7297 ) -> RadrootsFarm {
   7298     RadrootsFarm {
   7299         d_tag: d_tag_from_uuid(payload.farm_id.as_uuid()),
   7300         name: payload.display_name.trim().to_owned(),
   7301         about: None,
   7302         website: None,
   7303         picture: None,
   7304         banner: None,
   7305         location: None,
   7306         tags: payload.readiness.map(|readiness| match readiness {
   7307             FarmReadiness::Incomplete => vec!["radroots:readiness:incomplete".to_owned()],
   7308             FarmReadiness::Ready => vec!["radroots:readiness:ready".to_owned()],
   7309         }),
   7310     }
   7311 }
   7312 
   7313 fn farm_publish_source_record(
   7314     farm_id: FarmId,
   7315     source: &str,
   7316     source_local_event_id: Option<&str>,
   7317 ) -> (AppSdkMigrationReceiptSourceKind, String) {
   7318     source_local_event_id
   7319         .map(|record_id| {
   7320             (
   7321                 AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
   7322                 record_id.to_owned(),
   7323             )
   7324         })
   7325         .unwrap_or_else(|| {
   7326             (
   7327                 AppSdkMigrationReceiptSourceKind::LocalOutbox,
   7328                 format!("app:farm_publish:{farm_id}:{source}"),
   7329             )
   7330         })
   7331 }
   7332 
   7333 fn listing_publish_source_record(
   7334     product_id: ProductId,
   7335     source: &str,
   7336     source_local_event_id: Option<&str>,
   7337 ) -> (AppSdkMigrationReceiptSourceKind, String) {
   7338     source_local_event_id
   7339         .map(|record_id| {
   7340             (
   7341                 AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
   7342                 record_id.to_owned(),
   7343             )
   7344         })
   7345         .unwrap_or_else(|| {
   7346             (
   7347                 AppSdkMigrationReceiptSourceKind::LocalOutbox,
   7348                 format!("app:listing_publish:{product_id}:{source}"),
   7349             )
   7350         })
   7351 }
   7352 
   7353 fn order_decision_sdk_source_record_id(payload: &AppOrderDecisionPublishPayload) -> String {
   7354     format!("app:order_decision:{}", payload.app_order_id)
   7355 }
   7356 
   7357 fn order_revision_proposal_sdk_source_record_id(
   7358     payload: &AppOrderRevisionProposalPublishPayload,
   7359 ) -> String {
   7360     format!(
   7361         "app:order_revision_proposal:{}:{}",
   7362         payload.app_order_id, payload.revision_id
   7363     )
   7364 }
   7365 
   7366 fn order_revision_decision_sdk_source_record_id(
   7367     payload: &AppOrderRevisionDecisionPublishPayload,
   7368 ) -> String {
   7369     format!(
   7370         "app:order_revision_decision:{}:{}",
   7371         payload.app_order_id, payload.revision_id
   7372     )
   7373 }
   7374 
   7375 fn order_cancellation_sdk_source_record_id(payload: &AppOrderCancellationPublishPayload) -> String {
   7376     format!("app:order_cancellation:{}", payload.app_order_id)
   7377 }
   7378 
   7379 fn sdk_relay_url_policy_for_targets(target_relays: &[String]) -> AppSdkRelayUrlPolicy {
   7380     if target_relays
   7381         .iter()
   7382         .any(|relay_url| relay_url.trim().starts_with("ws://"))
   7383     {
   7384         AppSdkRelayUrlPolicy::Localhost
   7385     } else {
   7386         AppSdkRelayUrlPolicy::Public
   7387     }
   7388 }
   7389 
   7390 fn sdk_idempotency_key(source_record_id: &str) -> String {
   7391     format!(
   7392         "app-{}",
   7393         Uuid::new_v5(&Uuid::NAMESPACE_URL, source_record_id.as_bytes())
   7394     )
   7395 }
   7396 
   7397 fn sdk_runtime_unavailable_error() -> AppSdkRuntimeError {
   7398     AppSdkRuntimeError::CommandFailed(AppSdkRuntimeIssue {
   7399         code: "sdk_runtime_not_available".to_owned(),
   7400         class: "runtime".to_owned(),
   7401         retryable: true,
   7402         message: "app SDK runtime is not available".to_owned(),
   7403         recovery_actions: vec!["retry_startup".to_owned()],
   7404         detail_json: json!({
   7405             "code": "sdk_runtime_not_available",
   7406             "class": "runtime",
   7407             "retryable": true,
   7408             "recovery_actions": ["retry_startup"],
   7409         }),
   7410     })
   7411 }
   7412 
   7413 fn sync_transport_error_from_sdk_runtime_error(error: AppSdkRuntimeError) -> AppSyncTransportError {
   7414     AppSyncTransportError::failed(sdk_runtime_error_detail_json(&error).to_string())
   7415 }
   7416 
   7417 fn sdk_runtime_error_detail_json(error: &AppSdkRuntimeError) -> serde_json::Value {
   7418     match error {
   7419         AppSdkRuntimeError::CommandFailed(issue) => issue.detail_json.clone(),
   7420         AppSdkRuntimeError::CommandQueueCapacityZero => json!({
   7421             "code": "sdk_command_queue_capacity_zero",
   7422             "class": "runtime",
   7423             "retryable": false,
   7424             "message": error.to_string(),
   7425             "recovery_actions": ["review_runtime_configuration"],
   7426         }),
   7427         AppSdkRuntimeError::WorkerSpawn(_) => json!({
   7428             "code": "sdk_worker_spawn_failed",
   7429             "class": "runtime",
   7430             "retryable": true,
   7431             "message": error.to_string(),
   7432             "recovery_actions": ["retry_startup"],
   7433         }),
   7434         AppSdkRuntimeError::CommandQueueFull => json!({
   7435             "code": "sdk_command_queue_full",
   7436             "class": "runtime",
   7437             "retryable": true,
   7438             "message": error.to_string(),
   7439             "recovery_actions": ["retry_command"],
   7440         }),
   7441         AppSdkRuntimeError::CommandQueueClosed => json!({
   7442             "code": "sdk_command_queue_closed",
   7443             "class": "runtime",
   7444             "retryable": true,
   7445             "message": error.to_string(),
   7446             "recovery_actions": ["restart_runtime"],
   7447         }),
   7448         AppSdkRuntimeError::CommandResponseClosed => json!({
   7449             "code": "sdk_command_response_closed",
   7450             "class": "runtime",
   7451             "retryable": true,
   7452             "message": error.to_string(),
   7453             "recovery_actions": ["restart_runtime"],
   7454         }),
   7455         AppSdkRuntimeError::ShutdownAck => json!({
   7456             "code": "sdk_shutdown_ack_failed",
   7457             "class": "runtime",
   7458             "retryable": true,
   7459             "message": error.to_string(),
   7460             "recovery_actions": ["restart_runtime"],
   7461         }),
   7462         AppSdkRuntimeError::WorkerJoin => json!({
   7463             "code": "sdk_worker_join_failed",
   7464             "class": "runtime",
   7465             "retryable": true,
   7466             "message": error.to_string(),
   7467             "recovery_actions": ["restart_runtime"],
   7468         }),
   7469     }
   7470 }
   7471 
   7472 fn sync_transport_error_detail_json(error: &AppSyncTransportError) -> serde_json::Value {
   7473     match error {
   7474         AppSyncTransportError::Unavailable { message } => json!({
   7475             "code": "app_sync_transport_unavailable",
   7476             "class": "runtime",
   7477             "retryable": true,
   7478             "message": message,
   7479             "recovery_actions": ["retry_after_runtime_ready"],
   7480         }),
   7481         AppSyncTransportError::Failed { message } => serde_json::from_str(message.as_str())
   7482             .unwrap_or_else(|_| {
   7483                 json!({
   7484                     "code": "app_sync_transport_failed",
   7485                     "class": "operation",
   7486                     "retryable": true,
   7487                     "message": message,
   7488                     "recovery_actions": ["retry_publish"],
   7489                 })
   7490             }),
   7491     }
   7492 }
   7493 
   7494 fn listing_publish_payload_to_sdk_listing(
   7495     payload: &AppListingPublishPayload,
   7496 ) -> Result<RadrootsListing, AppSyncTransportError> {
   7497     let currency = payload
   7498         .price_currency
   7499         .parse::<RadrootsCoreCurrency>()
   7500         .map_err(|error| AppSyncTransportError::failed(error.to_string()))?;
   7501     let unit = parse_app_listing_unit(payload.unit_label.as_str())?;
   7502     let price_minor_units = payload.price_minor_units.ok_or_else(|| {
   7503         AppSyncTransportError::failed("publishable listing requires price minor units")
   7504     })?;
   7505     let farm_id = payload
   7506         .farm_id
   7507         .ok_or_else(|| AppSyncTransportError::failed("publishable listing requires farm id"))?;
   7508     let farm_pubkey = payload
   7509         .farm_pubkey
   7510         .as_deref()
   7511         .ok_or_else(|| AppSyncTransportError::failed("publishable listing requires farm pubkey"))?
   7512         .trim()
   7513         .to_owned();
   7514     let d_tag = payload
   7515         .listing_d_tag
   7516         .as_deref()
   7517         .filter(|value| !value.trim().is_empty())
   7518         .map(str::to_owned)
   7519         .unwrap_or_else(|| d_tag_from_uuid(payload.product_id.as_uuid()));
   7520     let d_tag = RadrootsDTag::parse(d_tag.as_str())
   7521         .map_err(|error| AppSyncTransportError::failed(error.to_string()))?;
   7522     let bin_id = RadrootsInventoryBinId::parse(listing_primary_bin_id(d_tag.as_str()))
   7523         .map_err(|error| AppSyncTransportError::failed(error.to_string()))?;
   7524 
   7525     Ok(RadrootsListing {
   7526         d_tag,
   7527         farm: RadrootsFarmRef {
   7528             pubkey: farm_pubkey,
   7529             d_tag: payload
   7530                 .farm_d_tag
   7531                 .as_deref()
   7532                 .filter(|value| !value.trim().is_empty())
   7533                 .map(str::to_owned)
   7534                 .unwrap_or_else(|| d_tag_from_uuid(farm_id.as_uuid())),
   7535         },
   7536         product: RadrootsListingProduct {
   7537             key: payload.product_id.to_string(),
   7538             title: payload.title.trim().to_owned(),
   7539             category: payload
   7540                 .category
   7541                 .as_deref()
   7542                 .unwrap_or_default()
   7543                 .trim()
   7544                 .to_owned(),
   7545             summary: payload
   7546                 .subtitle
   7547                 .as_deref()
   7548                 .filter(|value| !value.trim().is_empty())
   7549                 .map(str::to_owned),
   7550             process: None,
   7551             lot: None,
   7552             location: None,
   7553             profile: None,
   7554             year: None,
   7555         },
   7556         primary_bin_id: bin_id.clone(),
   7557         bins: vec![RadrootsListingBin {
   7558             bin_id,
   7559             quantity: RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), unit),
   7560             price_per_canonical_unit: RadrootsCoreQuantityPrice::new(
   7561                 RadrootsCoreMoney::from_minor_units_u32(price_minor_units, currency),
   7562                 RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), unit),
   7563             ),
   7564             display_amount: Some(RadrootsCoreDecimal::from(1u32)),
   7565             display_unit: Some(unit),
   7566             display_label: Some(payload.unit_label.trim().to_owned()),
   7567             display_price: Some(RadrootsCoreMoney::from_minor_units_u32(
   7568                 price_minor_units,
   7569                 currency,
   7570             )),
   7571             display_price_unit: Some(unit),
   7572         }],
   7573         resource_area: None,
   7574         plot: None,
   7575         discounts: None,
   7576         inventory_available: payload.stock_quantity.map(RadrootsCoreDecimal::from),
   7577         availability: listing_publish_payload_availability(payload)?,
   7578         delivery_method: Some(parse_app_listing_delivery_method(
   7579             payload.fulfillment_method.as_deref().unwrap_or_default(),
   7580         )?),
   7581         location: payload
   7582             .fulfillment_location
   7583             .as_deref()
   7584             .filter(|value| !value.trim().is_empty())
   7585             .map(|primary| RadrootsListingLocation {
   7586                 primary: primary.trim().to_owned(),
   7587                 city: None,
   7588                 region: None,
   7589                 country: None,
   7590                 lat: None,
   7591                 lng: None,
   7592                 geohash: None,
   7593             }),
   7594         published_at: None,
   7595         images: None,
   7596     })
   7597 }
   7598 
   7599 fn listing_publish_payload_availability(
   7600     payload: &AppListingPublishPayload,
   7601 ) -> Result<Option<RadrootsListingAvailability>, AppSyncTransportError> {
   7602     if payload.status == ProductStatus::Published {
   7603         let start = parse_listing_availability_timestamp(
   7604             payload.availability_starts_at.as_deref(),
   7605             "publishable listing requires availability start",
   7606         )?;
   7607         let end = parse_listing_availability_timestamp(
   7608             payload.availability_ends_at.as_deref(),
   7609             "publishable listing requires availability end",
   7610         )?;
   7611         if end <= start {
   7612             return Err(AppSyncTransportError::failed(
   7613                 "publishable listing availability end must be after start",
   7614             ));
   7615         }
   7616         return Ok(Some(RadrootsListingAvailability::Window {
   7617             start: Some(start),
   7618             end: Some(end),
   7619         }));
   7620     }
   7621 
   7622     Ok(Some(RadrootsListingAvailability::Status {
   7623         status: match payload.status {
   7624             ProductStatus::Archived => RadrootsListingStatus::Sold,
   7625             other => RadrootsListingStatus::Other {
   7626                 value: other.storage_key().to_owned(),
   7627             },
   7628         },
   7629     }))
   7630 }
   7631 
   7632 fn parse_listing_availability_timestamp(
   7633     value: Option<&str>,
   7634     missing_message: &'static str,
   7635 ) -> Result<u64, AppSyncTransportError> {
   7636     let value = value
   7637         .map(str::trim)
   7638         .filter(|value| !value.is_empty())
   7639         .ok_or_else(|| AppSyncTransportError::failed(missing_message))?;
   7640     let timestamp = DateTime::parse_from_rfc3339(value)
   7641         .map_err(|error| AppSyncTransportError::failed(error.to_string()))?
   7642         .timestamp();
   7643     u64::try_from(timestamp)
   7644         .map_err(|_| AppSyncTransportError::failed("listing availability timestamp is negative"))
   7645 }
   7646 
   7647 fn parse_app_listing_unit(value: &str) -> Result<RadrootsCoreUnit, AppSyncTransportError> {
   7648     match value.trim().to_ascii_lowercase().as_str() {
   7649         "each" | "ea" | "unit" | "units" => Ok(RadrootsCoreUnit::Each),
   7650         "kg" | "kilogram" | "kilograms" => Ok(RadrootsCoreUnit::MassKg),
   7651         "g" | "gram" | "grams" => Ok(RadrootsCoreUnit::MassG),
   7652         "oz" | "ounce" | "ounces" => Ok(RadrootsCoreUnit::MassOz),
   7653         "lb" | "pound" | "pounds" => Ok(RadrootsCoreUnit::MassLb),
   7654         "l" | "liter" | "liters" => Ok(RadrootsCoreUnit::VolumeL),
   7655         "ml" | "milliliter" | "milliliters" => Ok(RadrootsCoreUnit::VolumeMl),
   7656         other => Err(AppSyncTransportError::failed(format!(
   7657             "unsupported listing unit `{other}`"
   7658         ))),
   7659     }
   7660 }
   7661 
   7662 fn parse_app_listing_delivery_method(
   7663     value: &str,
   7664 ) -> Result<RadrootsListingDeliveryMethod, AppSyncTransportError> {
   7665     match value.trim().to_ascii_lowercase().as_str() {
   7666         "pickup" | "local_pickup" => Ok(RadrootsListingDeliveryMethod::Pickup),
   7667         "delivery" | "local_delivery" => Ok(RadrootsListingDeliveryMethod::LocalDelivery),
   7668         "shipping" | "ship" => Ok(RadrootsListingDeliveryMethod::Shipping),
   7669         "" => Err(AppSyncTransportError::failed(
   7670             "publishable listing requires fulfillment method",
   7671         )),
   7672         other => Ok(RadrootsListingDeliveryMethod::Other {
   7673             method: other.to_owned(),
   7674         }),
   7675     }
   7676 }
   7677 
   7678 fn order_request_sdk_listing_event_ptr(
   7679     payload: &AppOrderRequestPublishPayload,
   7680 ) -> Result<RadrootsNostrEventPtr, AppSyncTransportError> {
   7681     let listing_event_id = payload
   7682         .listing_event_id
   7683         .as_deref()
   7684         .ok_or_else(|| {
   7685             AppSyncTransportError::failed("order request publish requires listing event id")
   7686         })?
   7687         .trim()
   7688         .to_owned();
   7689     let listing_relay = normalized_listing_relays(payload.listing_relays.as_slice())?
   7690         .into_iter()
   7691         .next();
   7692 
   7693     Ok(RadrootsNostrEventPtr {
   7694         id: listing_event_id,
   7695         relays: listing_relay,
   7696     })
   7697 }
   7698 
   7699 fn order_request_sdk_target_relays(
   7700     payload: &AppOrderRequestPublishPayload,
   7701     configured_relay_urls: &[String],
   7702 ) -> Result<Vec<String>, AppSyncTransportError> {
   7703     let known_relays = normalized_listing_relays(payload.listing_relays.as_slice())?;
   7704     let configured_relays = configured_relay_urls
   7705         .iter()
   7706         .map(|relay| relay.trim())
   7707         .filter(|relay| !relay.is_empty())
   7708         .map(str::to_owned)
   7709         .collect::<BTreeSet<_>>();
   7710     if configured_relays.is_empty() {
   7711         return Ok(known_relays);
   7712     }
   7713     let selected_relays = known_relays
   7714         .iter()
   7715         .filter(|relay| configured_relays.contains(*relay))
   7716         .cloned()
   7717         .collect::<Vec<_>>();
   7718     if selected_relays.is_empty() {
   7719         return Ok(known_relays);
   7720     }
   7721     Ok(selected_relays)
   7722 }
   7723 
   7724 fn order_decision_sdk_request_event_ptr(
   7725     payload: &AppOrderDecisionPublishPayload,
   7726     target_relays: &[String],
   7727 ) -> Result<RadrootsNostrEventPtr, AppSyncTransportError> {
   7728     let request_event_id = payload.request_event_id.trim();
   7729     if request_event_id.is_empty() {
   7730         return Err(AppSyncTransportError::failed(
   7731             "order decision publish requires request event id",
   7732         ));
   7733     }
   7734     Ok(RadrootsNostrEventPtr {
   7735         id: request_event_id.to_owned(),
   7736         relays: target_relays.first().cloned(),
   7737     })
   7738 }
   7739 
   7740 fn order_lifecycle_sdk_event_ptr(
   7741     event_id: &str,
   7742     target_relays: &[String],
   7743     missing_message: &'static str,
   7744 ) -> Result<RadrootsNostrEventPtr, AppSyncTransportError> {
   7745     let event_id = event_id.trim();
   7746     if event_id.is_empty() {
   7747         return Err(AppSyncTransportError::failed(missing_message));
   7748     }
   7749     Ok(RadrootsNostrEventPtr {
   7750         id: event_id.to_owned(),
   7751         relays: target_relays.first().cloned(),
   7752     })
   7753 }
   7754 
   7755 #[cfg(test)]
   7756 fn selected_listing_relay(
   7757     listing_relays: &[String],
   7758     configured_relay_urls: &[String],
   7759 ) -> Result<String, AppSyncTransportError> {
   7760     let known_relays = normalized_listing_relays(listing_relays)?;
   7761     for configured_relay in configured_relay_urls {
   7762         let configured_relay = configured_relay.trim();
   7763         if !configured_relay.is_empty()
   7764             && known_relays.iter().any(|relay| relay == configured_relay)
   7765         {
   7766             return Ok(configured_relay.to_owned());
   7767         }
   7768     }
   7769     Err(missing_listing_provenance_relay_error(&known_relays))
   7770 }
   7771 
   7772 fn normalized_listing_relays(
   7773     listing_relays: &[String],
   7774 ) -> Result<Vec<String>, AppSyncTransportError> {
   7775     let mut seen = BTreeSet::new();
   7776     let mut known_relays = Vec::new();
   7777     for relay in listing_relays {
   7778         let relay = relay.trim();
   7779         if !relay.is_empty() && seen.insert(relay.to_owned()) {
   7780             known_relays.push(relay.to_owned());
   7781         }
   7782     }
   7783     if known_relays.is_empty() {
   7784         return Err(AppSyncTransportError::failed(
   7785             "order request publish requires listing relay",
   7786         ));
   7787     }
   7788     Ok(known_relays)
   7789 }
   7790 
   7791 #[cfg(test)]
   7792 fn missing_listing_provenance_relay_error(known_relays: &[String]) -> AppSyncTransportError {
   7793     AppSyncTransportError::failed(
   7794         json!({
   7795             "code": "missing_listing_provenance_relay",
   7796             "missing_provenance_relays": known_relays,
   7797         })
   7798         .to_string(),
   7799     )
   7800 }
   7801 
   7802 fn order_request_publish_payload_to_sdk_order(
   7803     payload: &AppOrderRequestPublishPayload,
   7804 ) -> Result<RadrootsOrderRequest, AppSyncTransportError> {
   7805     let Some(document_json) = payload.order_document_json.as_ref() else {
   7806         return Err(AppSyncTransportError::failed(
   7807             "order request publish requires order document",
   7808         ));
   7809     };
   7810     let order_json = document_json
   7811         .pointer("/document/order")
   7812         .or_else(|| document_json.get("order"))
   7813         .unwrap_or(document_json);
   7814     serde_json::from_value::<RadrootsOrderRequest>(order_json.clone())
   7815         .map_err(|error| AppSyncTransportError::failed(error.to_string()))
   7816 }
   7817 
   7818 fn d_tag_from_uuid(uuid: Uuid) -> String {
   7819     base64_url_no_pad(uuid.as_bytes())
   7820 }
   7821 
   7822 fn signed_event_farm_id(receipt: &AppPublishedOperationReceipt) -> Option<String> {
   7823     match receipt.event_kind {
   7824         KIND_FARM => signed_event_tag_value(&receipt.event_tags_json, "d", 1),
   7825         KIND_LISTING => signed_event_tag_value(&receipt.event_tags_json, "a", 1)
   7826             .and_then(|address| signed_event_address_d_tag(address.as_str())),
   7827         _ => None,
   7828     }
   7829 }
   7830 
   7831 fn signed_event_listing_addr(receipt: &AppPublishedOperationReceipt) -> Option<String> {
   7832     if receipt.event_kind != KIND_LISTING {
   7833         return None;
   7834     }
   7835     let pubkey = receipt.event_pubkey.trim();
   7836     if pubkey.is_empty() {
   7837         return None;
   7838     }
   7839     signed_event_tag_value(&receipt.event_tags_json, "d", 1)
   7840         .map(|d_tag| format!("{KIND_LISTING}:{pubkey}:{d_tag}"))
   7841 }
   7842 
   7843 fn signed_event_tag_value(
   7844     tags: &serde_json::Value,
   7845     tag_name: &str,
   7846     index: usize,
   7847 ) -> Option<String> {
   7848     tags.as_array()?.iter().find_map(|tag| {
   7849         let values = tag.as_array()?;
   7850         (values.first()?.as_str()? == tag_name)
   7851             .then(|| values.get(index).and_then(serde_json::Value::as_str))
   7852             .flatten()
   7853             .map(str::trim)
   7854             .filter(|value| !value.is_empty())
   7855             .map(str::to_owned)
   7856     })
   7857 }
   7858 
   7859 fn signed_event_address_d_tag(address: &str) -> Option<String> {
   7860     address
   7861         .rsplit(':')
   7862         .next()
   7863         .map(str::trim)
   7864         .filter(|value| !value.is_empty())
   7865         .map(str::to_owned)
   7866 }
   7867 
   7868 fn base64_url_no_pad(bytes: &[u8]) -> String {
   7869     const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
   7870     let mut output = String::with_capacity((bytes.len() * 4).div_ceil(3));
   7871     let mut chunks = bytes.chunks_exact(3);
   7872     for chunk in &mut chunks {
   7873         output.push(ALPHABET[(chunk[0] >> 2) as usize] as char);
   7874         output.push(ALPHABET[(((chunk[0] & 0b0000_0011) << 4) | (chunk[1] >> 4)) as usize] as char);
   7875         output.push(ALPHABET[(((chunk[1] & 0b0000_1111) << 2) | (chunk[2] >> 6)) as usize] as char);
   7876         output.push(ALPHABET[(chunk[2] & 0b0011_1111) as usize] as char);
   7877     }
   7878     match chunks.remainder() {
   7879         [one] => {
   7880             output.push(ALPHABET[(one >> 2) as usize] as char);
   7881             output.push(ALPHABET[((one & 0b0000_0011) << 4) as usize] as char);
   7882         }
   7883         [one, two] => {
   7884             output.push(ALPHABET[(one >> 2) as usize] as char);
   7885             output.push(ALPHABET[(((one & 0b0000_0011) << 4) | (two >> 4)) as usize] as char);
   7886             output.push(ALPHABET[((two & 0b0000_1111) << 2) as usize] as char);
   7887         }
   7888         [] => {}
   7889         _ => {}
   7890     }
   7891     output
   7892 }
   7893 
   7894 fn decimal_from_minor_units(value: u32) -> String {
   7895     format!("{}.{:02}", value / 100, value % 100)
   7896 }
   7897 
   7898 fn normalize_currency_code(value: &str) -> String {
   7899     let trimmed = value.trim();
   7900     if trimmed.is_empty() {
   7901         "USD".to_owned()
   7902     } else {
   7903         trimmed.to_ascii_uppercase()
   7904     }
   7905 }
   7906 
   7907 fn is_hex_64(value: &str) -> bool {
   7908     value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
   7909 }
   7910 
   7911 fn local_work_exportability(owner_pubkey: Option<&str>) -> serde_json::Value {
   7912     match owner_pubkey {
   7913         Some(_) => json!({
   7914             "state": "exportable"
   7915         }),
   7916         None => json!({
   7917             "state": "identity_unresolved",
   7918             "reason": "canonical_hex_pubkey_required"
   7919         }),
   7920     }
   7921 }
   7922 
   7923 #[derive(Clone, Debug, Eq, PartialEq)]
   7924 struct AppBuyerOrderRequestExport {
   7925     buyer_pubkey: Option<String>,
   7926     seller_pubkey: Option<String>,
   7927     listing_addr: Option<String>,
   7928     listing_event_id: Option<String>,
   7929     listing_relays: Vec<String>,
   7930     farm_key: Option<String>,
   7931     order_items: Vec<serde_json::Value>,
   7932     line_refs: Vec<serde_json::Value>,
   7933     economics: Option<serde_json::Value>,
   7934     support_issues: Vec<&'static str>,
   7935 }
   7936 
   7937 #[derive(Clone, Debug)]
   7938 struct AppOrderLocalWorkPublishSource {
   7939     record_id: String,
   7940     payload: serde_json::Value,
   7941 }
   7942 
   7943 impl AppBuyerOrderRequestExport {
   7944     fn from_order(
   7945         order: &BuyerOrderLocalEventExport,
   7946         buyer_pubkey: Option<&str>,
   7947     ) -> Result<Self, AppSqliteError> {
   7948         let mut support_issues = Vec::new();
   7949         if buyer_pubkey.is_none() {
   7950             support_issues.push("buyer_pubkey_required");
   7951         }
   7952         if order.lines.is_empty() {
   7953             support_issues.push("order_lines_required");
   7954         }
   7955         let listing_addr =
   7956             shared_optional_line_value(&order.lines, |line| line.listing_addr.as_deref());
   7957         let listing_event_id =
   7958             shared_optional_line_value(&order.lines, |line| line.listing_event_id.as_deref());
   7959         let listing_relays = shared_listing_relays(&order.lines);
   7960         let seller_pubkey =
   7961             shared_optional_line_value(&order.lines, |line| line.seller_pubkey.as_deref());
   7962         let farm_key = shared_optional_line_value(&order.lines, |line| line.farm_key.as_deref())
   7963             .or_else(|| Some(d_tag_from_uuid(order.farm_id.as_uuid())));
   7964 
   7965         if listing_addr.is_none() {
   7966             support_issues.push("single_listing_addr_required");
   7967         }
   7968         if listing_event_id.is_none() {
   7969             support_issues.push("listing_event_id_required");
   7970         }
   7971         if listing_relays.is_empty() {
   7972             support_issues.push("listing_relays_required");
   7973         }
   7974         if seller_pubkey.is_none() {
   7975             support_issues.push("seller_pubkey_required");
   7976         }
   7977 
   7978         let mut order_items = Vec::with_capacity(order.lines.len());
   7979         let mut line_refs = Vec::with_capacity(order.lines.len());
   7980         for line in &order.lines {
   7981             let line_bin_id = line
   7982                 .listing_bin_id
   7983                 .as_deref()
   7984                 .map(str::trim)
   7985                 .filter(|value| !value.is_empty());
   7986             if line_bin_id.is_none() && !support_issues.contains(&"listing_bin_id_required") {
   7987                 support_issues.push("listing_bin_id_required");
   7988             }
   7989             order_items.push(json!({
   7990                 "bin_id": line_bin_id.unwrap_or_default(),
   7991                 "bin_count": line.quantity,
   7992             }));
   7993             line_refs.push(json!({
   7994                 "product_id": line.product_id.to_string(),
   7995                 "title": line.title,
   7996                 "quantity": {
   7997                     "count": line.quantity,
   7998                     "display": line.quantity_display,
   7999                     "unit_label": line.quantity_unit_label,
   8000                 },
   8001                 "listing_addr": line.listing_addr,
   8002                 "listing_event_id": line.listing_event_id,
   8003                 "listing_relays": line.listing_relays,
   8004                 "listing_bin_id": line.listing_bin_id,
   8005                 "seller_pubkey": line.seller_pubkey,
   8006                 "farm_key": line.farm_key,
   8007             }));
   8008         }
   8009 
   8010         let economics = order_economics_json(order, &mut support_issues)?;
   8011 
   8012         Ok(Self {
   8013             buyer_pubkey: buyer_pubkey.map(str::to_owned),
   8014             seller_pubkey,
   8015             listing_addr,
   8016             listing_event_id,
   8017             listing_relays,
   8018             farm_key,
   8019             order_items,
   8020             line_refs,
   8021             economics,
   8022             support_issues,
   8023         })
   8024     }
   8025 
   8026     fn is_supported(&self) -> bool {
   8027         self.support_issues.is_empty()
   8028     }
   8029 }
   8030 
   8031 fn buyer_order_request_local_work_payload(
   8032     order: &BuyerOrderLocalEventExport,
   8033     buyer_context: &BuyerContext,
   8034     record_id: &str,
   8035     export: &AppBuyerOrderRequestExport,
   8036     timestamp: i64,
   8037 ) -> serde_json::Value {
   8038     let buyer_account_id = match buyer_context {
   8039         BuyerContext::Account(account_id) => account_id.as_str(),
   8040         BuyerContext::Guest => "",
   8041     };
   8042     let buyer_actor_source = if export.buyer_pubkey.is_some() {
   8043         BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT
   8044     } else {
   8045         BUYER_ORDER_REQUEST_ACTOR_SOURCE_UNRESOLVED_APP
   8046     };
   8047 
   8048     json!({
   8049         "record_kind": BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND,
   8050         "scope": "app",
   8051         "exportability": local_work_exportability(export.buyer_pubkey.as_deref()),
   8052         "support_status": {
   8053             "state": if export.is_supported() { "supported" } else { "unsupported" },
   8054             "issues": export.support_issues.clone(),
   8055         },
   8056         "currentness": {
   8057             "current": true,
   8058             "source": "app_sqlite_order",
   8059             "record_id": record_id,
   8060             "order_id": order.order_id.to_string(),
   8061             "order_updated_at": order.updated_at,
   8062             "created_at_ms": timestamp,
   8063         },
   8064         "document": {
   8065             "version": 1,
   8066             "kind": BUYER_ORDER_REQUEST_DOCUMENT_KIND,
   8067             "order": {
   8068                 "order_id": order.order_id.to_string(),
   8069                 "listing_addr": export.listing_addr.as_deref().unwrap_or_default(),
   8070                 "listing_event_id": export.listing_event_id.as_deref().unwrap_or_default(),
   8071                 "listing_relays": export.listing_relays.clone(),
   8072                 "buyer_pubkey": export.buyer_pubkey.as_deref().unwrap_or_default(),
   8073                 "seller_pubkey": export.seller_pubkey.as_deref().unwrap_or_default(),
   8074                 "items": export.order_items.clone(),
   8075                 "economics": export.economics.clone(),
   8076             },
   8077             "buyer_actor": {
   8078                 "account_id": buyer_account_id,
   8079                 "pubkey": export.buyer_pubkey.as_deref().unwrap_or_default(),
   8080                 "source": buyer_actor_source,
   8081             },
   8082             "listing_lookup": export.listing_addr.clone(),
   8083         },
   8084         "app_order": {
   8085             "order_id": order.order_id.to_string(),
   8086             "order_number": order.order_number,
   8087             "farm_id": order.farm_id.to_string(),
   8088             "farm_display_name": order.farm_display_name,
   8089             "farm_key": export.farm_key.clone(),
   8090             "status": order.status,
   8091             "buyer_context_key": order.buyer_context_key,
   8092             "buyer_name": order.buyer_name,
   8093             "buyer_email": order.buyer_email,
   8094             "buyer_phone": order.buyer_phone,
   8095             "buyer_order_note": order.buyer_order_note,
   8096             "fulfillment": {
   8097                 "window_id": order.fulfillment_window_id.map(|id| id.to_string()),
   8098                 "label": order.fulfillment_window_label,
   8099                 "starts_at": order.fulfillment_starts_at,
   8100                 "ends_at": order.fulfillment_ends_at,
   8101             },
   8102             "lines": export.line_refs.clone(),
   8103         },
   8104     })
   8105 }
   8106 
   8107 fn order_economics_json(
   8108     order: &BuyerOrderLocalEventExport,
   8109     support_issues: &mut Vec<&'static str>,
   8110 ) -> Result<Option<serde_json::Value>, AppSqliteError> {
   8111     let mut economics_items = Vec::with_capacity(order.lines.len());
   8112     let mut subtotal_minor_units = 0_u32;
   8113     let mut currency = None::<String>;
   8114 
   8115     for line in &order.lines {
   8116         let line_bin_id = line
   8117             .listing_bin_id
   8118             .as_deref()
   8119             .map(str::trim)
   8120             .filter(|value| !value.is_empty());
   8121         if line_bin_id.is_none() && !support_issues.contains(&"listing_bin_id_required") {
   8122             support_issues.push("listing_bin_id_required");
   8123             continue;
   8124         }
   8125         let Some(quantity_unit) = canonical_quantity_unit(line.quantity_unit_label.as_str()) else {
   8126             support_issues.push("canonical_quantity_unit_required");
   8127             continue;
   8128         };
   8129         let Some(unit_price_minor_units) = line.unit_price_minor_units else {
   8130             support_issues.push("unit_price_required");
   8131             continue;
   8132         };
   8133         if unit_price_minor_units == 0 {
   8134             support_issues.push("positive_unit_price_required");
   8135             continue;
   8136         }
   8137         let line_currency = normalize_currency_code(line.price_currency.as_str());
   8138         if line_currency.len() != 3 || !line_currency.bytes().all(|byte| byte.is_ascii_uppercase())
   8139         {
   8140             support_issues.push("canonical_currency_required");
   8141             continue;
   8142         }
   8143         if let Some(existing_currency) = currency.as_deref() {
   8144             if existing_currency != line_currency {
   8145                 support_issues.push("single_currency_required");
   8146                 continue;
   8147             }
   8148         } else {
   8149             currency = Some(line_currency.clone());
   8150         }
   8151         let line_subtotal_minor_units = unit_price_minor_units.checked_mul(line.quantity).ok_or(
   8152             AppSqliteError::InvalidProjection {
   8153                 reason: "buyer order local event line subtotal overflowed",
   8154             },
   8155         )?;
   8156         subtotal_minor_units = subtotal_minor_units
   8157             .checked_add(line_subtotal_minor_units)
   8158             .ok_or(AppSqliteError::InvalidProjection {
   8159                 reason: "buyer order local event subtotal overflowed",
   8160             })?;
   8161         economics_items.push(json!({
   8162             "bin_id": line_bin_id.unwrap_or_default(),
   8163             "bin_count": line.quantity,
   8164             "quantity_amount": "1",
   8165             "quantity_unit": quantity_unit,
   8166             "unit_price_amount": decimal_from_minor_units(unit_price_minor_units),
   8167             "unit_price_currency": line_currency,
   8168             "line_subtotal": {
   8169                 "amount": decimal_from_minor_units(line_subtotal_minor_units),
   8170                 "currency": line_currency,
   8171             },
   8172         }));
   8173     }
   8174 
   8175     if economics_items.len() != order.lines.len() || economics_items.is_empty() {
   8176         return Ok(None);
   8177     }
   8178 
   8179     let currency = currency.unwrap_or_else(|| "USD".to_owned());
   8180     let subtotal = json!({
   8181         "amount": decimal_from_minor_units(subtotal_minor_units),
   8182         "currency": currency,
   8183     });
   8184     Ok(Some(json!({
   8185         "quote_id": format!("app-order:{}", order.order_id),
   8186         "quote_version": 1,
   8187         "pricing_basis": "listing_event",
   8188         "currency": currency,
   8189         "items": economics_items,
   8190         "discounts": [],
   8191         "adjustments": [],
   8192         "subtotal": subtotal,
   8193         "discount_total": {
   8194             "amount": "0",
   8195             "currency": currency,
   8196         },
   8197         "adjustment_total": {
   8198             "amount": "0",
   8199             "currency": currency,
   8200         },
   8201         "total": subtotal,
   8202     })))
   8203 }
   8204 
   8205 fn order_currency_and_total(
   8206     order: &BuyerOrderLocalEventExport,
   8207 ) -> Result<Option<(String, u32)>, AppSqliteError> {
   8208     let mut currency = None::<String>;
   8209     let mut total_minor_units = 0_u32;
   8210 
   8211     for line in &order.lines {
   8212         let Some(unit_price_minor_units) = line.unit_price_minor_units else {
   8213             return Ok(None);
   8214         };
   8215         let line_currency = normalize_currency_code(line.price_currency.as_str());
   8216         if line_currency.len() != 3 || !line_currency.bytes().all(|byte| byte.is_ascii_uppercase())
   8217         {
   8218             return Ok(None);
   8219         }
   8220         if let Some(existing_currency) = currency.as_deref() {
   8221             if existing_currency != line_currency {
   8222                 return Ok(None);
   8223             }
   8224         } else {
   8225             currency = Some(line_currency.clone());
   8226         }
   8227         let line_total = unit_price_minor_units.checked_mul(line.quantity).ok_or(
   8228             AppSqliteError::InvalidProjection {
   8229                 reason: "buyer order publish line total overflowed",
   8230             },
   8231         )?;
   8232         total_minor_units =
   8233             total_minor_units
   8234                 .checked_add(line_total)
   8235                 .ok_or(AppSqliteError::InvalidProjection {
   8236                     reason: "buyer order publish total overflowed",
   8237                 })?;
   8238     }
   8239 
   8240     Ok(currency.map(|currency| (currency, total_minor_units)))
   8241 }
   8242 
   8243 fn shared_optional_line_value(
   8244     lines: &[BuyerOrderLocalEventLine],
   8245     value: impl Fn(&BuyerOrderLocalEventLine) -> Option<&str>,
   8246 ) -> Option<String> {
   8247     let mut resolved = None::<String>;
   8248     for line in lines {
   8249         let Some(next) = value(line).map(str::trim).filter(|next| !next.is_empty()) else {
   8250             return None;
   8251         };
   8252         if let Some(existing) = resolved.as_deref() {
   8253             if existing != next {
   8254                 return None;
   8255             }
   8256         } else {
   8257             resolved = Some(next.to_owned());
   8258         }
   8259     }
   8260     resolved
   8261 }
   8262 
   8263 fn shared_listing_relays(lines: &[BuyerOrderLocalEventLine]) -> Vec<String> {
   8264     let mut seen = BTreeSet::new();
   8265     let mut relays = Vec::new();
   8266     for line in lines {
   8267         for relay in &line.listing_relays {
   8268             let relay = relay.trim();
   8269             if !relay.is_empty() && seen.insert(relay.to_owned()) {
   8270                 relays.push(relay.to_owned());
   8271             }
   8272         }
   8273     }
   8274     relays
   8275 }
   8276 
   8277 fn canonical_quantity_unit(unit_label: &str) -> Option<&'static str> {
   8278     match unit_label.trim().to_ascii_lowercase().as_str() {
   8279         "each" | "ea" | "count" => Some("each"),
   8280         "kg" | "kilogram" | "kilograms" => Some("kg"),
   8281         "g" | "gram" | "grams" => Some("g"),
   8282         "oz" | "ounce" | "ounces" => Some("oz"),
   8283         "lb" | "pound" | "pounds" => Some("lb"),
   8284         "l" | "liter" | "litre" | "liters" | "litres" => Some("l"),
   8285         "ml" | "milliliter" | "millilitre" | "milliliters" | "millilitres" => Some("ml"),
   8286         _ => None,
   8287     }
   8288 }
   8289 
   8290 fn load_selected_account_context(
   8291     sqlite_store: &AppSqliteStore,
   8292     identity_projection: &AppIdentityProjection,
   8293     continuity_state: &PersistedAppState,
   8294 ) -> Result<DesktopSelectedAccountContext, AppSqliteError> {
   8295     load_selected_account_context_with_options(
   8296         sqlite_store,
   8297         identity_projection,
   8298         continuity_state,
   8299         true,
   8300     )
   8301 }
   8302 
   8303 fn selected_buyer_order_scope(
   8304     identity_projection: &AppIdentityProjection,
   8305 ) -> SelectedBuyerOrderScope {
   8306     let buyer_context = identity_projection.buyer_context();
   8307     match &buyer_context {
   8308         BuyerContext::Account(account_id) => SelectedBuyerOrderScope::for_selected_account(
   8309             account_id,
   8310             identity_projection
   8311                 .selected_account
   8312                 .as_ref()
   8313                 .and_then(selected_account_public_key_hex)
   8314                 .as_deref(),
   8315         ),
   8316         BuyerContext::Guest => SelectedBuyerOrderScope::from_buyer_context(&buyer_context),
   8317     }
   8318 }
   8319 
   8320 fn selected_account_public_key_hex(
   8321     selected_account: &radroots_app_view::SelectedAccountProjection,
   8322 ) -> Option<String> {
   8323     let npub = selected_account.account.npub.trim();
   8324     radroots_nostr_parse_pubkey(npub)
   8325         .ok()
   8326         .map(|public_key| public_key.to_hex())
   8327         .filter(|public_key| is_hex_64(public_key))
   8328         .or_else(|| {
   8329             let account_id = selected_account.account.account_id.trim();
   8330             is_hex_64(account_id).then(|| account_id.to_owned())
   8331         })
   8332 }
   8333 
   8334 fn load_selected_account_context_with_options(
   8335     sqlite_store: &AppSqliteStore,
   8336     identity_projection: &AppIdentityProjection,
   8337     continuity_state: &PersistedAppState,
   8338     allow_auto_present: bool,
   8339 ) -> Result<DesktopSelectedAccountContext, AppSqliteError> {
   8340     let buyer_context = identity_projection.buyer_context();
   8341     let buyer_order_scope = selected_buyer_order_scope(identity_projection);
   8342     let browse_fulfillment_methods = BTreeSet::new();
   8343     let browse_listings = sqlite_store.load_buyer_listings("", &browse_fulfillment_methods)?;
   8344     let search_query = continuity_state.buyer.search_query.clone();
   8345     let search_listings = sqlite_store.load_buyer_listings(
   8346         &search_query.search_query,
   8347         &search_query.fulfillment_methods,
   8348     )?;
   8349     let browse_detail = match continuity_state.buyer.browse_detail_product_id {
   8350         Some(product_id) => sqlite_store.load_buyer_product_detail(product_id)?,
   8351         None => None,
   8352     };
   8353     let search_detail = match continuity_state.buyer.search_detail_product_id {
   8354         Some(product_id) => sqlite_store.load_buyer_product_detail(product_id)?,
   8355         None => None,
   8356     };
   8357     let buyer_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
   8358     let buyer_order_review = sqlite_store.load_buyer_order_review(&buyer_context)?;
   8359     let buyer_orders = sqlite_store.load_buyer_orders_for_scope(&buyer_order_scope)?;
   8360     let has_recoverable_coordination = !sqlite_store
   8361         .load_recoverable_buyer_order_coordination_records(&buyer_context)?
   8362         .is_empty();
   8363     let buyer_order_detail = match continuity_state.buyer.orders_detail_order_id {
   8364         Some(order_id) => {
   8365             sqlite_store.load_buyer_order_detail_for_scope(&buyer_order_scope, order_id)?
   8366         }
   8367         None => None,
   8368     };
   8369     let personal_projection = PersonalWorkspaceProjection {
   8370         browse: BuyerBrowseScreenProjection {
   8371             listings: browse_listings,
   8372             detail: browse_detail,
   8373         },
   8374         search: BuyerSearchScreenProjection {
   8375             query: search_query,
   8376             listings: search_listings,
   8377             detail: search_detail,
   8378         },
   8379         cart: BuyerCartScreenProjection {
   8380             cart: buyer_cart,
   8381             order_review: buyer_order_review,
   8382         },
   8383         orders: BuyerOrdersScreenProjection {
   8384             list: buyer_orders,
   8385             detail: buyer_order_detail,
   8386             has_recoverable_coordination,
   8387         },
   8388         ..PersonalWorkspaceProjection::default()
   8389     };
   8390     let Some(selected_account) = identity_projection.selected_account.as_ref() else {
   8391         return Ok(DesktopSelectedAccountContext {
   8392             personal_projection,
   8393             products_query: ProductsScreenQueryState::default(),
   8394             orders_list: OrdersListProjection::default(),
   8395             orders_query: OrdersScreenQueryState::default(),
   8396             orders_reminders: ReminderFeedProjection::default(),
   8397             pack_day_query: PackDayScreenQueryState::default(),
   8398             product_editor_draft: None,
   8399             reminder_log: ReminderLogProjection::default(),
   8400             ..DesktopSelectedAccountContext::default()
   8401         });
   8402     };
   8403     let farm_setup_projection =
   8404         sqlite_store.load_farm_setup(&selected_account.account.account_id)?;
   8405     let today_farm_id = selected_farm_id_from_context(identity_projection, &farm_setup_projection);
   8406     let (
   8407         farm_rules_projection,
   8408         mut today_projection,
   8409         products_query,
   8410         products_list,
   8411         orders_query,
   8412         orders_list,
   8413         canonical_orders_list,
   8414         order_detail,
   8415         pack_day_query,
   8416         mut pack_day_projection,
   8417         product_editor_draft,
   8418     ) = match today_farm_id {
   8419         Some(farm_id) => {
   8420             let fallback_profile =
   8421                 fallback_farm_profile_for_projection(farm_id, &farm_setup_projection);
   8422             let farm_rules_projection =
   8423                 sqlite_store.load_farm_rules(farm_id).map(|projection| {
   8424                     prepare_loaded_farm_rules_projection(projection, &fallback_profile)
   8425                 })?;
   8426             let today_projection = sqlite_store.load_today_agenda(Some(farm_id))?;
   8427             let products_query = continuity_state.seller.products_query.clone();
   8428             let products_list = sqlite_store.load_products(
   8429                 farm_id,
   8430                 &products_query.search_query,
   8431                 products_query.filter,
   8432                 products_query.sort,
   8433             )?;
   8434             let orders_query = sanitize_orders_query(
   8435                 sqlite_store,
   8436                 farm_id,
   8437                 continuity_state.seller.orders_query.clone(),
   8438             )?;
   8439             let orders_list = sqlite_store.load_orders_list(farm_id, &orders_query)?;
   8440             let canonical_orders_list =
   8441                 sqlite_store.load_orders_list(farm_id, &OrdersScreenQueryState::default())?;
   8442             let order_detail = match continuity_state.seller.order_detail_order_id {
   8443                 Some(order_id) => sqlite_store.load_order_detail(farm_id, order_id)?,
   8444                 None => None,
   8445             };
   8446             let (pack_day_query, pack_day_projection) = sanitize_pack_day_query(
   8447                 sqlite_store,
   8448                 farm_id,
   8449                 continuity_state.seller.pack_day_query.clone(),
   8450             )?;
   8451             let product_editor_draft = if matches!(
   8452                 continuity_state.shell.selected_section,
   8453                 ShellSection::Farmer(FarmerSection::Products)
   8454             ) {
   8455                 match continuity_state.seller.product_editor_product_id {
   8456                     Some(product_id) => sqlite_store
   8457                         .load_product_editor_draft(product_id)?
   8458                         .map(|draft| (product_id, draft)),
   8459                     None => None,
   8460                 }
   8461             } else {
   8462                 None
   8463             };
   8464 
   8465             (
   8466                 farm_rules_projection,
   8467                 today_projection,
   8468                 products_query,
   8469                 products_list,
   8470                 orders_query,
   8471                 orders_list,
   8472                 canonical_orders_list,
   8473                 order_detail,
   8474                 pack_day_query,
   8475                 pack_day_projection,
   8476                 product_editor_draft,
   8477             )
   8478         }
   8479         None => (
   8480             FarmRulesProjection::default(),
   8481             TodayAgendaProjection::default(),
   8482             ProductsScreenQueryState::default(),
   8483             ProductsListProjection::default(),
   8484             OrdersScreenQueryState::default(),
   8485             OrdersListProjection::default(),
   8486             OrdersListProjection::default(),
   8487             None,
   8488             PackDayScreenQueryState::default(),
   8489             PackDayProjection::default(),
   8490             None,
   8491         ),
   8492     };
   8493     let (orders_reminders, reminder_log) = match today_farm_id {
   8494         Some(farm_id) => {
   8495             let reminder_context = load_selected_account_reminder_context_with_options(
   8496                 sqlite_store,
   8497                 selected_account.account.account_id.as_str(),
   8498                 farm_id,
   8499                 &today_projection,
   8500                 &canonical_orders_list,
   8501                 &pack_day_projection,
   8502                 order_detail.as_ref(),
   8503                 allow_auto_present,
   8504             )?;
   8505             today_projection.reminders = reminder_context.today_feed;
   8506             if let Some(summary) = today_projection.summary.as_mut() {
   8507                 summary.reminders_due_soon = reminder_context.due_soon_count;
   8508             }
   8509             pack_day_projection.reminders = reminder_context.pack_day_feed;
   8510 
   8511             (reminder_context.orders_feed, reminder_context.reminder_log)
   8512         }
   8513         None => (
   8514             ReminderFeedProjection::default(),
   8515             ReminderLogProjection::default(),
   8516         ),
   8517     };
   8518 
   8519     Ok(DesktopSelectedAccountContext {
   8520         personal_projection,
   8521         farm_setup_projection,
   8522         farm_rules_projection,
   8523         today_projection,
   8524         products_query,
   8525         products_list,
   8526         orders_query,
   8527         orders_list,
   8528         orders_reminders,
   8529         reminder_log,
   8530         order_detail,
   8531         pack_day_query,
   8532         pack_day_projection,
   8533         product_editor_draft,
   8534     })
   8535 }
   8536 
   8537 fn sanitize_orders_query(
   8538     sqlite_store: &AppSqliteStore,
   8539     farm_id: FarmId,
   8540     query: OrdersScreenQueryState,
   8541 ) -> Result<OrdersScreenQueryState, AppSqliteError> {
   8542     let Some(fulfillment_window_id) = query.fulfillment_window_id else {
   8543         return Ok(query);
   8544     };
   8545     let pack_day = sqlite_store.load_pack_day(
   8546         farm_id,
   8547         &PackDayScreenQueryState {
   8548             fulfillment_window_id: Some(fulfillment_window_id),
   8549         },
   8550     )?;
   8551     if pack_day
   8552         .fulfillment_window
   8553         .as_ref()
   8554         .map(|window| window.fulfillment_window_id)
   8555         == Some(fulfillment_window_id)
   8556     {
   8557         Ok(query)
   8558     } else {
   8559         Ok(OrdersScreenQueryState {
   8560             filter: query.filter,
   8561             fulfillment_window_id: None,
   8562         })
   8563     }
   8564 }
   8565 
   8566 fn sanitize_pack_day_query(
   8567     sqlite_store: &AppSqliteStore,
   8568     farm_id: FarmId,
   8569     query: PackDayScreenQueryState,
   8570 ) -> Result<(PackDayScreenQueryState, PackDayProjection), AppSqliteError> {
   8571     let projection = sqlite_store.load_pack_day(farm_id, &query)?;
   8572     if query.fulfillment_window_id.is_none()
   8573         || projection
   8574             .fulfillment_window
   8575             .as_ref()
   8576             .map(|window| window.fulfillment_window_id)
   8577             == query.fulfillment_window_id
   8578     {
   8579         return Ok((query, projection));
   8580     }
   8581 
   8582     let default_query = PackDayScreenQueryState::default();
   8583     let default_projection = sqlite_store.load_pack_day(farm_id, &default_query)?;
   8584 
   8585     Ok((default_query, default_projection))
   8586 }
   8587 
   8588 fn load_selected_account_reminder_context_with_options(
   8589     sqlite_store: &AppSqliteStore,
   8590     account_id: &str,
   8591     farm_id: FarmId,
   8592     today_projection: &TodayAgendaProjection,
   8593     canonical_orders_list: &OrdersListProjection,
   8594     pack_day_projection: &PackDayProjection,
   8595     _selected_order_detail: Option<&OrderDetailProjection>,
   8596     allow_auto_present: bool,
   8597 ) -> Result<DesktopSellerReminderContext, AppSqliteError> {
   8598     let existing_schedule = sqlite_store.load_reminder_schedule(account_id, farm_id)?;
   8599     let sync_truth = load_selected_account_reminder_sync_truth(sqlite_store, account_id)?;
   8600     let mut schedule = derive_selected_account_reminder_schedule(
   8601         farm_id,
   8602         today_projection,
   8603         canonical_orders_list,
   8604         pack_day_projection,
   8605         &sync_truth,
   8606         &existing_schedule,
   8607     );
   8608     let mut reminder_log_entries =
   8609         reconcile_resolved_reminder_log_entries(&existing_schedule, &schedule);
   8610     promote_desktop_reminder_presentation(
   8611         &mut schedule,
   8612         &mut reminder_log_entries,
   8613         allow_auto_present,
   8614     );
   8615     if schedule != existing_schedule || !reminder_log_entries.is_empty() {
   8616         sqlite_store.apply_reminder_schedule_update(
   8617             account_id,
   8618             farm_id,
   8619             &schedule,
   8620             &reminder_log_entries,
   8621         )?;
   8622     }
   8623     let reminder_log = sqlite_store.load_reminder_log(account_id, farm_id, 8)?;
   8624 
   8625     let due_soon_count = schedule
   8626         .items
   8627         .iter()
   8628         .filter(|item| {
   8629             matches!(
   8630                 item.urgency,
   8631                 ReminderUrgency::DueSoon | ReminderUrgency::Overdue | ReminderUrgency::Blocking
   8632             )
   8633         })
   8634         .count() as u32;
   8635 
   8636     Ok(DesktopSellerReminderContext {
   8637         today_feed: filter_reminder_surface(&schedule, ReminderSurface::Today),
   8638         orders_feed: filter_reminder_surface(&schedule, ReminderSurface::Orders),
   8639         pack_day_feed: filter_reminder_surface(&schedule, ReminderSurface::PackDay),
   8640         due_soon_count,
   8641         reminder_log,
   8642     })
   8643 }
   8644 
   8645 fn load_selected_account_reminder_sync_truth(
   8646     sqlite_store: &AppSqliteStore,
   8647     account_id: &str,
   8648 ) -> Result<DesktopReminderSyncTruth, AppSqliteError> {
   8649     let checkpoint = sqlite_store.load_sync_checkpoint(account_id)?;
   8650     let conflicts = sqlite_store.load_sync_conflicts(account_id)?;
   8651     let pending_write_count = sqlite_store.load_pending_sync_operations(account_id)?.len();
   8652     let unresolved_conflict_count = conflicts
   8653         .iter()
   8654         .filter(|stored| stored.conflict.is_unresolved())
   8655         .count();
   8656     let blocking_conflict_count = conflicts
   8657         .iter()
   8658         .filter(|stored| {
   8659             stored.conflict.is_unresolved()
   8660                 && matches!(stored.conflict.severity, SyncConflictSeverity::Blocking)
   8661         })
   8662         .count();
   8663 
   8664     Ok(DesktopReminderSyncTruth {
   8665         checkpoint,
   8666         pending_write_count,
   8667         unresolved_conflict_count,
   8668         blocking_conflict_count,
   8669     })
   8670 }
   8671 
   8672 fn derive_selected_account_reminder_schedule(
   8673     farm_id: FarmId,
   8674     today_projection: &TodayAgendaProjection,
   8675     canonical_orders_list: &OrdersListProjection,
   8676     pack_day_projection: &PackDayProjection,
   8677     sync_truth: &DesktopReminderSyncTruth,
   8678     existing_schedule: &ReminderFeedProjection,
   8679 ) -> ReminderFeedProjection {
   8680     let mut items = Vec::new();
   8681 
   8682     if let Some(window) = today_projection.next_fulfillment_window.as_ref() {
   8683         items.push(build_reminder_projection(
   8684             farm_id,
   8685             format!(
   8686                 "reminder:today:fulfillment_window:{}",
   8687                 window.fulfillment_window_id
   8688             ),
   8689             None,
   8690             Some(window.fulfillment_window_id),
   8691             ReminderKind::FulfillmentWindow,
   8692             ReminderSurface::Today,
   8693             "Prepare the next fulfillment window".to_owned(),
   8694             format!(
   8695                 "The next fulfillment window starts at {}.",
   8696                 window.starts_at
   8697             ),
   8698             window.starts_at.clone(),
   8699             Some("Open pack day".to_owned()),
   8700             None,
   8701             existing_schedule,
   8702         ));
   8703     }
   8704 
   8705     if canonical_orders_list.summary.needs_action_orders > 0 {
   8706         let deadline_at = today_projection
   8707             .next_fulfillment_window
   8708             .as_ref()
   8709             .map(|window| window.starts_at.clone())
   8710             .unwrap_or_else(current_utc_timestamp);
   8711         let detail = canonical_orders_list
   8712             .rows
   8713             .first()
   8714             .and_then(|row| row.fulfillment_window_label.as_ref())
   8715             .map(|label| {
   8716                 format!(
   8717                     "{} order(s) still need review before {}.",
   8718                     canonical_orders_list.summary.needs_action_orders, label
   8719                 )
   8720             })
   8721             .unwrap_or_else(|| {
   8722                 format!(
   8723                     "{} order(s) still need review.",
   8724                     canonical_orders_list.summary.needs_action_orders
   8725                 )
   8726             });
   8727         items.push(build_reminder_projection(
   8728             farm_id,
   8729             "reminder:orders:needs_action".to_owned(),
   8730             canonical_orders_list.rows.first().map(|row| row.order_id),
   8731             canonical_orders_list
   8732                 .rows
   8733                 .first()
   8734                 .and_then(|row| row.fulfillment_window_id),
   8735             ReminderKind::OrderAction,
   8736             ReminderSurface::Orders,
   8737             "Review open orders".to_owned(),
   8738             detail,
   8739             deadline_at,
   8740             Some("Review".to_owned()),
   8741             None,
   8742             existing_schedule,
   8743         ));
   8744     }
   8745 
   8746     if let Some(window) = pack_day_projection.fulfillment_window.as_ref() {
   8747         items.push(build_reminder_projection(
   8748             farm_id,
   8749             format!(
   8750                 "reminder:pack_day:fulfillment_window:{}",
   8751                 window.fulfillment_window_id
   8752             ),
   8753             None,
   8754             Some(window.fulfillment_window_id),
   8755             ReminderKind::FulfillmentWindow,
   8756             ReminderSurface::PackDay,
   8757             "Pack for this fulfillment window".to_owned(),
   8758             format!("Packing needs to be ready before {}.", window.starts_at),
   8759             window.starts_at.clone(),
   8760             Some("Review pack day".to_owned()),
   8761             None,
   8762             existing_schedule,
   8763         ));
   8764     }
   8765 
   8766     if let Some(sync_reminder) =
   8767         build_sync_reminder_projection(farm_id, sync_truth, existing_schedule)
   8768     {
   8769         items.push(sync_reminder);
   8770     }
   8771 
   8772     items.sort_by(|left, right| {
   8773         left.deadline_at.cmp(&right.deadline_at).then_with(|| {
   8774             left.reminder_id
   8775                 .to_string()
   8776                 .cmp(&right.reminder_id.to_string())
   8777         })
   8778     });
   8779 
   8780     ReminderFeedProjection { items }
   8781 }
   8782 
   8783 fn build_sync_reminder_projection(
   8784     farm_id: FarmId,
   8785     sync_truth: &DesktopReminderSyncTruth,
   8786     existing_schedule: &ReminderFeedProjection,
   8787 ) -> Option<ReminderDeadlineProjection> {
   8788     if sync_truth.blocking_conflict_count > 0 {
   8789         return Some(build_reminder_projection(
   8790             farm_id,
   8791             "reminder:orders:sync:blocking_conflicts".to_owned(),
   8792             None,
   8793             None,
   8794             ReminderKind::SyncImpact,
   8795             ReminderSurface::Orders,
   8796             "Resolve blocking sync conflicts".to_owned(),
   8797             format!(
   8798                 "{} blocking sync conflict(s) need review before the next sync run.",
   8799                 sync_truth.blocking_conflict_count
   8800             ),
   8801             current_utc_timestamp(),
   8802             Some("Review".to_owned()),
   8803             Some(ReminderUrgency::Blocking),
   8804             existing_schedule,
   8805         ));
   8806     }
   8807 
   8808     if sync_truth.unresolved_conflict_count > 0 {
   8809         return Some(build_reminder_projection(
   8810             farm_id,
   8811             "reminder:orders:sync:conflicts".to_owned(),
   8812             None,
   8813             None,
   8814             ReminderKind::SyncImpact,
   8815             ReminderSurface::Orders,
   8816             "Review sync conflicts".to_owned(),
   8817             format!(
   8818                 "{} sync conflict(s) are still unresolved.",
   8819                 sync_truth.unresolved_conflict_count
   8820             ),
   8821             current_utc_timestamp(),
   8822             Some("Review".to_owned()),
   8823             Some(ReminderUrgency::DueSoon),
   8824             existing_schedule,
   8825         ));
   8826     }
   8827 
   8828     if sync_truth.checkpoint.is_failed() {
   8829         return Some(build_reminder_projection(
   8830             farm_id,
   8831             "reminder:orders:sync:failed".to_owned(),
   8832             None,
   8833             None,
   8834             ReminderKind::SyncImpact,
   8835             ReminderSurface::Orders,
   8836             "Retry sync".to_owned(),
   8837             sync_truth
   8838                 .checkpoint
   8839                 .last_error_message
   8840                 .clone()
   8841                 .unwrap_or_else(|| "The last sync attempt failed.".to_owned()),
   8842             current_utc_timestamp(),
   8843             Some("Review".to_owned()),
   8844             Some(ReminderUrgency::Blocking),
   8845             existing_schedule,
   8846         ));
   8847     }
   8848 
   8849     if sync_truth.pending_write_count > 0 {
   8850         return Some(build_reminder_projection(
   8851             farm_id,
   8852             "reminder:orders:sync:pending".to_owned(),
   8853             None,
   8854             None,
   8855             ReminderKind::SyncImpact,
   8856             ReminderSurface::Orders,
   8857             "Pending local changes".to_owned(),
   8858             format!(
   8859                 "{} local change(s) are waiting to sync.",
   8860                 sync_truth.pending_write_count
   8861             ),
   8862             current_utc_timestamp(),
   8863             Some("Review".to_owned()),
   8864             Some(ReminderUrgency::Upcoming),
   8865             existing_schedule,
   8866         ));
   8867     }
   8868 
   8869     None
   8870 }
   8871 
   8872 fn build_reminder_projection(
   8873     farm_id: FarmId,
   8874     identity_key: String,
   8875     order_id: Option<OrderId>,
   8876     fulfillment_window_id: Option<FulfillmentWindowId>,
   8877     kind: ReminderKind,
   8878     surface: ReminderSurface,
   8879     title: String,
   8880     detail: String,
   8881     deadline_at: String,
   8882     action_label: Option<String>,
   8883     urgency_override: Option<ReminderUrgency>,
   8884     existing_schedule: &ReminderFeedProjection,
   8885 ) -> ReminderDeadlineProjection {
   8886     let reminder_id = stable_reminder_id(identity_key.as_str());
   8887     let urgency = urgency_override.unwrap_or_else(|| reminder_urgency(deadline_at.as_str()));
   8888     let delivery_state = existing_schedule
   8889         .items
   8890         .iter()
   8891         .find(|item| item.reminder_id == reminder_id)
   8892         .map(|item| item.delivery_state)
   8893         .unwrap_or(ReminderDeliveryState::Scheduled);
   8894 
   8895     ReminderDeadlineProjection {
   8896         reminder_id,
   8897         farm_id,
   8898         order_id,
   8899         fulfillment_window_id,
   8900         kind,
   8901         surface,
   8902         urgency,
   8903         title,
   8904         detail,
   8905         deadline_at,
   8906         action_label,
   8907         delivery_state,
   8908     }
   8909 }
   8910 
   8911 fn stable_reminder_id(identity_key: &str) -> ReminderId {
   8912     ReminderId::from(Uuid::new_v5(&Uuid::NAMESPACE_URL, identity_key.as_bytes()))
   8913 }
   8914 
   8915 fn reminder_urgency(deadline_at: &str) -> ReminderUrgency {
   8916     let Ok(deadline) = chrono::DateTime::parse_from_rfc3339(deadline_at) else {
   8917         return ReminderUrgency::Upcoming;
   8918     };
   8919     let deadline = deadline.with_timezone(&Utc);
   8920     let now = Utc::now();
   8921 
   8922     if deadline <= now {
   8923         ReminderUrgency::Overdue
   8924     } else if deadline <= now + Duration::hours(48) {
   8925         ReminderUrgency::DueSoon
   8926     } else {
   8927         ReminderUrgency::Upcoming
   8928     }
   8929 }
   8930 
   8931 fn filter_reminder_surface(
   8932     schedule: &ReminderFeedProjection,
   8933     surface: ReminderSurface,
   8934 ) -> ReminderFeedProjection {
   8935     ReminderFeedProjection {
   8936         items: schedule
   8937             .items
   8938             .iter()
   8939             .filter(|item| item.surface == surface)
   8940             .cloned()
   8941             .collect(),
   8942     }
   8943 }
   8944 
   8945 fn reconcile_resolved_reminder_log_entries(
   8946     existing_schedule: &ReminderFeedProjection,
   8947     schedule: &ReminderFeedProjection,
   8948 ) -> Vec<ReminderLogEntryProjection> {
   8949     existing_schedule
   8950         .items
   8951         .iter()
   8952         .filter(|existing| {
   8953             existing.delivery_state != ReminderDeliveryState::Scheduled
   8954                 && existing.delivery_state != ReminderDeliveryState::Resolved
   8955                 && !schedule
   8956                     .items
   8957                     .iter()
   8958                     .any(|current| current.reminder_id == existing.reminder_id)
   8959         })
   8960         .map(|reminder| build_reminder_log_entry(reminder, ReminderDeliveryState::Resolved))
   8961         .collect()
   8962 }
   8963 
   8964 fn promote_desktop_reminder_presentation(
   8965     schedule: &mut ReminderFeedProjection,
   8966     reminder_log_entries: &mut Vec<ReminderLogEntryProjection>,
   8967     allow_auto_present: bool,
   8968 ) {
   8969     if !allow_auto_present || schedule.items.iter().any(is_desktop_presented_reminder) {
   8970         return;
   8971     }
   8972 
   8973     let Some(index) = schedule
   8974         .items
   8975         .iter()
   8976         .enumerate()
   8977         .filter(|(_, reminder)| {
   8978             reminder.delivery_state == ReminderDeliveryState::Scheduled
   8979                 && is_desktop_presentation_candidate(reminder)
   8980         })
   8981         .min_by(|(_, left), (_, right)| desktop_reminder_sort(left, right))
   8982         .map(|(index, _)| index)
   8983     else {
   8984         return;
   8985     };
   8986 
   8987     schedule.items[index].delivery_state = ReminderDeliveryState::Presented;
   8988     reminder_log_entries.push(build_reminder_log_entry(
   8989         &schedule.items[index],
   8990         ReminderDeliveryState::Presented,
   8991     ));
   8992 }
   8993 
   8994 fn is_desktop_presented_reminder(reminder: &ReminderDeadlineProjection) -> bool {
   8995     reminder.delivery_state == ReminderDeliveryState::Presented
   8996         && is_desktop_presentation_candidate(reminder)
   8997 }
   8998 
   8999 fn is_desktop_presentation_candidate(reminder: &ReminderDeadlineProjection) -> bool {
   9000     matches!(
   9001         reminder.urgency,
   9002         ReminderUrgency::DueSoon | ReminderUrgency::Overdue | ReminderUrgency::Blocking
   9003     )
   9004 }
   9005 
   9006 fn desktop_reminder_sort(
   9007     left: &ReminderDeadlineProjection,
   9008     right: &ReminderDeadlineProjection,
   9009 ) -> std::cmp::Ordering {
   9010     desktop_reminder_priority(left.urgency)
   9011         .cmp(&desktop_reminder_priority(right.urgency))
   9012         .then_with(|| left.deadline_at.cmp(&right.deadline_at))
   9013         .then_with(|| left.reminder_id.cmp(&right.reminder_id))
   9014 }
   9015 
   9016 fn desktop_reminder_priority(urgency: ReminderUrgency) -> u8 {
   9017     match urgency {
   9018         ReminderUrgency::Blocking => 0,
   9019         ReminderUrgency::Overdue => 1,
   9020         ReminderUrgency::DueSoon => 2,
   9021         ReminderUrgency::Upcoming => 3,
   9022     }
   9023 }
   9024 
   9025 fn build_reminder_log_entry(
   9026     reminder: &ReminderDeadlineProjection,
   9027     delivery_state: ReminderDeliveryState,
   9028 ) -> ReminderLogEntryProjection {
   9029     ReminderLogEntryProjection {
   9030         reminder_id: reminder.reminder_id,
   9031         kind: reminder.kind,
   9032         title: reminder.title.clone(),
   9033         recorded_at: current_utc_timestamp(),
   9034         delivery_state,
   9035         detail: (!reminder.detail.trim().is_empty()).then_some(reminder.detail.clone()),
   9036     }
   9037 }
   9038 
   9039 fn load_selected_account_sync_context(
   9040     sqlite_store: &AppSqliteStore,
   9041     identity_projection: &AppIdentityProjection,
   9042     relay_urls: &[String],
   9043 ) -> Result<DesktopSelectedAccountSyncContext, AppSqliteError> {
   9044     let Some(selected_account) = identity_projection.selected_account.as_ref() else {
   9045         return Ok(DesktopSelectedAccountSyncContext::default());
   9046     };
   9047     let account_id = selected_account.account.account_id.as_str();
   9048     let checkpoint = sqlite_store.load_sync_checkpoint(account_id)?;
   9049     let stored_conflicts = sqlite_store.load_sync_conflicts(account_id)?;
   9050     let conflicts = stored_conflicts
   9051         .iter()
   9052         .map(|stored| stored.conflict.clone())
   9053         .collect::<Vec<_>>();
   9054     let pending_write_count = sqlite_store.load_pending_sync_operations(account_id)?.len();
   9055     let relay_urls = normalized_app_relay_ingest_urls(relay_urls)?;
   9056     let relay_ingest = sqlite_store.load_relay_ingest_freshness(
   9057         APP_DIRECT_RELAY_INGEST_SCOPE_KEY,
   9058         &relay_urls,
   9059         current_runtime_time_seconds()?,
   9060         APP_DIRECT_RELAY_INGEST_STALE_AFTER_SECONDS,
   9061     )?;
   9062 
   9063     Ok(DesktopSelectedAccountSyncContext {
   9064         projection: derive_sync_projection(&checkpoint, &conflicts),
   9065         relay_ingest,
   9066         pending_write_count,
   9067         conflicts: stored_conflicts
   9068             .into_iter()
   9069             .map(|stored| DesktopAppSyncConflictSummary {
   9070                 conflict_id: stored.conflict_id,
   9071                 conflict: stored.conflict,
   9072             })
   9073             .collect(),
   9074     })
   9075 }
   9076 
   9077 fn personal_detail(
   9078     projection: &PersonalWorkspaceProjection,
   9079     section: PersonalSection,
   9080 ) -> Option<&BuyerProductDetailProjection> {
   9081     match section {
   9082         PersonalSection::Browse => projection.browse.detail.as_ref(),
   9083         PersonalSection::Search => projection.search.detail.as_ref(),
   9084         PersonalSection::Cart | PersonalSection::Orders => None,
   9085     }
   9086 }
   9087 
   9088 fn personal_detail_mut(
   9089     projection: &mut PersonalWorkspaceProjection,
   9090     section: PersonalSection,
   9091 ) -> Option<&mut BuyerProductDetailProjection> {
   9092     match section {
   9093         PersonalSection::Browse => projection.browse.detail.as_mut(),
   9094         PersonalSection::Search => projection.search.detail.as_mut(),
   9095         PersonalSection::Cart | PersonalSection::Orders => None,
   9096     }
   9097 }
   9098 
   9099 fn next_buyer_cart_for_detail(
   9100     mut current_cart: BuyerCartProjection,
   9101     detail: &BuyerProductDetailProjection,
   9102     replace_existing: bool,
   9103 ) -> Result<BuyerCartProjection, AppSqliteError> {
   9104     let incoming_line = buyer_cart_line_from_detail(detail)?;
   9105     let current_farm_id = current_cart.farm_id;
   9106     let should_replace_lines = replace_existing
   9107         || current_cart.is_empty()
   9108         || current_farm_id != Some(detail.listing.farm_id);
   9109 
   9110     if should_replace_lines {
   9111         current_cart.lines.clear();
   9112     }
   9113 
   9114     current_cart.farm_id = Some(detail.listing.farm_id);
   9115     current_cart.farm_display_name = Some(detail.listing.farm_display_name.clone());
   9116     current_cart.replace_confirmation = None;
   9117 
   9118     if let Some(existing_line) = current_cart
   9119         .lines
   9120         .iter_mut()
   9121         .find(|line| line.product_id == incoming_line.product_id)
   9122     {
   9123         existing_line.quantity = existing_line
   9124             .quantity
   9125             .checked_add(incoming_line.quantity)
   9126             .ok_or(AppSqliteError::InvalidProjection {
   9127                 reason: "buyer cart quantity overflow",
   9128             })?;
   9129         existing_line.line_total_minor_units = existing_line
   9130             .unit_price
   9131             .amount_minor_units
   9132             .checked_mul(existing_line.quantity)
   9133             .ok_or(AppSqliteError::InvalidProjection {
   9134                 reason: "buyer cart line total overflow",
   9135             })?;
   9136     } else {
   9137         current_cart.lines.push(incoming_line);
   9138     }
   9139 
   9140     refresh_buyer_cart_totals(&mut current_cart)?;
   9141 
   9142     Ok(current_cart)
   9143 }
   9144 
   9145 fn next_buyer_cart_after_removing_line(
   9146     mut current_cart: BuyerCartProjection,
   9147     product_id: ProductId,
   9148 ) -> Result<Option<BuyerCartProjection>, AppSqliteError> {
   9149     let previous_line_count = current_cart.lines.len();
   9150     current_cart
   9151         .lines
   9152         .retain(|line| line.product_id != product_id);
   9153     if current_cart.lines.len() == previous_line_count {
   9154         return Ok(None);
   9155     }
   9156 
   9157     if current_cart.lines.is_empty() {
   9158         current_cart.farm_id = None;
   9159         current_cart.farm_display_name = None;
   9160         current_cart.replace_confirmation = None;
   9161         refresh_buyer_cart_totals(&mut current_cart)?;
   9162         return Ok(Some(current_cart));
   9163     }
   9164 
   9165     let farm_id = current_cart.lines[0].farm_id;
   9166     let farm_display_name = current_cart.lines[0].farm_display_name.clone();
   9167     current_cart.farm_id = Some(farm_id);
   9168     current_cart.farm_display_name = Some(farm_display_name);
   9169     current_cart.replace_confirmation = None;
   9170     refresh_buyer_cart_totals(&mut current_cart)?;
   9171 
   9172     Ok(Some(current_cart))
   9173 }
   9174 
   9175 fn buyer_cart_line_from_detail(
   9176     detail: &BuyerProductDetailProjection,
   9177 ) -> Result<BuyerCartLineProjection, AppSqliteError> {
   9178     Ok(BuyerCartLineProjection {
   9179         product_id: detail.listing.product_id,
   9180         farm_id: detail.listing.farm_id,
   9181         farm_display_name: detail.listing.farm_display_name.clone(),
   9182         title: detail.listing.title.clone(),
   9183         quantity: detail.selected_quantity,
   9184         unit_price: detail.listing.price.clone(),
   9185         line_total_minor_units: detail
   9186             .listing
   9187             .price
   9188             .amount_minor_units
   9189             .checked_mul(detail.selected_quantity)
   9190             .ok_or(AppSqliteError::InvalidProjection {
   9191                 reason: "buyer cart line total overflow",
   9192             })?,
   9193         fulfillment_summary: detail
   9194             .listing
   9195             .next_fulfillment_window_label
   9196             .clone()
   9197             .unwrap_or_else(|| detail.listing.availability.label.clone()),
   9198     })
   9199 }
   9200 
   9201 fn refresh_buyer_cart_totals(cart: &mut BuyerCartProjection) -> Result<(), AppSqliteError> {
   9202     if cart.lines.is_empty() {
   9203         cart.subtotal_minor_units = None;
   9204         cart.currency_code = None;
   9205         cart.replace_confirmation = None;
   9206         return Ok(());
   9207     }
   9208 
   9209     let currency_code = cart.lines[0].unit_price.currency_code.clone();
   9210     let subtotal_minor_units = cart.lines.iter().try_fold(0u32, |subtotal, line| {
   9211         if line.unit_price.currency_code != currency_code {
   9212             return Err(AppSqliteError::InvalidProjection {
   9213                 reason: "buyer cart currency mismatch",
   9214             });
   9215         }
   9216 
   9217         subtotal
   9218             .checked_add(line.line_total_minor_units)
   9219             .ok_or(AppSqliteError::InvalidProjection {
   9220                 reason: "buyer cart subtotal overflow",
   9221             })
   9222     })?;
   9223 
   9224     cart.subtotal_minor_units = Some(subtotal_minor_units);
   9225     cart.currency_code = Some(currency_code);
   9226 
   9227     Ok(())
   9228 }
   9229 
   9230 fn selected_farm_id_from_context(
   9231     identity_projection: &AppIdentityProjection,
   9232     farm_setup_projection: &FarmSetupProjection,
   9233 ) -> Option<FarmId> {
   9234     farm_setup_projection
   9235         .saved_farm
   9236         .as_ref()
   9237         .map(|farm| farm.farm_id)
   9238         .or_else(|| {
   9239             identity_projection
   9240                 .selected_account
   9241                 .as_ref()
   9242                 .and_then(|account| account.farmer_activation.farm_id)
   9243         })
   9244 }
   9245 
   9246 fn fallback_farm_profile_for_projection(
   9247     farm_id: FarmId,
   9248     farm_setup_projection: &FarmSetupProjection,
   9249 ) -> FarmProfileRecord {
   9250     let saved_farm_name = farm_setup_projection
   9251         .saved_farm
   9252         .as_ref()
   9253         .filter(|farm| farm.farm_id == farm_id)
   9254         .map(|farm| farm.display_name.clone());
   9255     let drafted_farm_name = farm_setup_projection.draft.farm_name.trim().to_owned();
   9256     let display_name = saved_farm_name
   9257         .or_else(|| (!drafted_farm_name.is_empty()).then_some(drafted_farm_name))
   9258         .unwrap_or_default();
   9259 
   9260     FarmProfileRecord {
   9261         farm_id,
   9262         display_name,
   9263         timezone: "UTC".to_owned(),
   9264         currency_code: "USD".to_owned(),
   9265     }
   9266 }
   9267 
   9268 fn prepare_loaded_farm_rules_projection(
   9269     mut projection: FarmRulesProjection,
   9270     fallback_profile: &FarmProfileRecord,
   9271 ) -> FarmRulesProjection {
   9272     if projection.farm_profile.is_none() {
   9273         projection.farm_profile = Some(fallback_profile.clone());
   9274     }
   9275 
   9276     normalize_pickup_location_defaults(&mut projection.pickup_locations);
   9277     projection.readiness = derive_farm_rules_readiness(&projection);
   9278     projection
   9279 }
   9280 
   9281 fn normalize_farm_rules_projection(
   9282     mut projection: FarmRulesProjection,
   9283     fallback_profile: &FarmProfileRecord,
   9284 ) -> FarmRulesProjection {
   9285     let mut farm_profile = projection
   9286         .farm_profile
   9287         .take()
   9288         .unwrap_or_else(|| fallback_profile.clone());
   9289     farm_profile.farm_id = fallback_profile.farm_id;
   9290     farm_profile.display_name = farm_profile.display_name.trim().to_owned();
   9291     farm_profile.timezone = farm_profile.timezone.trim().to_owned();
   9292     farm_profile.currency_code = farm_profile.currency_code.trim().to_uppercase();
   9293     projection.farm_profile = Some(farm_profile);
   9294 
   9295     for pickup_location in &mut projection.pickup_locations {
   9296         pickup_location.farm_id = fallback_profile.farm_id;
   9297         pickup_location.label = pickup_location.label.trim().to_owned();
   9298         pickup_location.address_line = pickup_location.address_line.trim().to_owned();
   9299         pickup_location.directions = pickup_location
   9300             .directions
   9301             .take()
   9302             .map(|directions| directions.trim().to_owned())
   9303             .filter(|directions| !directions.is_empty());
   9304     }
   9305 
   9306     if let Some(operating_rules) = projection.operating_rules.as_mut() {
   9307         operating_rules.farm_id = fallback_profile.farm_id;
   9308         operating_rules.substitution_policy = operating_rules.substitution_policy.trim().to_owned();
   9309     }
   9310 
   9311     for fulfillment_window in &mut projection.fulfillment_windows {
   9312         fulfillment_window.farm_id = fallback_profile.farm_id;
   9313         fulfillment_window.label = fulfillment_window.label.trim().to_owned();
   9314         fulfillment_window.starts_at = fulfillment_window.starts_at.trim().to_owned();
   9315         fulfillment_window.ends_at = fulfillment_window.ends_at.trim().to_owned();
   9316         fulfillment_window.order_cutoff_at = fulfillment_window.order_cutoff_at.trim().to_owned();
   9317     }
   9318 
   9319     for blackout_period in &mut projection.blackout_periods {
   9320         blackout_period.farm_id = fallback_profile.farm_id;
   9321         blackout_period.label = blackout_period.label.trim().to_owned();
   9322         blackout_period.starts_at = blackout_period.starts_at.trim().to_owned();
   9323         blackout_period.ends_at = blackout_period.ends_at.trim().to_owned();
   9324     }
   9325 
   9326     normalize_pickup_location_defaults(&mut projection.pickup_locations);
   9327     projection.readiness = derive_farm_rules_readiness(&projection);
   9328     projection
   9329 }
   9330 
   9331 fn normalize_pickup_location_defaults(pickup_locations: &mut [PickupLocationRecord]) {
   9332     let default_index = pickup_locations
   9333         .iter()
   9334         .position(|pickup_location| pickup_location.is_default)
   9335         .or_else(|| (!pickup_locations.is_empty()).then_some(0));
   9336 
   9337     for (index, pickup_location) in pickup_locations.iter_mut().enumerate() {
   9338         pickup_location.is_default = Some(index) == default_index;
   9339     }
   9340 }
   9341 
   9342 fn current_utc_timestamp() -> String {
   9343     Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()
   9344 }
   9345 
   9346 fn signed_event_from_local_record(
   9347     record: &LocalEventRecord,
   9348 ) -> Result<Option<SdkRadrootsNostrEvent>, AppSqliteError> {
   9349     let Some(id) = record.event_id.as_deref().map(str::trim) else {
   9350         return Ok(None);
   9351     };
   9352     let Some(author) = record.event_pubkey.as_deref().map(str::trim) else {
   9353         return Ok(None);
   9354     };
   9355     let Some(kind) = record.event_kind else {
   9356         return Ok(None);
   9357     };
   9358     let Some(content) = record.event_content.as_ref() else {
   9359         return Ok(None);
   9360     };
   9361     let Some(sig) = record.event_sig.as_deref().map(str::trim) else {
   9362         return Ok(None);
   9363     };
   9364     let created_at = record.event_created_at.unwrap_or_default();
   9365     let created_at = u32::try_from(created_at).map_err(|_| AppSqliteError::InvalidProjection {
   9366         reason: "signed local event created_at must fit u32",
   9367     })?;
   9368     let kind = u32::try_from(kind).map_err(|_| AppSqliteError::InvalidProjection {
   9369         reason: "signed local event kind must fit u32",
   9370     })?;
   9371 
   9372     Ok(Some(SdkRadrootsNostrEvent {
   9373         id: id.to_owned(),
   9374         author: author.to_owned(),
   9375         created_at,
   9376         kind,
   9377         tags: event_tags_from_value(record.event_tags_json.as_ref())?,
   9378         content: content.clone(),
   9379         sig: sig.to_owned(),
   9380     }))
   9381 }
   9382 
   9383 fn event_tags_from_value(
   9384     value: Option<&serde_json::Value>,
   9385 ) -> Result<Vec<Vec<String>>, AppSqliteError> {
   9386     let Some(value) = value else {
   9387         return Ok(Vec::new());
   9388     };
   9389     let Some(tags) = value.as_array() else {
   9390         return Err(AppSqliteError::InvalidProjection {
   9391             reason: "signed local event tags must be an array",
   9392         });
   9393     };
   9394 
   9395     tags.iter()
   9396         .map(|tag| {
   9397             let Some(values) = tag.as_array() else {
   9398                 return Err(AppSqliteError::InvalidProjection {
   9399                     reason: "signed local event tag must be an array",
   9400                 });
   9401             };
   9402             values
   9403                 .iter()
   9404                 .map(|value| {
   9405                     value
   9406                         .as_str()
   9407                         .map(str::to_owned)
   9408                         .ok_or(AppSqliteError::InvalidProjection {
   9409                             reason: "signed local event tag values must be strings",
   9410                         })
   9411                 })
   9412                 .collect()
   9413         })
   9414         .collect()
   9415 }
   9416 
   9417 fn trade_chain_tag_value(event: &SdkRadrootsNostrEvent, key: &str) -> Option<String> {
   9418     event.tags.iter().find_map(|tag| {
   9419         if tag.first().map(String::as_str) == Some(key) {
   9420             tag.get(1)
   9421                 .map(String::as_str)
   9422                 .map(str::trim)
   9423                 .filter(|value| !value.is_empty())
   9424                 .map(str::to_owned)
   9425         } else {
   9426             None
   9427         }
   9428     })
   9429 }
   9430 
   9431 fn active_order_event_id(
   9432     value: &str,
   9433     field: &'static str,
   9434 ) -> Result<RadrootsEventId, AppSqliteError> {
   9435     value.parse().map_err(|_| AppSqliteError::DecodeId {
   9436         field,
   9437         value: value.to_owned(),
   9438     })
   9439 }
   9440 
   9441 fn active_order_pubkey(
   9442     value: &str,
   9443     field: &'static str,
   9444 ) -> Result<RadrootsPublicKey, AppSqliteError> {
   9445     value.parse().map_err(|_| AppSqliteError::DecodeId {
   9446         field,
   9447         value: value.to_owned(),
   9448     })
   9449 }
   9450 
   9451 fn active_order_event_record_context(
   9452     event: &SdkRadrootsNostrEvent,
   9453     message_type: radroots_sdk::protocol::order::RadrootsOrderEventType,
   9454 ) -> Result<(RadrootsPublicKey, RadrootsEventId, RadrootsEventId), AppSqliteError> {
   9455     let context = order_event_context_from_tags(message_type, &event.tags).map_err(|_| {
   9456         AppSqliteError::InvalidProjection {
   9457             reason: "order lifecycle evidence is invalid",
   9458         }
   9459     })?;
   9460     let root_event_id = context
   9461         .root_event_id
   9462         .ok_or(AppSqliteError::InvalidProjection {
   9463             reason: "order lifecycle evidence is invalid",
   9464         })?;
   9465     let prev_event_id = context
   9466         .prev_event_id
   9467         .ok_or(AppSqliteError::InvalidProjection {
   9468             reason: "order lifecycle evidence is invalid",
   9469         })?;
   9470     Ok((context.counterparty_pubkey, root_event_id, prev_event_id))
   9471 }
   9472 
   9473 fn active_order_pending_revision_proposal(
   9474     lifecycle: &ResolvedAppOrderLifecycleEvidence,
   9475 ) -> Option<&ResolvedAppOrderRevisionProposalEvidence> {
   9476     let mut parent_event_id = lifecycle.request_event_id.as_str();
   9477     loop {
   9478         let proposals = lifecycle
   9479             .revision_proposals
   9480             .iter()
   9481             .filter(|proposal| proposal.payload.prev_event_id == parent_event_id)
   9482             .collect::<Vec<_>>();
   9483         let proposal = match proposals.as_slice() {
   9484             [] => return None,
   9485             [proposal] => *proposal,
   9486             _ => return None,
   9487         };
   9488         let decisions = lifecycle
   9489             .revision_decisions
   9490             .iter()
   9491             .filter(|decision| {
   9492                 decision.payload.prev_event_id == proposal.event_id
   9493                     && decision.payload.revision_id == proposal.payload.revision_id
   9494             })
   9495             .collect::<Vec<_>>();
   9496         let decision = match decisions.as_slice() {
   9497             [] => return Some(proposal),
   9498             [decision] => *decision,
   9499             _ => return None,
   9500         };
   9501         parent_event_id = decision.event_id.as_str();
   9502     }
   9503 }
   9504 
   9505 fn insert_seller_order_request_evidence(
   9506     order_id: &OrderId,
   9507     event: &SdkRadrootsNostrEvent,
   9508     payload: RadrootsOrderRequest,
   9509     matched_requests: &mut BTreeMap<String, ResolvedAppSellerOrderRequest>,
   9510 ) {
   9511     let app_order_id = projected_order_id_from_trade_request(
   9512         payload.order_id.as_str(),
   9513         payload.buyer_pubkey.as_str(),
   9514     );
   9515     if app_order_id != *order_id {
   9516         return;
   9517     }
   9518     matched_requests
   9519         .entry(event.id.clone())
   9520         .or_insert_with(|| ResolvedAppSellerOrderRequest {
   9521             request_event: event.clone(),
   9522             request_event_id: event.id.clone(),
   9523             request_author_pubkey: event.author.clone(),
   9524             listing_event_id: listing_event_id_from_tags(&event.tags),
   9525             payload,
   9526         });
   9527 }
   9528 
   9529 fn listing_event_id_from_tags(tags: &[Vec<String>]) -> Option<String> {
   9530     tags.iter().find_map(|tag| {
   9531         if tag.first().map(String::as_str) == Some("listing_event") {
   9532             tag.get(1)
   9533                 .map(String::as_str)
   9534                 .map(str::trim)
   9535                 .filter(|value| !value.is_empty())
   9536                 .map(str::to_owned)
   9537         } else {
   9538             None
   9539         }
   9540     })
   9541 }
   9542 
   9543 fn seller_order_inventory_commitments(
   9544     order: &SellerOrderDecisionExport,
   9545 ) -> Result<Vec<AppOrderDecisionInventoryCommitment>, AppSqliteError> {
   9546     if order.lines.is_empty() {
   9547         return Err(AppSqliteError::InvalidProjection {
   9548             reason: "seller order decision requires order lines",
   9549         });
   9550     }
   9551 
   9552     order
   9553         .lines
   9554         .iter()
   9555         .map(|line| {
   9556             let bin_id =
   9557                 line.listing_bin_id
   9558                     .as_deref()
   9559                     .ok_or(AppSqliteError::InvalidProjection {
   9560                         reason: "seller order decision requires listing bin evidence",
   9561                     })?;
   9562             let stock_count = line.stock_count.ok_or(AppSqliteError::InvalidProjection {
   9563                 reason: "seller order decision requires current product stock",
   9564             })?;
   9565             let available_quantity = stock_count.checked_sub(line.reserved_quantity).ok_or(
   9566                 AppSqliteError::InvalidProjection {
   9567                     reason: "seller order decision inventory is over-reserved",
   9568                 },
   9569             )?;
   9570             if line.quantity > available_quantity {
   9571                 return Err(AppSqliteError::InvalidProjection {
   9572                     reason: "seller order decision would over-reserve inventory",
   9573                 });
   9574             }
   9575 
   9576             Ok(AppOrderDecisionInventoryCommitment {
   9577                 bin_id: bin_id.to_owned(),
   9578                 bin_count: line.quantity,
   9579             })
   9580         })
   9581         .collect()
   9582 }
   9583 
   9584 fn publish_order_id(value: &str) -> Result<RadrootsOrderId, AppSyncTransportError> {
   9585     RadrootsOrderId::parse(value).map_err(|error| AppSyncTransportError::failed(error.to_string()))
   9586 }
   9587 
   9588 fn publish_revision_id(value: &str) -> Result<RadrootsOrderRevisionId, AppSyncTransportError> {
   9589     RadrootsOrderRevisionId::parse(value)
   9590         .map_err(|error| AppSyncTransportError::failed(error.to_string()))
   9591 }
   9592 
   9593 fn publish_event_id(value: &str) -> Result<RadrootsEventId, AppSyncTransportError> {
   9594     RadrootsEventId::parse(value).map_err(|error| AppSyncTransportError::failed(error.to_string()))
   9595 }
   9596 
   9597 fn publish_pubkey(value: &str) -> Result<RadrootsPublicKey, AppSyncTransportError> {
   9598     RadrootsPublicKey::parse(value)
   9599         .map_err(|error| AppSyncTransportError::failed(error.to_string()))
   9600 }
   9601 
   9602 fn publish_listing_addr(value: &str) -> Result<RadrootsListingAddress, AppSyncTransportError> {
   9603     RadrootsListingAddress::parse(value)
   9604         .map_err(|error| AppSyncTransportError::failed(error.to_string()))
   9605 }
   9606 
   9607 fn publish_bin_id(value: &str) -> Result<RadrootsInventoryBinId, AppSyncTransportError> {
   9608     RadrootsInventoryBinId::parse(value)
   9609         .map_err(|error| AppSyncTransportError::failed(error.to_string()))
   9610 }
   9611 
   9612 fn order_decision_publish_payload_to_sdk_decision(
   9613     payload: &AppOrderDecisionPublishPayload,
   9614 ) -> Result<RadrootsOrderDecision, AppSyncTransportError> {
   9615     Ok(RadrootsOrderDecision {
   9616         order_id: publish_order_id(payload.trade_order_id.as_str())?,
   9617         listing_addr: publish_listing_addr(payload.listing_addr.as_str())?,
   9618         buyer_pubkey: publish_pubkey(payload.buyer_pubkey.as_str())?,
   9619         seller_pubkey: publish_pubkey(payload.seller_pubkey.as_str())?,
   9620         decision: match &payload.decision {
   9621             AppOrderDecisionPayload::Accepted {
   9622                 inventory_commitments,
   9623             } => RadrootsOrderDecisionOutcome::Accepted {
   9624                 inventory_commitments: inventory_commitments
   9625                     .iter()
   9626                     .map(|commitment| {
   9627                         Ok(RadrootsOrderInventoryCommitment {
   9628                             bin_id: publish_bin_id(commitment.bin_id.as_str())?,
   9629                             bin_count: commitment.bin_count,
   9630                         })
   9631                     })
   9632                     .collect::<Result<Vec<_>, AppSyncTransportError>>()?,
   9633             },
   9634             AppOrderDecisionPayload::Declined { reason } => {
   9635                 RadrootsOrderDecisionOutcome::Declined {
   9636                     reason: reason.clone(),
   9637                 }
   9638             }
   9639         },
   9640     })
   9641 }
   9642 
   9643 fn order_revision_proposal_publish_payload_to_sdk_revision(
   9644     payload: &AppOrderRevisionProposalPublishPayload,
   9645 ) -> Result<RadrootsOrderRevisionProposal, AppSyncTransportError> {
   9646     Ok(RadrootsOrderRevisionProposal {
   9647         revision_id: publish_revision_id(payload.revision_id.as_str())?,
   9648         order_id: publish_order_id(payload.trade_order_id.as_str())?,
   9649         listing_addr: publish_listing_addr(payload.listing_addr.as_str())?,
   9650         buyer_pubkey: publish_pubkey(payload.buyer_pubkey.as_str())?,
   9651         seller_pubkey: publish_pubkey(payload.seller_pubkey.as_str())?,
   9652         root_event_id: publish_event_id(payload.request_event_id.as_str())?,
   9653         prev_event_id: publish_event_id(payload.prev_event_id.as_str())?,
   9654         items: payload.items.clone(),
   9655         economics: payload.economics.clone(),
   9656         reason: payload.reason.clone(),
   9657     })
   9658 }
   9659 
   9660 fn order_revision_decision_publish_payload_to_sdk_revision_decision(
   9661     payload: &AppOrderRevisionDecisionPublishPayload,
   9662 ) -> Result<RadrootsOrderRevisionDecision, AppSyncTransportError> {
   9663     Ok(RadrootsOrderRevisionDecision {
   9664         revision_id: publish_revision_id(payload.revision_id.as_str())?,
   9665         order_id: publish_order_id(payload.trade_order_id.as_str())?,
   9666         listing_addr: publish_listing_addr(payload.listing_addr.as_str())?,
   9667         buyer_pubkey: publish_pubkey(payload.buyer_pubkey.as_str())?,
   9668         seller_pubkey: publish_pubkey(payload.seller_pubkey.as_str())?,
   9669         root_event_id: publish_event_id(payload.request_event_id.as_str())?,
   9670         prev_event_id: publish_event_id(payload.prev_event_id.as_str())?,
   9671         decision: payload.decision.clone(),
   9672     })
   9673 }
   9674 
   9675 fn order_cancellation_publish_payload_to_sdk_cancellation(
   9676     payload: &AppOrderCancellationPublishPayload,
   9677 ) -> Result<RadrootsOrderCancellation, AppSyncTransportError> {
   9678     Ok(RadrootsOrderCancellation {
   9679         order_id: publish_order_id(payload.trade_order_id.as_str())?,
   9680         listing_addr: publish_listing_addr(payload.listing_addr.as_str())?,
   9681         buyer_pubkey: publish_pubkey(payload.buyer_pubkey.as_str())?,
   9682         seller_pubkey: publish_pubkey(payload.seller_pubkey.as_str())?,
   9683         reason: payload.reason.clone(),
   9684     })
   9685 }
   9686 
   9687 #[cfg(test)]
   9688 fn pending_sync_upsert(aggregate: SyncAggregateRef, payload_json: String) -> PendingSyncOperation {
   9689     let created_at = current_utc_timestamp();
   9690 
   9691     PendingSyncOperation::new(
   9692         aggregate,
   9693         SyncOperationKind::Upsert,
   9694         payload_json,
   9695         created_at,
   9696     )
   9697 }
   9698 
   9699 #[cfg(test)]
   9700 fn farm_sync_payload(
   9701     farm_id: FarmId,
   9702     display_name: &str,
   9703     readiness: Option<FarmReadiness>,
   9704     source: &str,
   9705 ) -> String {
   9706     json!({
   9707         "aggregate_kind": "farm",
   9708         "farm_id": farm_id.to_string(),
   9709         "display_name": display_name,
   9710         "readiness": readiness.map(|value| match value {
   9711             FarmReadiness::Incomplete => "incomplete",
   9712             FarmReadiness::Ready => "ready",
   9713         }),
   9714         "source": source,
   9715     })
   9716     .to_string()
   9717 }
   9718 
   9719 fn signed_order_request_evidence_record_is_usable(record: &LocalEventRecord) -> bool {
   9720     if record.status != LocalRecordStatus::Published
   9721         || matches!(
   9722             record.outbox_status,
   9723             PublishOutboxStatus::Pending | PublishOutboxStatus::Failed
   9724         )
   9725     {
   9726         return false;
   9727     }
   9728     let Some(relay_delivery_json) = record.relay_delivery_json.as_ref() else {
   9729         return false;
   9730     };
   9731     let Ok(relay_delivery) = RelayDeliveryEvidence::from_json_value(relay_delivery_json) else {
   9732         return false;
   9733     };
   9734     matches!(relay_delivery.state.as_str(), "acknowledged" | "observed")
   9735 }
   9736 
   9737 #[cfg(test)]
   9738 mod tests {
   9739     use std::{
   9740         collections::BTreeSet,
   9741         fs,
   9742         path::PathBuf,
   9743         sync::mpsc,
   9744         sync::{Arc, Mutex},
   9745         thread,
   9746         time::{Duration as StdDuration, SystemTime, UNIX_EPOCH},
   9747     };
   9748 
   9749     use chrono::{Duration, Utc};
   9750     use futures_util::{SinkExt, StreamExt};
   9751     use radroots_app_core::{
   9752         AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform,
   9753         AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSharedAccountsPaths,
   9754         SHARED_ACCOUNTS_STORE_FILE_NAME, SHARED_IDENTITY_FILE_NAME,
   9755     };
   9756     use radroots_app_remote_signer::{
   9757         RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord,
   9758     };
   9759     use radroots_app_sqlite::{
   9760         AppSdkMigrationReceiptSourceKind, AppSdkMigrationState, AppSqliteError, AppSqliteStore,
   9761         BuyerOrderCoordinationState, DatabaseTarget, latest_schema_version,
   9762         projected_order_id_from_trade_request,
   9763     };
   9764     use radroots_app_state::{
   9765         APP_STATE_FILE_NAME, AppStateCommand, AppStatePersistenceRepository, AppStateRepository,
   9766         AppStateRepositoryError, AppStateStore, AppStateStoreError, FileBackedAppStateRepository,
   9767         HomeRoute,
   9768     };
   9769     use radroots_app_sync::{
   9770         AppFarmProfilePublishPayload, AppListingPublishPayload, AppOrderCancellationPublishPayload,
   9771         AppOrderDecisionInventoryCommitment, AppOrderDecisionPayload,
   9772         AppOrderDecisionPublishPayload, AppOrderRequestItemPayload, AppOrderRequestPublishPayload,
   9773         AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload,
   9774         AppPublishContext, AppPublishPayload, AppPublishedOperationReceipt,
   9775         AppRelayIngestScopeFreshness, AppRelayIngestScopeStatus, AppSyncRequest, AppSyncResult,
   9776         AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, PendingSyncOperation,
   9777         PendingSyncOperationState, RecordedAppSyncTransport, SyncAggregateRef, SyncCheckpointState,
   9778         SyncCheckpointStatus, SyncConflict, SyncConflictKind, SyncConflictResolutionStatus,
   9779         SyncConflictSeverity, SyncOperationKind, SyncTrigger,
   9780     };
   9781     use radroots_app_view::{
   9782         AccountCustody, AccountSummary, AccountSurfaceActivationProjection, ActiveSurface,
   9783         AppActivityKind, AppIdentityProjection, AppStartupGate, BlackoutPeriodId,
   9784         BlackoutPeriodRecord, BuyerOrderReviewDisabledReason, BuyerOrderReviewDraft,
   9785         BuyerOrderStatus, FarmId, FarmOperatingRulesRecord, FarmOrderMethod, FarmProfileRecord,
   9786         FarmReadiness, FarmReadinessBlocker, FarmRulesProjection, FarmSetupDraft,
   9787         FarmSetupProjection, FarmSummary, FarmerActivationProjection, FarmerSection,
   9788         FulfillmentWindowId, FulfillmentWindowRecord, LoggedOutStartupProjection, OrderId,
   9789         OrderStatus, OrdersFilter, PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind,
   9790         PackDayBatchPrintStatus, PackDayExportInstanceId, PackDayExportStatus,
   9791         PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow,
   9792         PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow,
   9793         PackDayProjection, PackDayRosterRow, PersonalSection, PickupLocationId,
   9794         PickupLocationRecord, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductStatus,
   9795         ProductsFilter, ProductsSort, ReminderDeliveryState, ReminderFeedProjection, ReminderKind,
   9796         SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection,
   9797         ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary,
   9798     };
   9799     use radroots_core::{
   9800         RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
   9801     };
   9802     use radroots_events::ids::{
   9803         RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
   9804         RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey,
   9805     };
   9806     use radroots_events_codec::wire::WireEventParts;
   9807     use radroots_identity::{RadrootsIdentity, RadrootsIdentityId};
   9808     use radroots_local_events::{
   9809         BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalEventRecordInput,
   9810         LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus,
   9811         RelayDeliveryEvidence, SourceRuntime,
   9812     };
   9813     use radroots_nostr::prelude::{
   9814         RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrKeys, RadrootsNostrSecretKey,
   9815         RadrootsNostrTimestamp, radroots_event_from_nostr, radroots_nostr_build_event,
   9816     };
   9817     use radroots_nostr_accounts::prelude::{
   9818         RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore,
   9819         RadrootsNostrMemoryAccountStore, RadrootsNostrSecretVaultMemory, RadrootsSecretVault,
   9820         account_secret_slot,
   9821     };
   9822     use radroots_sdk::protocol::events::{
   9823         RadrootsNostrEvent as SdkRadrootsNostrEvent, RadrootsNostrEventPtr,
   9824     };
   9825     use radroots_sdk::protocol::order::{
   9826         RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
   9827         RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment,
   9828         RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest,
   9829         RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal,
   9830     };
   9831     use radroots_sdk::{
   9832         LISTING_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND,
   9833         ORDER_DECISION_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND,
   9834         ORDER_SUBMIT_OPERATION_KIND,
   9835     };
   9836     use radroots_sql_core::{SqlExecutor, SqliteExecutor};
   9837     use serde_json::json;
   9838     use tokio::net::TcpListener;
   9839     use tokio::sync::oneshot;
   9840     use tokio_tungstenite::tungstenite::Message;
   9841     use uuid::Uuid;
   9842 
   9843     use crate::accounts::DesktopLocalIdentityImportRequest;
   9844 
   9845     const SDK_TEST_BUYER_SECRET_KEY_HEX: &str =
   9846         "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
   9847     const SDK_TEST_BUYER_PUBLIC_KEY_HEX: &str =
   9848         "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
   9849     const SDK_TEST_SELLER_SECRET_KEY_HEX: &str =
   9850         "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8";
   9851     const SDK_TEST_SELLER_PUBLIC_KEY_HEX: &str =
   9852         "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
   9853 
   9854     use super::{
   9855         APP_DATABASE_FILE_NAME, APP_SYNC_PUBLISH_USES_SDK_RUNTIME_MESSAGE,
   9856         ConfiguredRelayAppSyncTransport, DesktopAppRuntime, DesktopAppRuntimeActivityContextError,
   9857         DesktopAppRuntimeCommandError, DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeState,
   9858         DesktopAppSdkDiagnosticsState, DesktopAppSyncStatusSummary, DesktopRemoteSignerPaths,
   9859         SYNC_TRANSPORT_UNAVAILABLE_MESSAGE, TokioRuntimeBuilder, default_sync_transport,
   9860         direct_relay_event_source_runtime, farm_sync_payload, is_hex_64,
   9861         order_decision_publish_payload_to_sdk_decision, pending_sync_upsert,
   9862         signed_event_from_local_record,
   9863     };
   9864     use crate::pack_day_host_handoff::PackDayHostHandoffError;
   9865     use crate::pack_day_print::{
   9866         PackDayBatchPrintError, PackDayPrintCommandResult, PackDayPrintError,
   9867         execute_pack_day_batch_print_plan_with, prepared_customer_label_asset_root,
   9868     };
   9869 
   9870     const BUYER_VISIBLE_SELLER_PUBKEY: &str =
   9871         "2222222222222222222222222222222222222222222222222222222222222222";
   9872 
   9873     #[derive(Clone)]
   9874     struct SharedRecordedSyncTransport(Arc<Mutex<RecordedAppSyncTransport>>);
   9875 
   9876     impl AppSyncTransport for SharedRecordedSyncTransport {
   9877         fn sync(
   9878             &mut self,
   9879             request: AppSyncRequest,
   9880         ) -> Result<AppSyncResult, AppSyncTransportError> {
   9881             self.0
   9882                 .lock()
   9883                 .expect("recorded sync transport lock")
   9884                 .sync(request)
   9885         }
   9886     }
   9887 
   9888     #[test]
   9889     fn direct_relay_trade_events_use_network_source_runtime_for_app_shaped_d_tags() {
   9890         let app_shaped_d_tag =
   9891             super::d_tag_from_uuid(Uuid::from_u128(0x12345678123446789123456781234567));
   9892 
   9893         assert_eq!(
   9894             direct_relay_event_source_runtime(30340, Some(app_shaped_d_tag.as_str())),
   9895             SourceRuntime::Network
   9896         );
   9897         assert_eq!(
   9898             direct_relay_event_source_runtime(30402, Some(app_shaped_d_tag.as_str())),
   9899             SourceRuntime::Network
   9900         );
   9901         assert_eq!(
   9902             direct_relay_event_source_runtime(30403, Some(app_shaped_d_tag.as_str())),
   9903             SourceRuntime::Network
   9904         );
   9905     }
   9906 
   9907     struct ThreadedAckRelay {
   9908         url: String,
   9909         events: Arc<Mutex<Vec<serde_json::Value>>>,
   9910         shutdown_tx: Option<oneshot::Sender<()>>,
   9911         join_handle: Option<thread::JoinHandle<()>>,
   9912     }
   9913 
   9914     impl ThreadedAckRelay {
   9915         fn spawn() -> Self {
   9916             let (url_tx, url_rx) = mpsc::channel();
   9917             let (shutdown_tx, shutdown_rx) = oneshot::channel();
   9918             let events: Arc<Mutex<Vec<serde_json::Value>>> = Arc::new(Mutex::new(Vec::new()));
   9919             let thread_events = events.clone();
   9920             let join_handle = thread::spawn(move || {
   9921                 let runtime = TokioRuntimeBuilder::new_current_thread()
   9922                     .enable_all()
   9923                     .build()
   9924                     .expect("relay runtime should build");
   9925                 runtime.block_on(async move {
   9926                     let listener = TcpListener::bind("127.0.0.1:0")
   9927                         .await
   9928                         .expect("test relay should bind");
   9929                     let url = format!(
   9930                         "ws://{}",
   9931                         listener.local_addr().expect("test relay local addr")
   9932                     );
   9933                     url_tx.send(url).expect("relay url should send");
   9934                     let mut shutdown_rx = shutdown_rx;
   9935                     loop {
   9936                         tokio::select! {
   9937                             _ = &mut shutdown_rx => break,
   9938                             accepted = listener.accept() => {
   9939                                 let Ok((stream, _)) = accepted else {
   9940                                     break;
   9941                                 };
   9942                                 let events = thread_events.clone();
   9943                                 tokio::spawn(async move {
   9944                                     let Ok(websocket) = tokio_tungstenite::accept_async(stream).await else {
   9945                                         return;
   9946                                     };
   9947                                     let (mut writer, mut reader) = websocket.split();
   9948                                     while let Some(message) = reader.next().await {
   9949                                         let Ok(Message::Text(text)) = message else {
   9950                                             continue;
   9951                                         };
   9952                                         let Ok(value) = serde_json::from_str::<serde_json::Value>(text.as_str()) else {
   9953                                             continue;
   9954                                         };
   9955                                         let Some(items) = value.as_array() else {
   9956                                             continue;
   9957                                         };
   9958                                         match items.as_slice() {
   9959                                             [kind, event, ..] if kind.as_str() == Some("EVENT") => {
   9960                                                 let Some(event_id) = event.get("id").and_then(|id| id.as_str()) else {
   9961                                                     continue;
   9962                                                 };
   9963                                                 events.lock().expect("relay events lock").push(event.clone());
   9964                                                 let response = json!(["OK", event_id, true, ""]).to_string();
   9965                                                 if writer.send(Message::Text(response.into())).await.is_err() {
   9966                                                     break;
   9967                                                 }
   9968                                             }
   9969                                             [kind, subscription_id, filters @ ..] if kind.as_str() == Some("REQ") => {
   9970                                                 let Some(subscription_id) = subscription_id.as_str() else {
   9971                                                     continue;
   9972                                                 };
   9973                                                 let snapshot = events.lock().expect("relay events lock").clone();
   9974                                                 for event in snapshot.iter().filter(|event| relay_event_matches_filters(event, filters)) {
   9975                                                     let response = json!(["EVENT", subscription_id, event]).to_string();
   9976                                                     if writer.send(Message::Text(response.into())).await.is_err() {
   9977                                                         break;
   9978                                                     }
   9979                                                 }
   9980                                                 let response = json!(["EOSE", subscription_id]).to_string();
   9981                                                 if writer.send(Message::Text(response.into())).await.is_err() {
   9982                                                     break;
   9983                                                 }
   9984                                             }
   9985                                             [kind, ..] if kind.as_str() == Some("CLOSE") => break,
   9986                                             _ => {}
   9987                                         }
   9988                                     }
   9989                                 });
   9990                             }
   9991                         }
   9992                     }
   9993                 });
   9994             });
   9995             let url = url_rx.recv().expect("relay url should be received");
   9996 
   9997             Self {
   9998                 url,
   9999                 events,
  10000                 shutdown_tx: Some(shutdown_tx),
  10001                 join_handle: Some(join_handle),
  10002             }
  10003         }
  10004 
  10005         fn url(&self) -> &str {
  10006             self.url.as_str()
  10007         }
  10008 
  10009         fn event_count(&self) -> usize {
  10010             self.events.lock().expect("relay events lock").len()
  10011         }
  10012     }
  10013 
  10014     fn relay_event_matches_filters(
  10015         event: &serde_json::Value,
  10016         filters: &[serde_json::Value],
  10017     ) -> bool {
  10018         filters.is_empty()
  10019             || filters
  10020                 .iter()
  10021                 .any(|filter| relay_event_matches_filter(event, filter))
  10022     }
  10023 
  10024     fn relay_event_matches_filter(event: &serde_json::Value, filter: &serde_json::Value) -> bool {
  10025         let event_kind = event.get("kind").and_then(serde_json::Value::as_u64);
  10026         if let Some(kinds) = filter.get("kinds").and_then(serde_json::Value::as_array)
  10027             && !kinds
  10028                 .iter()
  10029                 .filter_map(serde_json::Value::as_u64)
  10030                 .any(|kind| Some(kind) == event_kind)
  10031         {
  10032             return false;
  10033         }
  10034 
  10035         let event_created_at = event.get("created_at").and_then(serde_json::Value::as_u64);
  10036         if let Some(until) = filter.get("until").and_then(serde_json::Value::as_u64)
  10037             && event_created_at.is_some_and(|created_at| created_at > until)
  10038         {
  10039             return false;
  10040         }
  10041 
  10042         true
  10043     }
  10044 
  10045     fn assert_missing_listing_provenance_relay_error(
  10046         error: &AppSyncTransportError,
  10047         relay_url: &str,
  10048     ) {
  10049         let AppSyncTransportError::Failed { message } = error else {
  10050             panic!("unexpected error: {error}");
  10051         };
  10052         let value =
  10053             serde_json::from_str::<serde_json::Value>(message).expect("structured relay error");
  10054         assert_eq!(value["code"], "missing_listing_provenance_relay");
  10055         assert_eq!(value["missing_provenance_relays"], json!([relay_url]));
  10056     }
  10057 
  10058     fn assert_migrated_payload_uses_sdk_runtime(error: AppSyncTransportError) {
  10059         match error {
  10060             AppSyncTransportError::Failed { message } => {
  10061                 assert_eq!(message, APP_SYNC_PUBLISH_USES_SDK_RUNTIME_MESSAGE)
  10062             }
  10063             unexpected => panic!("unexpected migrated payload error: {unexpected}"),
  10064         }
  10065     }
  10066 
  10067     fn direct_relay_listing_payload(
  10068         account_id: &str,
  10069         farm_pubkey: String,
  10070         source: &str,
  10071     ) -> AppPublishPayload {
  10072         let farm_id = FarmId::new();
  10073         let product_id = ProductId::new();
  10074         AppPublishPayload::Listing(AppListingPublishPayload {
  10075             context: AppPublishContext::new(account_id.to_owned(), source),
  10076             product_id,
  10077             listing_d_tag: Some(super::d_tag_from_uuid(product_id.as_uuid())),
  10078             farm_id: Some(farm_id),
  10079             farm_pubkey: Some(farm_pubkey),
  10080             farm_d_tag: Some(super::d_tag_from_uuid(farm_id.as_uuid())),
  10081             title: "North field eggs".to_owned(),
  10082             subtitle: Some("Pasture raised".to_owned()),
  10083             category: Some("eggs".to_owned()),
  10084             unit_label: "each".to_owned(),
  10085             price_minor_units: Some(750),
  10086             price_currency: "USD".to_owned(),
  10087             stock_quantity: Some(12),
  10088             availability_window_id: Some(FulfillmentWindowId::new()),
  10089             availability_starts_at: Some("2099-05-25T14:00:00Z".to_owned()),
  10090             availability_ends_at: Some("2099-05-25T18:00:00Z".to_owned()),
  10091             fulfillment_method: Some("pickup".to_owned()),
  10092             fulfillment_location: Some("farmstand".to_owned()),
  10093             status: ProductStatus::Published,
  10094         })
  10095     }
  10096 
  10097     fn publish_signed_test_event_to_relay(relay: &ThreadedAckRelay, event: &RadrootsNostrEvent) {
  10098         let runtime = TokioRuntimeBuilder::new_current_thread()
  10099             .enable_all()
  10100             .build()
  10101             .expect("test relay publish runtime should build");
  10102         runtime.block_on(async {
  10103             let client = RadrootsNostrClient::new_signerless();
  10104             client
  10105                 .add_write_relay(relay.url())
  10106                 .await
  10107                 .expect("test relay should accept write relay");
  10108             let connection_output = client.try_connect(StdDuration::from_secs(5)).await;
  10109             assert!(
  10110                 !connection_output.success.is_empty(),
  10111                 "test relay write connection should succeed"
  10112             );
  10113             let output = client
  10114                 .send_event_to(vec![relay.url().to_owned()], event)
  10115                 .await
  10116                 .expect("test event should publish to relay");
  10117             assert!(
  10118                 !output.success.is_empty(),
  10119                 "test event publish should be acknowledged"
  10120             );
  10121         });
  10122     }
  10123 
  10124     impl Drop for ThreadedAckRelay {
  10125         fn drop(&mut self) {
  10126             if let Some(shutdown_tx) = self.shutdown_tx.take() {
  10127                 let _ = shutdown_tx.send(());
  10128             }
  10129             if let Some(join_handle) = self.join_handle.take() {
  10130                 let _ = join_handle.join();
  10131             }
  10132         }
  10133     }
  10134 
  10135     fn test_event_id_seed(seed: &str) -> String {
  10136         if RadrootsEventId::parse(seed).is_ok() {
  10137             return seed.to_owned();
  10138         }
  10139         let mut bytes = [0u8; 32];
  10140         for (index, byte) in seed.bytes().enumerate() {
  10141             let primary = index % bytes.len();
  10142             let secondary = (index * 7 + 13) % bytes.len();
  10143             bytes[primary] = bytes[primary]
  10144                 .wrapping_add(byte)
  10145                 .wrapping_add((index as u8).wrapping_mul(31));
  10146             bytes[secondary] ^= byte.rotate_left((index % 8) as u32);
  10147         }
  10148         let mut hex = String::with_capacity(64);
  10149         for byte in bytes {
  10150             hex.push_str(format!("{byte:02x}").as_str());
  10151         }
  10152         hex
  10153     }
  10154 
  10155     fn test_event_id(seed: &str) -> RadrootsEventId {
  10156         RadrootsEventId::parse(test_event_id_seed(seed)).expect("event id")
  10157     }
  10158 
  10159     fn signed_event_id(record_id: &str) -> String {
  10160         test_event_id_seed(record_id)
  10161     }
  10162 
  10163     fn test_event_signature_seed(seed: &str) -> String {
  10164         let base = test_event_id_seed(seed);
  10165         format!("{base}{base}")
  10166     }
  10167 
  10168     fn test_event_created_at(seed: &str, base: u32) -> u32 {
  10169         let offset = seed
  10170             .bytes()
  10171             .fold(0u32, |acc, byte| (acc + u32::from(byte)) % 900);
  10172         base + offset
  10173     }
  10174 
  10175     fn signed_test_event(
  10176         secret_key_hex: &str,
  10177         created_at: u32,
  10178         parts: WireEventParts,
  10179     ) -> SdkRadrootsNostrEvent {
  10180         let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
  10181         let keys = RadrootsNostrKeys::new(secret_key);
  10182         let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
  10183             .expect("test event builder")
  10184             .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at)))
  10185             .sign_with_keys(&keys)
  10186             .expect("test event should sign");
  10187         radroots_event_from_nostr(&event)
  10188     }
  10189 
  10190     fn signed_test_event_for_pubkey(
  10191         pubkey: &str,
  10192         created_at: u32,
  10193         parts: WireEventParts,
  10194     ) -> Option<SdkRadrootsNostrEvent> {
  10195         match pubkey {
  10196             SDK_TEST_BUYER_PUBLIC_KEY_HEX => Some(signed_test_event(
  10197                 SDK_TEST_BUYER_SECRET_KEY_HEX,
  10198                 created_at,
  10199                 parts,
  10200             )),
  10201             SDK_TEST_SELLER_PUBLIC_KEY_HEX => Some(signed_test_event(
  10202                 SDK_TEST_SELLER_SECRET_KEY_HEX,
  10203                 created_at,
  10204                 parts,
  10205             )),
  10206             _ => None,
  10207         }
  10208     }
  10209 
  10210     fn test_event_from_parts(
  10211         record_id: &str,
  10212         event_id: String,
  10213         event_pubkey: &str,
  10214         created_at: u32,
  10215         parts: WireEventParts,
  10216     ) -> SdkRadrootsNostrEvent {
  10217         signed_test_event_for_pubkey(event_pubkey, created_at, parts.clone()).unwrap_or_else(|| {
  10218             SdkRadrootsNostrEvent {
  10219                 id: event_id,
  10220                 author: event_pubkey.to_owned(),
  10221                 created_at,
  10222                 kind: parts.kind,
  10223                 tags: parts.tags,
  10224                 content: parts.content,
  10225                 sig: test_event_signature_seed(record_id),
  10226             }
  10227         })
  10228     }
  10229 
  10230     fn signed_listing_event_id(label: &str) -> String {
  10231         signed_event_id(format!("app:signed_event:listing:{label}").as_str())
  10232     }
  10233 
  10234     fn test_order_id(value: &str) -> RadrootsOrderId {
  10235         RadrootsOrderId::parse(value).expect("order id")
  10236     }
  10237 
  10238     fn test_revision_id(value: &str) -> RadrootsOrderRevisionId {
  10239         RadrootsOrderRevisionId::parse(value).expect("revision id")
  10240     }
  10241 
  10242     fn test_quote_id(value: &str) -> RadrootsOrderQuoteId {
  10243         RadrootsOrderQuoteId::parse(value).expect("quote id")
  10244     }
  10245 
  10246     fn test_bin_id(value: &str) -> RadrootsInventoryBinId {
  10247         RadrootsInventoryBinId::parse(value).expect("bin id")
  10248     }
  10249 
  10250     fn test_pubkey(value: &str) -> RadrootsPublicKey {
  10251         RadrootsPublicKey::parse(value).expect("pubkey")
  10252     }
  10253 
  10254     fn test_listing_addr(value: &str) -> RadrootsListingAddress {
  10255         RadrootsListingAddress::parse(value).expect("listing address")
  10256     }
  10257 
  10258     fn install_recorded_sync_transport(
  10259         runtime: &DesktopAppRuntime,
  10260         transport: RecordedAppSyncTransport,
  10261     ) -> Arc<Mutex<RecordedAppSyncTransport>> {
  10262         let shared = Arc::new(Mutex::new(transport));
  10263         runtime.lock_state_mut().sync_transport =
  10264             Box::new(SharedRecordedSyncTransport(shared.clone()));
  10265         shared
  10266     }
  10267 
  10268     fn install_direct_relay_sync_transport(runtime: &DesktopAppRuntime, relay: &ThreadedAckRelay) {
  10269         let accounts_manager = runtime
  10270             .lock_state()
  10271             .accounts_manager
  10272             .as_ref()
  10273             .expect("accounts manager")
  10274             .clone();
  10275         runtime.lock_state_mut().nostr_relay_urls = vec![relay.url().to_owned()];
  10276         runtime.lock_state_mut().sync_transport =
  10277             Box::new(ConfiguredRelayAppSyncTransport::with_relay_urls(
  10278                 accounts_manager,
  10279                 vec![relay.url().to_owned()],
  10280             ));
  10281     }
  10282 
  10283     fn configure_runtime_relay_ingest(runtime: &DesktopAppRuntime, relay: &ThreadedAckRelay) {
  10284         runtime.lock_state_mut().nostr_relay_urls = vec![relay.url().to_owned()];
  10285     }
  10286 
  10287     #[test]
  10288     fn runtime_direct_relay_transport_rejects_typed_farm_work() {
  10289         let relay_a = ThreadedAckRelay::spawn();
  10290         let relay_b = ThreadedAckRelay::spawn();
  10291         let manager = RadrootsNostrAccountsManager::new_in_memory();
  10292         let account_id = manager
  10293             .generate_identity(Some("Farmer".to_owned()), true)
  10294             .expect("local signing account should generate");
  10295         let farm_id = FarmId::new();
  10296         let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload {
  10297             context: AppPublishContext::new(account_id.to_string(), "farm_setup")
  10298                 .with_source_local_event_id("app:local_work:farm:direct"),
  10299             farm_id,
  10300             display_name: "North field farm".to_owned(),
  10301             readiness: Some(FarmReadiness::Ready),
  10302         });
  10303         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  10304             .expect("typed farm publish work should serialize");
  10305         let mut transport = ConfiguredRelayAppSyncTransport::with_relay_urls(
  10306             manager,
  10307             vec![relay_a.url().to_owned(), relay_b.url().to_owned()],
  10308         );
  10309 
  10310         let error = transport
  10311             .sync(AppSyncRequest {
  10312                 trigger: SyncTrigger::ManualRefresh,
  10313                 checkpoint: SyncCheckpointStatus::never_synced(),
  10314                 pending_operations: vec![operation],
  10315                 known_conflicts: Vec::new(),
  10316             })
  10317             .expect_err("direct relay farm publish should use AppSdkRuntime");
  10318 
  10319         assert_migrated_payload_uses_sdk_runtime(error);
  10320         assert_eq!(relay_a.event_count(), 0);
  10321         assert_eq!(relay_b.event_count(), 0);
  10322     }
  10323 
  10324     #[test]
  10325     fn runtime_direct_relay_transport_rejects_typed_listing_work() {
  10326         let relay = ThreadedAckRelay::spawn();
  10327         let manager = RadrootsNostrAccountsManager::new_in_memory();
  10328         let account_id = manager
  10329             .generate_identity(Some("Farmer".to_owned()), true)
  10330             .expect("local signing account should generate");
  10331         let identity = manager
  10332             .get_signing_identity(&account_id)
  10333             .expect("seller signer lookup should succeed")
  10334             .expect("seller account should have local signer");
  10335         let payload = direct_relay_listing_payload(
  10336             account_id.to_string().as_str(),
  10337             identity.public_key_hex(),
  10338             "listing_publish",
  10339         );
  10340         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  10341             .expect("typed listing publish work should serialize");
  10342         let mut transport =
  10343             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  10344 
  10345         let error = transport
  10346             .sync(AppSyncRequest {
  10347                 trigger: SyncTrigger::ManualRefresh,
  10348                 checkpoint: SyncCheckpointStatus::never_synced(),
  10349                 pending_operations: vec![operation],
  10350                 known_conflicts: Vec::new(),
  10351             })
  10352             .expect_err("direct relay listing publish should use AppSdkRuntime");
  10353 
  10354         assert_migrated_payload_uses_sdk_runtime(error);
  10355         assert_eq!(relay.event_count(), 0);
  10356     }
  10357 
  10358     #[test]
  10359     fn runtime_direct_relay_transport_rejects_typed_order_request_work() {
  10360         let relay = ThreadedAckRelay::spawn();
  10361         let manager = RadrootsNostrAccountsManager::new_in_memory();
  10362         let account_id = manager
  10363             .generate_identity(Some("Buyer".to_owned()), true)
  10364             .expect("local signing account should generate");
  10365         let buyer_identity = manager
  10366             .get_signing_identity(&account_id)
  10367             .expect("buyer signer lookup should succeed")
  10368             .expect("buyer account should have local signer");
  10369         let seller_identity = RadrootsIdentity::generate();
  10370         let product_id = ProductId::new();
  10371         let order_id = OrderId::new();
  10372         let listing_event_id = "1".repeat(64);
  10373         let listing_addr = format!(
  10374             "30402:{}:{}",
  10375             seller_identity.public_key_hex(),
  10376             super::d_tag_from_uuid(ProductId::new().as_uuid())
  10377         );
  10378         let order_document = RadrootsOrderRequest {
  10379             order_id: test_order_id(order_id.to_string().as_str()),
  10380             listing_addr: test_listing_addr(listing_addr.as_str()),
  10381             buyer_pubkey: test_pubkey(buyer_identity.public_key_hex().as_str()),
  10382             seller_pubkey: test_pubkey(seller_identity.public_key_hex().as_str()),
  10383             items: vec![RadrootsOrderItem {
  10384                 bin_id: test_bin_id("bin-1"),
  10385                 bin_count: 1,
  10386             }],
  10387             economics: RadrootsOrderEconomics {
  10388                 quote_id: test_quote_id(format!("quote-{order_id}").as_str()),
  10389                 quote_version: 1,
  10390                 pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
  10391                 currency: RadrootsCoreCurrency::USD,
  10392                 items: vec![RadrootsOrderEconomicItem {
  10393                     bin_id: test_bin_id("bin-1"),
  10394                     bin_count: 1,
  10395                     quantity_amount: RadrootsCoreDecimal::from(1u32),
  10396                     quantity_unit: RadrootsCoreUnit::Each,
  10397                     unit_price_amount: RadrootsCoreDecimal::from(5u32),
  10398                     unit_price_currency: RadrootsCoreCurrency::USD,
  10399                     line_subtotal: RadrootsCoreMoney::from_minor_units_u32(
  10400                         500,
  10401                         RadrootsCoreCurrency::USD,
  10402                     ),
  10403                 }],
  10404                 discounts: Vec::new(),
  10405                 adjustments: Vec::new(),
  10406                 subtotal: RadrootsCoreMoney::from_minor_units_u32(500, RadrootsCoreCurrency::USD),
  10407                 discount_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
  10408                 adjustment_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
  10409                 total: RadrootsCoreMoney::from_minor_units_u32(500, RadrootsCoreCurrency::USD),
  10410             },
  10411         };
  10412         let payload = AppPublishPayload::OrderRequest(AppOrderRequestPublishPayload {
  10413             context: AppPublishContext::new(account_id.to_string(), "place_personal_order")
  10414                 .with_source_local_event_id("app:local_work:order_request:direct"),
  10415             order_id,
  10416             farm_id: FarmId::new(),
  10417             status: Some("needs_action".to_owned()),
  10418             order_document_json: Some(json!({"document": {"order": order_document}})),
  10419             listing_addr: Some(listing_addr),
  10420             listing_event_id: Some(listing_event_id),
  10421             listing_relays: vec![relay.url().to_owned()],
  10422             buyer_pubkey: Some(buyer_identity.public_key_hex()),
  10423             seller_pubkey: Some(seller_identity.public_key_hex()),
  10424             items: vec![AppOrderRequestItemPayload {
  10425                 product_id,
  10426                 quantity: 1,
  10427             }],
  10428             currency_code: Some("USD".to_owned()),
  10429             total_minor_units: Some(500),
  10430             note: Some("coordinate pickup".to_owned()),
  10431         });
  10432         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  10433             .expect("typed order request publish work should serialize");
  10434         let mut transport =
  10435             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  10436 
  10437         let error = transport
  10438             .sync(AppSyncRequest {
  10439                 trigger: SyncTrigger::ManualRefresh,
  10440                 checkpoint: SyncCheckpointStatus::never_synced(),
  10441                 pending_operations: vec![operation],
  10442                 known_conflicts: Vec::new(),
  10443             })
  10444             .expect_err("direct relay order request publish should use AppSdkRuntime");
  10445 
  10446         assert_migrated_payload_uses_sdk_runtime(error);
  10447         assert_eq!(relay.event_count(), 0);
  10448     }
  10449 
  10450     #[test]
  10451     fn runtime_direct_relay_transport_rejects_typed_order_decision_work() {
  10452         let relay = ThreadedAckRelay::spawn();
  10453         let manager = RadrootsNostrAccountsManager::new_in_memory();
  10454         let account_id = manager
  10455             .generate_identity(Some("Seller".to_owned()), true)
  10456             .expect("local signing account should generate");
  10457         let identity = manager
  10458             .get_signing_identity(&account_id)
  10459             .expect("seller signer lookup should succeed")
  10460             .expect("seller account should have local signer");
  10461         let buyer_pubkey = "1111111111111111111111111111111111111111111111111111111111111111";
  10462         let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload {
  10463             context: AppPublishContext::new(account_id.to_string(), "seller_order_decision"),
  10464             app_order_id: OrderId::new(),
  10465             farm_id: FarmId::new(),
  10466             trade_order_id: "order-1".to_owned(),
  10467             request_event_id: test_event_id_seed("order-request-event-1"),
  10468             listing_event_id: Some(test_event_id_seed("listing-event-1")),
  10469             listing_addr: format!("30402:{}:listing-key", identity.public_key_hex()),
  10470             buyer_pubkey: buyer_pubkey.to_owned(),
  10471             seller_pubkey: identity.public_key_hex(),
  10472             decision: AppOrderDecisionPayload::Accepted {
  10473                 inventory_commitments: vec![AppOrderDecisionInventoryCommitment {
  10474                     bin_id: "bin-1".to_owned(),
  10475                     bin_count: 2,
  10476                 }],
  10477             },
  10478         });
  10479         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  10480             .expect("typed order decision publish work should serialize");
  10481         let mut transport =
  10482             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  10483 
  10484         let error = transport
  10485             .sync(AppSyncRequest {
  10486                 trigger: SyncTrigger::ManualRefresh,
  10487                 checkpoint: SyncCheckpointStatus::never_synced(),
  10488                 pending_operations: vec![operation],
  10489                 known_conflicts: Vec::new(),
  10490             })
  10491             .expect_err("direct relay order decision publish should use AppSdkRuntime");
  10492 
  10493         assert_migrated_payload_uses_sdk_runtime(error);
  10494         assert_eq!(relay.event_count(), 0);
  10495     }
  10496 
  10497     #[test]
  10498     fn runtime_direct_relay_transport_rejects_typed_order_lifecycle_work() {
  10499         let relay = ThreadedAckRelay::spawn();
  10500         let manager = RadrootsNostrAccountsManager::new_in_memory();
  10501         let buyer_account_id = manager
  10502             .generate_identity(Some("Buyer".to_owned()), true)
  10503             .expect("buyer account should generate");
  10504         let seller_account_id = manager
  10505             .generate_identity(Some("Seller".to_owned()), true)
  10506             .expect("seller account should generate");
  10507         let buyer_identity = manager
  10508             .get_signing_identity(&buyer_account_id)
  10509             .expect("buyer signer lookup should succeed")
  10510             .expect("buyer account should have local signer");
  10511         let seller_identity = manager
  10512             .get_signing_identity(&seller_account_id)
  10513             .expect("seller signer lookup should succeed")
  10514             .expect("seller account should have local signer");
  10515         let app_order_id = OrderId::new();
  10516         let farm_id = FarmId::new();
  10517         let listing_addr = format!(
  10518             "30402:{}:AAAAAAAAAAAAAAAAAAAAAg",
  10519             seller_identity.public_key_hex()
  10520         );
  10521         let common = (
  10522             app_order_id,
  10523             farm_id,
  10524             "order-1".to_owned(),
  10525             test_event_id_seed("order-request-event-1"),
  10526             listing_addr,
  10527             buyer_identity.public_key_hex(),
  10528             seller_identity.public_key_hex(),
  10529         );
  10530         let revision_economics = RadrootsOrderEconomics {
  10531             quote_id: test_quote_id("quote-revision-1"),
  10532             quote_version: 2,
  10533             pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
  10534             currency: RadrootsCoreCurrency::USD,
  10535             items: vec![RadrootsOrderEconomicItem {
  10536                 bin_id: test_bin_id("bin-1"),
  10537                 bin_count: 3,
  10538                 quantity_amount: RadrootsCoreDecimal::from(1u32),
  10539                 quantity_unit: RadrootsCoreUnit::Each,
  10540                 unit_price_amount: RadrootsCoreDecimal::from(8u32),
  10541                 unit_price_currency: RadrootsCoreCurrency::USD,
  10542                 line_subtotal: RadrootsCoreMoney::from_minor_units_u32(
  10543                     2400,
  10544                     RadrootsCoreCurrency::USD,
  10545                 ),
  10546             }],
  10547             discounts: Vec::new(),
  10548             adjustments: Vec::new(),
  10549             subtotal: RadrootsCoreMoney::from_minor_units_u32(2400, RadrootsCoreCurrency::USD),
  10550             discount_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
  10551             adjustment_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
  10552             total: RadrootsCoreMoney::from_minor_units_u32(2400, RadrootsCoreCurrency::USD),
  10553         };
  10554         let revision_proposal =
  10555             AppPublishPayload::OrderRevisionProposal(AppOrderRevisionProposalPublishPayload {
  10556                 context: AppPublishContext::new(
  10557                     seller_account_id.to_string(),
  10558                     "seller_order_revision_proposal",
  10559                 ),
  10560                 app_order_id: common.0,
  10561                 farm_id: common.1,
  10562                 trade_order_id: common.2.clone(),
  10563                 request_event_id: common.3.clone(),
  10564                 prev_event_id: test_event_id_seed("order-decision-event-1"),
  10565                 revision_id: "revision-1".to_owned(),
  10566                 listing_addr: common.4.clone(),
  10567                 buyer_pubkey: common.5.clone(),
  10568                 seller_pubkey: common.6.clone(),
  10569                 items: vec![RadrootsOrderItem {
  10570                     bin_id: test_bin_id("bin-1"),
  10571                     bin_count: 3,
  10572                 }],
  10573                 economics: revision_economics,
  10574                 reason: "harvest count updated".to_owned(),
  10575             });
  10576         let revision_decision =
  10577             AppPublishPayload::OrderRevisionDecision(AppOrderRevisionDecisionPublishPayload {
  10578                 context: AppPublishContext::new(
  10579                     buyer_account_id.to_string(),
  10580                     "buyer_order_revision_decision",
  10581                 ),
  10582                 app_order_id: common.0,
  10583                 farm_id: common.1,
  10584                 trade_order_id: common.2.clone(),
  10585                 request_event_id: common.3.clone(),
  10586                 prev_event_id: test_event_id_seed("order-revision-proposal-event-1"),
  10587                 revision_id: "revision-1".to_owned(),
  10588                 listing_addr: common.4.clone(),
  10589                 buyer_pubkey: common.5.clone(),
  10590                 seller_pubkey: common.6.clone(),
  10591                 decision: RadrootsOrderRevisionOutcome::Accepted,
  10592             });
  10593         let cancellation =
  10594             AppPublishPayload::OrderCancellation(AppOrderCancellationPublishPayload {
  10595                 context: AppPublishContext::new(
  10596                     buyer_account_id.to_string(),
  10597                     "buyer_order_cancellation",
  10598                 ),
  10599                 app_order_id: common.0,
  10600                 farm_id: common.1,
  10601                 trade_order_id: common.2.clone(),
  10602                 request_event_id: common.3.clone(),
  10603                 prev_event_id: common.3.clone(),
  10604                 listing_addr: common.4.clone(),
  10605                 buyer_pubkey: common.5.clone(),
  10606                 seller_pubkey: common.6.clone(),
  10607                 reason: "buyer cancelled order".to_owned(),
  10608             });
  10609         let operations = [revision_proposal, revision_decision, cancellation]
  10610             .into_iter()
  10611             .map(|payload| {
  10612                 PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  10613                     .expect("typed lifecycle publish work should serialize")
  10614             })
  10615             .collect::<Vec<_>>();
  10616         let mut transport =
  10617             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  10618 
  10619         let error = transport
  10620             .sync(AppSyncRequest {
  10621                 trigger: SyncTrigger::ManualRefresh,
  10622                 checkpoint: SyncCheckpointStatus::never_synced(),
  10623                 pending_operations: operations,
  10624                 known_conflicts: Vec::new(),
  10625             })
  10626             .expect_err("direct relay lifecycle publish should use AppSdkRuntime");
  10627 
  10628         assert_migrated_payload_uses_sdk_runtime(error);
  10629         assert_eq!(relay.event_count(), 0);
  10630     }
  10631 
  10632     #[test]
  10633     fn runtime_configured_relay_sync_triggers_ingest_listing_into_fresh_buyer_projection() {
  10634         let relay = ThreadedAckRelay::spawn();
  10635         let projected_product_id = publish_relay_ingest_listing_fixture(&relay);
  10636 
  10637         assert_fresh_buyer_relay_ingest(
  10638             relay.url(),
  10639             "relay_ingest_manual_refresh",
  10640             SyncTrigger::ManualRefresh,
  10641             projected_product_id,
  10642         );
  10643         assert_fresh_buyer_relay_ingest(
  10644             relay.url(),
  10645             "relay_ingest_app_launch",
  10646             SyncTrigger::AppLaunch,
  10647             projected_product_id,
  10648         );
  10649         assert_fresh_buyer_relay_ingest(
  10650             relay.url(),
  10651             "relay_ingest_foreground_resume",
  10652             SyncTrigger::ForegroundResume,
  10653             projected_product_id,
  10654         );
  10655     }
  10656 
  10657     #[test]
  10658     fn runtime_relay_ingest_does_not_use_connected_relays_as_listing_provenance() {
  10659         let listing_relay = ThreadedAckRelay::spawn();
  10660         let empty_relay = ThreadedAckRelay::spawn();
  10661         let projected_product_id = publish_relay_ingest_listing_fixture(&listing_relay);
  10662         let (runtime, paths) = bootstrapped_runtime("relay_ingest_connected_not_provenance");
  10663         assert!(
  10664             runtime
  10665                 .generate_local_account(Some("Buyer".to_owned()))
  10666                 .expect("buyer account should generate")
  10667         );
  10668         runtime.lock_state_mut().nostr_relay_urls =
  10669             vec![listing_relay.url().to_owned(), empty_relay.url().to_owned()];
  10670 
  10671         assert!(
  10672             runtime
  10673                 .sync_on_manual_refresh()
  10674                 .expect("manual relay ingest should complete")
  10675         );
  10676 
  10677         let summary = runtime.summary();
  10678         let listing = summary
  10679             .personal_projection
  10680             .browse
  10681             .listings
  10682             .rows
  10683             .iter()
  10684             .find(|listing| listing.product_id == projected_product_id)
  10685             .expect("fresh buyer app should project relay listing");
  10686         assert_eq!(listing.title, "Relay ingest lettuce");
  10687         assert_eq!(listing.listing_relays, vec![listing_relay.url().to_owned()]);
  10688 
  10689         let product_id_string = projected_product_id.to_string();
  10690         let imports = runtime
  10691             .lock_state()
  10692             .sqlite_store
  10693             .as_ref()
  10694             .expect("sqlite store")
  10695             .load_local_interop_records()
  10696             .expect("local interop records should load");
  10697         let listing_import = imports
  10698             .iter()
  10699             .find(|record| {
  10700                 record.projected_kind == "listing"
  10701                     && record.projected_id.as_deref() == Some(product_id_string.as_str())
  10702             })
  10703             .expect("listing import");
  10704         let delivery = serde_json::from_str::<serde_json::Value>(
  10705             listing_import
  10706                 .relay_delivery_json
  10707                 .as_deref()
  10708                 .expect("listing delivery evidence"),
  10709         )
  10710         .expect("delivery json");
  10711 
  10712         assert_eq!(
  10713             listing_import.source_runtime,
  10714             SourceRuntime::Network.as_str()
  10715         );
  10716         assert_eq!(listing_import.outbox_status, "none");
  10717         assert_eq!(delivery["state"], json!("observed"));
  10718         assert_eq!(delivery["acknowledged_relays"], json!([]));
  10719         assert_eq!(delivery["observed_relays"], json!([listing_relay.url()]));
  10720         assert_eq!(
  10721             delivery["target_relays"],
  10722             json!([listing_relay.url(), empty_relay.url()])
  10723         );
  10724 
  10725         cleanup_bootstrapped_runtime_paths(&paths);
  10726     }
  10727 
  10728     fn publish_relay_ingest_listing_fixture(relay: &ThreadedAckRelay) -> ProductId {
  10729         let manager = RadrootsNostrAccountsManager::new_in_memory();
  10730         let account_id = manager
  10731             .generate_identity(Some("Farmer".to_owned()), true)
  10732             .expect("local signing account should generate");
  10733         let identity = manager
  10734             .get_signing_identity(&account_id)
  10735             .expect("seller signing lookup should succeed")
  10736             .expect("seller account should have local signer");
  10737         let seller_pubkey = identity.public_key_hex();
  10738         let farm_id = FarmId::new();
  10739         let product_id = ProductId::new();
  10740         let listing_d_tag = super::d_tag_from_uuid(product_id.as_uuid());
  10741         let projected_product_id = deterministic_cli_listing_product_id(
  10742             Some(seller_pubkey.as_str()),
  10743             listing_d_tag.as_str(),
  10744         );
  10745         let listing_payload = AppListingPublishPayload {
  10746             context: AppPublishContext::new(account_id.to_string(), "relay_ingest_listing"),
  10747             product_id,
  10748             listing_d_tag: Some(listing_d_tag),
  10749             farm_id: Some(farm_id),
  10750             farm_pubkey: Some(seller_pubkey),
  10751             farm_d_tag: Some(super::d_tag_from_uuid(farm_id.as_uuid())),
  10752             title: "Relay ingest lettuce".to_owned(),
  10753             subtitle: Some("Pulled into a fresh buyer app".to_owned()),
  10754             category: Some("greens".to_owned()),
  10755             unit_label: "each".to_owned(),
  10756             price_minor_units: Some(450),
  10757             price_currency: "USD".to_owned(),
  10758             stock_quantity: Some(6),
  10759             availability_window_id: Some(FulfillmentWindowId::new()),
  10760             availability_starts_at: Some("2099-04-25T14:00:00Z".to_owned()),
  10761             availability_ends_at: Some("2099-04-25T18:00:00Z".to_owned()),
  10762             fulfillment_method: Some("pickup".to_owned()),
  10763             fulfillment_location: Some("Relay barn".to_owned()),
  10764             status: ProductStatus::Published,
  10765         };
  10766         let listing = super::listing_publish_payload_to_sdk_listing(&listing_payload)
  10767             .expect("listing payload should convert to SDK listing");
  10768         let parts = radroots_sdk::protocol::listing::build_draft(&listing)
  10769             .expect("listing draft should build")
  10770             .into_wire_parts();
  10771         let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
  10772             .expect("listing event builder should build")
  10773             .sign_with_keys(identity.keys())
  10774             .expect("listing event should sign");
  10775         publish_signed_test_event_to_relay(relay, &event);
  10776         assert_eq!(relay.event_count(), 1);
  10777 
  10778         projected_product_id
  10779     }
  10780 
  10781     fn assert_fresh_buyer_relay_ingest(
  10782         relay_url: &str,
  10783         label: &str,
  10784         trigger: SyncTrigger,
  10785         projected_product_id: ProductId,
  10786     ) {
  10787         let (runtime, paths) = bootstrapped_runtime(label);
  10788         assert!(
  10789             runtime
  10790                 .generate_local_account(Some("Buyer".to_owned()))
  10791                 .expect("buyer account should generate")
  10792         );
  10793         runtime.lock_state_mut().nostr_relay_urls = vec![relay_url.to_owned()];
  10794 
  10795         let changed = match trigger {
  10796             SyncTrigger::ManualRefresh => runtime
  10797                 .sync_on_manual_refresh()
  10798                 .expect("manual relay ingest should complete"),
  10799             SyncTrigger::AppLaunch => runtime
  10800                 .sync_on_app_launch()
  10801                 .expect("launch relay ingest should complete"),
  10802             SyncTrigger::ForegroundResume => runtime
  10803                 .sync_on_foreground_resume()
  10804                 .expect("foreground relay ingest should complete"),
  10805             SyncTrigger::LocalMutation => panic!("local mutation is not a relay ingest trigger"),
  10806         };
  10807         assert!(changed);
  10808 
  10809         let summary = runtime.summary();
  10810         let listing = summary
  10811             .personal_projection
  10812             .browse
  10813             .listings
  10814             .rows
  10815             .iter()
  10816             .find(|listing| listing.product_id == projected_product_id)
  10817             .expect("fresh buyer app should project relay listing");
  10818         assert_eq!(listing.title, "Relay ingest lettuce");
  10819         assert_eq!(listing.farm_display_name, "Local farm");
  10820         assert_eq!(listing.listing_relays, vec![relay_url.to_owned()]);
  10821         let relay_ingest = runtime
  10822             .lock_state()
  10823             .selected_account_relay_ingest_freshness
  10824             .clone();
  10825         assert_eq!(relay_ingest.status, AppRelayIngestScopeStatus::Fresh);
  10826         assert_eq!(relay_ingest.relays.len(), 1);
  10827         assert_eq!(relay_ingest.relays[0].relay_url, relay_url);
  10828         assert!(relay_ingest.relays[0].cursor_since_unix_seconds.is_some());
  10829 
  10830         let product_id_string = projected_product_id.to_string();
  10831         let imports = runtime
  10832             .lock_state()
  10833             .sqlite_store
  10834             .as_ref()
  10835             .expect("sqlite store")
  10836             .load_local_interop_records()
  10837             .expect("local interop records should load");
  10838         let listing_import = imports
  10839             .iter()
  10840             .find(|record| {
  10841                 record.projected_kind == "listing"
  10842                     && record.projected_id.as_deref() == Some(product_id_string.as_str())
  10843             })
  10844             .expect("listing import");
  10845         let delivery = serde_json::from_str::<serde_json::Value>(
  10846             listing_import
  10847                 .relay_delivery_json
  10848                 .as_deref()
  10849                 .expect("listing delivery evidence"),
  10850         )
  10851         .expect("delivery json");
  10852 
  10853         assert_eq!(
  10854             listing_import.source_runtime,
  10855             SourceRuntime::Network.as_str()
  10856         );
  10857         assert_eq!(listing_import.outbox_status, "none");
  10858         assert_eq!(delivery["state"], json!("observed"));
  10859         assert_eq!(delivery["acknowledged_relays"], json!([]));
  10860         assert_eq!(delivery["observed_relays"], json!([relay_url]));
  10861         assert_eq!(
  10862             imports
  10863                 .iter()
  10864                 .filter(|record| record.projected_kind == "listing"
  10865                     && record.projected_id.as_deref() == Some(product_id_string.as_str()))
  10866                 .count(),
  10867             1
  10868         );
  10869         assert!(
  10870             runtime
  10871                 .sync_on_manual_refresh()
  10872                 .expect("repeat relay ingest should complete")
  10873         );
  10874         let repeated_imports = runtime
  10875             .lock_state()
  10876             .sqlite_store
  10877             .as_ref()
  10878             .expect("sqlite store")
  10879             .load_local_interop_records()
  10880             .expect("repeated local interop records should load");
  10881         assert_eq!(
  10882             repeated_imports
  10883                 .iter()
  10884                 .filter(|record| record.projected_kind == "listing"
  10885                     && record.projected_id.as_deref() == Some(product_id_string.as_str()))
  10886                 .count(),
  10887             1
  10888         );
  10889 
  10890         cleanup_bootstrapped_runtime_paths(&paths);
  10891     }
  10892 
  10893     #[test]
  10894     fn runtime_relay_ingest_runs_after_outbound_sync_failure() {
  10895         let relay = ThreadedAckRelay::spawn();
  10896         let projected_product_id = publish_relay_ingest_listing_fixture(&relay);
  10897 
  10898         assert_relay_ingest_after_outbound_failure(
  10899             relay.url(),
  10900             "relay_ingest_after_manual_failure",
  10901             SyncTrigger::ManualRefresh,
  10902             projected_product_id,
  10903         );
  10904         assert_relay_ingest_after_outbound_failure(
  10905             relay.url(),
  10906             "relay_ingest_after_launch_failure",
  10907             SyncTrigger::AppLaunch,
  10908             projected_product_id,
  10909         );
  10910         assert_relay_ingest_after_outbound_failure(
  10911             relay.url(),
  10912             "relay_ingest_after_foreground_failure",
  10913             SyncTrigger::ForegroundResume,
  10914             projected_product_id,
  10915         );
  10916     }
  10917 
  10918     fn assert_relay_ingest_after_outbound_failure(
  10919         relay_url: &str,
  10920         label: &str,
  10921         trigger: SyncTrigger,
  10922         projected_product_id: ProductId,
  10923     ) {
  10924         let (runtime, paths) = bootstrapped_runtime(label);
  10925         assert!(
  10926             runtime
  10927                 .generate_local_account(Some("Buyer".to_owned()))
  10928                 .expect("buyer account should generate")
  10929         );
  10930         runtime.lock_state_mut().nostr_relay_urls = vec![relay_url.to_owned()];
  10931         let buyer_account_id = runtime
  10932             .summary()
  10933             .settings_account_projection
  10934             .selected_account
  10935             .as_ref()
  10936             .expect("selected account")
  10937             .account
  10938             .account_id
  10939             .clone();
  10940         let pending_farm_id = FarmId::new();
  10941         runtime
  10942             .lock_state_mut()
  10943             .enqueue_selected_account_sync_operations(vec![pending_sync_upsert(
  10944                 SyncAggregateRef::Farm(pending_farm_id),
  10945                 farm_sync_payload(
  10946                     pending_farm_id,
  10947                     "Pending outbound farm",
  10948                     Some(FarmReadiness::Ready),
  10949                     "relay_ingest_after_outbound_failure",
  10950                 ),
  10951             )])
  10952             .expect("pending sync should enqueue");
  10953         let recorded = install_recorded_sync_transport(
  10954             &runtime,
  10955             RecordedAppSyncTransport::fail(AppSyncTransportError::unavailable(
  10956                 "test outbound sync unavailable",
  10957             )),
  10958         );
  10959 
  10960         let changed = match trigger {
  10961             SyncTrigger::ManualRefresh => runtime
  10962                 .sync_on_manual_refresh()
  10963                 .expect("manual refresh should complete"),
  10964             SyncTrigger::AppLaunch => runtime
  10965                 .sync_on_app_launch()
  10966                 .expect("launch sync should complete"),
  10967             SyncTrigger::ForegroundResume => runtime
  10968                 .sync_on_foreground_resume()
  10969                 .expect("foreground sync should complete"),
  10970             SyncTrigger::LocalMutation => panic!("local mutation is not a relay ingest trigger"),
  10971         };
  10972 
  10973         assert!(changed);
  10974         assert_eq!(recorded.lock().expect("recorded transport").call_count(), 1);
  10975         let summary = runtime.summary();
  10976         assert_eq!(
  10977             summary.sync_status.projection.run_status,
  10978             AppSyncRunStatus::Failed
  10979         );
  10980         assert_eq!(
  10981             summary.sync_status.projection.checkpoint.state,
  10982             SyncCheckpointState::Failed
  10983         );
  10984         assert_eq!(
  10985             runtime
  10986                 .lock_state()
  10987                 .selected_account_relay_ingest_freshness
  10988                 .status,
  10989             AppRelayIngestScopeStatus::Fresh
  10990         );
  10991         assert_eq!(summary.sync_status.pending_write_count, 1);
  10992         let listing = summary
  10993             .personal_projection
  10994             .browse
  10995             .listings
  10996             .rows
  10997             .iter()
  10998             .find(|listing| listing.product_id == projected_product_id)
  10999             .expect("relay listing should still project after outbound failure");
  11000         assert_eq!(listing.title, "Relay ingest lettuce");
  11001         assert_eq!(listing.listing_relays, vec![relay_url.to_owned()]);
  11002 
  11003         let pending_operations = runtime
  11004             .lock_state()
  11005             .sqlite_store
  11006             .as_ref()
  11007             .expect("sqlite store")
  11008             .load_pending_sync_operations(buyer_account_id.as_str())
  11009             .expect("pending sync operations should load");
  11010         assert_eq!(pending_operations.len(), 1);
  11011         assert_eq!(pending_operations[0].operation.attempt_count, 1);
  11012         assert!(
  11013             pending_operations[0]
  11014                 .operation
  11015                 .last_error_message
  11016                 .as_deref()
  11017                 .is_some_and(|message| message.contains("test outbound sync unavailable"))
  11018         );
  11019 
  11020         cleanup_bootstrapped_runtime_paths(&paths);
  11021     }
  11022 
  11023     #[test]
  11024     fn runtime_direct_relay_transport_rejects_publish_work_before_partial_progress() {
  11025         let relay = ThreadedAckRelay::spawn();
  11026         let manager = RadrootsNostrAccountsManager::new_in_memory();
  11027         let account_id = manager
  11028             .generate_identity(Some("Farmer".to_owned()), true)
  11029             .expect("local signing account should generate");
  11030         let identity = manager
  11031             .get_signing_identity(&account_id)
  11032             .expect("farmer signer lookup should succeed")
  11033             .expect("farmer account should have local signer");
  11034         let payload = direct_relay_listing_payload(
  11035             account_id.to_string().as_str(),
  11036             identity.public_key_hex(),
  11037             "listing_publish",
  11038         );
  11039         let successful_operation =
  11040             PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  11041                 .expect("typed listing publish work should serialize");
  11042         let unsupported_operation = PendingSyncOperation::new(
  11043             SyncAggregateRef::Product(ProductId::new()),
  11044             SyncOperationKind::Delete,
  11045             "{}",
  11046             "2026-05-24T12:01:00Z",
  11047         );
  11048         let mut transport =
  11049             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  11050 
  11051         let error = transport
  11052             .sync(AppSyncRequest {
  11053                 trigger: SyncTrigger::ManualRefresh,
  11054                 checkpoint: SyncCheckpointStatus::never_synced(),
  11055                 pending_operations: vec![successful_operation, unsupported_operation],
  11056                 known_conflicts: Vec::new(),
  11057             })
  11058             .expect_err("publish work should use AppSdkRuntime before partial progress");
  11059 
  11060         assert_migrated_payload_uses_sdk_runtime(error);
  11061         assert_eq!(relay.event_count(), 0);
  11062     }
  11063 
  11064     #[test]
  11065     fn runtime_direct_relay_transport_normalizes_configured_relay_set() {
  11066         let relay_urls = super::normalized_app_sync_relay_urls(&[
  11067             " ws://127.0.0.1:8081 ".to_owned(),
  11068             "ws://127.0.0.1:8080".to_owned(),
  11069             "ws://127.0.0.1:8081".to_owned(),
  11070         ])
  11071         .expect("relay set should normalize");
  11072 
  11073         assert_eq!(
  11074             relay_urls,
  11075             vec!["ws://127.0.0.1:8081", "ws://127.0.0.1:8080"]
  11076         );
  11077     }
  11078 
  11079     #[test]
  11080     fn runtime_direct_relay_transport_rejects_invalid_configured_relay_urls() {
  11081         for relay_url in [
  11082             " ",
  11083             "https://relay.example",
  11084             "wss://",
  11085             "wss://user@relay.example",
  11086             "wss://relay.example:abc",
  11087         ] {
  11088             let error = super::normalized_app_sync_relay_urls(&[relay_url.to_owned()])
  11089                 .expect_err("invalid app sync relay url");
  11090             assert!(
  11091                 error.to_string().contains("relay url"),
  11092                 "unexpected error for {relay_url}: {error}"
  11093             );
  11094         }
  11095     }
  11096 
  11097     #[test]
  11098     fn order_request_listing_pointer_prefers_configured_listing_relay() {
  11099         let selected = super::selected_listing_relay(
  11100             &[
  11101                 "wss://relay-b.example".to_owned(),
  11102                 "wss://relay-a.example".to_owned(),
  11103             ],
  11104             &[
  11105                 "wss://relay-a.example".to_owned(),
  11106                 "wss://relay-c.example".to_owned(),
  11107             ],
  11108         )
  11109         .expect("configured listing relay should be selected");
  11110 
  11111         assert_eq!(selected.as_str(), "wss://relay-a.example");
  11112     }
  11113 
  11114     #[test]
  11115     fn order_request_listing_pointer_rejects_missing_configured_provenance_relay() {
  11116         let error = super::selected_listing_relay(
  11117             &["wss://listing.example".to_owned()],
  11118             &["wss://target.example".to_owned()],
  11119         )
  11120         .expect_err("missing listing provenance relay should fail");
  11121 
  11122         assert_missing_listing_provenance_relay_error(&error, "wss://listing.example");
  11123     }
  11124 
  11125     #[test]
  11126     fn runtime_direct_relay_transport_rejects_order_request_missing_listing_provenance_target() {
  11127         let relay = ThreadedAckRelay::spawn();
  11128         let manager = RadrootsNostrAccountsManager::new_in_memory();
  11129         let account_id = manager
  11130             .generate_identity(Some("Buyer".to_owned()), true)
  11131             .expect("buyer account should generate");
  11132         let identity = manager
  11133             .get_signing_identity(&account_id)
  11134             .expect("buyer signer lookup should succeed")
  11135             .expect("buyer account should have local signer");
  11136         let seller_pubkey = "2222222222222222222222222222222222222222222222222222222222222222";
  11137         let payload = AppPublishPayload::OrderRequest(AppOrderRequestPublishPayload {
  11138             context: AppPublishContext::new(account_id.to_string(), "order_missing_listing_relay"),
  11139             order_id: OrderId::new(),
  11140             farm_id: FarmId::new(),
  11141             status: Some("needs_action".to_owned()),
  11142             order_document_json: Some(json!({"document": {"order": {}}})),
  11143             listing_addr: Some(format!("30402:{seller_pubkey}:listing-key")),
  11144             listing_event_id: Some("listing-event-id".to_owned()),
  11145             listing_relays: vec!["wss://listing.example".to_owned()],
  11146             buyer_pubkey: Some(identity.public_key_hex()),
  11147             seller_pubkey: Some(seller_pubkey.to_owned()),
  11148             items: vec![AppOrderRequestItemPayload {
  11149                 product_id: ProductId::new(),
  11150                 quantity: 1,
  11151             }],
  11152             currency_code: Some("USD".to_owned()),
  11153             total_minor_units: Some(450),
  11154             note: None,
  11155         });
  11156         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-25T07:00:00Z")
  11157             .expect("order publish payload should serialize");
  11158         let mut transport =
  11159             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  11160 
  11161         let error = transport
  11162             .sync(AppSyncRequest {
  11163                 trigger: SyncTrigger::ManualRefresh,
  11164                 checkpoint: SyncCheckpointStatus::never_synced(),
  11165                 pending_operations: vec![operation],
  11166                 known_conflicts: Vec::new(),
  11167             })
  11168             .expect_err("direct relay order request should use AppSdkRuntime");
  11169 
  11170         assert_migrated_payload_uses_sdk_runtime(error);
  11171         assert_eq!(relay.event_count(), 0);
  11172     }
  11173 
  11174     #[test]
  11175     fn runtime_direct_relay_transport_rejects_payload_account_context_publish_work() {
  11176         let relay = ThreadedAckRelay::spawn();
  11177         let manager = RadrootsNostrAccountsManager::new_in_memory();
  11178         let first_account_id = manager
  11179             .generate_identity(Some("First".to_owned()), true)
  11180             .expect("first account");
  11181         let first_identity = manager
  11182             .get_signing_identity(&first_account_id)
  11183             .expect("first signer")
  11184             .expect("first local signer");
  11185         let payload = direct_relay_listing_payload(
  11186             first_account_id.to_string().as_str(),
  11187             first_identity.public_key_hex(),
  11188             "listing_publish",
  11189         );
  11190         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  11191             .expect("typed listing publish work should serialize");
  11192         let mut transport =
  11193             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  11194 
  11195         let error = transport
  11196             .sync(AppSyncRequest {
  11197                 trigger: SyncTrigger::ManualRefresh,
  11198                 checkpoint: SyncCheckpointStatus::never_synced(),
  11199                 pending_operations: vec![operation],
  11200                 known_conflicts: Vec::new(),
  11201             })
  11202             .expect_err("payload account publish work should use AppSdkRuntime");
  11203 
  11204         assert_migrated_payload_uses_sdk_runtime(error);
  11205         assert_eq!(relay.event_count(), 0);
  11206     }
  11207 
  11208     #[test]
  11209     fn runtime_direct_relay_transport_rejects_missing_account_publish_work() {
  11210         let relay = ThreadedAckRelay::spawn();
  11211         let manager = RadrootsNostrAccountsManager::new_in_memory();
  11212         let farm_id = FarmId::new();
  11213         let missing_account_id = RadrootsIdentity::generate().id();
  11214         let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload {
  11215             context: AppPublishContext::new(missing_account_id.to_string(), "farm_setup"),
  11216             farm_id,
  11217             display_name: "North field farm".to_owned(),
  11218             readiness: Some(FarmReadiness::Ready),
  11219         });
  11220         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  11221             .expect("typed farm publish work should serialize");
  11222         let mut transport =
  11223             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  11224 
  11225         let error = transport
  11226             .sync(AppSyncRequest {
  11227                 trigger: SyncTrigger::ManualRefresh,
  11228                 checkpoint: SyncCheckpointStatus::never_synced(),
  11229                 pending_operations: vec![operation],
  11230                 known_conflicts: Vec::new(),
  11231             })
  11232             .expect_err("missing account publish work should use AppSdkRuntime");
  11233 
  11234         assert_migrated_payload_uses_sdk_runtime(error);
  11235         assert_eq!(relay.event_count(), 0);
  11236     }
  11237 
  11238     #[test]
  11239     fn runtime_direct_relay_transport_rejects_watch_only_account_publish_work() {
  11240         let relay = ThreadedAckRelay::spawn();
  11241         let manager = RadrootsNostrAccountsManager::new_in_memory();
  11242         let identity = RadrootsIdentity::generate();
  11243         let account_id = manager
  11244             .upsert_public_identity(identity.to_public(), Some("Watch".to_owned()), true)
  11245             .expect("watch-only account");
  11246         let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload {
  11247             context: AppPublishContext::new(account_id.to_string(), "farm_setup"),
  11248             farm_id: FarmId::new(),
  11249             display_name: "North field farm".to_owned(),
  11250             readiness: Some(FarmReadiness::Ready),
  11251         });
  11252         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  11253             .expect("typed farm publish work should serialize");
  11254         let mut transport =
  11255             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  11256 
  11257         let error = transport
  11258             .sync(AppSyncRequest {
  11259                 trigger: SyncTrigger::ManualRefresh,
  11260                 checkpoint: SyncCheckpointStatus::never_synced(),
  11261                 pending_operations: vec![operation],
  11262                 known_conflicts: Vec::new(),
  11263             })
  11264             .expect_err("watch-only account publish work should use AppSdkRuntime");
  11265 
  11266         assert_migrated_payload_uses_sdk_runtime(error);
  11267         assert_eq!(relay.event_count(), 0);
  11268     }
  11269 
  11270     #[test]
  11271     fn runtime_direct_relay_transport_rejects_mismatched_local_signing_publish_work() {
  11272         let relay = ThreadedAckRelay::spawn();
  11273         let store = Arc::new(RadrootsNostrMemoryAccountStore::new());
  11274         let vault = Arc::new(RadrootsNostrSecretVaultMemory::new());
  11275         let manager =
  11276             RadrootsNostrAccountsManager::new(store, vault.clone()).expect("accounts manager");
  11277         let account_identity = RadrootsIdentity::generate();
  11278         let secret_identity = RadrootsIdentity::generate();
  11279         let account_id = manager
  11280             .upsert_public_identity(
  11281                 account_identity.to_public(),
  11282                 Some("Mismatched".to_owned()),
  11283                 true,
  11284             )
  11285             .expect("public account");
  11286         vault
  11287             .store_secret(
  11288                 account_secret_slot(&account_id).as_str(),
  11289                 secret_identity.secret_key_hex().as_str(),
  11290             )
  11291             .expect("mismatched secret");
  11292         let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload {
  11293             context: AppPublishContext::new(account_id.to_string(), "farm_setup"),
  11294             farm_id: FarmId::new(),
  11295             display_name: "North field farm".to_owned(),
  11296             readiness: Some(FarmReadiness::Ready),
  11297         });
  11298         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  11299             .expect("typed farm publish work should serialize");
  11300         let mut transport =
  11301             ConfiguredRelayAppSyncTransport::with_relay_urls(manager, vec![relay.url().to_owned()]);
  11302 
  11303         let error = transport
  11304             .sync(AppSyncRequest {
  11305                 trigger: SyncTrigger::ManualRefresh,
  11306                 checkpoint: SyncCheckpointStatus::never_synced(),
  11307                 pending_operations: vec![operation],
  11308                 known_conflicts: Vec::new(),
  11309             })
  11310             .expect_err("mismatched custody publish work should use AppSdkRuntime");
  11311 
  11312         assert_migrated_payload_uses_sdk_runtime(error);
  11313         assert_eq!(relay.event_count(), 0);
  11314     }
  11315 
  11316     #[test]
  11317     fn desktop_namespace_uses_canonical_app_and_shared_runtime_roots() {
  11318         let paths = AppDesktopRuntimePaths::for_desktop(
  11319             AppRuntimePlatform::Macos,
  11320             AppRuntimeHostEnvironment {
  11321                 home_dir: Some(PathBuf::from("/Users/treesap")),
  11322                 ..AppRuntimeHostEnvironment::default()
  11323             },
  11324         )
  11325         .expect("interactive user roots should resolve");
  11326 
  11327         assert_eq!(
  11328             paths.app.data,
  11329             PathBuf::from("/Users/treesap/.radroots/data/apps/app")
  11330         );
  11331         assert_eq!(
  11332             paths.app.logs,
  11333             PathBuf::from("/Users/treesap/.radroots/logs/apps/app")
  11334         );
  11335         assert_eq!(
  11336             paths.app.data.join(APP_DATABASE_FILE_NAME),
  11337             PathBuf::from("/Users/treesap/.radroots/data/apps/app/app.sqlite3")
  11338         );
  11339         assert_eq!(
  11340             paths.shared_accounts.data_root,
  11341             PathBuf::from("/Users/treesap/.radroots/data/shared/accounts")
  11342         );
  11343         assert_eq!(
  11344             paths.shared_accounts.secrets_root,
  11345             PathBuf::from("/Users/treesap/.radroots/secrets/shared/accounts")
  11346         );
  11347         assert_eq!(
  11348             paths.shared_accounts.store_path,
  11349             PathBuf::from("/Users/treesap/.radroots/data/shared/accounts")
  11350                 .join(SHARED_ACCOUNTS_STORE_FILE_NAME)
  11351         );
  11352         assert_eq!(
  11353             paths.shared_identity.default_identity_path,
  11354             PathBuf::from("/Users/treesap/.radroots/secrets/shared/identities")
  11355                 .join(SHARED_IDENTITY_FILE_NAME)
  11356         );
  11357     }
  11358 
  11359     #[test]
  11360     fn cloned_runtime_handles_shared_settings_state() {
  11361         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  11362             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  11363                 .expect("in-memory state store should load"),
  11364             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  11365             shared_accounts_paths: None,
  11366             remote_signer_paths: None,
  11367             accounts_manager: None,
  11368             sqlite_store: Some(
  11369                 AppSqliteStore::open(DatabaseTarget::InMemory)
  11370                     .expect("in-memory sqlite store should open"),
  11371             ),
  11372             sdk_runtime: None,
  11373             sync_transport: default_sync_transport(),
  11374             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  11375             selected_account_pending_sync_write_count: 0,
  11376             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  11377             selected_account_sync_conflicts: Vec::new(),
  11378             startup_issue: None,
  11379         });
  11380         let cloned_runtime = runtime.clone();
  11381 
  11382         assert!(runtime.sync_settings_section(SettingsSection::About));
  11383         assert!(cloned_runtime.set_settings_preference(SettingsPreference::LaunchAtLogin, true));
  11384 
  11385         let summary = runtime.summary();
  11386 
  11387         assert_eq!(
  11388             summary.shell_projection.selected_section,
  11389             ShellSection::Home
  11390         );
  11391         assert_eq!(
  11392             summary.shell_projection.settings.selected_section,
  11393             SettingsSection::About
  11394         );
  11395         assert!(summary.shell_projection.settings.general.launch_at_login);
  11396         assert_eq!(
  11397             cloned_runtime.selected_settings_section(),
  11398             SettingsSection::About
  11399         );
  11400         assert_eq!(summary.startup_gate, AppStartupGate::SetupRequired);
  11401         assert_eq!(summary.home_route, HomeRoute::SetupRequired);
  11402         assert!(summary.settings_account_projection.roster.is_empty());
  11403         assert!(
  11404             summary
  11405                 .settings_account_projection
  11406                 .selected_account
  11407                 .is_none()
  11408         );
  11409         assert_eq!(
  11410             summary.logged_out_startup,
  11411             LoggedOutStartupProjection::default()
  11412         );
  11413     }
  11414 
  11415     #[test]
  11416     fn cloned_runtime_handles_shared_startup_identity_choice_state() {
  11417         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  11418             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  11419                 .expect("in-memory state store should load"),
  11420             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  11421             shared_accounts_paths: None,
  11422             remote_signer_paths: None,
  11423             accounts_manager: None,
  11424             sqlite_store: Some(
  11425                 AppSqliteStore::open(DatabaseTarget::InMemory)
  11426                     .expect("in-memory sqlite store should open"),
  11427             ),
  11428             sdk_runtime: None,
  11429             sync_transport: default_sync_transport(),
  11430             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  11431             selected_account_pending_sync_write_count: 0,
  11432             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  11433             selected_account_sync_conflicts: Vec::new(),
  11434             startup_issue: None,
  11435         });
  11436         let cloned_runtime = runtime.clone();
  11437 
  11438         assert!(runtime.show_startup_identity_choice());
  11439         assert!(cloned_runtime.show_startup_signer_entry());
  11440         assert!(cloned_runtime.set_startup_signer_source_input(
  11441             "bunker://npub1signer?relay=wss%3A%2F%2Frelay.radroots.example"
  11442         ));
  11443         assert!(runtime.begin_generate_key_startup());
  11444 
  11445         let summary = runtime.summary();
  11446 
  11447         assert_eq!(
  11448             summary.logged_out_startup.phase,
  11449             radroots_app_view::LoggedOutStartupPhase::GenerateKeyStarting
  11450         );
  11451         assert_eq!(
  11452             summary.logged_out_startup.signer_entry.source_input,
  11453             "bunker://npub1signer?relay=wss%3A%2F%2Frelay.radroots.example"
  11454         );
  11455     }
  11456 
  11457     #[test]
  11458     fn runtime_summary_keeps_sync_disabled_without_a_selected_account() {
  11459         let runtime = memory_runtime();
  11460         let summary = runtime.summary();
  11461 
  11462         assert_eq!(summary.sync_status, DesktopAppSyncStatusSummary::default());
  11463         assert!(!summary.sync_status.is_enabled());
  11464     }
  11465 
  11466     #[test]
  11467     fn runtime_summary_refreshes_selected_account_sync_status_from_sqlite() {
  11468         let (runtime, paths) = bootstrapped_runtime("selected_account_sync_status");
  11469         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  11470 
  11471         {
  11472             let state = runtime.lock_state();
  11473             let sqlite_store = state
  11474                 .sqlite_store
  11475                 .as_ref()
  11476                 .expect("sqlite store should exist");
  11477 
  11478             sqlite_store
  11479                 .save_sync_checkpoint(
  11480                     &account_id,
  11481                     &SyncCheckpointStatus::current(
  11482                         None,
  11483                         "2026-04-20T19:00:00Z",
  11484                         Some("cursor-3".to_owned()),
  11485                     ),
  11486                 )
  11487                 .expect("sync checkpoint should save");
  11488             sqlite_store
  11489                 .record_sync_conflict(
  11490                     &account_id,
  11491                     &SyncConflict {
  11492                         aggregate: SyncAggregateRef::Farm(farm_id),
  11493                         kind: SyncConflictKind::RevisionMismatch,
  11494                         severity: SyncConflictSeverity::Blocking,
  11495                         resolution: SyncConflictResolutionStatus::Unresolved,
  11496                         local_payload_json: "{\"farm\":\"local\"}".to_owned(),
  11497                         remote_payload_json: Some("{\"farm\":\"remote\"}".to_owned()),
  11498                         detected_at: "2026-04-20T19:01:00Z".to_owned(),
  11499                         resolved_at: None,
  11500                     },
  11501                 )
  11502                 .expect("sync conflict should save");
  11503             sqlite_store
  11504                 .enqueue_pending_sync_operation(
  11505                     &account_id,
  11506                     &PendingSyncOperation::new(
  11507                         SyncAggregateRef::Farm(farm_id),
  11508                         SyncOperationKind::Upsert,
  11509                         "{\"farm\":\"queued\"}",
  11510                         "2026-04-20T19:02:00Z",
  11511                     ),
  11512                 )
  11513                 .expect("pending sync operation should save");
  11514         }
  11515 
  11516         assert!(
  11517             runtime
  11518                 .lock_state_mut()
  11519                 .refresh_selected_account_sync()
  11520                 .expect("sync status should refresh")
  11521         );
  11522 
  11523         let summary = runtime.summary();
  11524 
  11525         assert_eq!(
  11526             summary.sync_status.account_id.as_deref(),
  11527             Some(account_id.as_str())
  11528         );
  11529         assert!(summary.sync_status.is_enabled());
  11530         assert_eq!(summary.sync_status.pending_write_count, 1);
  11531         assert_eq!(
  11532             summary.sync_status.projection.run_status,
  11533             AppSyncRunStatus::Conflicted
  11534         );
  11535         assert_eq!(
  11536             summary
  11537                 .sync_status
  11538                 .projection
  11539                 .conflict_status
  11540                 .unresolved_count,
  11541             1
  11542         );
  11543         assert_eq!(
  11544             summary
  11545                 .sync_status
  11546                 .projection
  11547                 .checkpoint
  11548                 .last_remote_cursor
  11549                 .as_deref(),
  11550             Some("cursor-3")
  11551         );
  11552 
  11553         cleanup_bootstrapped_runtime_paths(&paths);
  11554     }
  11555 
  11556     #[test]
  11557     fn runtime_product_incomplete_save_does_not_enqueue_publish_work() {
  11558         let runtime = memory_runtime();
  11559         let (account_id, _) = provision_ready_farmer_account(&runtime);
  11560 
  11561         assert!(
  11562             runtime
  11563                 .open_new_product_editor()
  11564                 .expect("new product editor should open")
  11565         );
  11566         let product_id = match runtime.summary().products_projection.editor {
  11567             radroots_app_state::ProductEditorState::Open(session) => session
  11568                 .selected_product_id
  11569                 .expect("open product editor should select a product"),
  11570             radroots_app_state::ProductEditorState::Closed => {
  11571                 panic!("product editor should be open")
  11572             }
  11573         };
  11574         let first_draft = ProductEditorDraft {
  11575             title: "Salad mix".to_owned(),
  11576             subtitle: "Spring blend".to_owned(),
  11577             category: String::new(),
  11578             unit_label: "bag".to_owned(),
  11579             price_minor_units: Some(700),
  11580             price_currency: "USD".to_owned(),
  11581             stock_quantity: Some(8),
  11582             availability_window_id: None,
  11583             status: ProductStatus::Draft,
  11584         };
  11585         let second_draft = ProductEditorDraft {
  11586             title: "Winter greens".to_owned(),
  11587             subtitle: "Cut this morning".to_owned(),
  11588             category: "greens".to_owned(),
  11589             unit_label: "bag".to_owned(),
  11590             price_minor_units: Some(900),
  11591             price_currency: "USD".to_owned(),
  11592             stock_quantity: Some(11),
  11593             availability_window_id: None,
  11594             status: ProductStatus::Published,
  11595         };
  11596 
  11597         assert!(
  11598             runtime
  11599                 .save_product_editor_draft(first_draft)
  11600                 .expect("first product editor save should succeed")
  11601         );
  11602         assert!(
  11603             runtime
  11604                 .save_product_editor_draft(second_draft.clone())
  11605                 .expect("second product editor save should succeed")
  11606         );
  11607 
  11608         let pending_operations = runtime
  11609             .lock_state()
  11610             .sqlite_store
  11611             .as_ref()
  11612             .expect("sqlite store")
  11613             .load_pending_sync_operations(account_id.as_str())
  11614             .expect("pending sync operations should load");
  11615 
  11616         assert_eq!(pending_operations.len(), 0);
  11617         assert_eq!(
  11618             runtime
  11619                 .lock_state()
  11620                 .sqlite_store
  11621                 .as_ref()
  11622                 .expect("sqlite store")
  11623                 .load_product_editor_draft(product_id)
  11624                 .expect("saved product draft should load"),
  11625             Some(second_draft)
  11626         );
  11627     }
  11628 
  11629     #[test]
  11630     fn runtime_product_publishable_save_enqueues_typed_listing_publish_work() {
  11631         let (runtime, paths) = bootstrapped_runtime("publishable_product_listing_work");
  11632         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  11633         let pickup_location_id = PickupLocationId::new();
  11634         let fulfillment_window_id = FulfillmentWindowId::new();
  11635 
  11636         runtime
  11637             .save_farm_rules_projection(FarmRulesProjection {
  11638                 farm_profile: Some(FarmProfileRecord {
  11639                     farm_id,
  11640                     display_name: "North field farm".to_owned(),
  11641                     timezone: "UTC".to_owned(),
  11642                     currency_code: "USD".to_owned(),
  11643                 }),
  11644                 pickup_locations: vec![PickupLocationRecord {
  11645                     pickup_location_id,
  11646                     farm_id,
  11647                     label: "Barn pickup".to_owned(),
  11648                     address_line: "14 Orchard Lane".to_owned(),
  11649                     directions: None,
  11650                     is_default: true,
  11651                 }],
  11652                 operating_rules: Some(FarmOperatingRulesRecord {
  11653                     farm_id,
  11654                     promise_lead_hours: 24,
  11655                     substitution_policy: "ask_customer".to_owned(),
  11656                 }),
  11657                 fulfillment_windows: vec![FulfillmentWindowRecord {
  11658                     fulfillment_window_id,
  11659                     farm_id,
  11660                     pickup_location_id,
  11661                     label: "Friday pickup".to_owned(),
  11662                     starts_at: "2099-04-25T14:00:00Z".to_owned(),
  11663                     ends_at: "2099-04-25T18:00:00Z".to_owned(),
  11664                     order_cutoff_at: "2099-04-24T18:00:00Z".to_owned(),
  11665                 }],
  11666                 blackout_periods: Vec::new(),
  11667                 ..runtime
  11668                     .load_farm_rules_projection()
  11669                     .expect("farm rules projection should load")
  11670             })
  11671             .expect("farm rules should save");
  11672 
  11673         assert!(
  11674             runtime
  11675                 .open_new_product_editor()
  11676                 .expect("new product editor should open")
  11677         );
  11678         let product_id = match runtime.summary().products_projection.editor {
  11679             radroots_app_state::ProductEditorState::Open(session) => session
  11680                 .selected_product_id
  11681                 .expect("open product editor should select a product"),
  11682             radroots_app_state::ProductEditorState::Closed => {
  11683                 panic!("product editor should be open")
  11684             }
  11685         };
  11686 
  11687         assert!(
  11688             runtime
  11689                 .save_product_editor_draft(ProductEditorDraft {
  11690                     title: "Salad mix".to_owned(),
  11691                     subtitle: "Cut this morning".to_owned(),
  11692                     category: "greens".to_owned(),
  11693                     unit_label: "each".to_owned(),
  11694                     price_minor_units: Some(900),
  11695                     price_currency: "usd".to_owned(),
  11696                     stock_quantity: Some(11),
  11697                     availability_window_id: Some(fulfillment_window_id),
  11698                     status: ProductStatus::Published,
  11699                 })
  11700                 .expect("publishable product save should succeed")
  11701         );
  11702 
  11703         let pending_operations = runtime
  11704             .lock_state()
  11705             .sqlite_store
  11706             .as_ref()
  11707             .expect("sqlite store")
  11708             .load_pending_sync_operations(account_id.as_str())
  11709             .expect("pending sync operations should load");
  11710         let product_pending_operations = pending_operations
  11711             .iter()
  11712             .filter(|pending| pending.operation.aggregate == SyncAggregateRef::Product(product_id))
  11713             .collect::<Vec<_>>();
  11714         assert!(product_pending_operations.is_empty());
  11715 
  11716         let records = shared_local_event_records(&paths);
  11717         let listing_record = records
  11718             .iter()
  11719             .find(|record| {
  11720                 record
  11721                     .local_work_json
  11722                     .as_ref()
  11723                     .and_then(|payload| payload["record_kind"].as_str())
  11724                     == Some("listing_draft_v1")
  11725             })
  11726             .expect("listing local work record");
  11727         let listing_payload = listing_record
  11728             .local_work_json
  11729             .as_ref()
  11730             .expect("listing local work payload");
  11731         assert_eq!(listing_payload["publishability"]["state"], "publishable");
  11732         assert_eq!(listing_payload["document"]["product"]["category"], "greens");
  11733         assert_eq!(
  11734             listing_payload["document"]["primary_bin"]["bin_id"]
  11735                 .as_str()
  11736                 .expect("primary bin id should be present"),
  11737             super::listing_primary_bin_id(super::d_tag_from_uuid(product_id.as_uuid()).as_str())
  11738         );
  11739         assert_eq!(listing_payload["document"]["delivery"]["method"], "pickup");
  11740         assert_eq!(
  11741             listing_payload["document"]["location"]["primary"],
  11742             "14 Orchard Lane"
  11743         );
  11744         let receipt = runtime
  11745             .lock_state()
  11746             .sqlite_store
  11747             .as_ref()
  11748             .expect("sqlite store")
  11749             .sdk_migration_receipt_repository()
  11750             .load_receipt(
  11751                 AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
  11752                 listing_record.record_id.as_str(),
  11753             )
  11754             .expect("listing SDK migration receipt should load")
  11755             .expect("listing SDK migration receipt should exist");
  11756         assert_eq!(receipt.source_record_id, listing_record.record_id);
  11757         assert_eq!(receipt.sdk_operation_kind, LISTING_PUBLISH_OPERATION_KIND);
  11758         assert_eq!(receipt.migration_state, AppSdkMigrationState::Enqueued);
  11759         assert!(receipt.expected_event_id.is_some());
  11760         assert!(
  11761             receipt
  11762                 .actor_pubkey
  11763                 .as_deref()
  11764                 .is_some_and(super::is_hex_64)
  11765         );
  11766         assert!(!receipt.sdk_outbox_event_ids.is_empty());
  11767         assert!(receipt.idempotency_digest_prefix.is_some());
  11768         assert_eq!(
  11769             receipt.detail_json["operation_kind"],
  11770             LISTING_PUBLISH_OPERATION_KIND
  11771         );
  11772 
  11773         cleanup_bootstrapped_runtime_paths(&paths);
  11774     }
  11775 
  11776     #[test]
  11777     fn runtime_product_publishable_save_returns_error_when_sdk_listing_enqueue_fails() {
  11778         let (runtime, paths) = bootstrapped_runtime("publishable_product_listing_sdk_failure");
  11779         let (_account_id, farm_id) = provision_ready_farmer_account(&runtime);
  11780         let pickup_location_id = PickupLocationId::new();
  11781         let fulfillment_window_id = FulfillmentWindowId::new();
  11782 
  11783         runtime
  11784             .save_farm_rules_projection(FarmRulesProjection {
  11785                 farm_profile: Some(FarmProfileRecord {
  11786                     farm_id,
  11787                     display_name: "North field farm".to_owned(),
  11788                     timezone: "UTC".to_owned(),
  11789                     currency_code: "USD".to_owned(),
  11790                 }),
  11791                 pickup_locations: vec![PickupLocationRecord {
  11792                     pickup_location_id,
  11793                     farm_id,
  11794                     label: "Barn pickup".to_owned(),
  11795                     address_line: "14 Orchard Lane".to_owned(),
  11796                     directions: None,
  11797                     is_default: true,
  11798                 }],
  11799                 operating_rules: Some(FarmOperatingRulesRecord {
  11800                     farm_id,
  11801                     promise_lead_hours: 24,
  11802                     substitution_policy: "ask_customer".to_owned(),
  11803                 }),
  11804                 fulfillment_windows: vec![FulfillmentWindowRecord {
  11805                     fulfillment_window_id,
  11806                     farm_id,
  11807                     pickup_location_id,
  11808                     label: "Friday pickup".to_owned(),
  11809                     starts_at: "2099-04-25T14:00:00Z".to_owned(),
  11810                     ends_at: "2099-04-25T18:00:00Z".to_owned(),
  11811                     order_cutoff_at: "2099-04-24T18:00:00Z".to_owned(),
  11812                 }],
  11813                 blackout_periods: Vec::new(),
  11814                 ..runtime
  11815                     .load_farm_rules_projection()
  11816                     .expect("farm rules projection should load")
  11817             })
  11818             .expect("farm rules should save");
  11819 
  11820         assert!(
  11821             runtime
  11822                 .open_new_product_editor()
  11823                 .expect("new product editor should open")
  11824         );
  11825         let product_id = match runtime.summary().products_projection.editor {
  11826             radroots_app_state::ProductEditorState::Open(session) => session
  11827                 .selected_product_id
  11828                 .expect("open product editor should select a product"),
  11829             radroots_app_state::ProductEditorState::Closed => {
  11830                 panic!("product editor should be open")
  11831             }
  11832         };
  11833         assert!(
  11834             runtime
  11835                 .shutdown_sdk_runtime()
  11836                 .expect("sdk runtime should shut down")
  11837         );
  11838         let draft = ProductEditorDraft {
  11839             title: "Salad mix".to_owned(),
  11840             subtitle: "Cut this morning".to_owned(),
  11841             category: "greens".to_owned(),
  11842             unit_label: "each".to_owned(),
  11843             price_minor_units: Some(900),
  11844             price_currency: "USD".to_owned(),
  11845             stock_quantity: Some(11),
  11846             availability_window_id: Some(fulfillment_window_id),
  11847             status: ProductStatus::Published,
  11848         };
  11849 
  11850         let error = runtime
  11851             .save_product_editor_draft(draft.clone())
  11852             .expect_err("SDK listing enqueue failure should fail the save action");
  11853         assert!(matches!(
  11854             error,
  11855             super::DesktopAppRuntimeProductEditorSaveError::ListingPublishSdkEnqueueFailed
  11856         ));
  11857         assert_eq!(
  11858             runtime
  11859                 .lock_state()
  11860                 .sqlite_store
  11861                 .as_ref()
  11862                 .expect("sqlite store")
  11863                 .load_product_editor_draft(product_id)
  11864                 .expect("saved product draft should load"),
  11865             Some(draft.clone())
  11866         );
  11867 
  11868         let records = shared_local_event_records(&paths);
  11869         let listing_record = records
  11870             .iter()
  11871             .find(|record| {
  11872                 record
  11873                     .local_work_json
  11874                     .as_ref()
  11875                     .and_then(|payload| payload["record_kind"].as_str())
  11876                     == Some("listing_draft_v1")
  11877             })
  11878             .expect("listing local work record");
  11879         let receipt = runtime
  11880             .lock_state()
  11881             .sqlite_store
  11882             .as_ref()
  11883             .expect("sqlite store")
  11884             .sdk_migration_receipt_repository()
  11885             .load_receipt(
  11886                 AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
  11887                 listing_record.record_id.as_str(),
  11888             )
  11889             .expect("failed listing SDK migration receipt should load")
  11890             .expect("failed listing SDK migration receipt should exist");
  11891         assert_eq!(receipt.source_record_id, listing_record.record_id);
  11892         assert_eq!(receipt.sdk_operation_kind, LISTING_PUBLISH_OPERATION_KIND);
  11893         assert_eq!(receipt.migration_state, AppSdkMigrationState::Failed);
  11894         assert!(receipt.sdk_outbox_event_ids.is_empty());
  11895         assert!(receipt.expected_event_id.is_none());
  11896         assert!(receipt.actor_pubkey.is_none());
  11897         assert!(receipt.idempotency_digest_prefix.is_none());
  11898         assert_eq!(receipt.detail_json["code"], "sdk_runtime_not_available");
  11899         assert_eq!(receipt.detail_json["class"], "runtime");
  11900         assert_eq!(receipt.detail_json["retryable"], true);
  11901 
  11902         restore_sdk_runtime(&runtime, &paths);
  11903         assert!(
  11904             runtime
  11905                 .save_product_editor_draft(draft.clone())
  11906                 .expect("retry should enqueue listing publish through SDK runtime")
  11907         );
  11908         let retry_records = shared_local_event_records(&paths);
  11909         let enqueued_listing_receipts = {
  11910             let state = runtime.lock_state();
  11911             let repository = state
  11912                 .sqlite_store
  11913                 .as_ref()
  11914                 .expect("sqlite store")
  11915                 .sdk_migration_receipt_repository();
  11916             retry_records
  11917                 .iter()
  11918                 .filter(|record| {
  11919                     record
  11920                         .local_work_json
  11921                         .as_ref()
  11922                         .and_then(|payload| payload["record_kind"].as_str())
  11923                         == Some("listing_draft_v1")
  11924                 })
  11925                 .filter_map(|record| {
  11926                     repository
  11927                         .load_receipt(
  11928                             AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
  11929                             record.record_id.as_str(),
  11930                         )
  11931                         .expect("retry listing SDK migration receipt should load")
  11932                 })
  11933                 .filter(|receipt| receipt.migration_state == AppSdkMigrationState::Enqueued)
  11934                 .count()
  11935         };
  11936         assert!(enqueued_listing_receipts >= 1);
  11937         assert!(
  11938             runtime
  11939                 .shutdown_sdk_runtime()
  11940                 .expect("sdk runtime should shut down after retry")
  11941         );
  11942 
  11943         cleanup_bootstrapped_runtime_paths(&paths);
  11944     }
  11945 
  11946     #[test]
  11947     fn runtime_product_stock_update_retries_sdk_listing_enqueue_after_local_save() {
  11948         let (runtime, paths) = bootstrapped_runtime("stock_listing_sdk_retry");
  11949         let (_account_id, farm_id) = provision_ready_farmer_account(&runtime);
  11950         let pickup_location_id = PickupLocationId::new();
  11951         let fulfillment_window_id = FulfillmentWindowId::new();
  11952 
  11953         runtime
  11954             .save_farm_rules_projection(FarmRulesProjection {
  11955                 farm_profile: Some(FarmProfileRecord {
  11956                     farm_id,
  11957                     display_name: "North field farm".to_owned(),
  11958                     timezone: "UTC".to_owned(),
  11959                     currency_code: "USD".to_owned(),
  11960                 }),
  11961                 pickup_locations: vec![PickupLocationRecord {
  11962                     pickup_location_id,
  11963                     farm_id,
  11964                     label: "Barn pickup".to_owned(),
  11965                     address_line: "14 Orchard Lane".to_owned(),
  11966                     directions: None,
  11967                     is_default: true,
  11968                 }],
  11969                 operating_rules: Some(FarmOperatingRulesRecord {
  11970                     farm_id,
  11971                     promise_lead_hours: 24,
  11972                     substitution_policy: "ask_customer".to_owned(),
  11973                 }),
  11974                 fulfillment_windows: vec![FulfillmentWindowRecord {
  11975                     fulfillment_window_id,
  11976                     farm_id,
  11977                     pickup_location_id,
  11978                     label: "Friday pickup".to_owned(),
  11979                     starts_at: "2099-04-25T14:00:00Z".to_owned(),
  11980                     ends_at: "2099-04-25T18:00:00Z".to_owned(),
  11981                     order_cutoff_at: "2099-04-24T18:00:00Z".to_owned(),
  11982                 }],
  11983                 blackout_periods: Vec::new(),
  11984                 ..runtime
  11985                     .load_farm_rules_projection()
  11986                     .expect("farm rules projection should load")
  11987             })
  11988             .expect("farm rules should save");
  11989 
  11990         assert!(
  11991             runtime
  11992                 .open_new_product_editor()
  11993                 .expect("new product editor should open")
  11994         );
  11995         let product_id = match runtime.summary().products_projection.editor {
  11996             radroots_app_state::ProductEditorState::Open(session) => session
  11997                 .selected_product_id
  11998                 .expect("open product editor should select a product"),
  11999             radroots_app_state::ProductEditorState::Closed => {
  12000                 panic!("product editor should be open")
  12001             }
  12002         };
  12003         let draft = ProductEditorDraft {
  12004             title: "Salad mix".to_owned(),
  12005             subtitle: "Cut this morning".to_owned(),
  12006             category: "greens".to_owned(),
  12007             unit_label: "each".to_owned(),
  12008             price_minor_units: Some(900),
  12009             price_currency: "USD".to_owned(),
  12010             stock_quantity: Some(11),
  12011             availability_window_id: Some(fulfillment_window_id),
  12012             status: ProductStatus::Published,
  12013         };
  12014         assert!(
  12015             runtime
  12016                 .save_product_editor_draft(draft)
  12017                 .expect("initial published product save should enqueue")
  12018         );
  12019         assert!(
  12020             runtime
  12021                 .shutdown_sdk_runtime()
  12022                 .expect("sdk runtime should shut down")
  12023         );
  12024 
  12025         let error = runtime
  12026             .update_product_stock(product_id, 13)
  12027             .expect_err("SDK listing enqueue failure should fail stock update action");
  12028         assert!(matches!(
  12029             error,
  12030             super::DesktopAppRuntimeProductStockUpdateError::ListingPublishSdkEnqueueFailed
  12031         ));
  12032         assert_eq!(
  12033             runtime
  12034                 .lock_state()
  12035                 .sqlite_store
  12036                 .as_ref()
  12037                 .expect("sqlite store")
  12038                 .load_product_editor_draft(product_id)
  12039                 .expect("saved product draft should load")
  12040                 .expect("saved product draft should exist")
  12041                 .stock_quantity,
  12042             Some(13)
  12043         );
  12044         let (source_kind, source_record_id) =
  12045             super::listing_publish_source_record(product_id, "update_product_stock", None);
  12046         let failed_receipt = runtime
  12047             .lock_state()
  12048             .sqlite_store
  12049             .as_ref()
  12050             .expect("sqlite store")
  12051             .sdk_migration_receipt_repository()
  12052             .load_receipt(source_kind, source_record_id.as_str())
  12053             .expect("failed stock listing SDK migration receipt should load")
  12054             .expect("failed stock listing SDK migration receipt should exist");
  12055         assert_eq!(failed_receipt.migration_state, AppSdkMigrationState::Failed);
  12056         assert_eq!(
  12057             failed_receipt.detail_json["code"],
  12058             "sdk_runtime_not_available"
  12059         );
  12060 
  12061         restore_sdk_runtime(&runtime, &paths);
  12062         assert!(
  12063             runtime
  12064                 .update_product_stock(product_id, 13)
  12065                 .expect("retry should enqueue stock listing publish through SDK runtime")
  12066         );
  12067         let retry_receipt = runtime
  12068             .lock_state()
  12069             .sqlite_store
  12070             .as_ref()
  12071             .expect("sqlite store")
  12072             .sdk_migration_receipt_repository()
  12073             .load_receipt(source_kind, source_record_id.as_str())
  12074             .expect("retry stock listing SDK migration receipt should load")
  12075             .expect("retry stock listing SDK migration receipt should exist");
  12076         assert_eq!(
  12077             retry_receipt.migration_state,
  12078             AppSdkMigrationState::Enqueued
  12079         );
  12080         assert!(retry_receipt.expected_event_id.is_some());
  12081         assert!(
  12082             runtime
  12083                 .shutdown_sdk_runtime()
  12084                 .expect("sdk runtime should shut down after retry")
  12085         );
  12086 
  12087         cleanup_bootstrapped_runtime_paths(&paths);
  12088     }
  12089 
  12090     #[test]
  12091     fn runtime_product_stale_availability_save_records_blocker_without_publish_work() {
  12092         let (runtime, paths) = bootstrapped_runtime("stale_product_listing_work");
  12093         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  12094         let pickup_location_id = PickupLocationId::new();
  12095         let active_window_id = FulfillmentWindowId::new();
  12096         let stale_window_id = FulfillmentWindowId::new();
  12097 
  12098         runtime
  12099             .save_farm_rules_projection(FarmRulesProjection {
  12100                 farm_profile: Some(FarmProfileRecord {
  12101                     farm_id,
  12102                     display_name: "North field farm".to_owned(),
  12103                     timezone: "UTC".to_owned(),
  12104                     currency_code: "USD".to_owned(),
  12105                 }),
  12106                 pickup_locations: vec![PickupLocationRecord {
  12107                     pickup_location_id,
  12108                     farm_id,
  12109                     label: "Barn pickup".to_owned(),
  12110                     address_line: "14 Orchard Lane".to_owned(),
  12111                     directions: None,
  12112                     is_default: true,
  12113                 }],
  12114                 operating_rules: Some(FarmOperatingRulesRecord {
  12115                     farm_id,
  12116                     promise_lead_hours: 24,
  12117                     substitution_policy: "ask_customer".to_owned(),
  12118                 }),
  12119                 fulfillment_windows: vec![FulfillmentWindowRecord {
  12120                     fulfillment_window_id: active_window_id,
  12121                     farm_id,
  12122                     pickup_location_id,
  12123                     label: "Friday pickup".to_owned(),
  12124                     starts_at: "2099-04-25T14:00:00Z".to_owned(),
  12125                     ends_at: "2099-04-25T18:00:00Z".to_owned(),
  12126                     order_cutoff_at: "2099-04-24T18:00:00Z".to_owned(),
  12127                 }],
  12128                 blackout_periods: Vec::new(),
  12129                 ..runtime
  12130                     .load_farm_rules_projection()
  12131                     .expect("farm rules projection should load")
  12132             })
  12133             .expect("farm rules should save");
  12134 
  12135         assert!(
  12136             runtime
  12137                 .open_new_product_editor()
  12138                 .expect("new product editor should open")
  12139         );
  12140         let product_id = match runtime.summary().products_projection.editor {
  12141             radroots_app_state::ProductEditorState::Open(session) => session
  12142                 .selected_product_id
  12143                 .expect("open product editor should select a product"),
  12144             radroots_app_state::ProductEditorState::Closed => {
  12145                 panic!("product editor should be open")
  12146             }
  12147         };
  12148 
  12149         runtime
  12150             .lock_state()
  12151             .sqlite_store
  12152             .as_ref()
  12153             .expect("sqlite store")
  12154             .connection()
  12155             .execute_batch("PRAGMA foreign_keys = OFF;")
  12156             .expect("foreign keys should disable for stale fixture");
  12157         let save_result = runtime.save_product_editor_draft(ProductEditorDraft {
  12158             title: "Salad mix".to_owned(),
  12159             subtitle: "Cut this morning".to_owned(),
  12160             category: "greens".to_owned(),
  12161             unit_label: "bag".to_owned(),
  12162             price_minor_units: Some(900),
  12163             price_currency: "usd".to_owned(),
  12164             stock_quantity: Some(11),
  12165             availability_window_id: Some(stale_window_id),
  12166             status: ProductStatus::Published,
  12167         });
  12168         runtime
  12169             .lock_state()
  12170             .sqlite_store
  12171             .as_ref()
  12172             .expect("sqlite store")
  12173             .connection()
  12174             .execute_batch("PRAGMA foreign_keys = ON;")
  12175             .expect("foreign keys should restore");
  12176         assert!(save_result.expect("stale product editor save should succeed"));
  12177 
  12178         let summary = runtime.summary();
  12179         let radroots_app_state::ProductEditorState::Open(session) =
  12180             summary.products_projection.editor
  12181         else {
  12182             panic!("product editor should stay open")
  12183         };
  12184         assert_eq!(
  12185             session.publish_blockers,
  12186             vec![ProductPublishBlocker::AttachAvailability]
  12187         );
  12188 
  12189         let pending_operations = runtime
  12190             .lock_state()
  12191             .sqlite_store
  12192             .as_ref()
  12193             .expect("sqlite store")
  12194             .load_pending_sync_operations(account_id.as_str())
  12195             .expect("pending sync operations should load");
  12196         let product_pending_operations = pending_operations
  12197             .iter()
  12198             .filter(|pending| pending.operation.aggregate == SyncAggregateRef::Product(product_id))
  12199             .collect::<Vec<_>>();
  12200         assert!(product_pending_operations.is_empty());
  12201 
  12202         let records = shared_local_event_records(&paths);
  12203         let listing_record = records
  12204             .iter()
  12205             .find(|record| {
  12206                 record
  12207                     .local_work_json
  12208                     .as_ref()
  12209                     .and_then(|payload| payload["record_kind"].as_str())
  12210                     == Some("listing_draft_v1")
  12211             })
  12212             .expect("listing local work record");
  12213         let listing_payload = listing_record
  12214             .local_work_json
  12215             .as_ref()
  12216             .expect("listing local work payload");
  12217         assert_eq!(listing_payload["publishability"]["state"], "blocked");
  12218         assert_eq!(
  12219             listing_payload["publishability"]["blockers"],
  12220             json!(["attach_availability"])
  12221         );
  12222 
  12223         cleanup_bootstrapped_runtime_paths(&paths);
  12224     }
  12225 
  12226     #[test]
  12227     fn runtime_product_local_drafts_do_not_enqueue_publish_work_without_required_fields() {
  12228         let runtime = memory_runtime();
  12229         let (account_id, _) = provision_ready_farmer_account(&runtime);
  12230         let recorded = install_recorded_sync_transport(
  12231             &runtime,
  12232             RecordedAppSyncTransport::succeed(AppSyncResult {
  12233                 run_status: AppSyncRunStatus::Succeeded,
  12234                 checkpoint: SyncCheckpointStatus::current(
  12235                     None,
  12236                     "2026-04-20T19:30:00Z",
  12237                     Some("cursor-product".to_owned()),
  12238                 ),
  12239                 pushed_operation_count: 1,
  12240                 pulled_record_count: 0,
  12241                 conflicts: Vec::new(),
  12242                 published_receipts: Vec::new(),
  12243             }),
  12244         );
  12245 
  12246         assert!(
  12247             runtime
  12248                 .open_new_product_editor()
  12249                 .expect("new product editor should open")
  12250         );
  12251 
  12252         let summary = runtime.summary();
  12253         let pending_operations = runtime
  12254             .lock_state()
  12255             .sqlite_store
  12256             .as_ref()
  12257             .expect("sqlite store")
  12258             .load_pending_sync_operations(account_id.as_str())
  12259             .expect("pending sync operations should load");
  12260 
  12261         assert_eq!(recorded.lock().expect("recorded transport").call_count(), 0);
  12262         assert_eq!(summary.sync_status.pending_write_count, 0);
  12263         assert_eq!(pending_operations.len(), 0);
  12264     }
  12265 
  12266     #[test]
  12267     fn runtime_launch_sync_attempt_dequeues_pushed_operations() {
  12268         let runtime = memory_runtime();
  12269         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  12270         runtime
  12271             .lock_state_mut()
  12272             .enqueue_selected_account_sync_operations(vec![pending_sync_upsert(
  12273                 SyncAggregateRef::Farm(farm_id),
  12274                 farm_sync_payload(
  12275                     farm_id,
  12276                     "North field farm",
  12277                     Some(FarmReadiness::Ready),
  12278                     "launch_sync_attempt_dequeues_pushed_operations",
  12279                 ),
  12280             )])
  12281             .expect("pending farm sync should enqueue");
  12282 
  12283         let recorded = install_recorded_sync_transport(
  12284             &runtime,
  12285             RecordedAppSyncTransport::succeed(AppSyncResult {
  12286                 run_status: AppSyncRunStatus::Succeeded,
  12287                 checkpoint: SyncCheckpointStatus::current(
  12288                     Some("2026-04-20T19:40:00Z".to_owned()),
  12289                     "2026-04-20T19:40:05Z",
  12290                     Some("cursor-launch".to_owned()),
  12291                 ),
  12292                 pushed_operation_count: 1,
  12293                 pulled_record_count: 0,
  12294                 conflicts: Vec::new(),
  12295                 published_receipts: Vec::new(),
  12296             }),
  12297         );
  12298 
  12299         assert!(
  12300             runtime
  12301                 .sync_on_app_launch()
  12302                 .expect("launch sync should succeed")
  12303         );
  12304 
  12305         let summary = runtime.summary();
  12306         let recorded = recorded.lock().expect("recorded transport");
  12307         let request = recorded
  12308             .last_request()
  12309             .cloned()
  12310             .expect("launch sync request should record");
  12311 
  12312         assert_eq!(recorded.call_count(), 1);
  12313         assert_eq!(request.trigger, SyncTrigger::AppLaunch);
  12314         assert_eq!(request.pending_operations.len(), 1);
  12315         assert_eq!(summary.sync_status.pending_write_count, 0);
  12316         assert_eq!(
  12317             summary.sync_status.projection.run_status,
  12318             AppSyncRunStatus::Succeeded
  12319         );
  12320         assert_eq!(
  12321             summary.sync_status.projection.checkpoint.state,
  12322             SyncCheckpointState::Current
  12323         );
  12324         assert_eq!(
  12325             runtime
  12326                 .lock_state()
  12327                 .sqlite_store
  12328                 .as_ref()
  12329                 .expect("sqlite store")
  12330                 .load_pending_sync_operations(account_id.as_str())
  12331                 .expect("pending sync operations should load")
  12332                 .len(),
  12333             0
  12334         );
  12335     }
  12336 
  12337     #[test]
  12338     fn runtime_sync_result_refreshes_sync_status_after_receipt_import_changes() {
  12339         let (runtime, paths) = bootstrapped_runtime("sync_status_after_receipt_import");
  12340         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  12341         runtime
  12342             .lock_state_mut()
  12343             .enqueue_selected_account_sync_operations(vec![pending_sync_upsert(
  12344                 SyncAggregateRef::Farm(farm_id),
  12345                 farm_sync_payload(
  12346                     farm_id,
  12347                     "Receipt import farm",
  12348                     Some(FarmReadiness::Ready),
  12349                     "sync_result_refreshes_after_receipt_import",
  12350                 ),
  12351             )])
  12352             .expect("pending farm sync should enqueue");
  12353 
  12354         install_recorded_sync_transport(
  12355             &runtime,
  12356             RecordedAppSyncTransport::succeed(AppSyncResult {
  12357                 run_status: AppSyncRunStatus::Succeeded,
  12358                 checkpoint: SyncCheckpointStatus::current(
  12359                     Some("2026-04-20T19:41:00Z".to_owned()),
  12360                     "2026-04-20T19:41:05Z",
  12361                     Some("cursor-receipt-import".to_owned()),
  12362                 ),
  12363                 pushed_operation_count: 1,
  12364                 pulled_record_count: 0,
  12365                 conflicts: Vec::new(),
  12366                 published_receipts: vec![published_operation_receipt_fixture(
  12367                     account_id.to_string(),
  12368                     None,
  12369                     "1111111111111111111111111111111111111111111111111111111111111111",
  12370                 )],
  12371             }),
  12372         );
  12373 
  12374         assert!(
  12375             runtime
  12376                 .sync_on_app_launch()
  12377                 .expect("launch sync should import published receipt")
  12378         );
  12379 
  12380         let summary = runtime.summary();
  12381         assert_eq!(summary.sync_status.pending_write_count, 0);
  12382         assert_eq!(
  12383             summary.sync_status.projection.run_status,
  12384             AppSyncRunStatus::Succeeded
  12385         );
  12386         assert_eq!(
  12387             summary.sync_status.projection.checkpoint.state,
  12388             SyncCheckpointState::Current
  12389         );
  12390         assert_eq!(
  12391             shared_local_event_records(&paths)
  12392                 .into_iter()
  12393                 .filter(|record| record.family == LocalRecordFamily::SignedEvent)
  12394                 .count(),
  12395             1
  12396         );
  12397 
  12398         cleanup_bootstrapped_runtime_paths(&paths);
  12399     }
  12400 
  12401     #[test]
  12402     fn runtime_partial_sync_result_dequeues_successful_prefix_only() {
  12403         let runtime = memory_runtime();
  12404         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  12405         let product_id = ProductId::new();
  12406         runtime
  12407             .lock_state_mut()
  12408             .enqueue_selected_account_sync_operations(vec![
  12409                 pending_sync_upsert(
  12410                     SyncAggregateRef::Farm(farm_id),
  12411                     farm_sync_payload(
  12412                         farm_id,
  12413                         "North field farm",
  12414                         Some(FarmReadiness::Ready),
  12415                         "partial_sync_prefix",
  12416                     ),
  12417                 ),
  12418                 pending_sync_upsert(SyncAggregateRef::Product(product_id), "{}".to_owned()),
  12419             ])
  12420             .expect("pending sync should enqueue");
  12421 
  12422         let recorded = install_recorded_sync_transport(
  12423             &runtime,
  12424             RecordedAppSyncTransport::succeed(AppSyncResult {
  12425                 run_status: AppSyncRunStatus::Failed,
  12426                 checkpoint: SyncCheckpointStatus::failed(
  12427                     Some("2026-04-20T19:45:00Z".to_owned()),
  12428                     Some("2026-04-20T19:45:05Z".to_owned()),
  12429                     Some("cursor-partial".to_owned()),
  12430                     "relay refused second operation",
  12431                 ),
  12432                 pushed_operation_count: 1,
  12433                 pulled_record_count: 0,
  12434                 conflicts: Vec::new(),
  12435                 published_receipts: Vec::new(),
  12436             }),
  12437         );
  12438 
  12439         assert!(
  12440             runtime
  12441                 .sync_on_app_launch()
  12442                 .expect("partial launch sync should apply")
  12443         );
  12444 
  12445         let summary = runtime.summary();
  12446         let pending_operations = runtime
  12447             .lock_state()
  12448             .sqlite_store
  12449             .as_ref()
  12450             .expect("sqlite store")
  12451             .load_pending_sync_operations(account_id.as_str())
  12452             .expect("pending operations should load");
  12453 
  12454         assert_eq!(recorded.lock().expect("recorded transport").call_count(), 1);
  12455         assert_eq!(summary.sync_status.pending_write_count, 1);
  12456         assert_eq!(
  12457             summary.sync_status.projection.run_status,
  12458             AppSyncRunStatus::Failed
  12459         );
  12460         assert_eq!(pending_operations.len(), 1);
  12461         assert_eq!(
  12462             pending_operations[0].operation.aggregate,
  12463             SyncAggregateRef::Product(product_id)
  12464         );
  12465         assert_eq!(
  12466             pending_operations[0].operation.state,
  12467             PendingSyncOperationState::Retryable
  12468         );
  12469         assert_eq!(pending_operations[0].operation.attempt_count, 1);
  12470         assert_eq!(
  12471             pending_operations[0]
  12472                 .operation
  12473                 .last_error_message
  12474                 .as_deref(),
  12475             Some("relay refused second operation")
  12476         );
  12477     }
  12478 
  12479     #[test]
  12480     fn runtime_foreground_resume_sync_uses_the_resume_trigger() {
  12481         let runtime = memory_runtime();
  12482         let (_, _) = provision_ready_farmer_account(&runtime);
  12483 
  12484         assert!(
  12485             runtime
  12486                 .open_new_product_editor()
  12487                 .expect("new product editor should open")
  12488         );
  12489 
  12490         let recorded = install_recorded_sync_transport(
  12491             &runtime,
  12492             RecordedAppSyncTransport::succeed(AppSyncResult {
  12493                 run_status: AppSyncRunStatus::Succeeded,
  12494                 checkpoint: SyncCheckpointStatus::current(
  12495                     Some("2026-04-20T19:50:00Z".to_owned()),
  12496                     "2026-04-20T19:50:03Z",
  12497                     Some("cursor-resume".to_owned()),
  12498                 ),
  12499                 pushed_operation_count: 1,
  12500                 pulled_record_count: 0,
  12501                 conflicts: Vec::new(),
  12502                 published_receipts: Vec::new(),
  12503             }),
  12504         );
  12505 
  12506         assert!(
  12507             runtime
  12508                 .sync_on_foreground_resume()
  12509                 .expect("resume sync should succeed")
  12510         );
  12511 
  12512         let request = recorded
  12513             .lock()
  12514             .expect("recorded transport")
  12515             .last_request()
  12516             .cloned()
  12517             .expect("resume sync request should record");
  12518 
  12519         assert_eq!(request.trigger, SyncTrigger::ForegroundResume);
  12520     }
  12521 
  12522     #[test]
  12523     fn runtime_shared_local_events_refresh_reports_and_reloads_products() {
  12524         let (runtime, paths) = bootstrapped_runtime("shared_local_events_refresh");
  12525         assert!(
  12526             runtime
  12527                 .generate_local_account(Some("Farmer".to_owned()))
  12528                 .expect("account should generate")
  12529         );
  12530         let account_id = runtime
  12531             .summary()
  12532             .settings_account_projection
  12533             .selected_account
  12534             .as_ref()
  12535             .expect("selected account")
  12536             .account
  12537             .account_id
  12538             .clone();
  12539         append_cli_local_listing_records(&paths, account_id.as_str());
  12540 
  12541         let report = runtime
  12542             .refresh_shared_local_events()
  12543             .expect("shared local events should refresh");
  12544         let summary = runtime.summary();
  12545 
  12546         assert_eq!(report.scanned_records, 2);
  12547         assert_eq!(report.imported_records, 2);
  12548         assert_eq!(report.skipped_records, 0);
  12549         assert_eq!(summary.farm_setup_projection.draft.farm_name, "Green Farm");
  12550         let saved_farm_id = summary
  12551             .farm_setup_projection
  12552             .saved_farm
  12553             .as_ref()
  12554             .expect("saved farm should import")
  12555             .farm_id;
  12556         let direct_products = runtime
  12557             .lock_state()
  12558             .sqlite_store
  12559             .as_ref()
  12560             .expect("sqlite store")
  12561             .load_products(
  12562                 saved_farm_id,
  12563                 "",
  12564                 ProductsFilter::Drafts,
  12565                 ProductsSort::default(),
  12566             )
  12567             .expect("imported products should load directly");
  12568         assert_eq!(direct_products.rows.len(), 1);
  12569         assert!(
  12570             runtime
  12571                 .select_products_filter(ProductsFilter::Drafts)
  12572                 .expect("draft products filter should reload")
  12573         );
  12574         let summary = runtime.summary();
  12575         assert_eq!(summary.products_projection.list.rows.len(), 1);
  12576         assert_eq!(summary.products_projection.list.rows[0].title, "Eggs");
  12577         assert_eq!(
  12578             summary.products_projection.list.rows[0].status,
  12579             ProductStatus::Draft
  12580         );
  12581 
  12582         cleanup_bootstrapped_runtime_paths(&paths);
  12583     }
  12584 
  12585     #[test]
  12586     fn runtime_buyer_search_imports_shared_local_events_before_read() {
  12587         let (runtime, paths) = bootstrapped_runtime("buyer_search_shared_local_events_refresh");
  12588         assert!(
  12589             runtime
  12590                 .generate_local_account(Some("Buyer".to_owned()))
  12591                 .expect("account should generate")
  12592         );
  12593         assert_eq!(
  12594             runtime
  12595                 .summary()
  12596                 .personal_projection
  12597                 .search
  12598                 .listings
  12599                 .rows
  12600                 .len(),
  12601             0
  12602         );
  12603 
  12604         append_cli_signed_buyer_listing_record(&paths);
  12605 
  12606         assert!(
  12607             runtime
  12608                 .set_personal_search_query("eggs")
  12609                 .expect("buyer search query should refresh")
  12610         );
  12611         let summary = runtime.summary();
  12612         assert_eq!(summary.personal_projection.search.listings.rows.len(), 1);
  12613         assert_eq!(
  12614             summary.personal_projection.search.listings.rows[0].title,
  12615             "Buyer Visible Eggs"
  12616         );
  12617         assert_eq!(
  12618             summary.personal_projection.search.listings.rows[0].fulfillment_methods,
  12619             BTreeSet::from([FarmOrderMethod::Pickup])
  12620         );
  12621 
  12622         cleanup_bootstrapped_runtime_paths(&paths);
  12623     }
  12624 
  12625     #[test]
  12626     fn runtime_buyer_search_repeated_query_refreshes_shared_local_events() {
  12627         let (runtime, paths) =
  12628             bootstrapped_runtime("buyer_search_same_query_shared_local_events_refresh");
  12629         assert!(
  12630             runtime
  12631                 .generate_local_account(Some("Buyer".to_owned()))
  12632                 .expect("account should generate")
  12633         );
  12634 
  12635         append_cli_signed_buyer_listing_record_with(
  12636             &paths,
  12637             "first-buyer-visible-listing",
  12638             "DDDDDDDDDDDDDDDDDDDDDD",
  12639             "Buyer Visible Eggs",
  12640             1100,
  12641         );
  12642 
  12643         assert!(
  12644             runtime
  12645                 .set_personal_search_query("eggs")
  12646                 .expect("buyer search query should refresh")
  12647         );
  12648         let first_summary = runtime.summary();
  12649         assert_eq!(
  12650             first_summary.personal_projection.search.listings.rows.len(),
  12651             1
  12652         );
  12653 
  12654         append_cli_signed_buyer_listing_record_with(
  12655             &paths,
  12656             "second-buyer-visible-listing",
  12657             "EEEEEEEEEEEEEEEEEEEEEE",
  12658             "Buyer Visible Eggs Two",
  12659             1200,
  12660         );
  12661 
  12662         assert!(
  12663             runtime
  12664                 .set_personal_search_query("eggs")
  12665                 .expect("same buyer search query should refresh")
  12666         );
  12667         let refreshed_summary = runtime.summary();
  12668         let titles = refreshed_summary
  12669             .personal_projection
  12670             .search
  12671             .listings
  12672             .rows
  12673             .iter()
  12674             .map(|row| row.title.as_str())
  12675             .collect::<BTreeSet<_>>();
  12676         assert_eq!(
  12677             titles,
  12678             BTreeSet::from(["Buyer Visible Eggs", "Buyer Visible Eggs Two"])
  12679         );
  12680 
  12681         assert!(
  12682             !runtime
  12683                 .set_personal_search_query("eggs")
  12684                 .expect("idempotent same buyer search query should refresh")
  12685         );
  12686 
  12687         cleanup_bootstrapped_runtime_paths(&paths);
  12688     }
  12689 
  12690     #[test]
  12691     fn runtime_shared_local_events_refresh_reloads_buyer_browse_idempotently() {
  12692         let (runtime, paths) = bootstrapped_runtime("buyer_browse_shared_local_events_refresh");
  12693         assert!(
  12694             runtime
  12695                 .generate_local_account(Some("Buyer".to_owned()))
  12696                 .expect("account should generate")
  12697         );
  12698         append_cli_signed_buyer_listing_record(&paths);
  12699 
  12700         let report = runtime
  12701             .refresh_shared_local_events()
  12702             .expect("shared local events should refresh");
  12703         let summary = runtime.summary();
  12704         assert_eq!(report.scanned_records, 1);
  12705         assert_eq!(report.imported_records, 1);
  12706         assert_eq!(report.skipped_records, 0);
  12707         assert_eq!(summary.personal_projection.browse.listings.rows.len(), 1);
  12708         assert_eq!(
  12709             summary.personal_projection.browse.listings.rows[0].title,
  12710             "Buyer Visible Eggs"
  12711         );
  12712 
  12713         let second_report = runtime
  12714             .refresh_shared_local_events()
  12715             .expect("second shared local events refresh should succeed");
  12716         assert_eq!(second_report.scanned_records, 0);
  12717         assert_eq!(second_report.imported_records, 0);
  12718         assert_eq!(second_report.skipped_records, 0);
  12719         assert_eq!(
  12720             runtime
  12721                 .summary()
  12722                 .personal_projection
  12723                 .browse
  12724                 .listings
  12725                 .rows
  12726                 .len(),
  12727             1
  12728         );
  12729 
  12730         cleanup_bootstrapped_runtime_paths(&paths);
  12731     }
  12732 
  12733     #[test]
  12734     fn runtime_buyer_browse_selection_refreshes_shared_local_events() {
  12735         let (runtime, paths) = bootstrapped_runtime("buyer_browse_selection_shared_events_refresh");
  12736         assert!(
  12737             runtime
  12738                 .generate_local_account(Some("Buyer".to_owned()))
  12739                 .expect("account should generate")
  12740         );
  12741         assert_eq!(
  12742             runtime
  12743                 .summary()
  12744                 .personal_projection
  12745                 .browse
  12746                 .listings
  12747                 .rows
  12748                 .len(),
  12749             0
  12750         );
  12751 
  12752         append_cli_signed_buyer_listing_record_with(
  12753             &paths,
  12754             "browse-selection-first-listing",
  12755             "DDDDDDDDDDDDDDDDDDDDDD",
  12756             "Buyer Visible Eggs",
  12757             1100,
  12758         );
  12759 
  12760         assert!(
  12761             runtime
  12762                 .select_personal_section(PersonalSection::Browse)
  12763                 .expect("buyer Browse selection should refresh")
  12764         );
  12765         let first_summary = runtime.summary();
  12766         assert_eq!(
  12767             first_summary.personal_projection.browse.listings.rows.len(),
  12768             1
  12769         );
  12770 
  12771         append_cli_signed_buyer_listing_record_with(
  12772             &paths,
  12773             "browse-selection-second-listing",
  12774             "EEEEEEEEEEEEEEEEEEEEEE",
  12775             "Buyer Visible Eggs Two",
  12776             1200,
  12777         );
  12778 
  12779         assert!(
  12780             runtime
  12781                 .select_personal_section(PersonalSection::Browse)
  12782                 .expect("same buyer Browse selection should refresh")
  12783         );
  12784         let refreshed_summary = runtime.summary();
  12785         let titles = refreshed_summary
  12786             .personal_projection
  12787             .browse
  12788             .listings
  12789             .rows
  12790             .iter()
  12791             .map(|row| row.title.as_str())
  12792             .collect::<BTreeSet<_>>();
  12793         assert_eq!(
  12794             titles,
  12795             BTreeSet::from(["Buyer Visible Eggs", "Buyer Visible Eggs Two"])
  12796         );
  12797 
  12798         assert!(
  12799             !runtime
  12800                 .select_personal_section(PersonalSection::Browse)
  12801                 .expect("idempotent buyer Browse selection should refresh")
  12802         );
  12803 
  12804         cleanup_bootstrapped_runtime_paths(&paths);
  12805     }
  12806 
  12807     #[test]
  12808     fn runtime_buyer_browse_selection_surfaces_shared_local_events_import_errors() {
  12809         let (runtime, paths) = bootstrapped_runtime("buyer_browse_selection_import_error");
  12810         assert!(
  12811             runtime
  12812                 .generate_local_account(Some("Buyer".to_owned()))
  12813                 .expect("account should generate")
  12814         );
  12815         let database_path = paths
  12816             .shared_local_events_database_path()
  12817             .expect("shared local events path");
  12818         if let Some(parent) = database_path.parent() {
  12819             fs::create_dir_all(parent).expect("shared local events parent directory");
  12820         }
  12821         if database_path.is_file() {
  12822             fs::remove_file(&database_path).expect("shared local events file should be removable");
  12823         } else if database_path.is_dir() {
  12824             fs::remove_dir_all(&database_path)
  12825                 .expect("shared local events directory should be removable");
  12826         }
  12827         fs::create_dir(&database_path).expect("directory should block sqlite open");
  12828 
  12829         let error = runtime
  12830             .select_personal_section(PersonalSection::Browse)
  12831             .expect_err("buyer Browse selection should surface import errors");
  12832         match error {
  12833             AppSqliteError::LocalEventsSql { operation, .. } => {
  12834                 assert_eq!(operation, "open shared local events database");
  12835             }
  12836             unexpected => panic!("unexpected Browse selection error: {unexpected:?}"),
  12837         }
  12838 
  12839         cleanup_bootstrapped_runtime_paths(&paths);
  12840     }
  12841 
  12842     #[test]
  12843     fn runtime_buyer_detail_open_imports_shared_local_events_before_lookup() {
  12844         assert_detail_open_imports_shared_local_events_before_lookup(
  12845             "buyer_browse_detail_shared_local_events_refresh",
  12846             PersonalSection::Browse,
  12847         );
  12848         assert_detail_open_imports_shared_local_events_before_lookup(
  12849             "buyer_search_detail_shared_local_events_refresh",
  12850             PersonalSection::Search,
  12851         );
  12852     }
  12853 
  12854     #[test]
  12855     fn runtime_app_farm_and_listing_writes_append_shared_local_work_records() {
  12856         let (runtime, paths) = bootstrapped_runtime("app_local_work_records");
  12857         assert!(
  12858             runtime
  12859                 .generate_local_account(Some("Farmer".to_owned()))
  12860                 .expect("account should generate")
  12861         );
  12862         let account_id = runtime
  12863             .summary()
  12864             .settings_account_projection
  12865             .selected_account
  12866             .as_ref()
  12867             .expect("selected account")
  12868             .account
  12869             .account_id
  12870             .clone();
  12871 
  12872         runtime
  12873             .save_farm_setup_draft(FarmSetupDraft::new(
  12874                 "Green Farm",
  12875                 "farmstand",
  12876                 [FarmOrderMethod::Pickup],
  12877             ))
  12878             .expect("farm setup draft should save");
  12879         runtime
  12880             .finish_farm_setup()
  12881             .expect("farm setup should finish");
  12882         assert!(
  12883             runtime
  12884                 .open_new_product_editor()
  12885                 .expect("product editor should open")
  12886         );
  12887         assert!(
  12888             runtime
  12889                 .save_product_editor_draft(ProductEditorDraft {
  12890                     title: "Eggs".to_owned(),
  12891                     subtitle: "Fresh eggs".to_owned(),
  12892                     category: "eggs".to_owned(),
  12893                     unit_label: "dozen".to_owned(),
  12894                     price_minor_units: Some(750),
  12895                     price_currency: "USD".to_owned(),
  12896                     stock_quantity: Some(12),
  12897                     availability_window_id: None,
  12898                     status: ProductStatus::Draft,
  12899                 })
  12900                 .expect("product draft should save")
  12901         );
  12902 
  12903         let records = shared_local_event_records(&paths);
  12904         let app_records = records
  12905             .iter()
  12906             .filter(|record| record.source_runtime == SourceRuntime::App)
  12907             .collect::<Vec<_>>();
  12908         assert_eq!(app_records.len(), 2);
  12909 
  12910         let farm_record = app_records
  12911             .iter()
  12912             .find(|record| {
  12913                 record
  12914                     .local_work_json
  12915                     .as_ref()
  12916                     .and_then(|payload| payload["record_kind"].as_str())
  12917                     == Some("farm_config_v1")
  12918             })
  12919             .expect("farm local work record");
  12920         assert_eq!(farm_record.family, LocalRecordFamily::LocalWork);
  12921         assert_eq!(farm_record.status, LocalRecordStatus::LocalSaved);
  12922         assert_eq!(farm_record.outbox_status, PublishOutboxStatus::None);
  12923         assert_eq!(
  12924             farm_record.owner_account_id.as_deref(),
  12925             Some(account_id.as_str())
  12926         );
  12927         let owner_pubkey = farm_record
  12928             .owner_pubkey
  12929             .as_deref()
  12930             .expect("farm owner pubkey");
  12931         assert!(is_hex_64(owner_pubkey));
  12932         assert!(
  12933             farm_record
  12934                 .farm_id
  12935                 .as_ref()
  12936                 .is_some_and(|value| value.len() == 22)
  12937         );
  12938         assert_eq!(farm_record.listing_addr, None);
  12939         let farm_payload = farm_record
  12940             .local_work_json
  12941             .as_ref()
  12942             .expect("farm local work payload");
  12943         assert_eq!(farm_payload["scope"], "app");
  12944         assert_eq!(farm_payload["exportability"]["state"], "exportable");
  12945         assert_eq!(farm_payload["document"]["farm"]["name"], "Green Farm");
  12946         assert_eq!(
  12947             farm_payload["document"]["listing_defaults"]["delivery_method"],
  12948             "pickup"
  12949         );
  12950         assert!(farm_payload.get("draft").is_none());
  12951         assert!(farm_payload.get("editor").is_none());
  12952 
  12953         let listing_record = app_records
  12954             .iter()
  12955             .find(|record| {
  12956                 record
  12957                     .local_work_json
  12958                     .as_ref()
  12959                     .and_then(|payload| payload["record_kind"].as_str())
  12960                     == Some("listing_draft_v1")
  12961             })
  12962             .expect("listing local work record");
  12963         assert_eq!(listing_record.family, LocalRecordFamily::LocalWork);
  12964         assert_eq!(listing_record.status, LocalRecordStatus::LocalSaved);
  12965         assert_eq!(listing_record.outbox_status, PublishOutboxStatus::None);
  12966         assert_eq!(
  12967             listing_record.owner_account_id.as_deref(),
  12968             Some(account_id.as_str())
  12969         );
  12970         assert_eq!(listing_record.owner_pubkey.as_deref(), Some(owner_pubkey));
  12971         assert_eq!(listing_record.farm_id, farm_record.farm_id);
  12972         let expected_listing_addr_prefix = format!("30402:{owner_pubkey}:");
  12973         assert!(
  12974             listing_record
  12975                 .listing_addr
  12976                 .as_deref()
  12977                 .expect("listing address")
  12978                 .starts_with(expected_listing_addr_prefix.as_str())
  12979         );
  12980         let listing_payload = listing_record
  12981             .local_work_json
  12982             .as_ref()
  12983             .expect("listing local work payload");
  12984         assert_eq!(listing_payload["exportability"]["state"], "exportable");
  12985         assert_eq!(listing_payload["publishability"]["state"], "blocked");
  12986         assert_eq!(listing_payload["document"]["kind"], "listing_draft_v1");
  12987         assert_eq!(
  12988             listing_payload["document"]["seller_actor"]["pubkey"],
  12989             owner_pubkey
  12990         );
  12991         assert_eq!(listing_payload["document"]["product"]["title"], "Eggs");
  12992         assert_eq!(listing_payload["document"]["product"]["category"], "eggs");
  12993         assert!(
  12994             listing_payload["document"]["primary_bin"]["bin_id"]
  12995                 .as_str()
  12996                 .is_some_and(|value| value.ends_with(":primary"))
  12997         );
  12998         assert_eq!(
  12999             listing_payload["document"]["primary_bin"]["price_amount"],
  13000             "7.50"
  13001         );
  13002         assert_eq!(listing_payload["document"]["inventory"]["available"], "12");
  13003         assert_eq!(listing_payload["document"]["delivery"]["method"], "pickup");
  13004         assert_eq!(
  13005             listing_payload["document"]["location"]["primary"],
  13006             "farmstand"
  13007         );
  13008         assert!(listing_payload.get("draft").is_none());
  13009         assert!(listing_payload.get("editor").is_none());
  13010 
  13011         cleanup_bootstrapped_runtime_paths(&paths);
  13012     }
  13013 
  13014     #[test]
  13015     fn runtime_published_receipts_record_payload_account_owner() {
  13016         let (runtime, paths) = bootstrapped_runtime("published_receipt_payload_owner");
  13017         assert!(
  13018             runtime
  13019                 .generate_local_account(Some("First".to_owned()))
  13020                 .expect("first account should generate")
  13021         );
  13022         let payload_account_id = runtime
  13023             .summary()
  13024             .settings_account_projection
  13025             .selected_account
  13026             .as_ref()
  13027             .expect("first selected account")
  13028             .account
  13029             .account_id
  13030             .clone();
  13031         assert!(
  13032             runtime
  13033                 .generate_local_account(Some("Second".to_owned()))
  13034                 .expect("second account should generate")
  13035         );
  13036         let selected_account_id = runtime
  13037             .summary()
  13038             .settings_account_projection
  13039             .selected_account
  13040             .as_ref()
  13041             .expect("second selected account")
  13042             .account
  13043             .account_id
  13044             .clone();
  13045         assert_ne!(payload_account_id, selected_account_id);
  13046 
  13047         let receipt = published_operation_receipt_fixture(
  13048             payload_account_id.clone(),
  13049             None,
  13050             "event-app-owner",
  13051         );
  13052         runtime
  13053             .lock_state()
  13054             .record_published_sync_receipts(&[receipt])
  13055             .expect("published receipt should record");
  13056 
  13057         let records = shared_local_event_records(&paths);
  13058         let signed_record = records
  13059             .iter()
  13060             .find(|record| record.record_id == "app:signed_event:event-app-owner")
  13061             .expect("signed event record");
  13062         assert_eq!(
  13063             signed_record.owner_account_id.as_deref(),
  13064             Some(payload_account_id.as_str())
  13065         );
  13066 
  13067         cleanup_bootstrapped_runtime_paths(&paths);
  13068     }
  13069 
  13070     #[test]
  13071     fn runtime_published_receipts_reject_conflicting_source_owner() {
  13072         let (runtime, paths) = bootstrapped_runtime("published_receipt_owner_conflict");
  13073         let database_path = paths
  13074             .shared_local_events_database_path()
  13075             .expect("shared local events path");
  13076         let executor =
  13077             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  13078         let store = LocalEventsStore::new(executor);
  13079         store.migrate_up().expect("migrate shared local events");
  13080         store
  13081             .append_record(&local_work_record(
  13082                 "app:local_work:conflict-source",
  13083                 "other-account",
  13084                 "farm-key",
  13085                 None,
  13086                 json!({"record_kind": "farm_config_v1"}),
  13087             ))
  13088             .expect("append conflicting source record");
  13089         let receipt = published_operation_receipt_fixture(
  13090             "payload-account".to_owned(),
  13091             Some("app:local_work:conflict-source".to_owned()),
  13092             "event-app-owner-conflict",
  13093         );
  13094 
  13095         let error = runtime
  13096             .lock_state()
  13097             .record_published_sync_receipts(&[receipt])
  13098             .expect_err("conflicting source owner should fail closed");
  13099 
  13100         assert!(matches!(
  13101             error,
  13102             AppSqliteError::InvalidProjection {
  13103                 reason: "published operation source account does not match local event owner"
  13104             }
  13105         ));
  13106         assert!(
  13107             shared_local_event_records(&paths)
  13108                 .iter()
  13109                 .all(|record| record.record_id != "app:signed_event:event-app-owner-conflict")
  13110         );
  13111 
  13112         cleanup_bootstrapped_runtime_paths(&paths);
  13113     }
  13114 
  13115     #[test]
  13116     fn runtime_app_local_work_without_resolved_pubkey_is_non_exportable() {
  13117         let (runtime, paths) = bootstrapped_runtime("app_local_work_unresolved_pubkey");
  13118         let farm_id = FarmId::new();
  13119         let account = SelectedAccountProjection::new(
  13120             AccountSummary {
  13121                 account_id: "acct_unresolved".to_owned(),
  13122                 npub: "npub1unresolved".to_owned(),
  13123                 label: Some("Unresolved".to_owned()),
  13124                 custody: AccountCustody::RemoteSigner,
  13125             },
  13126             SelectedSurfaceProjection::new(ActiveSurface::Farmer),
  13127             FarmerActivationProjection::active(farm_id),
  13128         );
  13129         let saved_farm = FarmSummary {
  13130             farm_id,
  13131             display_name: "Green Farm".to_owned(),
  13132             readiness: FarmReadiness::Ready,
  13133         };
  13134         let farm_projection = FarmSetupProjection::from_saved_farm(saved_farm.clone());
  13135 
  13136         {
  13137             let mut state = runtime.lock_state_mut();
  13138             let identity =
  13139                 AppIdentityProjection::ready(vec![account.account.clone()], account.clone());
  13140             let _ = state
  13141                 .state_store
  13142                 .apply_in_memory(AppStateCommand::replace_identity_projection(identity));
  13143             state
  13144                 .append_app_farm_local_work_record(&account, &farm_projection, &saved_farm)
  13145                 .expect("unresolved farm local work should append");
  13146             state
  13147                 .append_app_listing_local_work_record(
  13148                     ProductId::new(),
  13149                     &ProductEditorDraft {
  13150                         title: "Eggs".to_owned(),
  13151                         subtitle: "Fresh eggs".to_owned(),
  13152                         category: "eggs".to_owned(),
  13153                         unit_label: "dozen".to_owned(),
  13154                         price_minor_units: Some(750),
  13155                         price_currency: "USD".to_owned(),
  13156                         stock_quantity: Some(12),
  13157                         availability_window_id: None,
  13158                         status: ProductStatus::Draft,
  13159                     },
  13160                 )
  13161                 .expect("unresolved listing local work should append");
  13162         }
  13163 
  13164         let records = shared_local_event_records(&paths);
  13165         let app_records = records
  13166             .iter()
  13167             .filter(|record| record.source_runtime == SourceRuntime::App)
  13168             .collect::<Vec<_>>();
  13169         assert_eq!(app_records.len(), 2);
  13170         assert!(
  13171             app_records
  13172                 .iter()
  13173                 .all(|record| record.owner_account_id.as_deref() == Some("acct_unresolved"))
  13174         );
  13175         assert!(
  13176             app_records
  13177                 .iter()
  13178                 .all(|record| record.owner_pubkey.is_none())
  13179         );
  13180         assert!(
  13181             app_records
  13182                 .iter()
  13183                 .all(|record| record
  13184                     .local_work_json
  13185                     .as_ref()
  13186                     .is_some_and(|payload| payload["exportability"]["state"]
  13187                         == "identity_unresolved"
  13188                         && payload["exportability"]["reason"] == "canonical_hex_pubkey_required"))
  13189         );
  13190         let listing_record = app_records
  13191             .iter()
  13192             .find(|record| {
  13193                 record
  13194                     .local_work_json
  13195                     .as_ref()
  13196                     .and_then(|payload| payload["record_kind"].as_str())
  13197                     == Some("listing_draft_v1")
  13198             })
  13199             .expect("listing local work record");
  13200         assert_eq!(listing_record.listing_addr, None);
  13201         assert!(
  13202             listing_record
  13203                 .local_work_json
  13204                 .as_ref()
  13205                 .expect("listing payload")["document"]["seller_actor"]["pubkey"]
  13206                 .is_null()
  13207         );
  13208 
  13209         cleanup_bootstrapped_runtime_paths(&paths);
  13210     }
  13211 
  13212     #[test]
  13213     fn runtime_manual_refresh_marks_failed_checkpoint_when_transport_is_unavailable() {
  13214         let runtime = memory_runtime();
  13215         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  13216         runtime
  13217             .lock_state_mut()
  13218             .enqueue_selected_account_sync_operations(vec![pending_sync_upsert(
  13219                 SyncAggregateRef::Farm(farm_id),
  13220                 farm_sync_payload(
  13221                     farm_id,
  13222                     "North field farm",
  13223                     Some(FarmReadiness::Ready),
  13224                     "manual_refresh_unavailable_transport",
  13225                 ),
  13226             )])
  13227             .expect("pending farm sync should enqueue");
  13228 
  13229         assert!(
  13230             runtime
  13231                 .sync_on_manual_refresh()
  13232                 .expect("manual refresh should complete")
  13233         );
  13234 
  13235         let summary = runtime.summary();
  13236         let pending_operations = runtime
  13237             .lock_state()
  13238             .sqlite_store
  13239             .as_ref()
  13240             .expect("sqlite store")
  13241             .load_pending_sync_operations(account_id.as_str())
  13242             .expect("pending sync operations should load");
  13243 
  13244         assert_eq!(
  13245             summary.sync_status.projection.run_status,
  13246             AppSyncRunStatus::Failed
  13247         );
  13248         assert_eq!(
  13249             summary.sync_status.projection.checkpoint.state,
  13250             SyncCheckpointState::Failed
  13251         );
  13252         assert_eq!(summary.sync_status.pending_write_count, 1);
  13253         assert!(
  13254             summary
  13255                 .sync_status
  13256                 .projection
  13257                 .checkpoint
  13258                 .last_error_message
  13259                 .as_deref()
  13260                 .is_some_and(|message| { message.contains(SYNC_TRANSPORT_UNAVAILABLE_MESSAGE) })
  13261         );
  13262         assert_eq!(pending_operations.len(), 1);
  13263         assert_eq!(pending_operations[0].operation.attempt_count, 1);
  13264     }
  13265 
  13266     #[test]
  13267     fn runtime_sync_attempts_stop_when_blocking_conflicts_are_present() {
  13268         let runtime = memory_runtime();
  13269         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  13270         runtime
  13271             .lock_state_mut()
  13272             .enqueue_selected_account_sync_operations(vec![pending_sync_upsert(
  13273                 SyncAggregateRef::Farm(farm_id),
  13274                 farm_sync_payload(
  13275                     farm_id,
  13276                     "North field farm",
  13277                     Some(FarmReadiness::Ready),
  13278                     "blocking_conflict_stops_sync",
  13279                 ),
  13280             )])
  13281             .expect("pending farm sync should enqueue");
  13282 
  13283         runtime
  13284             .lock_state()
  13285             .sqlite_store
  13286             .as_ref()
  13287             .expect("sqlite store")
  13288             .record_sync_conflict(
  13289                 account_id.as_str(),
  13290                 &SyncConflict {
  13291                     aggregate: SyncAggregateRef::Farm(farm_id),
  13292                     kind: SyncConflictKind::RevisionMismatch,
  13293                     severity: SyncConflictSeverity::Blocking,
  13294                     resolution: SyncConflictResolutionStatus::Unresolved,
  13295                     local_payload_json: "{\"farm\":\"local\"}".to_owned(),
  13296                     remote_payload_json: Some("{\"farm\":\"remote\"}".to_owned()),
  13297                     detected_at: "2026-04-20T20:00:00Z".to_owned(),
  13298                     resolved_at: None,
  13299                 },
  13300             )
  13301             .expect("blocking conflict should save");
  13302         assert!(
  13303             runtime
  13304                 .lock_state_mut()
  13305                 .refresh_selected_account_sync()
  13306                 .expect("sync status should refresh")
  13307         );
  13308 
  13309         let recorded = install_recorded_sync_transport(
  13310             &runtime,
  13311             RecordedAppSyncTransport::succeed(AppSyncResult {
  13312                 run_status: AppSyncRunStatus::Succeeded,
  13313                 checkpoint: SyncCheckpointStatus::current(
  13314                     None,
  13315                     "2026-04-20T20:00:05Z",
  13316                     Some("cursor-blocked".to_owned()),
  13317                 ),
  13318                 pushed_operation_count: 1,
  13319                 pulled_record_count: 0,
  13320                 conflicts: Vec::new(),
  13321                 published_receipts: Vec::new(),
  13322             }),
  13323         );
  13324 
  13325         assert!(
  13326             !runtime
  13327                 .sync_on_app_launch()
  13328                 .expect("blocked launch sync should skip")
  13329         );
  13330 
  13331         let summary = runtime.summary();
  13332 
  13333         assert_eq!(recorded.lock().expect("recorded transport").call_count(), 0);
  13334         assert_eq!(summary.sync_status.pending_write_count, 1);
  13335         assert_eq!(
  13336             summary.sync_status.projection.run_status,
  13337             AppSyncRunStatus::Conflicted
  13338         );
  13339         assert_eq!(
  13340             summary
  13341                 .sync_status
  13342                 .projection
  13343                 .conflict_status
  13344                 .blocking_count,
  13345             1
  13346         );
  13347     }
  13348 
  13349     #[test]
  13350     fn runtime_resolving_a_blocking_conflict_refreshes_sync_summary() {
  13351         let runtime = memory_runtime();
  13352         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  13353 
  13354         let conflict_id = runtime
  13355             .lock_state()
  13356             .sqlite_store
  13357             .as_ref()
  13358             .expect("sqlite store")
  13359             .record_sync_conflict(
  13360                 account_id.as_str(),
  13361                 &SyncConflict {
  13362                     aggregate: SyncAggregateRef::Farm(farm_id),
  13363                     kind: SyncConflictKind::RevisionMismatch,
  13364                     severity: SyncConflictSeverity::Blocking,
  13365                     resolution: SyncConflictResolutionStatus::Unresolved,
  13366                     local_payload_json: "{\"farm\":\"local\"}".to_owned(),
  13367                     remote_payload_json: Some("{\"farm\":\"remote\"}".to_owned()),
  13368                     detected_at: "2026-04-20T20:05:00Z".to_owned(),
  13369                     resolved_at: None,
  13370                 },
  13371             )
  13372             .expect("blocking conflict should save");
  13373         assert!(
  13374             runtime
  13375                 .lock_state_mut()
  13376                 .refresh_selected_account_sync()
  13377                 .expect("sync status should refresh")
  13378         );
  13379 
  13380         assert!(
  13381             runtime
  13382                 .resolve_sync_conflict(
  13383                     conflict_id.as_str(),
  13384                     SyncConflictResolutionStatus::AcceptedLocal,
  13385                 )
  13386                 .expect("conflict resolution should succeed")
  13387         );
  13388 
  13389         let summary = runtime.summary();
  13390 
  13391         assert_eq!(
  13392             summary
  13393                 .sync_status
  13394                 .projection
  13395                 .conflict_status
  13396                 .unresolved_count,
  13397             0
  13398         );
  13399         assert_eq!(
  13400             summary
  13401                 .sync_status
  13402                 .projection
  13403                 .conflict_status
  13404                 .blocking_count,
  13405             0
  13406         );
  13407         assert_eq!(summary.sync_status.conflicts.len(), 1);
  13408         assert_eq!(
  13409             summary.sync_status.conflicts[0].conflict.resolution,
  13410             SyncConflictResolutionStatus::AcceptedLocal
  13411         );
  13412         assert!(
  13413             summary.sync_status.conflicts[0]
  13414                 .conflict
  13415                 .resolved_at
  13416                 .as_deref()
  13417                 .is_some()
  13418         );
  13419     }
  13420 
  13421     #[test]
  13422     fn runtime_review_required_conflicts_do_not_block_manual_refresh() {
  13423         let runtime = memory_runtime();
  13424         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  13425 
  13426         assert!(
  13427             runtime
  13428                 .open_new_product_editor()
  13429                 .expect("new product editor should open")
  13430         );
  13431 
  13432         runtime
  13433             .lock_state()
  13434             .sqlite_store
  13435             .as_ref()
  13436             .expect("sqlite store")
  13437             .record_sync_conflict(
  13438                 account_id.as_str(),
  13439                 &SyncConflict {
  13440                     aggregate: SyncAggregateRef::Farm(farm_id),
  13441                     kind: SyncConflictKind::RemoteValidationReject,
  13442                     severity: SyncConflictSeverity::ReviewRequired,
  13443                     resolution: SyncConflictResolutionStatus::Unresolved,
  13444                     local_payload_json: "{\"farm\":\"local\"}".to_owned(),
  13445                     remote_payload_json: Some("{\"farm\":\"remote\"}".to_owned()),
  13446                     detected_at: "2026-04-20T20:10:00Z".to_owned(),
  13447                     resolved_at: None,
  13448                 },
  13449             )
  13450             .expect("review-required conflict should save");
  13451         assert!(
  13452             runtime
  13453                 .lock_state_mut()
  13454                 .refresh_selected_account_sync()
  13455                 .expect("sync status should refresh")
  13456         );
  13457 
  13458         let recorded = install_recorded_sync_transport(
  13459             &runtime,
  13460             RecordedAppSyncTransport::succeed(AppSyncResult {
  13461                 run_status: AppSyncRunStatus::Succeeded,
  13462                 checkpoint: SyncCheckpointStatus::current(
  13463                     Some("2026-04-20T20:10:05Z".to_owned()),
  13464                     "2026-04-20T20:10:08Z",
  13465                     Some("cursor-review-required".to_owned()),
  13466                 ),
  13467                 pushed_operation_count: 1,
  13468                 pulled_record_count: 0,
  13469                 conflicts: Vec::new(),
  13470                 published_receipts: Vec::new(),
  13471             }),
  13472         );
  13473 
  13474         assert!(
  13475             runtime
  13476                 .sync_on_manual_refresh()
  13477                 .expect("manual refresh should succeed")
  13478         );
  13479 
  13480         let recorded = recorded.lock().expect("recorded transport");
  13481         let request = recorded
  13482             .last_request()
  13483             .cloned()
  13484             .expect("manual refresh request should record");
  13485 
  13486         assert_eq!(recorded.call_count(), 1);
  13487         assert_eq!(request.trigger, SyncTrigger::ManualRefresh);
  13488     }
  13489 
  13490     #[test]
  13491     fn runtime_summary_surfaces_runtime_metadata_from_bootstrap() {
  13492         let (runtime, paths) = bootstrapped_runtime("runtime_metadata");
  13493         let summary = runtime.summary();
  13494 
  13495         assert_eq!(
  13496             summary.runtime_metadata.snapshot.host.app_name,
  13497             radroots_app_core::APP_NAME
  13498         );
  13499         assert_eq!(
  13500             summary.runtime_metadata.data_root.as_ref(),
  13501             Some(&paths.app.data)
  13502         );
  13503         assert_eq!(
  13504             summary.runtime_metadata.logs_root.as_ref(),
  13505             Some(&paths.app.logs)
  13506         );
  13507         assert_eq!(
  13508             summary.runtime_metadata.database_path.as_ref(),
  13509             Some(&paths.app.data.join(APP_DATABASE_FILE_NAME))
  13510         );
  13511         assert_eq!(
  13512             summary.runtime_metadata.database_schema_version,
  13513             Some(latest_schema_version())
  13514         );
  13515 
  13516         cleanup_bootstrapped_runtime_paths(&paths);
  13517     }
  13518 
  13519     #[test]
  13520     fn runtime_bootstrap_starts_sdk_runtime_under_app_data_root() {
  13521         let paths = temp_desktop_runtime_paths("sdk_runtime");
  13522         let runtime = DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
  13523             paths.clone(),
  13524             vec!["ws://127.0.0.1:8080".to_owned()],
  13525             super::default_runtime_snapshot(),
  13526         );
  13527         let status = runtime
  13528             .wait_for_sdk_startup(StdDuration::from_secs(5))
  13529             .expect("sdk runtime should be present");
  13530 
  13531         assert_eq!(status.state, AppSdkLifecycleState::Ready);
  13532         assert_eq!(status.storage_root, paths.app.data.join("sdk"));
  13533         assert_eq!(
  13534             status
  13535                 .storage_paths
  13536                 .as_ref()
  13537                 .expect("sdk storage paths")
  13538                 .event_store_path,
  13539             paths.app.data.join("sdk").join("event_store.sqlite")
  13540         );
  13541         let diagnostics = runtime
  13542             .sdk_diagnostics()
  13543             .expect("sdk diagnostics should load")
  13544             .expect("sdk diagnostics should be present");
  13545         assert_eq!(diagnostics.runtime.state, AppSdkLifecycleState::Ready);
  13546         assert_eq!(diagnostics.storage.storage_kind, "directory");
  13547         assert_eq!(diagnostics.sync.relay_targets.configured_count, 1);
  13548         assert!(
  13549             runtime
  13550                 .shutdown_sdk_runtime()
  13551                 .expect("sdk runtime should shut down")
  13552         );
  13553 
  13554         cleanup_bootstrapped_runtime_paths(&paths);
  13555     }
  13556 
  13557     #[test]
  13558     fn runtime_summary_surfaces_lightweight_sdk_status() {
  13559         let paths = temp_desktop_runtime_paths("sdk_summary_status");
  13560         let runtime = DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
  13561             paths.clone(),
  13562             vec!["ws://127.0.0.1:8080".to_owned()],
  13563             super::default_runtime_snapshot(),
  13564         );
  13565         runtime
  13566             .wait_for_sdk_startup(StdDuration::from_secs(5))
  13567             .expect("sdk runtime should be present");
  13568 
  13569         let summary = runtime.summary();
  13570         let sdk_status = summary.sdk_status.expect("sdk status summary");
  13571 
  13572         assert_eq!(sdk_status.lifecycle_state, AppSdkLifecycleState::Ready);
  13573         assert_eq!(
  13574             sdk_status.projection_lifecycle_state,
  13575             AppSdkProjectionLifecycleState::Current
  13576         );
  13577         assert_eq!(sdk_status.storage_root, paths.app.data.join("sdk"));
  13578         assert_eq!(
  13579             sdk_status.event_store_path.as_ref(),
  13580             Some(&paths.app.data.join("sdk").join("event_store.sqlite"))
  13581         );
  13582         assert_eq!(sdk_status.relay_target_count, 1);
  13583         assert!(
  13584             runtime
  13585                 .shutdown_sdk_runtime()
  13586                 .expect("sdk runtime should shut down")
  13587         );
  13588 
  13589         cleanup_bootstrapped_runtime_paths(&paths);
  13590     }
  13591 
  13592     #[test]
  13593     fn runtime_sdk_diagnostics_summary_preserves_degraded_issue_metadata() {
  13594         let paths = temp_desktop_runtime_paths("sdk_summary_degraded");
  13595         let runtime = DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
  13596             paths.clone(),
  13597             vec!["ws://relay.example".to_owned()],
  13598             super::default_runtime_snapshot(),
  13599         );
  13600         let status = runtime
  13601             .wait_for_sdk_startup(StdDuration::from_secs(5))
  13602             .expect("sdk runtime should be present");
  13603         assert_eq!(status.state, AppSdkLifecycleState::Degraded);
  13604 
  13605         let diagnostics = runtime
  13606             .sdk_diagnostics_summary()
  13607             .expect("sdk diagnostics summary");
  13608 
  13609         assert_eq!(
  13610             diagnostics.status.lifecycle_state,
  13611             AppSdkLifecycleState::Degraded
  13612         );
  13613         match diagnostics.state {
  13614             DesktopAppSdkDiagnosticsState::Blocked(issue) => {
  13615                 assert_eq!(issue.code, "invalid_relay_url");
  13616                 assert_eq!(issue.class, "configuration");
  13617                 assert!(!issue.retryable);
  13618                 assert!(
  13619                     issue
  13620                         .recovery_actions
  13621                         .contains(&"configure_relay_targets".to_owned())
  13622                 );
  13623             }
  13624             unexpected => panic!("unexpected diagnostics state: {unexpected:?}"),
  13625         }
  13626         assert!(
  13627             runtime
  13628                 .shutdown_sdk_runtime()
  13629                 .expect("sdk runtime should shut down")
  13630         );
  13631 
  13632         cleanup_bootstrapped_runtime_paths(&paths);
  13633     }
  13634 
  13635     #[test]
  13636     fn runtime_sdk_diagnostics_summary_keeps_lifecycle_busy_visible() {
  13637         let paths = temp_desktop_runtime_paths("sdk_summary_busy");
  13638         let runtime = DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
  13639             paths.clone(),
  13640             vec!["ws://127.0.0.1:8080".to_owned()],
  13641             super::default_runtime_snapshot(),
  13642         );
  13643         runtime
  13644             .wait_for_sdk_startup(StdDuration::from_secs(5))
  13645             .expect("sdk runtime should be present");
  13646         {
  13647             let sdk_runtime = runtime.sdk_runtime.lock().expect("sdk runtime lock");
  13648             sdk_runtime
  13649                 .as_ref()
  13650                 .expect("sdk runtime")
  13651                 .begin_projection_rebuild()
  13652                 .expect("projection rebuild should begin");
  13653         }
  13654 
  13655         let diagnostics = runtime
  13656             .sdk_diagnostics_summary()
  13657             .expect("sdk diagnostics summary");
  13658 
  13659         assert_eq!(
  13660             diagnostics.status.lifecycle_state,
  13661             AppSdkLifecycleState::RebuildingProjections
  13662         );
  13663         assert_eq!(
  13664             diagnostics.status.projection_lifecycle_state,
  13665             AppSdkProjectionLifecycleState::Rebuilding
  13666         );
  13667         match diagnostics.state {
  13668             DesktopAppSdkDiagnosticsState::Blocked(issue) => {
  13669                 assert_eq!(issue.code, "sdk_lifecycle_busy");
  13670                 assert!(issue.retryable);
  13671                 assert!(
  13672                     issue
  13673                         .recovery_actions
  13674                         .contains(&"wait_for_sdk_lifecycle".to_owned())
  13675                 );
  13676             }
  13677             unexpected => panic!("unexpected diagnostics state: {unexpected:?}"),
  13678         }
  13679         {
  13680             let sdk_runtime = runtime.sdk_runtime.lock().expect("sdk runtime lock");
  13681             sdk_runtime
  13682                 .as_ref()
  13683                 .expect("sdk runtime")
  13684                 .complete_projection_rebuild()
  13685                 .expect("projection rebuild should complete");
  13686         }
  13687         assert!(
  13688             runtime
  13689                 .shutdown_sdk_runtime()
  13690                 .expect("sdk runtime should shut down")
  13691         );
  13692 
  13693         cleanup_bootstrapped_runtime_paths(&paths);
  13694     }
  13695 
  13696     #[test]
  13697     fn clearing_startup_pending_remote_signer_session_is_idempotent_without_record() {
  13698         let paths = temp_remote_signer_paths("clear_pending_none");
  13699         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  13700             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  13701                 .expect("in-memory state store should load"),
  13702             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  13703             shared_accounts_paths: None,
  13704             remote_signer_paths: Some(paths.clone()),
  13705             accounts_manager: None,
  13706             sqlite_store: Some(
  13707                 AppSqliteStore::open(DatabaseTarget::InMemory)
  13708                     .expect("in-memory sqlite store should open"),
  13709             ),
  13710             sdk_runtime: None,
  13711             sync_transport: default_sync_transport(),
  13712             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  13713             selected_account_pending_sync_write_count: 0,
  13714             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  13715             selected_account_sync_conflicts: Vec::new(),
  13716             startup_issue: None,
  13717         });
  13718 
  13719         assert!(
  13720             runtime
  13721                 .clear_startup_pending_remote_signer_session()
  13722                 .expect("clear pending should succeed"),
  13723             "missing pending startup session should count as a successful cleanup"
  13724         );
  13725 
  13726         cleanup_remote_signer_paths(&paths);
  13727     }
  13728 
  13729     #[test]
  13730     fn clean_startup_cleanup_allows_generate_key_phase_transition() {
  13731         let paths = temp_remote_signer_paths("generate_key_after_clean_cleanup");
  13732         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  13733             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  13734                 .expect("in-memory state store should load"),
  13735             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  13736             shared_accounts_paths: None,
  13737             remote_signer_paths: Some(paths.clone()),
  13738             accounts_manager: None,
  13739             sqlite_store: Some(
  13740                 AppSqliteStore::open(DatabaseTarget::InMemory)
  13741                     .expect("in-memory sqlite store should open"),
  13742             ),
  13743             sdk_runtime: None,
  13744             sync_transport: default_sync_transport(),
  13745             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  13746             selected_account_pending_sync_write_count: 0,
  13747             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  13748             selected_account_sync_conflicts: Vec::new(),
  13749             startup_issue: None,
  13750         });
  13751 
  13752         assert!(
  13753             runtime
  13754                 .clear_startup_pending_remote_signer_session()
  13755                 .expect("clear pending should succeed")
  13756         );
  13757         assert!(runtime.begin_generate_key_startup());
  13758         assert_eq!(
  13759             runtime.summary().logged_out_startup.phase,
  13760             radroots_app_view::LoggedOutStartupPhase::GenerateKeyStarting
  13761         );
  13762 
  13763         cleanup_remote_signer_paths(&paths);
  13764     }
  13765 
  13766     #[test]
  13767     fn pending_startup_signer_session_recovers_after_runtime_restart() {
  13768         let (runtime, paths) = bootstrapped_runtime("restart_pending_recovery");
  13769         let pending_session = fixture_pending_session();
  13770 
  13771         assert!(
  13772             runtime
  13773                 .store_startup_pending_remote_signer_session(&pending_session)
  13774                 .expect("store pending should succeed")
  13775         );
  13776 
  13777         let restarted = restart_runtime(paths.clone());
  13778         let restored = restarted
  13779             .load_startup_pending_remote_signer_session()
  13780             .expect("load pending should succeed")
  13781             .expect("pending session should recover after restart");
  13782 
  13783         assert_eq!(
  13784             restarted.summary().logged_out_startup.phase,
  13785             radroots_app_view::LoggedOutStartupPhase::SignerEntry
  13786         );
  13787         assert_eq!(
  13788             restored.record.client_account_id(),
  13789             pending_session.record.client_account_id()
  13790         );
  13791         assert_eq!(
  13792             restored.record.signer_identity.id,
  13793             pending_session.record.signer_identity.id
  13794         );
  13795 
  13796         cleanup_bootstrapped_runtime_paths(&paths);
  13797     }
  13798 
  13799     #[test]
  13800     fn clearing_pending_startup_signer_session_prevents_restart_recovery() {
  13801         let (runtime, paths) = bootstrapped_runtime("restart_after_explicit_cancel");
  13802         let pending_session = fixture_pending_session();
  13803 
  13804         assert!(
  13805             runtime
  13806                 .store_startup_pending_remote_signer_session(&pending_session)
  13807                 .expect("store pending should succeed")
  13808         );
  13809         assert!(
  13810             runtime
  13811                 .clear_startup_pending_remote_signer_session()
  13812                 .expect("clear pending should succeed")
  13813         );
  13814 
  13815         let restarted = restart_runtime(paths.clone());
  13816 
  13817         assert_eq!(
  13818             restarted.summary().logged_out_startup.phase,
  13819             radroots_app_view::LoggedOutStartupPhase::ContinuePrompt
  13820         );
  13821         assert!(
  13822             restarted
  13823                 .load_startup_pending_remote_signer_session()
  13824                 .expect("load pending should succeed")
  13825                 .is_none(),
  13826             "explicit cancel should leave no pending startup session to recover"
  13827         );
  13828 
  13829         cleanup_bootstrapped_runtime_paths(&paths);
  13830     }
  13831 
  13832     #[test]
  13833     fn startup_signer_entry_source_input_recovers_after_runtime_restart() {
  13834         let (runtime, paths) = bootstrapped_runtime("restart_startup_signer_entry");
  13835 
  13836         assert!(runtime.show_startup_identity_choice());
  13837         assert!(runtime.show_startup_signer_entry());
  13838         assert!(runtime.set_startup_signer_source_input(
  13839             "bunker://npub1signer?relay=wss%3A%2F%2Frelay.radroots.example"
  13840         ));
  13841 
  13842         let restarted = restart_runtime(paths.clone());
  13843         let summary = restarted.summary();
  13844 
  13845         assert_eq!(
  13846             summary.logged_out_startup.phase,
  13847             radroots_app_view::LoggedOutStartupPhase::SignerEntry
  13848         );
  13849         assert_eq!(
  13850             summary.logged_out_startup.signer_entry.source_input,
  13851             "bunker://npub1signer?relay=wss%3A%2F%2Frelay.radroots.example"
  13852         );
  13853 
  13854         cleanup_bootstrapped_runtime_paths(&paths);
  13855     }
  13856 
  13857     #[test]
  13858     fn generate_key_startup_phase_fails_closed_to_identity_choice_after_restart() {
  13859         let (runtime, paths) = bootstrapped_runtime("restart_generate_key_sanitize");
  13860 
  13861         assert!(runtime.show_startup_identity_choice());
  13862         assert!(runtime.begin_generate_key_startup());
  13863 
  13864         let restarted = restart_runtime(paths.clone());
  13865 
  13866         assert_eq!(
  13867             restarted.summary().logged_out_startup.phase,
  13868             radroots_app_view::LoggedOutStartupPhase::IdentityChoice
  13869         );
  13870 
  13871         cleanup_bootstrapped_runtime_paths(&paths);
  13872     }
  13873 
  13874     #[test]
  13875     fn buyer_search_query_and_detail_recover_after_runtime_restart() {
  13876         let (runtime, paths) = bootstrapped_runtime("restart_buyer_search_detail");
  13877         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  13878 
  13879         assert!(
  13880             runtime
  13881                 .select_active_surface(ActiveSurface::Personal)
  13882                 .expect("surface should switch into marketplace")
  13883         );
  13884         let fulfillment_window_id = seed_buyer_marketplace_support(
  13885             &runtime,
  13886             account_id.as_str(),
  13887             farm_id,
  13888             "North field farm",
  13889             "Friday pickup",
  13890         );
  13891         let product_id = seed_product(
  13892             &runtime,
  13893             farm_id,
  13894             "Salad mix",
  13895             "Spring blend",
  13896             "published",
  13897             Some(8),
  13898             "2026-04-20T09:00:00Z",
  13899         );
  13900         runtime
  13901             .lock_state()
  13902             .sqlite_store
  13903             .as_ref()
  13904             .expect("sqlite store")
  13905             .connection()
  13906             .execute_batch(&format!(
  13907                 "update products
  13908                  set availability_window_id = '{fulfillment_window_id}'
  13909                  where id = '{product_id}'"
  13910             ))
  13911             .expect("buyer detail product should attach a fulfillment window");
  13912 
  13913         assert!(
  13914             runtime
  13915                 .set_personal_search_query("salad")
  13916                 .expect("buyer search query should update")
  13917         );
  13918         assert!(
  13919             runtime
  13920                 .open_personal_product_detail(PersonalSection::Search, product_id)
  13921                 .expect("buyer search detail should open")
  13922         );
  13923 
  13924         let restarted = restart_runtime(paths.clone());
  13925         let summary = restarted.summary();
  13926 
  13927         assert_eq!(
  13928             summary.shell_projection.selected_section,
  13929             ShellSection::Personal(PersonalSection::Search)
  13930         );
  13931         assert_eq!(
  13932             summary.personal_projection.search.query.search_query,
  13933             "salad"
  13934         );
  13935         assert_eq!(
  13936             summary
  13937                 .personal_projection
  13938                 .search
  13939                 .detail
  13940                 .as_ref()
  13941                 .map(|detail| detail.listing.product_id),
  13942             Some(product_id)
  13943         );
  13944 
  13945         cleanup_bootstrapped_runtime_paths(&paths);
  13946     }
  13947 
  13948     #[test]
  13949     fn products_query_and_editor_recover_after_runtime_restart() {
  13950         let (runtime, paths) = bootstrapped_runtime("restart_products_editor");
  13951         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  13952         let product_id = seed_product(
  13953             &runtime,
  13954             farm_id,
  13955             "Pea shoots",
  13956             "Tray",
  13957             "draft",
  13958             Some(4),
  13959             "2026-04-20T09:30:00Z",
  13960         );
  13961 
  13962         assert!(
  13963             runtime
  13964                 .set_products_search_query("pea")
  13965                 .expect("products query should update")
  13966         );
  13967         assert!(
  13968             runtime
  13969                 .select_products_filter(ProductsFilter::Drafts)
  13970                 .expect("products filter should update")
  13971         );
  13972         assert!(
  13973             runtime
  13974                 .select_products_sort(ProductsSort::Name)
  13975                 .expect("products sort should update")
  13976         );
  13977         assert!(
  13978             runtime
  13979                 .open_existing_product_editor(product_id)
  13980                 .expect("product editor should open")
  13981         );
  13982 
  13983         let restarted = restart_runtime(paths.clone());
  13984         let summary = restarted.summary();
  13985 
  13986         assert_eq!(
  13987             summary.shell_projection.selected_section,
  13988             ShellSection::Farmer(FarmerSection::Products)
  13989         );
  13990         assert_eq!(summary.products_projection.query.search_query, "pea");
  13991         assert_eq!(
  13992             summary.products_projection.query.filter,
  13993             ProductsFilter::Drafts
  13994         );
  13995         assert_eq!(summary.products_projection.query.sort, ProductsSort::Name);
  13996         match &summary.products_projection.editor {
  13997             radroots_app_state::ProductEditorState::Open(session) => {
  13998                 assert_eq!(session.selected_product_id, Some(product_id));
  13999             }
  14000             radroots_app_state::ProductEditorState::Closed => {
  14001                 panic!("product editor should recover after restart")
  14002             }
  14003         }
  14004 
  14005         cleanup_bootstrapped_runtime_paths(&paths);
  14006     }
  14007 
  14008     #[test]
  14009     fn orders_query_and_detail_recover_after_runtime_restart() {
  14010         let (runtime, paths) = bootstrapped_runtime("restart_orders_detail");
  14011         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  14012         let (_, order_id) = seed_order_workspace(&runtime, farm_id);
  14013 
  14014         runtime
  14015             .lock_state()
  14016             .sqlite_store
  14017             .as_ref()
  14018             .expect("sqlite store")
  14019             .connection()
  14020             .execute_batch(&format!(
  14021                 "update orders
  14022                  set status = 'packed', updated_at = '2026-04-20T09:45:00Z'
  14023                  where id = '{order_id}' and farm_id = '{farm_id}'"
  14024             ))
  14025             .expect("order should update to packed");
  14026 
  14027         assert!(
  14028             runtime
  14029                 .select_orders_filter(OrdersFilter::Packed)
  14030                 .expect("orders filter should update")
  14031         );
  14032         assert!(
  14033             runtime
  14034                 .open_order_detail(order_id)
  14035                 .expect("order detail should open")
  14036         );
  14037 
  14038         let restarted = restart_runtime(paths.clone());
  14039         let summary = restarted.summary();
  14040 
  14041         assert_eq!(
  14042             summary.shell_projection.selected_section,
  14043             ShellSection::Farmer(FarmerSection::Orders)
  14044         );
  14045         assert_eq!(summary.orders_projection.query.filter, OrdersFilter::Packed);
  14046         assert_eq!(
  14047             summary
  14048                 .orders_projection
  14049                 .detail
  14050                 .as_ref()
  14051                 .map(|detail| detail.order_id),
  14052             Some(order_id)
  14053         );
  14054 
  14055         cleanup_bootstrapped_runtime_paths(&paths);
  14056     }
  14057 
  14058     #[test]
  14059     fn stale_orders_selection_clears_invalid_window_after_runtime_restart() {
  14060         let (runtime, paths) = bootstrapped_runtime("restart_stale_orders");
  14061         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  14062         let (fulfillment_window_id, _) = seed_order_workspace(&runtime, farm_id);
  14063 
  14064         assert!(
  14065             runtime
  14066                 .open_orders_fulfillment_window(fulfillment_window_id)
  14067                 .expect("orders window should open")
  14068         );
  14069         let mut persisted_state = runtime.lock_state().state_store.persisted_state().clone();
  14070         persisted_state.seller.orders_query.fulfillment_window_id =
  14071             Some(FulfillmentWindowId::new());
  14072         let mut repository =
  14073             FileBackedAppStateRepository::new(paths.app.data.join(APP_STATE_FILE_NAME));
  14074         repository
  14075             .save_persisted_state(&persisted_state)
  14076             .expect("stale orders selection should persist");
  14077 
  14078         let restarted = restart_runtime(paths.clone());
  14079         let summary = restarted.summary();
  14080 
  14081         assert_eq!(
  14082             summary.shell_projection.selected_section,
  14083             ShellSection::Farmer(FarmerSection::Orders)
  14084         );
  14085         assert_eq!(summary.orders_projection.query.fulfillment_window_id, None);
  14086         assert!(
  14087             summary
  14088                 .orders_projection
  14089                 .list
  14090                 .rows
  14091                 .iter()
  14092                 .any(|row| { row.fulfillment_window_id == Some(fulfillment_window_id) })
  14093         );
  14094 
  14095         cleanup_bootstrapped_runtime_paths(&paths);
  14096     }
  14097 
  14098     #[test]
  14099     fn stale_pack_day_selection_clears_invalid_window_after_runtime_restart() {
  14100         let (runtime, paths) = bootstrapped_runtime("restart_stale_pack_day");
  14101         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  14102         let (_, _) = seed_order_workspace(&runtime, farm_id);
  14103 
  14104         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  14105         let mut persisted_state = runtime.lock_state().state_store.persisted_state().clone();
  14106         let stale_fulfillment_window_id = FulfillmentWindowId::new();
  14107         persisted_state.seller.pack_day_query.fulfillment_window_id =
  14108             Some(stale_fulfillment_window_id);
  14109         let mut repository =
  14110             FileBackedAppStateRepository::new(paths.app.data.join(APP_STATE_FILE_NAME));
  14111         repository
  14112             .save_persisted_state(&persisted_state)
  14113             .expect("stale pack day selection should persist");
  14114 
  14115         let restarted = restart_runtime(paths.clone());
  14116         let summary = restarted.summary();
  14117 
  14118         assert_eq!(
  14119             summary.shell_projection.selected_section,
  14120             ShellSection::Farmer(FarmerSection::PackDay)
  14121         );
  14122         assert_eq!(
  14123             summary.pack_day_projection.query.fulfillment_window_id,
  14124             None
  14125         );
  14126         assert!(
  14127             summary
  14128                 .pack_day_projection
  14129                 .projection
  14130                 .fulfillment_window
  14131                 .is_some()
  14132         );
  14133         assert_ne!(
  14134             summary.pack_day_projection.query.fulfillment_window_id,
  14135             Some(stale_fulfillment_window_id)
  14136         );
  14137 
  14138         cleanup_bootstrapped_runtime_paths(&paths);
  14139     }
  14140 
  14141     #[test]
  14142     fn replacing_today_agenda_is_shared_without_clobbering_home_shell() {
  14143         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  14144             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  14145                 .expect("in-memory state store should load"),
  14146             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  14147             shared_accounts_paths: None,
  14148             remote_signer_paths: None,
  14149             accounts_manager: None,
  14150             sqlite_store: Some(
  14151                 AppSqliteStore::open(DatabaseTarget::InMemory)
  14152                     .expect("in-memory sqlite store should open"),
  14153             ),
  14154             sdk_runtime: None,
  14155             sync_transport: default_sync_transport(),
  14156             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  14157             selected_account_pending_sync_write_count: 0,
  14158             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  14159             selected_account_sync_conflicts: Vec::new(),
  14160             startup_issue: None,
  14161         });
  14162         let cloned_runtime = runtime.clone();
  14163         let today_agenda = TodayAgendaProjection {
  14164             farm: Some(FarmSummary {
  14165                 farm_id: radroots_app_view::FarmId::new(),
  14166                 display_name: "North field farm".to_owned(),
  14167                 readiness: FarmReadiness::Incomplete,
  14168             }),
  14169             summary: Some(TodaySummary {
  14170                 farm_id: radroots_app_view::FarmId::new(),
  14171                 orders_needing_action: 2,
  14172                 low_stock_products: 1,
  14173                 draft_products: 3,
  14174                 reminders_due_soon: 0,
  14175             }),
  14176             setup_checklist: vec![TodaySetupTask {
  14177                 kind: TodaySetupTaskKind::AddFulfillmentWindow,
  14178                 is_complete: false,
  14179             }],
  14180             ..TodayAgendaProjection::default()
  14181         };
  14182 
  14183         assert!(runtime.select_settings_section(SettingsSection::About));
  14184         assert!(cloned_runtime.replace_today_agenda(today_agenda.clone()));
  14185 
  14186         let summary = runtime.summary();
  14187 
  14188         assert_eq!(summary.today_projection.farm, today_agenda.farm);
  14189         assert_eq!(summary.today_projection.summary, today_agenda.summary);
  14190         assert_eq!(summary.today_projection.setup_checklist.len(), 6);
  14191         assert!(summary.today_projection.needs_setup());
  14192         assert_eq!(summary.home_route, HomeRoute::SetupRequired);
  14193         assert_eq!(
  14194             summary.shell_projection.active_surface,
  14195             radroots_app_view::ActiveSurface::Personal
  14196         );
  14197         assert_eq!(
  14198             summary.shell_projection.selected_section,
  14199             ShellSection::Home
  14200         );
  14201         assert_eq!(
  14202             summary.shell_projection.settings.selected_section,
  14203             SettingsSection::About
  14204         );
  14205         assert!(summary.today_projection.needs_setup());
  14206     }
  14207 
  14208     #[test]
  14209     fn degraded_runtime_surfaces_startup_issue_with_default_today_projection() {
  14210         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState::degraded(
  14211             super::DesktopAppRuntimeBootstrapError::State(AppStateStoreError::Repository(
  14212                 AppStateRepositoryError::load("state unavailable"),
  14213             )),
  14214         ));
  14215 
  14216         let summary = runtime.summary();
  14217 
  14218         assert_eq!(
  14219             summary.shell_projection.active_surface,
  14220             radroots_app_view::ActiveSurface::Personal
  14221         );
  14222         assert_eq!(
  14223             summary.shell_projection.selected_section,
  14224             ShellSection::Home
  14225         );
  14226         assert_eq!(
  14227             summary.shell_projection.settings.selected_section,
  14228             SettingsSection::Account
  14229         );
  14230         assert_eq!(summary.startup_gate, AppStartupGate::SetupRequired);
  14231         assert_eq!(
  14232             summary.logged_out_startup,
  14233             LoggedOutStartupProjection::default()
  14234         );
  14235         assert!(summary.settings_account_projection.roster.is_empty());
  14236         assert_eq!(summary.home_route, HomeRoute::SetupRequired);
  14237         assert_eq!(summary.today_projection, TodayAgendaProjection::default());
  14238         assert_eq!(
  14239             summary.startup_issue.as_deref(),
  14240             Some("app state repository load failed: state unavailable")
  14241         );
  14242     }
  14243 
  14244     #[test]
  14245     fn runtime_records_activity_context_for_user_visible_actions() {
  14246         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  14247             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  14248                 .expect("in-memory state store should load"),
  14249             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  14250             shared_accounts_paths: None,
  14251             remote_signer_paths: None,
  14252             accounts_manager: None,
  14253             sqlite_store: Some(
  14254                 AppSqliteStore::open(DatabaseTarget::InMemory)
  14255                     .expect("in-memory sqlite store should open"),
  14256             ),
  14257             sdk_runtime: None,
  14258             sync_transport: default_sync_transport(),
  14259             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  14260             selected_account_pending_sync_write_count: 0,
  14261             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  14262             selected_account_sync_conflicts: Vec::new(),
  14263             startup_issue: None,
  14264         });
  14265 
  14266         assert!(runtime.record_home_opened());
  14267         assert!(runtime.sync_settings_section(SettingsSection::About));
  14268         assert!(runtime.record_settings_opened(SettingsSection::About));
  14269         assert!(runtime.select_settings_section(SettingsSection::Settings));
  14270         assert!(runtime.set_settings_preference(SettingsPreference::LaunchAtLogin, true));
  14271 
  14272         let context = runtime
  14273             .activity_context(Some(8))
  14274             .expect("activity context should load");
  14275 
  14276         assert_eq!(context.recent_events.len(), 4);
  14277         assert_eq!(
  14278             context.recent_events[0].kind,
  14279             AppActivityKind::SettingsPreferenceUpdated {
  14280                 preference: SettingsPreference::LaunchAtLogin,
  14281                 enabled: true,
  14282             }
  14283         );
  14284         assert_eq!(
  14285             context.recent_events[1].kind,
  14286             AppActivityKind::SettingsSectionSelected {
  14287                 section: SettingsSection::Settings,
  14288             }
  14289         );
  14290         assert_eq!(
  14291             context.recent_events[2].kind,
  14292             AppActivityKind::SettingsOpened {
  14293                 section: SettingsSection::About,
  14294             }
  14295         );
  14296         assert_eq!(context.recent_events[3].kind, AppActivityKind::HomeOpened);
  14297     }
  14298 
  14299     #[test]
  14300     fn activity_context_distinguishes_empty_history_from_runtime_unavailable() {
  14301         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  14302             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  14303                 .expect("in-memory state store should load"),
  14304             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  14305             shared_accounts_paths: None,
  14306             remote_signer_paths: None,
  14307             accounts_manager: None,
  14308             sqlite_store: Some(
  14309                 AppSqliteStore::open(DatabaseTarget::InMemory)
  14310                     .expect("in-memory sqlite store should open"),
  14311             ),
  14312             sdk_runtime: None,
  14313             sync_transport: default_sync_transport(),
  14314             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  14315             selected_account_pending_sync_write_count: 0,
  14316             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  14317             selected_account_sync_conflicts: Vec::new(),
  14318             startup_issue: None,
  14319         });
  14320 
  14321         let empty_context = runtime
  14322             .activity_context(Some(8))
  14323             .expect("empty activity history should still load");
  14324         assert!(empty_context.recent_events.is_empty());
  14325 
  14326         let degraded = DesktopAppRuntime::from_state(DesktopAppRuntimeState::degraded(
  14327             super::DesktopAppRuntimeBootstrapError::State(AppStateStoreError::Repository(
  14328                 AppStateRepositoryError::load("state unavailable"),
  14329             )),
  14330         ));
  14331 
  14332         assert!(matches!(
  14333             degraded.activity_context(Some(8)),
  14334             Err(DesktopAppRuntimeActivityContextError::RuntimeUnavailable)
  14335         ));
  14336     }
  14337 
  14338     #[test]
  14339     fn activity_context_surfaces_store_load_failure() {
  14340         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  14341             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  14342                 .expect("in-memory state store should load"),
  14343             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  14344             shared_accounts_paths: None,
  14345             remote_signer_paths: None,
  14346             accounts_manager: None,
  14347             sqlite_store: Some(
  14348                 AppSqliteStore::open(DatabaseTarget::InMemory)
  14349                     .expect("in-memory sqlite store should open"),
  14350             ),
  14351             sdk_runtime: None,
  14352             sync_transport: default_sync_transport(),
  14353             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  14354             selected_account_pending_sync_write_count: 0,
  14355             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  14356             selected_account_sync_conflicts: Vec::new(),
  14357             startup_issue: None,
  14358         });
  14359 
  14360         runtime
  14361             .lock_state()
  14362             .sqlite_store
  14363             .as_ref()
  14364             .expect("sqlite store")
  14365             .connection()
  14366             .execute_batch("DROP TABLE activity_events")
  14367             .expect("activity table should drop");
  14368 
  14369         assert!(matches!(
  14370             runtime.activity_context(Some(8)),
  14371             Err(DesktopAppRuntimeActivityContextError::Sqlite(_))
  14372         ));
  14373     }
  14374 
  14375     #[test]
  14376     fn selecting_farmer_section_requires_farmer_identity_gate() {
  14377         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  14378             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  14379                 .expect("in-memory state store should load"),
  14380             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  14381             shared_accounts_paths: None,
  14382             remote_signer_paths: None,
  14383             accounts_manager: None,
  14384             sqlite_store: Some(
  14385                 AppSqliteStore::open(DatabaseTarget::InMemory)
  14386                     .expect("in-memory sqlite store should open"),
  14387             ),
  14388             sdk_runtime: None,
  14389             sync_transport: default_sync_transport(),
  14390             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  14391             selected_account_pending_sync_write_count: 0,
  14392             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  14393             selected_account_sync_conflicts: Vec::new(),
  14394             startup_issue: None,
  14395         });
  14396 
  14397         assert!(!runtime.select_farmer_section(FarmerSection::Products));
  14398         assert!(!runtime.select_farmer_section(FarmerSection::Orders));
  14399         assert!(!runtime.select_farmer_section(FarmerSection::PackDay));
  14400         assert_eq!(
  14401             runtime.summary().shell_projection.selected_section,
  14402             ShellSection::Home
  14403         );
  14404     }
  14405 
  14406     #[test]
  14407     fn pack_day_stays_blocked_without_a_window_context() {
  14408         let runtime = memory_runtime();
  14409         let _ = provision_ready_farmer_account(&runtime);
  14410 
  14411         assert!(!runtime.select_farmer_section(FarmerSection::PackDay));
  14412         assert!(
  14413             !runtime
  14414                 .open_pack_day(None)
  14415                 .expect("pack day route should stay blocked")
  14416         );
  14417         assert_eq!(
  14418             runtime.summary().shell_projection.selected_section,
  14419             ShellSection::Farmer(FarmerSection::Today)
  14420         );
  14421         assert!(
  14422             runtime
  14423                 .summary()
  14424                 .pack_day_projection
  14425                 .projection
  14426                 .fulfillment_window
  14427                 .is_none()
  14428         );
  14429     }
  14430 
  14431     #[test]
  14432     fn runtime_routes_between_farmer_home_and_products_through_explicit_methods() {
  14433         let runtime = memory_runtime();
  14434 
  14435         assert!(
  14436             runtime
  14437                 .generate_local_account(Some("Farmer".to_owned()))
  14438                 .expect("account should generate")
  14439         );
  14440         let account_id = runtime
  14441             .summary()
  14442             .settings_account_projection
  14443             .selected_account
  14444             .as_ref()
  14445             .expect("selected account")
  14446             .account
  14447             .account_id
  14448             .clone();
  14449         let farm_id =
  14450             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  14451         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  14452             farm_id,
  14453             display_name: "North field farm".to_owned(),
  14454             readiness: FarmReadiness::Ready,
  14455         });
  14456         runtime
  14457             .lock_state()
  14458             .sqlite_store
  14459             .as_ref()
  14460             .expect("sqlite store")
  14461             .save_farm_summary(
  14462                 farm_setup_projection
  14463                     .saved_farm
  14464                     .as_ref()
  14465                     .expect("saved farm should exist"),
  14466             )
  14467             .expect("farm summary should save");
  14468         runtime
  14469             .lock_state()
  14470             .sqlite_store
  14471             .as_ref()
  14472             .expect("sqlite store")
  14473             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  14474             .expect("farm setup should save");
  14475         assert!(
  14476             runtime
  14477                 .select_local_account(account_id.as_str())
  14478                 .expect("account should select")
  14479         );
  14480 
  14481         assert!(runtime.select_farmer_section(FarmerSection::Products));
  14482         assert_eq!(
  14483             runtime.summary().shell_projection.selected_section,
  14484             ShellSection::Farmer(FarmerSection::Products)
  14485         );
  14486 
  14487         assert!(runtime.select_home());
  14488         assert_eq!(
  14489             runtime.summary().shell_projection.selected_section,
  14490             ShellSection::Farmer(FarmerSection::Today)
  14491         );
  14492     }
  14493 
  14494     #[test]
  14495     fn guest_marketplace_entry_selects_personal_browse_without_an_account() {
  14496         let runtime = memory_runtime();
  14497 
  14498         assert!(
  14499             runtime
  14500                 .select_personal_section(PersonalSection::Browse)
  14501                 .expect("guest Browse selection should succeed")
  14502         );
  14503 
  14504         let summary = runtime.summary();
  14505         assert_eq!(summary.startup_gate, AppStartupGate::SetupRequired);
  14506         assert_eq!(
  14507             summary.shell_projection.selected_section,
  14508             ShellSection::Personal(PersonalSection::Browse)
  14509         );
  14510         assert_eq!(
  14511             summary.personal_projection.entry.state,
  14512             radroots_app_view::PersonalEntryState::Guest
  14513         );
  14514     }
  14515 
  14516     #[test]
  14517     fn runtime_personal_search_queries_refresh_repository_backed_marketplace_projection() {
  14518         let runtime = memory_runtime();
  14519         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  14520         let pickup_location_id = PickupLocationId::new();
  14521         let fulfillment_window_id = FulfillmentWindowId::new();
  14522         let sql = format!(
  14523             "insert into pickup_locations (
  14524                 id,
  14525                 farm_id,
  14526                 label,
  14527                 address_line,
  14528                 directions,
  14529                 is_default,
  14530                 created_at,
  14531                 updated_at
  14532              ) values (
  14533                 '{pickup_location_id}',
  14534                 '{farm_id}',
  14535                 'North barn',
  14536                 '14 County Road',
  14537                 null,
  14538                 1,
  14539                 '2026-04-20T08:00:00Z',
  14540                 '2026-04-20T08:00:00Z'
  14541              );
  14542              insert into fulfillment_windows (
  14543                 id,
  14544                 farm_id,
  14545                 starts_at,
  14546                 ends_at,
  14547                 capacity_limit,
  14548                 created_at,
  14549                 updated_at,
  14550                 pickup_location_id,
  14551                 label,
  14552                 order_cutoff_at
  14553              ) values (
  14554                 '{fulfillment_window_id}',
  14555                 '{farm_id}',
  14556                 '2099-04-18T16:00:00Z',
  14557                 '2099-04-18T18:00:00Z',
  14558                 null,
  14559                 '2099-04-18T16:00:00Z',
  14560                 '2099-04-18T16:00:00Z',
  14561                 '{pickup_location_id}',
  14562                 'Friday pickup',
  14563                 '2099-04-17T18:00:00Z'
  14564              );
  14565              update account_farm_setups
  14566              set
  14567                 pickup_enabled = 1,
  14568                 delivery_enabled = 0,
  14569                 shipping_enabled = 0,
  14570                 saved_farm_id = '{farm_id}',
  14571                 saved_farm_display_name = 'North field farm',
  14572                 saved_farm_readiness = 'ready',
  14573                 updated_at = '2026-04-20T08:00:00Z'
  14574              where account_id = '{account_id}';"
  14575         );
  14576         runtime
  14577             .lock_state()
  14578             .sqlite_store
  14579             .as_ref()
  14580             .expect("sqlite store")
  14581             .connection()
  14582             .execute_batch(&sql)
  14583             .expect("buyer search workspace should seed");
  14584         let salad_mix_id = seed_product(
  14585             &runtime,
  14586             farm_id,
  14587             "Salad mix",
  14588             "Spring blend",
  14589             "published",
  14590             Some(8),
  14591             "2026-04-20T09:00:00Z",
  14592         );
  14593         let pea_shoots_id = seed_product(
  14594             &runtime,
  14595             farm_id,
  14596             "Pea shoots",
  14597             "Tray-grown",
  14598             "published",
  14599             Some(4),
  14600             "2026-04-20T09:30:00Z",
  14601         );
  14602         runtime
  14603             .lock_state()
  14604             .sqlite_store
  14605             .as_ref()
  14606             .expect("sqlite store")
  14607             .connection()
  14608             .execute_batch(&format!(
  14609                 "update products
  14610                  set availability_window_id = '{fulfillment_window_id}'
  14611                  where id in ('{salad_mix_id}', '{pea_shoots_id}')"
  14612             ))
  14613             .expect("buyer-visible products should attach a fulfillment window");
  14614 
  14615         let _ = runtime
  14616             .select_local_account(account_id.as_str())
  14617             .expect("account should refresh after buyer workspace seeding");
  14618         let summary = runtime.summary();
  14619         assert_eq!(summary.personal_projection.search.listings.rows.len(), 2);
  14620         assert!(
  14621             summary
  14622                 .personal_projection
  14623                 .search
  14624                 .query
  14625                 .fulfillment_methods
  14626                 .is_empty()
  14627         );
  14628 
  14629         assert!(
  14630             runtime
  14631                 .set_personal_search_query("pea")
  14632                 .expect("buyer search query should apply")
  14633         );
  14634         let searched = runtime.summary();
  14635         assert_eq!(searched.personal_projection.search.listings.rows.len(), 1);
  14636         assert_eq!(
  14637             searched.personal_projection.search.listings.rows[0].title,
  14638             "Pea shoots"
  14639         );
  14640 
  14641         assert!(
  14642             runtime
  14643                 .set_personal_search_fulfillment_method(FarmOrderMethod::Pickup, true)
  14644                 .expect("buyer fulfillment filter should apply")
  14645         );
  14646         let filtered = runtime.summary();
  14647         assert_eq!(
  14648             filtered
  14649                 .personal_projection
  14650                 .search
  14651                 .query
  14652                 .fulfillment_methods,
  14653             BTreeSet::from([FarmOrderMethod::Pickup])
  14654         );
  14655         assert_eq!(filtered.personal_projection.search.listings.rows.len(), 1);
  14656         assert_eq!(
  14657             filtered.personal_projection.search.listings.rows[0]
  14658                 .next_fulfillment_window_label
  14659                 .as_deref(),
  14660             Some("Friday pickup")
  14661         );
  14662     }
  14663 
  14664     #[test]
  14665     fn runtime_personal_product_detail_adds_to_cart_and_routes_into_cart() {
  14666         let runtime = memory_runtime();
  14667         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  14668         assert!(
  14669             runtime
  14670                 .select_active_surface(ActiveSurface::Personal)
  14671                 .expect("surface should switch into marketplace")
  14672         );
  14673         let fulfillment_window_id = seed_buyer_marketplace_support(
  14674             &runtime,
  14675             account_id.as_str(),
  14676             farm_id,
  14677             "North field farm",
  14678             "Friday pickup",
  14679         );
  14680         let product_id = seed_product(
  14681             &runtime,
  14682             farm_id,
  14683             "Salad mix",
  14684             "Spring blend",
  14685             "published",
  14686             Some(8),
  14687             "2026-04-20T09:00:00Z",
  14688         );
  14689         runtime
  14690             .lock_state()
  14691             .sqlite_store
  14692             .as_ref()
  14693             .expect("sqlite store")
  14694             .connection()
  14695             .execute_batch(&format!(
  14696                 "update products
  14697                  set availability_window_id = '{fulfillment_window_id}'
  14698                  where id = '{product_id}'"
  14699             ))
  14700             .expect("buyer detail product should attach a fulfillment window");
  14701 
  14702         assert!(
  14703             runtime
  14704                 .open_personal_product_detail(PersonalSection::Browse, product_id)
  14705                 .expect("buyer detail should open")
  14706         );
  14707         assert!(runtime.increase_personal_product_quantity(PersonalSection::Browse));
  14708         assert!(
  14709             runtime
  14710                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  14711                 .expect("buyer product should add to cart")
  14712         );
  14713 
  14714         let summary = runtime.summary();
  14715         assert_eq!(
  14716             summary.shell_projection.selected_section,
  14717             ShellSection::Personal(PersonalSection::Cart)
  14718         );
  14719         assert_eq!(summary.personal_projection.cart.cart.lines.len(), 1);
  14720         assert_eq!(
  14721             summary.personal_projection.cart.cart.lines[0].title,
  14722             "Salad mix"
  14723         );
  14724         assert_eq!(summary.personal_projection.cart.cart.lines[0].quantity, 2);
  14725         assert_eq!(
  14726             summary.personal_projection.cart.cart.subtotal_minor_units,
  14727             Some(1200)
  14728         );
  14729         assert_eq!(
  14730             summary
  14731                 .personal_projection
  14732                 .cart
  14733                 .cart
  14734                 .farm_display_name
  14735                 .as_deref(),
  14736             Some("North field farm")
  14737         );
  14738         assert!(
  14739             summary
  14740                 .personal_projection
  14741                 .cart
  14742                 .cart
  14743                 .replace_confirmation
  14744                 .is_none()
  14745         );
  14746         assert_eq!(
  14747             summary
  14748                 .personal_projection
  14749                 .browse
  14750                 .detail
  14751                 .as_ref()
  14752                 .expect("buyer detail should persist on browse")
  14753                 .selected_quantity,
  14754             2
  14755         );
  14756     }
  14757 
  14758     #[test]
  14759     fn runtime_cross_farm_buyer_add_requires_replace_confirmation() {
  14760         let runtime = memory_runtime();
  14761         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  14762         assert!(
  14763             runtime
  14764                 .select_active_surface(ActiveSurface::Personal)
  14765                 .expect("surface should switch into marketplace")
  14766         );
  14767         let first_window_id = seed_buyer_marketplace_support(
  14768             &runtime,
  14769             account_id.as_str(),
  14770             farm_id,
  14771             "North field farm",
  14772             "Friday pickup",
  14773         );
  14774         let first_product_id = seed_product(
  14775             &runtime,
  14776             farm_id,
  14777             "Salad mix",
  14778             "Spring blend",
  14779             "published",
  14780             Some(8),
  14781             "2026-04-20T09:00:00Z",
  14782         );
  14783         runtime
  14784             .lock_state()
  14785             .sqlite_store
  14786             .as_ref()
  14787             .expect("sqlite store")
  14788             .connection()
  14789             .execute_batch(&format!(
  14790                 "update products
  14791                  set availability_window_id = '{first_window_id}'
  14792                  where id = '{first_product_id}'"
  14793             ))
  14794             .expect("first product should attach a fulfillment window");
  14795         assert!(
  14796             runtime
  14797                 .open_personal_product_detail(PersonalSection::Browse, first_product_id)
  14798                 .expect("first buyer detail should open")
  14799         );
  14800         assert!(
  14801             runtime
  14802                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  14803                 .expect("first buyer product should add to cart")
  14804         );
  14805 
  14806         let other_farm_id = FarmId::new();
  14807         runtime
  14808             .lock_state()
  14809             .sqlite_store
  14810             .as_ref()
  14811             .expect("sqlite store")
  14812             .save_farm_summary(&FarmSummary {
  14813                 farm_id: other_farm_id,
  14814                 display_name: "Willow Farm".to_owned(),
  14815                 readiness: FarmReadiness::Ready,
  14816             })
  14817             .expect("other farm summary should save");
  14818         let second_window_id = seed_buyer_marketplace_support(
  14819             &runtime,
  14820             "acct_other_farmer",
  14821             other_farm_id,
  14822             "Willow Farm",
  14823             "Saturday pickup",
  14824         );
  14825         let second_product_id = seed_product(
  14826             &runtime,
  14827             other_farm_id,
  14828             "Pea shoots",
  14829             "Tray-grown",
  14830             "published",
  14831             Some(5),
  14832             "2026-04-20T10:00:00Z",
  14833         );
  14834         runtime
  14835             .lock_state()
  14836             .sqlite_store
  14837             .as_ref()
  14838             .expect("sqlite store")
  14839             .connection()
  14840             .execute_batch(&format!(
  14841                 "update products
  14842                  set availability_window_id = '{second_window_id}'
  14843                  where id = '{second_product_id}'"
  14844             ))
  14845             .expect("second product should attach a fulfillment window");
  14846 
  14847         assert!(
  14848             runtime
  14849                 .open_personal_product_detail(PersonalSection::Browse, second_product_id)
  14850                 .expect("second buyer detail should open")
  14851         );
  14852         assert!(
  14853             runtime
  14854                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  14855                 .expect("cross-farm add should require confirmation")
  14856         );
  14857 
  14858         let confirmation_summary = runtime.summary();
  14859         assert_eq!(
  14860             confirmation_summary.shell_projection.selected_section,
  14861             ShellSection::Personal(PersonalSection::Browse)
  14862         );
  14863         assert_eq!(
  14864             confirmation_summary
  14865                 .personal_projection
  14866                 .cart
  14867                 .cart
  14868                 .lines
  14869                 .len(),
  14870             1
  14871         );
  14872         assert_eq!(
  14873             confirmation_summary.personal_projection.cart.cart.lines[0].title,
  14874             "Salad mix"
  14875         );
  14876         assert_eq!(
  14877             confirmation_summary
  14878                 .personal_projection
  14879                 .cart
  14880                 .cart
  14881                 .replace_confirmation
  14882                 .as_ref()
  14883                 .expect("replace confirmation should exist")
  14884                 .incoming_farm_display_name,
  14885             "Willow Farm"
  14886         );
  14887 
  14888         assert!(
  14889             runtime
  14890                 .add_personal_product_to_cart(PersonalSection::Browse, true)
  14891                 .expect("confirmed cross-farm add should replace the cart")
  14892         );
  14893         let replaced_summary = runtime.summary();
  14894         assert_eq!(
  14895             replaced_summary.shell_projection.selected_section,
  14896             ShellSection::Personal(PersonalSection::Cart)
  14897         );
  14898         assert_eq!(
  14899             replaced_summary.personal_projection.cart.cart.lines.len(),
  14900             1
  14901         );
  14902         assert_eq!(
  14903             replaced_summary.personal_projection.cart.cart.lines[0].title,
  14904             "Pea shoots"
  14905         );
  14906         assert_eq!(
  14907             replaced_summary
  14908                 .personal_projection
  14909                 .cart
  14910                 .cart
  14911                 .farm_display_name
  14912                 .as_deref(),
  14913             Some("Willow Farm")
  14914         );
  14915         assert!(
  14916             replaced_summary
  14917                 .personal_projection
  14918                 .cart
  14919                 .cart
  14920                 .replace_confirmation
  14921                 .is_none()
  14922         );
  14923     }
  14924 
  14925     #[test]
  14926     fn runtime_removing_buyer_cart_line_clears_cart_and_order_review_readiness() {
  14927         let runtime = memory_runtime();
  14928         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  14929         assert!(
  14930             runtime
  14931                 .select_active_surface(ActiveSurface::Personal)
  14932                 .expect("surface should switch into marketplace")
  14933         );
  14934         let fulfillment_window_id = seed_buyer_marketplace_support(
  14935             &runtime,
  14936             account_id.as_str(),
  14937             farm_id,
  14938             "North field farm",
  14939             "Friday pickup",
  14940         );
  14941         let product_id = seed_product(
  14942             &runtime,
  14943             farm_id,
  14944             "Salad mix",
  14945             "Spring blend",
  14946             "published",
  14947             Some(8),
  14948             "2026-04-20T09:00:00Z",
  14949         );
  14950         runtime
  14951             .lock_state()
  14952             .sqlite_store
  14953             .as_ref()
  14954             .expect("sqlite store")
  14955             .connection()
  14956             .execute_batch(&format!(
  14957                 "update products
  14958                  set availability_window_id = '{fulfillment_window_id}'
  14959                  where id = '{product_id}'"
  14960             ))
  14961             .expect("buyer detail product should attach a fulfillment window");
  14962         assert!(
  14963             runtime
  14964                 .open_personal_product_detail(PersonalSection::Browse, product_id)
  14965                 .expect("buyer detail should open")
  14966         );
  14967         assert!(
  14968             runtime
  14969                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  14970                 .expect("buyer product should add to cart")
  14971         );
  14972 
  14973         assert!(
  14974             runtime
  14975                 .remove_personal_cart_line(product_id)
  14976                 .expect("buyer cart line should remove")
  14977         );
  14978 
  14979         let summary = runtime.summary();
  14980         assert!(summary.personal_projection.cart.cart.lines.is_empty());
  14981         assert!(summary.personal_projection.cart.cart.farm_id.is_none());
  14982         assert!(
  14983             !summary
  14984                 .personal_projection
  14985                 .cart
  14986                 .order_review
  14987                 .can_place_order
  14988         );
  14989         assert_eq!(
  14990             summary
  14991                 .personal_projection
  14992                 .cart
  14993                 .order_review
  14994                 .summary
  14995                 .line_count,
  14996             0
  14997         );
  14998     }
  14999 
  15000     #[test]
  15001     fn runtime_places_buyer_order_and_routes_into_personal_orders() {
  15002         let runtime = memory_runtime();
  15003         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  15004         assert!(
  15005             runtime
  15006                 .select_active_surface(ActiveSurface::Personal)
  15007                 .expect("surface should switch into marketplace")
  15008         );
  15009         let fulfillment_window_id = seed_buyer_marketplace_support(
  15010             &runtime,
  15011             account_id.as_str(),
  15012             farm_id,
  15013             "North field farm",
  15014             "Friday pickup",
  15015         );
  15016         let product_id = seed_product(
  15017             &runtime,
  15018             farm_id,
  15019             "Salad mix",
  15020             "Spring blend",
  15021             "published",
  15022             Some(8),
  15023             "2026-04-20T09:00:00Z",
  15024         );
  15025         runtime
  15026             .lock_state()
  15027             .sqlite_store
  15028             .as_ref()
  15029             .expect("sqlite store")
  15030             .connection()
  15031             .execute_batch(&format!(
  15032                 "update products
  15033                  set availability_window_id = '{fulfillment_window_id}'
  15034                  where id = '{product_id}'"
  15035             ))
  15036             .expect("buyer detail product should attach a fulfillment window");
  15037         assert!(
  15038             runtime
  15039                 .open_personal_product_detail(PersonalSection::Browse, product_id)
  15040                 .expect("buyer detail should open")
  15041         );
  15042         assert!(
  15043             runtime
  15044                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  15045                 .expect("buyer product should add to cart")
  15046         );
  15047         assert!(
  15048             runtime
  15049                 .save_personal_order_review_draft(BuyerOrderReviewDraft {
  15050                     name: "Casey Buyer".to_owned(),
  15051                     email: "casey@example.com".to_owned(),
  15052                     phone: "555-0101".to_owned(),
  15053                     order_note: "Leave by the cooler".to_owned(),
  15054                 })
  15055                 .expect("buyer order review draft should save")
  15056         );
  15057         let order_review = runtime.summary().personal_projection.cart.order_review;
  15058         assert!(order_review.can_place_order);
  15059         assert_eq!(order_review.place_order_disabled_reason, None);
  15060         assert!(
  15061             runtime
  15062                 .place_personal_order()
  15063                 .expect("buyer order should place")
  15064         );
  15065 
  15066         let summary = runtime.summary();
  15067         assert_eq!(
  15068             summary.shell_projection.selected_section,
  15069             ShellSection::Personal(PersonalSection::Orders)
  15070         );
  15071         assert!(summary.personal_projection.cart.cart.lines.is_empty());
  15072         assert!(
  15073             !summary
  15074                 .personal_projection
  15075                 .cart
  15076                 .order_review
  15077                 .can_place_order
  15078         );
  15079         assert_eq!(
  15080             summary
  15081                 .personal_projection
  15082                 .cart
  15083                 .order_review
  15084                 .place_order_disabled_reason,
  15085             Some(BuyerOrderReviewDisabledReason::EmptyCart)
  15086         );
  15087         assert_eq!(summary.personal_projection.orders.list.rows.len(), 1);
  15088         assert_eq!(
  15089             summary.personal_projection.orders.list.rows[0].farm_display_name,
  15090             "North field farm"
  15091         );
  15092         assert_eq!(
  15093             summary.personal_projection.orders.list.rows[0]
  15094                 .status
  15095                 .storage_key(),
  15096             "placed"
  15097         );
  15098         assert_eq!(
  15099             summary
  15100                 .personal_projection
  15101                 .orders
  15102                 .detail
  15103                 .as_ref()
  15104                 .expect("buyer order detail should be selected")
  15105                 .order_id,
  15106             summary.personal_projection.orders.list.rows[0].order_id
  15107         );
  15108         assert_eq!(
  15109             summary
  15110                 .personal_projection
  15111                 .orders
  15112                 .detail
  15113                 .as_ref()
  15114                 .expect("buyer order detail")
  15115                 .order_note
  15116                 .as_deref(),
  15117             Some("Leave by the cooler")
  15118         );
  15119     }
  15120 
  15121     #[test]
  15122     fn runtime_guest_order_review_requires_account_before_order_write() {
  15123         let runtime = memory_runtime();
  15124         let farm_id = FarmId::new();
  15125         runtime
  15126             .lock_state()
  15127             .sqlite_store
  15128             .as_ref()
  15129             .expect("sqlite store")
  15130             .save_farm_summary(&FarmSummary {
  15131                 farm_id,
  15132                 display_name: "North field farm".to_owned(),
  15133                 readiness: FarmReadiness::Ready,
  15134             })
  15135             .expect("farm summary should save");
  15136         assert!(
  15137             runtime
  15138                 .select_active_surface(ActiveSurface::Personal)
  15139                 .expect("surface should switch into marketplace")
  15140         );
  15141         let fulfillment_window_id = seed_buyer_marketplace_support(
  15142             &runtime,
  15143             "acct_farmer",
  15144             farm_id,
  15145             "North field farm",
  15146             "Friday pickup",
  15147         );
  15148         let product_id = seed_product(
  15149             &runtime,
  15150             farm_id,
  15151             "Salad mix",
  15152             "Spring blend",
  15153             "published",
  15154             Some(8),
  15155             "2026-04-20T09:00:00Z",
  15156         );
  15157         runtime
  15158             .lock_state()
  15159             .sqlite_store
  15160             .as_ref()
  15161             .expect("sqlite store")
  15162             .connection()
  15163             .execute_batch(&format!(
  15164                 "update products
  15165                  set availability_window_id = '{fulfillment_window_id}'
  15166                  where id = '{product_id}'"
  15167             ))
  15168             .expect("buyer detail product should attach a fulfillment window");
  15169         assert!(
  15170             runtime
  15171                 .open_personal_product_detail(PersonalSection::Browse, product_id)
  15172                 .expect("buyer detail should open")
  15173         );
  15174         assert!(
  15175             runtime
  15176                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  15177                 .expect("buyer product should add to cart")
  15178         );
  15179         assert!(
  15180             runtime
  15181                 .save_personal_order_review_draft(BuyerOrderReviewDraft {
  15182                     name: "Casey Buyer".to_owned(),
  15183                     email: "casey@example.com".to_owned(),
  15184                     phone: "555-0101".to_owned(),
  15185                     order_note: "Leave by the cooler".to_owned(),
  15186                 })
  15187                 .expect("buyer order review draft should save")
  15188         );
  15189 
  15190         let ready_summary = runtime.summary();
  15191         assert!(
  15192             !ready_summary
  15193                 .personal_projection
  15194                 .cart
  15195                 .order_review
  15196                 .can_place_order
  15197         );
  15198         assert_eq!(
  15199             ready_summary
  15200                 .personal_projection
  15201                 .cart
  15202                 .order_review
  15203                 .place_order_disabled_reason,
  15204             Some(BuyerOrderReviewDisabledReason::AccountRequired)
  15205         );
  15206         assert_eq!(
  15207             ready_summary
  15208                 .personal_projection
  15209                 .cart
  15210                 .order_review
  15211                 .summary
  15212                 .line_count,
  15213             1
  15214         );
  15215 
  15216         let error = runtime
  15217             .place_personal_order()
  15218             .expect_err("guest order review should require an account");
  15219         assert!(matches!(error, AppSqliteError::InvalidProjection { .. }));
  15220 
  15221         let summary = runtime.summary();
  15222         assert_eq!(
  15223             summary.shell_projection.selected_section,
  15224             ShellSection::Personal(PersonalSection::Cart)
  15225         );
  15226         assert_eq!(summary.personal_projection.cart.cart.lines.len(), 1);
  15227         assert_eq!(summary.personal_projection.orders.list.rows.len(), 0);
  15228         let order_count: i64 = runtime
  15229             .lock_state()
  15230             .sqlite_store
  15231             .as_ref()
  15232             .expect("sqlite store")
  15233             .connection()
  15234             .query_row("select count(*) from orders", [], |row| row.get(0))
  15235             .expect("order count should load");
  15236         let coordination_count: i64 = runtime
  15237             .lock_state()
  15238             .sqlite_store
  15239             .as_ref()
  15240             .expect("sqlite store")
  15241             .connection()
  15242             .query_row(
  15243                 "select count(*) from buyer_order_coordination_records",
  15244                 [],
  15245                 |row| row.get(0),
  15246             )
  15247             .expect("coordination count should load");
  15248         assert_eq!(order_count, 0);
  15249         assert_eq!(coordination_count, 0);
  15250     }
  15251 
  15252     #[test]
  15253     fn runtime_prepares_seller_order_accept_payload_from_signed_request() {
  15254         let relay = ThreadedAckRelay::spawn();
  15255         let (runtime, paths, order_id, _product_id, seller_pubkey, buyer_pubkey) =
  15256             seller_order_decision_runtime("seller_order_accept_payload", 6, 2);
  15257         configure_runtime_relay_ingest(&runtime, &relay);
  15258 
  15259         let payload = runtime
  15260             .prepare_order_accept(order_id)
  15261             .expect("seller order accept payload should prepare");
  15262         let decision = order_decision_publish_payload_to_sdk_decision(&payload)
  15263             .expect("order accept payload should convert to SDK decision");
  15264 
  15265         assert_eq!(payload.app_order_id, order_id);
  15266         assert_eq!(payload.trade_order_id, "seller-order-decision-1");
  15267         assert_eq!(
  15268             payload.request_event_id,
  15269             shared_order_request_event_id(&paths, "seller-order-decision-1")
  15270         );
  15271         assert_eq!(
  15272             payload.listing_event_id,
  15273             Some(signed_listing_event_id("seller-order-decision"))
  15274         );
  15275         assert_eq!(payload.buyer_pubkey, buyer_pubkey);
  15276         assert_eq!(payload.seller_pubkey, seller_pubkey);
  15277         assert_eq!(decision.order_id, "seller-order-decision-1");
  15278         let RadrootsOrderDecisionOutcome::Accepted {
  15279             inventory_commitments,
  15280         } = decision.decision
  15281         else {
  15282             panic!("expected accepted decision");
  15283         };
  15284         assert_eq!(inventory_commitments.len(), 1);
  15285         assert_eq!(inventory_commitments[0].bin_id, "seller-order-primary-bin");
  15286         assert_eq!(inventory_commitments[0].bin_count, 2);
  15287 
  15288         cleanup_bootstrapped_runtime_paths(&paths);
  15289     }
  15290 
  15291     #[test]
  15292     fn runtime_prepares_seller_order_decline_payload_with_trimmed_reason() {
  15293         let relay = ThreadedAckRelay::spawn();
  15294         let (runtime, paths, order_id, _product_id, seller_pubkey, buyer_pubkey) =
  15295             seller_order_decision_runtime("seller_order_decline_payload", 6, 2);
  15296         configure_runtime_relay_ingest(&runtime, &relay);
  15297 
  15298         let payload = runtime
  15299             .prepare_order_decline(order_id, "  out of stock  ")
  15300             .expect("seller order decline payload should prepare");
  15301         let decision = order_decision_publish_payload_to_sdk_decision(&payload)
  15302             .expect("order decline payload should convert to SDK decision");
  15303 
  15304         assert_eq!(payload.buyer_pubkey, buyer_pubkey);
  15305         assert_eq!(payload.seller_pubkey, seller_pubkey);
  15306         assert_eq!(
  15307             payload.decision,
  15308             AppOrderDecisionPayload::Declined {
  15309                 reason: "out of stock".to_owned()
  15310             }
  15311         );
  15312         let RadrootsOrderDecisionOutcome::Declined { reason } = decision.decision else {
  15313             panic!("expected declined decision");
  15314         };
  15315         assert_eq!(reason, "out of stock");
  15316 
  15317         cleanup_bootstrapped_runtime_paths(&paths);
  15318     }
  15319 
  15320     #[test]
  15321     fn runtime_finds_seller_order_request_evidence_past_first_local_events_page() {
  15322         let relay = ThreadedAckRelay::spawn();
  15323         let (runtime, paths, order_id, _product_id, _seller_pubkey, _buyer_pubkey) =
  15324             seller_order_decision_runtime("seller_order_old_request_evidence", 6, 2);
  15325         configure_runtime_relay_ingest(&runtime, &relay);
  15326         append_unrelated_signed_event_records(&paths, 1_005);
  15327 
  15328         let payload = runtime
  15329             .prepare_order_accept(order_id)
  15330             .expect("seller order accept payload should prepare from older evidence");
  15331 
  15332         assert_eq!(payload.trade_order_id, "seller-order-decision-1");
  15333         assert_eq!(
  15334             payload.request_event_id,
  15335             shared_order_request_event_id(&paths, "seller-order-decision-1")
  15336         );
  15337 
  15338         cleanup_bootstrapped_runtime_paths(&paths);
  15339     }
  15340 
  15341     #[test]
  15342     fn runtime_rejects_seller_order_decision_with_unusable_request_evidence() {
  15343         let relay = ThreadedAckRelay::spawn();
  15344         let (runtime, paths, order_id, _product_id, _seller_pubkey, _buyer_pubkey) =
  15345             seller_order_decision_runtime("seller_order_unusable_request_evidence", 6, 2);
  15346         configure_runtime_relay_ingest(&runtime, &relay);
  15347         mark_shared_seller_order_request_evidence_pending(&paths);
  15348 
  15349         let error = runtime
  15350             .prepare_order_accept(order_id)
  15351             .expect_err("seller order decision should require usable request evidence");
  15352 
  15353         assert!(matches!(
  15354             error,
  15355             AppSqliteError::InvalidProjection {
  15356                 reason: "seller order decision requires signed order request evidence"
  15357             }
  15358         ));
  15359 
  15360         cleanup_bootstrapped_runtime_paths(&paths);
  15361     }
  15362 
  15363     #[test]
  15364     fn runtime_refreshes_configured_relay_before_seller_order_decision_signing() {
  15365         let relay = ThreadedAckRelay::spawn();
  15366         let (runtime, paths, order_id, product_id, seller_pubkey, buyer_pubkey) =
  15367             seller_order_decision_runtime("seller_order_relay_freshness_pre_sign", 6, 2);
  15368         configure_runtime_relay_ingest(&runtime, &relay);
  15369         publish_prior_relay_seller_order_accept(
  15370             &runtime,
  15371             &relay,
  15372             order_id,
  15373             product_id,
  15374             seller_pubkey.as_str(),
  15375             buyer_pubkey.as_str(),
  15376         );
  15377 
  15378         let error = runtime
  15379             .prepare_order_accept(order_id)
  15380             .expect_err("stale seller order decision should fail pre-signing");
  15381 
  15382         assert!(matches!(
  15383             error,
  15384             AppSqliteError::InvalidProjection {
  15385                 reason: "seller order decision requires an undecided order"
  15386             }
  15387         ));
  15388         assert_eq!(persisted_order_status(&runtime, order_id), "scheduled");
  15389         assert_eq!(relay.event_count(), 1);
  15390 
  15391         cleanup_bootstrapped_runtime_paths(&paths);
  15392     }
  15393 
  15394     #[test]
  15395     fn runtime_rejects_seller_order_decision_when_relay_freshness_fails() {
  15396         let (runtime, paths, order_id, _product_id, _seller_pubkey, _buyer_pubkey) =
  15397             seller_order_decision_runtime("seller_order_relay_freshness_failure", 6, 2);
  15398         runtime.lock_state_mut().nostr_relay_urls = vec!["ws://127.0.0.1:9".to_owned()];
  15399 
  15400         let error = runtime
  15401             .prepare_order_accept(order_id)
  15402             .expect_err("seller order decision should require fresh relay state");
  15403 
  15404         assert!(matches!(
  15405             error,
  15406             AppSqliteError::InvalidProjection {
  15407                 reason: "order lifecycle publish requires fresh configured relay state"
  15408             }
  15409         ));
  15410         assert_eq!(persisted_order_status(&runtime, order_id), "needs_action");
  15411 
  15412         cleanup_bootstrapped_runtime_paths(&paths);
  15413     }
  15414 
  15415     #[test]
  15416     fn runtime_rejects_seller_order_decision_for_wrong_selected_account() {
  15417         let relay = ThreadedAckRelay::spawn();
  15418         let (runtime, paths, order_id, _product_id, _seller_pubkey, _buyer_pubkey) =
  15419             seller_order_decision_runtime("seller_order_wrong_account", 6, 2);
  15420         configure_runtime_relay_ingest(&runtime, &relay);
  15421         assert!(
  15422             runtime
  15423                 .generate_local_account(Some("Other seller".to_owned()))
  15424                 .expect("other account should generate")
  15425         );
  15426         configure_runtime_relay_ingest(&runtime, &relay);
  15427 
  15428         let error = runtime
  15429             .prepare_order_accept(order_id)
  15430             .expect_err("wrong seller account should fail preflight");
  15431 
  15432         assert!(matches!(error, AppSqliteError::InvalidProjection { .. }));
  15433 
  15434         cleanup_bootstrapped_runtime_paths(&paths);
  15435     }
  15436 
  15437     #[test]
  15438     fn runtime_rejects_seller_order_accept_that_would_over_reserve_inventory() {
  15439         let relay = ThreadedAckRelay::spawn();
  15440         let (runtime, paths, order_id, _product_id, _seller_pubkey, _buyer_pubkey) =
  15441             seller_order_decision_runtime("seller_order_over_reserved", 1, 2);
  15442         configure_runtime_relay_ingest(&runtime, &relay);
  15443 
  15444         let error = runtime
  15445             .prepare_order_accept(order_id)
  15446             .expect_err("over-reserved seller order should fail preflight");
  15447 
  15448         assert!(matches!(error, AppSqliteError::InvalidProjection { .. }));
  15449 
  15450         cleanup_bootstrapped_runtime_paths(&paths);
  15451     }
  15452 
  15453     #[test]
  15454     fn runtime_enqueues_seller_order_accept_via_sdk() {
  15455         let relay = ThreadedAckRelay::spawn();
  15456         let (runtime, paths, order_id, _product_id, seller_pubkey, _buyer_pubkey) =
  15457             seller_order_decision_sdk_runtime("seller_order_accept_publish", 6, 2);
  15458         install_direct_relay_sync_transport(&runtime, &relay);
  15459 
  15460         assert!(
  15461             runtime
  15462                 .publish_order_accept(order_id)
  15463                 .expect("seller order accept should publish")
  15464         );
  15465 
  15466         assert_eq!(persisted_order_status(&runtime, order_id), "needs_action");
  15467         assert_eq!(relay.event_count(), 0);
  15468         assert!(!shared_local_event_records(&paths).iter().any(|record| {
  15469             record.family == LocalRecordFamily::SignedEvent
  15470                 && record.event_kind == Some(3423)
  15471                 && record.event_pubkey.as_deref() == Some(seller_pubkey.as_str())
  15472         }));
  15473         assert_order_decision_sdk_migration_receipt(
  15474             &runtime,
  15475             order_id,
  15476             AppSdkMigrationState::Enqueued,
  15477         );
  15478 
  15479         cleanup_bootstrapped_runtime_paths(&paths);
  15480     }
  15481 
  15482     #[test]
  15483     fn runtime_enqueues_seller_order_decline_via_sdk() {
  15484         let relay = ThreadedAckRelay::spawn();
  15485         let (runtime, paths, order_id, _product_id, seller_pubkey, _buyer_pubkey) =
  15486             seller_order_decision_sdk_runtime("seller_order_decline_publish", 6, 2);
  15487         install_direct_relay_sync_transport(&runtime, &relay);
  15488 
  15489         assert!(
  15490             runtime
  15491                 .publish_order_decline(order_id, "not available")
  15492                 .expect("seller order decline should publish")
  15493         );
  15494 
  15495         assert_eq!(persisted_order_status(&runtime, order_id), "needs_action");
  15496         assert_eq!(relay.event_count(), 0);
  15497         assert!(!shared_local_event_records(&paths).iter().any(|record| {
  15498             record.family == LocalRecordFamily::SignedEvent
  15499                 && record.event_kind == Some(3423)
  15500                 && record.event_pubkey.as_deref() == Some(seller_pubkey.as_str())
  15501         }));
  15502         assert_order_decision_sdk_migration_receipt(
  15503             &runtime,
  15504             order_id,
  15505             AppSdkMigrationState::Enqueued,
  15506         );
  15507 
  15508         cleanup_bootstrapped_runtime_paths(&paths);
  15509     }
  15510 
  15511     #[test]
  15512     fn runtime_rejects_seller_order_revision_with_reducer_invalid_parent_evidence() {
  15513         let relay = ThreadedAckRelay::spawn();
  15514         let (runtime, paths, order_id, product_id, seller_pubkey, buyer_pubkey) =
  15515             seller_order_decision_runtime("seller_order_revision_invalid_parent", 6, 2);
  15516         install_direct_relay_sync_transport(&runtime, &relay);
  15517         let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
  15518         let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
  15519         let request_event_id = shared_order_request_event_id(&paths, "seller-order-decision-1");
  15520         let request_event_id = request_event_id.as_str();
  15521         append_signed_order_decision_record(
  15522             &paths,
  15523             "seller-order-decision-1",
  15524             request_event_id,
  15525             listing_addr.as_str(),
  15526             buyer_pubkey.as_str(),
  15527             seller_pubkey.as_str(),
  15528             2,
  15529         );
  15530         append_signed_order_revision_proposal_record_with_prev(
  15531             &paths,
  15532             "seller-order-decision-1",
  15533             "seller-order-decision-1-stale-revision",
  15534             request_event_id,
  15535             request_event_id,
  15536             listing_addr.as_str(),
  15537             buyer_pubkey.as_str(),
  15538             seller_pubkey.as_str(),
  15539         );
  15540 
  15541         let error = runtime
  15542             .publish_order_revision_proposal(
  15543                 order_id,
  15544                 revision_test_order_items(),
  15545                 revision_test_order_economics(),
  15546                 "harvest count updated",
  15547             )
  15548             .expect_err("seller revision proposal should reject reducer-invalid parent evidence");
  15549 
  15550         assert_order_lifecycle_evidence_invalid(error);
  15551         assert_eq!(relay.event_count(), 0);
  15552         cleanup_bootstrapped_runtime_paths(&paths);
  15553     }
  15554 
  15555     #[test]
  15556     fn runtime_places_supported_buyer_order_into_shared_local_events() {
  15557         let (runtime, paths) = bootstrapped_runtime("buyer_order_local_event");
  15558         assert!(
  15559             runtime
  15560                 .generate_local_account(Some("Buyer".to_owned()))
  15561                 .expect("account should generate")
  15562         );
  15563         assert!(
  15564             runtime
  15565                 .select_active_surface(ActiveSurface::Personal)
  15566                 .expect("surface should switch into marketplace")
  15567         );
  15568         let buyer_account_id = runtime
  15569             .summary()
  15570             .settings_account_projection
  15571             .selected_account
  15572             .as_ref()
  15573             .expect("selected account")
  15574             .account
  15575             .account_id
  15576             .clone();
  15577         let listing_key = "DDDDDDDDDDDDDDDDDDDDDD";
  15578         append_cli_signed_buyer_listing_record_with_bin(
  15579             &paths,
  15580             "buyer-order-supported-listing",
  15581             listing_key,
  15582             "Buyer Visible Eggs",
  15583             1100,
  15584             "dozen-eggs",
  15585         );
  15586         let product_id =
  15587             deterministic_cli_listing_product_id(Some(BUYER_VISIBLE_SELLER_PUBKEY), listing_key);
  15588 
  15589         assert!(
  15590             runtime
  15591                 .open_personal_product_detail(PersonalSection::Browse, product_id)
  15592                 .expect("buyer detail should import before lookup")
  15593         );
  15594         assert!(runtime.increase_personal_product_quantity(PersonalSection::Browse));
  15595         assert!(
  15596             runtime
  15597                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  15598                 .expect("buyer product should add to cart")
  15599         );
  15600         runtime
  15601             .lock_state()
  15602             .sqlite_store
  15603             .as_ref()
  15604             .expect("sqlite store")
  15605             .connection()
  15606             .execute(
  15607                 "update products set listing_bin_id = 'mutated-bin' where id = ?1",
  15608                 [product_id.to_string()],
  15609             )
  15610             .expect("listing projection should mutate after cart snapshot");
  15611         assert!(
  15612             runtime
  15613                 .save_personal_order_review_draft(BuyerOrderReviewDraft {
  15614                     name: "Casey Buyer".to_owned(),
  15615                     email: "casey@example.com".to_owned(),
  15616                     phone: "555-0101".to_owned(),
  15617                     order_note: "Leave by the cooler".to_owned(),
  15618                 })
  15619                 .expect("buyer order review draft should save")
  15620         );
  15621         assert!(
  15622             runtime
  15623                 .place_personal_order()
  15624                 .expect("buyer order should place")
  15625         );
  15626         let order_id = runtime.summary().personal_projection.orders.list.rows[0].order_id;
  15627         assert_no_order_request_pending_sync_payloads(
  15628             &runtime,
  15629             buyer_account_id.as_str(),
  15630             order_id,
  15631         );
  15632         assert_order_request_sdk_migration_receipt(
  15633             &runtime,
  15634             order_id,
  15635             AppSdkMigrationState::Enqueued,
  15636         );
  15637 
  15638         {
  15639             let state = runtime.lock_state_mut();
  15640             let buyer_context = state.state_store.identity_projection().buyer_context();
  15641             let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store");
  15642             let order_export = state
  15643                 .sqlite_store
  15644                 .as_ref()
  15645                 .expect("sqlite store")
  15646                 .load_buyer_order_local_event_export(&buyer_context, order_id)
  15647                 .expect("order export should load")
  15648                 .expect("order export should exist");
  15649             let coordination = sqlite_store
  15650                 .load_buyer_order_coordination_record(&buyer_context, order_id)
  15651                 .expect("order coordination should load")
  15652                 .expect("order coordination should exist");
  15653             assert_eq!(coordination.state, BuyerOrderCoordinationState::Synced);
  15654             assert_eq!(
  15655                 coordination.record_id.as_deref(),
  15656                 Some(format!("app:local_work:order_request:{order_id}").as_str())
  15657             );
  15658             assert!(coordination.payload_json.is_some());
  15659             assert_eq!(coordination.attempt_count, 1);
  15660             assert_eq!(coordination.last_error_message, None);
  15661             assert!(
  15662                 state
  15663                     .append_app_buyer_order_request_local_work_record(
  15664                         sqlite_store,
  15665                         &buyer_context,
  15666                         &order_export,
  15667                     )
  15668                     .expect("order local event reappend should be idempotent")
  15669                     .is_some()
  15670             );
  15671             let coordination_after = sqlite_store
  15672                 .load_buyer_order_coordination_record(&buyer_context, order_id)
  15673                 .expect("order coordination should reload")
  15674                 .expect("order coordination should still exist");
  15675             assert_eq!(coordination_after.attempt_count, 1);
  15676         }
  15677 
  15678         let records = shared_local_event_records(&paths);
  15679         let order_records = records
  15680             .iter()
  15681             .filter(|record| {
  15682                 record.source_runtime == SourceRuntime::App
  15683                     && record
  15684                         .local_work_json
  15685                         .as_ref()
  15686                         .and_then(|payload| payload["record_kind"].as_str())
  15687                         == Some(BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND)
  15688             })
  15689             .collect::<Vec<_>>();
  15690         assert_eq!(order_records.len(), 1);
  15691         let order_record = order_records[0];
  15692         assert_eq!(order_record.family, LocalRecordFamily::LocalWork);
  15693         assert_eq!(order_record.status, LocalRecordStatus::LocalSaved);
  15694         assert_eq!(order_record.outbox_status, PublishOutboxStatus::None);
  15695         assert_eq!(
  15696             order_record.record_id,
  15697             format!("app:local_work:order_request:{order_id}")
  15698         );
  15699         assert_eq!(
  15700             order_record.owner_account_id.as_deref(),
  15701             Some(buyer_account_id.as_str())
  15702         );
  15703         assert!(order_record.owner_pubkey.as_deref().is_some_and(is_hex_64));
  15704         assert_eq!(
  15705             order_record.listing_addr.as_deref(),
  15706             Some(format!("30402:{BUYER_VISIBLE_SELLER_PUBKEY}:{listing_key}").as_str())
  15707         );
  15708         let payload = order_record
  15709             .local_work_json
  15710             .as_ref()
  15711             .expect("order local work payload");
  15712         assert_eq!(payload["support_status"]["state"], "supported");
  15713         assert_eq!(payload["currentness"]["current"], true);
  15714         assert_eq!(payload["document"]["kind"], "order_draft_v1");
  15715         assert_eq!(
  15716             payload["document"]["order"]["order_id"],
  15717             order_id.to_string()
  15718         );
  15719         assert_eq!(
  15720             payload["document"]["order"]["listing_event_id"],
  15721             signed_event_id("cli:signed_event:buyer-order-supported-listing")
  15722         );
  15723         assert_eq!(
  15724             payload["document"]["order"]["listing_relays"],
  15725             json!(["ws://127.0.0.1:1234/"])
  15726         );
  15727         assert_eq!(
  15728             payload["document"]["order"]["seller_pubkey"],
  15729             BUYER_VISIBLE_SELLER_PUBKEY
  15730         );
  15731         assert_eq!(
  15732             payload["document"]["order"]["items"][0]["bin_id"],
  15733             "dozen-eggs"
  15734         );
  15735         assert_eq!(payload["document"]["order"]["items"][0]["bin_count"], 2);
  15736         assert_eq!(
  15737             payload["document"]["order"]["economics"]["items"][0]["quantity_amount"],
  15738             "1"
  15739         );
  15740         assert_eq!(
  15741             payload["document"]["order"]["economics"]["items"][0]["bin_id"],
  15742             "dozen-eggs"
  15743         );
  15744         assert_eq!(
  15745             payload["document"]["order"]["economics"]["pricing_basis"],
  15746             "listing_event"
  15747         );
  15748         assert_eq!(
  15749             payload["document"]["order"]["economics"]["total"]["amount"],
  15750             "16.00"
  15751         );
  15752         assert_eq!(
  15753             payload["app_order"]["buyer_order_note"],
  15754             "Leave by the cooler"
  15755         );
  15756         assert_eq!(
  15757             payload["app_order"]["lines"][0]["listing_bin_id"],
  15758             "dozen-eggs"
  15759         );
  15760 
  15761         cleanup_bootstrapped_runtime_paths(&paths);
  15762     }
  15763 
  15764     #[test]
  15765     fn runtime_buyer_order_shared_append_failure_is_recoverable_in_same_session() {
  15766         let (runtime, paths, buyer_account_id, order_id) =
  15767             blocked_buyer_order_runtime("buyer_order_append_failure_same_session");
  15768         {
  15769             let state = runtime.lock_state_mut();
  15770             let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store");
  15771             sqlite_store
  15772                 .connection()
  15773                 .execute(
  15774                     "update orders set status = 'scheduled' where id = ?1",
  15775                     [order_id.to_string()],
  15776                 )
  15777                 .expect("buyer order status should mutate before retry refresh");
  15778         }
  15779         unblock_shared_local_events_database(&paths);
  15780         assert!(
  15781             runtime
  15782                 .retry_pending_personal_order_coordination()
  15783                 .expect("same-session buyer order coordination retry should sync")
  15784         );
  15785         let summary_after_retry = runtime.summary();
  15786         assert!(
  15787             !summary_after_retry
  15788                 .personal_projection
  15789                 .orders
  15790                 .has_recoverable_coordination
  15791         );
  15792         assert_no_order_request_pending_sync_payloads(
  15793             &runtime,
  15794             buyer_account_id.as_str(),
  15795             order_id,
  15796         );
  15797         assert_order_request_sdk_migration_receipt(
  15798             &runtime,
  15799             order_id,
  15800             AppSdkMigrationState::Enqueued,
  15801         );
  15802         assert_eq!(
  15803             summary_after_retry
  15804                 .personal_projection
  15805                 .orders
  15806                 .list
  15807                 .rows
  15808                 .len(),
  15809             1
  15810         );
  15811         assert_eq!(
  15812             summary_after_retry.personal_projection.orders.list.rows[0].order_id,
  15813             order_id
  15814         );
  15815         assert_eq!(
  15816             summary_after_retry.personal_projection.orders.list.rows[0].status,
  15817             BuyerOrderStatus::Scheduled
  15818         );
  15819         assert_eq!(
  15820             summary_after_retry
  15821                 .personal_projection
  15822                 .orders
  15823                 .detail
  15824                 .as_ref()
  15825                 .expect("buyer order detail should refresh after same-session retry")
  15826                 .status,
  15827             BuyerOrderStatus::Scheduled
  15828         );
  15829         assert_eq!(
  15830             buyer_order_local_work_record_ids(&paths),
  15831             vec![format!("app:local_work:order_request:{order_id}")]
  15832         );
  15833         {
  15834             let state = runtime.lock_state_mut();
  15835             let buyer_context = state.state_store.identity_projection().buyer_context();
  15836             let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store");
  15837             let buyer_orders = sqlite_store
  15838                 .load_buyer_orders(&buyer_context)
  15839                 .expect("buyer orders should reload");
  15840             assert_eq!(buyer_orders.rows.len(), 1);
  15841             let coordination = sqlite_store
  15842                 .load_buyer_order_coordination_record(&buyer_context, order_id)
  15843                 .expect("buyer order coordination should reload")
  15844                 .expect("buyer order coordination should still exist");
  15845             assert_eq!(coordination.state, BuyerOrderCoordinationState::Synced);
  15846             assert_eq!(coordination.attempt_count, 2);
  15847             assert_eq!(coordination.last_error_message, None);
  15848         }
  15849         assert!(
  15850             !runtime
  15851                 .retry_pending_personal_order_coordination()
  15852                 .expect("same-session synced buyer order coordination retry should be idempotent")
  15853         );
  15854         assert_no_order_request_pending_sync_payloads(
  15855             &runtime,
  15856             buyer_account_id.as_str(),
  15857             order_id,
  15858         );
  15859         assert_order_request_sdk_migration_receipt(
  15860             &runtime,
  15861             order_id,
  15862             AppSdkMigrationState::Enqueued,
  15863         );
  15864 
  15865         cleanup_bootstrapped_runtime_paths(&paths);
  15866     }
  15867 
  15868     #[test]
  15869     fn runtime_buyer_order_shared_append_failure_is_recoverable_after_restart() {
  15870         let (runtime, paths, buyer_account_id, order_id) =
  15871             blocked_buyer_order_runtime("buyer_order_append_failure_restart");
  15872         unblock_shared_local_events_database(&paths);
  15873         drop(runtime);
  15874 
  15875         let restarted_runtime = restart_runtime(paths.clone());
  15876         assert_eq!(
  15877             buyer_order_local_work_record_ids(&paths),
  15878             vec![format!("app:local_work:order_request:{order_id}")]
  15879         );
  15880         let summary = restarted_runtime.summary();
  15881         assert!(
  15882             !summary
  15883                 .personal_projection
  15884                 .orders
  15885                 .has_recoverable_coordination
  15886         );
  15887         assert_eq!(summary.personal_projection.orders.list.rows.len(), 1);
  15888         assert_eq!(
  15889             summary.personal_projection.orders.list.rows[0].order_id,
  15890             order_id
  15891         );
  15892         assert_eq!(
  15893             summary
  15894                 .personal_projection
  15895                 .orders
  15896                 .detail
  15897                 .as_ref()
  15898                 .expect("buyer order detail should reload after restart")
  15899                 .order_id,
  15900             order_id
  15901         );
  15902         {
  15903             let state = restarted_runtime.lock_state_mut();
  15904             let buyer_context = state.state_store.identity_projection().buyer_context();
  15905             let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store");
  15906             let buyer_orders = sqlite_store
  15907                 .load_buyer_orders(&buyer_context)
  15908                 .expect("buyer orders should reload");
  15909             assert_eq!(buyer_orders.rows.len(), 1);
  15910             let coordination = sqlite_store
  15911                 .load_buyer_order_coordination_record(&buyer_context, order_id)
  15912                 .expect("buyer order coordination should reload")
  15913                 .expect("buyer order coordination should still exist");
  15914             assert_eq!(coordination.state, BuyerOrderCoordinationState::Synced);
  15915             assert_eq!(coordination.attempt_count, 2);
  15916             assert_eq!(coordination.last_error_message, None);
  15917         }
  15918         assert!(
  15919             !restarted_runtime
  15920                 .retry_pending_personal_order_coordination()
  15921                 .expect("synced buyer order coordination retry should be idempotent")
  15922         );
  15923         assert_no_order_request_pending_sync_payloads(
  15924             &restarted_runtime,
  15925             buyer_account_id.as_str(),
  15926             order_id,
  15927         );
  15928         assert_order_request_sdk_migration_receipt(
  15929             &restarted_runtime,
  15930             order_id,
  15931             AppSdkMigrationState::Enqueued,
  15932         );
  15933 
  15934         cleanup_bootstrapped_runtime_paths(&paths);
  15935     }
  15936 
  15937     #[test]
  15938     fn runtime_outbox_recovery_buyer_order_shared_append_failure_is_recoverable_on_foreground_resume()
  15939      {
  15940         let (runtime, paths, buyer_account_id, order_id) =
  15941             blocked_buyer_order_runtime("buyer_order_append_failure_foreground_resume");
  15942         unblock_shared_local_events_database(&paths);
  15943         assert!(
  15944             runtime
  15945                 .sync_on_foreground_resume()
  15946                 .expect("foreground resume should repair buyer order coordination")
  15947         );
  15948         let summary = runtime.summary();
  15949         assert!(
  15950             !summary
  15951                 .personal_projection
  15952                 .orders
  15953                 .has_recoverable_coordination
  15954         );
  15955         assert_eq!(
  15956             buyer_order_local_work_record_ids(&paths),
  15957             vec![format!("app:local_work:order_request:{order_id}")]
  15958         );
  15959         assert_no_order_request_pending_sync_payloads(
  15960             &runtime,
  15961             buyer_account_id.as_str(),
  15962             order_id,
  15963         );
  15964         assert_order_request_sdk_migration_receipt(
  15965             &runtime,
  15966             order_id,
  15967             AppSdkMigrationState::Enqueued,
  15968         );
  15969         {
  15970             let state = runtime.lock_state_mut();
  15971             let buyer_context = state.state_store.identity_projection().buyer_context();
  15972             let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store");
  15973             let coordination = sqlite_store
  15974                 .load_buyer_order_coordination_record(&buyer_context, order_id)
  15975                 .expect("buyer order coordination should reload")
  15976                 .expect("buyer order coordination should still exist");
  15977             assert_eq!(coordination.state, BuyerOrderCoordinationState::Synced);
  15978             assert_eq!(coordination.attempt_count, 2);
  15979             assert_eq!(coordination.last_error_message, None);
  15980         }
  15981         assert!(
  15982             !runtime
  15983                 .retry_pending_personal_order_coordination()
  15984                 .expect("foreground-resumed buyer order retry should be idempotent")
  15985         );
  15986 
  15987         cleanup_bootstrapped_runtime_paths(&paths);
  15988     }
  15989 
  15990     #[test]
  15991     fn runtime_opens_buyer_order_detail_from_personal_orders() {
  15992         let runtime = memory_runtime();
  15993         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  15994         assert!(
  15995             runtime
  15996                 .select_active_surface(ActiveSurface::Personal)
  15997                 .expect("surface should switch into marketplace")
  15998         );
  15999         let fulfillment_window_id = seed_buyer_marketplace_support(
  16000             &runtime,
  16001             account_id.as_str(),
  16002             farm_id,
  16003             "North field farm",
  16004             "Friday pickup",
  16005         );
  16006         let product_id = seed_product(
  16007             &runtime,
  16008             farm_id,
  16009             "Salad mix",
  16010             "Spring blend",
  16011             "published",
  16012             Some(8),
  16013             "2026-04-20T09:00:00Z",
  16014         );
  16015         runtime
  16016             .lock_state()
  16017             .sqlite_store
  16018             .as_ref()
  16019             .expect("sqlite store")
  16020             .connection()
  16021             .execute_batch(&format!(
  16022                 "update products
  16023                  set availability_window_id = '{fulfillment_window_id}'
  16024                  where id = '{product_id}'"
  16025             ))
  16026             .expect("buyer detail product should attach a fulfillment window");
  16027         assert!(
  16028             runtime
  16029                 .open_personal_product_detail(PersonalSection::Browse, product_id)
  16030                 .expect("buyer detail should open")
  16031         );
  16032         assert!(
  16033             runtime
  16034                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  16035                 .expect("buyer product should add to cart")
  16036         );
  16037         assert!(
  16038             runtime
  16039                 .save_personal_order_review_draft(BuyerOrderReviewDraft {
  16040                     name: "Casey Buyer".to_owned(),
  16041                     email: "casey@example.com".to_owned(),
  16042                     phone: String::new(),
  16043                     order_note: String::new(),
  16044                 })
  16045                 .expect("buyer order review draft should save")
  16046         );
  16047         assert!(
  16048             runtime
  16049                 .place_personal_order()
  16050                 .expect("buyer order should place")
  16051         );
  16052         let order_id = runtime.summary().personal_projection.orders.list.rows[0].order_id;
  16053         assert!(
  16054             runtime
  16055                 .select_personal_section(PersonalSection::Browse)
  16056                 .expect("buyer Browse selection should succeed")
  16057         );
  16058         assert!(runtime.lock_state_mut().set_personal_order_detail(None));
  16059 
  16060         assert!(
  16061             runtime
  16062                 .open_personal_order_detail(order_id)
  16063                 .expect("buyer order detail should open")
  16064         );
  16065 
  16066         let summary = runtime.summary();
  16067         assert_eq!(
  16068             summary.shell_projection.selected_section,
  16069             ShellSection::Personal(PersonalSection::Orders)
  16070         );
  16071         assert_eq!(
  16072             summary
  16073                 .personal_projection
  16074                 .orders
  16075                 .detail
  16076                 .as_ref()
  16077                 .expect("buyer order detail")
  16078                 .order_id,
  16079             order_id
  16080         );
  16081     }
  16082 
  16083     #[test]
  16084     fn runtime_opens_linked_buyer_order_detail_from_selected_account_nostr_scope() {
  16085         let fixture = linked_buyer_lifecycle_runtime("linked_buyer_order_open");
  16086         let report = fixture
  16087             .runtime
  16088             .refresh_shared_local_events()
  16089             .expect("linked buyer local events should import");
  16090         assert!(report.imported_records > 0);
  16091 
  16092         assert!(
  16093             fixture
  16094                 .runtime
  16095                 .open_personal_order_detail(fixture.order_id)
  16096                 .expect("linked buyer order detail should open")
  16097         );
  16098 
  16099         let summary = fixture.runtime.summary();
  16100         let row = summary
  16101             .personal_projection
  16102             .orders
  16103             .list
  16104             .rows
  16105             .iter()
  16106             .find(|row| row.order_id == fixture.order_id)
  16107             .expect("linked buyer order row should exist");
  16108         let detail = summary
  16109             .personal_projection
  16110             .orders
  16111             .detail
  16112             .as_ref()
  16113             .expect("linked buyer order detail should exist");
  16114         assert_eq!(row.status, BuyerOrderStatus::Scheduled);
  16115         assert_eq!(detail.order_id, fixture.order_id);
  16116         assert_eq!(detail.status, BuyerOrderStatus::Scheduled);
  16117         assert_eq!(
  16118             detail.workflow.provenance.last_event_id.as_deref(),
  16119             Some(fixture.decision_event_id.as_str())
  16120         );
  16121 
  16122         cleanup_bootstrapped_runtime_paths(&fixture.paths);
  16123     }
  16124 
  16125     #[test]
  16126     fn runtime_publishes_linked_buyer_cancellation_from_selected_account_nostr_scope() {
  16127         let relay = ThreadedAckRelay::spawn();
  16128         let fixture = linked_buyer_request_runtime("linked_buyer_order_cancel");
  16129         install_direct_relay_sync_transport(&fixture.runtime, &relay);
  16130         fixture
  16131             .runtime
  16132             .refresh_shared_local_events()
  16133             .expect("linked buyer local events should import");
  16134         assert!(
  16135             fixture
  16136                 .runtime
  16137                 .open_personal_order_detail(fixture.order_id)
  16138                 .expect("linked buyer order detail should open")
  16139         );
  16140 
  16141         assert!(
  16142             fixture
  16143                 .runtime
  16144                 .publish_buyer_order_cancel(fixture.order_id)
  16145                 .expect("linked buyer cancellation should publish")
  16146         );
  16147 
  16148         assert_eq!(
  16149             persisted_order_status(&fixture.runtime, fixture.order_id),
  16150             "needs_action"
  16151         );
  16152         assert_eq!(relay.event_count(), 0);
  16153         let cancellation_events =
  16154             shared_order_events_by_kind(&fixture.paths, 3432, fixture.buyer_pubkey.as_str());
  16155         assert!(cancellation_events.is_empty());
  16156         assert_order_cancellation_sdk_migration_receipt(
  16157             &fixture.runtime,
  16158             fixture.order_id,
  16159             AppSdkMigrationState::Enqueued,
  16160         );
  16161 
  16162         cleanup_bootstrapped_runtime_paths(&fixture.paths);
  16163     }
  16164 
  16165     #[test]
  16166     fn runtime_rejects_linked_buyer_cancellation_from_pending_revision_proposal() {
  16167         let relay = ThreadedAckRelay::spawn();
  16168         let fixture = linked_buyer_request_runtime("linked_buyer_order_cancel_revision");
  16169         let proposal_key = "linked-buyer-order-cancel-revision-proposal";
  16170         append_signed_order_revision_proposal_record_with_prev(
  16171             &fixture.paths,
  16172             fixture.trade_order_id.as_str(),
  16173             proposal_key,
  16174             fixture.request_event_id.as_str(),
  16175             fixture.request_event_id.as_str(),
  16176             fixture.listing_addr.as_str(),
  16177             fixture.buyer_pubkey.as_str(),
  16178             fixture.seller_pubkey.as_str(),
  16179         );
  16180         install_direct_relay_sync_transport(&fixture.runtime, &relay);
  16181         fixture
  16182             .runtime
  16183             .refresh_shared_local_events()
  16184             .expect("linked buyer revision proposal should import");
  16185         assert!(
  16186             fixture
  16187                 .runtime
  16188                 .open_personal_order_detail(fixture.order_id)
  16189                 .expect("linked buyer order detail should open")
  16190         );
  16191 
  16192         let error = fixture
  16193             .runtime
  16194             .publish_buyer_order_cancel(fixture.order_id)
  16195             .expect_err("linked buyer cancellation should reject from pending proposal");
  16196 
  16197         assert_invalid_projection_reason(
  16198             error,
  16199             "buyer order cancellation requires no pending seller proposal",
  16200         );
  16201         assert_eq!(relay.event_count(), 0);
  16202         let cancellation_events =
  16203             shared_order_events_by_kind(&fixture.paths, 3432, fixture.buyer_pubkey.as_str());
  16204         assert!(cancellation_events.is_empty());
  16205 
  16206         cleanup_bootstrapped_runtime_paths(&fixture.paths);
  16207     }
  16208 
  16209     #[test]
  16210     fn runtime_rejects_linked_buyer_cancellation_after_agreement() {
  16211         let relay = ThreadedAckRelay::spawn();
  16212         let fixture = linked_buyer_lifecycle_runtime("linked_buyer_order_cancel_after_agreement");
  16213         install_direct_relay_sync_transport(&fixture.runtime, &relay);
  16214         fixture
  16215             .runtime
  16216             .refresh_shared_local_events()
  16217             .expect("linked buyer local events should import");
  16218         assert!(
  16219             fixture
  16220                 .runtime
  16221                 .open_personal_order_detail(fixture.order_id)
  16222                 .expect("linked buyer order detail should open")
  16223         );
  16224 
  16225         let error = fixture
  16226             .runtime
  16227             .publish_buyer_order_cancel(fixture.order_id)
  16228             .expect_err("post-agreement buyer cancellation should reject");
  16229 
  16230         assert_invalid_projection_reason(
  16231             error,
  16232             "buyer order cancellation requires an open pre-agreement order",
  16233         );
  16234         assert_eq!(relay.event_count(), 0);
  16235         let cancellation_events =
  16236             shared_order_events_by_kind(&fixture.paths, 3432, fixture.buyer_pubkey.as_str());
  16237         assert!(cancellation_events.is_empty());
  16238 
  16239         cleanup_bootstrapped_runtime_paths(&fixture.paths);
  16240     }
  16241 
  16242     #[test]
  16243     fn runtime_rejects_linked_buyer_cancellation_with_reducer_invalid_evidence() {
  16244         let relay = ThreadedAckRelay::spawn();
  16245         let fixture = linked_buyer_lifecycle_runtime("linked_buyer_order_cancel_invalid");
  16246         install_direct_relay_sync_transport(&fixture.runtime, &relay);
  16247         fixture
  16248             .runtime
  16249             .refresh_shared_local_events()
  16250             .expect("linked buyer local events should import");
  16251         assert!(
  16252             fixture
  16253                 .runtime
  16254                 .open_personal_order_detail(fixture.order_id)
  16255                 .expect("linked buyer order detail should open")
  16256         );
  16257         append_signed_order_cancellation_record_with_prev(
  16258             &fixture.paths,
  16259             fixture.trade_order_id.as_str(),
  16260             "linked-buyer-order-cancel-invalid-a",
  16261             fixture.request_event_id.as_str(),
  16262             fixture.decision_event_id.as_str(),
  16263             fixture.listing_addr.as_str(),
  16264             fixture.buyer_pubkey.as_str(),
  16265             fixture.seller_pubkey.as_str(),
  16266         );
  16267         append_signed_order_cancellation_record_with_prev(
  16268             &fixture.paths,
  16269             fixture.trade_order_id.as_str(),
  16270             "linked-buyer-order-cancel-invalid-b",
  16271             fixture.request_event_id.as_str(),
  16272             fixture.decision_event_id.as_str(),
  16273             fixture.listing_addr.as_str(),
  16274             fixture.buyer_pubkey.as_str(),
  16275             fixture.seller_pubkey.as_str(),
  16276         );
  16277 
  16278         let error = fixture
  16279             .runtime
  16280             .publish_buyer_order_cancel(fixture.order_id)
  16281             .expect_err("linked buyer cancellation should reject reducer-invalid evidence");
  16282 
  16283         assert_order_lifecycle_evidence_invalid(error);
  16284         assert_eq!(relay.event_count(), 0);
  16285         cleanup_bootstrapped_runtime_paths(&fixture.paths);
  16286     }
  16287 
  16288     #[test]
  16289     fn runtime_publishes_linked_buyer_revision_decision_from_reducer_valid_parent() {
  16290         let relay = ThreadedAckRelay::spawn();
  16291         let fixture = linked_buyer_request_runtime("linked_buyer_order_revision");
  16292         let proposal_key = "linked-buyer-order-revision-proposal";
  16293         let _proposal_event_id = append_signed_order_revision_proposal_record_with_prev(
  16294             &fixture.paths,
  16295             fixture.trade_order_id.as_str(),
  16296             proposal_key,
  16297             fixture.request_event_id.as_str(),
  16298             fixture.request_event_id.as_str(),
  16299             fixture.listing_addr.as_str(),
  16300             fixture.buyer_pubkey.as_str(),
  16301             fixture.seller_pubkey.as_str(),
  16302         );
  16303         let revision_id = format!("revision-{proposal_key}");
  16304         install_direct_relay_sync_transport(&fixture.runtime, &relay);
  16305         fixture
  16306             .runtime
  16307             .refresh_shared_local_events()
  16308             .expect("linked buyer local events should import");
  16309         assert!(
  16310             fixture
  16311                 .runtime
  16312                 .open_personal_order_detail(fixture.order_id)
  16313                 .expect("linked buyer order detail should open")
  16314         );
  16315 
  16316         assert!(
  16317             fixture
  16318                 .runtime
  16319                 .publish_buyer_order_revision_accept(fixture.order_id)
  16320                 .expect("linked buyer revision decision should publish")
  16321         );
  16322 
  16323         assert_eq!(relay.event_count(), 0);
  16324         let revision_decision_events =
  16325             shared_order_events_by_kind(&fixture.paths, 3425, fixture.buyer_pubkey.as_str());
  16326         assert!(revision_decision_events.is_empty());
  16327         assert_order_revision_decision_sdk_migration_receipt(
  16328             &fixture.runtime,
  16329             fixture.order_id,
  16330             revision_id.as_str(),
  16331             AppSdkMigrationState::Enqueued,
  16332         );
  16333 
  16334         cleanup_bootstrapped_runtime_paths(&fixture.paths);
  16335     }
  16336 
  16337     #[test]
  16338     fn runtime_repeat_personal_order_readds_only_currently_eligible_items() {
  16339         let runtime = memory_runtime();
  16340         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  16341         assert!(
  16342             runtime
  16343                 .select_active_surface(ActiveSurface::Personal)
  16344                 .expect("surface should switch into marketplace")
  16345         );
  16346         let fulfillment_window_id = seed_buyer_marketplace_support(
  16347             &runtime,
  16348             account_id.as_str(),
  16349             farm_id,
  16350             "North field farm",
  16351             "Friday pickup",
  16352         );
  16353         let available_product_id = seed_product(
  16354             &runtime,
  16355             farm_id,
  16356             "Salad mix",
  16357             "Spring blend",
  16358             "published",
  16359             Some(8),
  16360             "2026-04-20T09:00:00Z",
  16361         );
  16362         let unavailable_product_id = seed_product(
  16363             &runtime,
  16364             farm_id,
  16365             "Pea shoots",
  16366             "Tray-grown",
  16367             "published",
  16368             Some(6),
  16369             "2026-04-20T10:00:00Z",
  16370         );
  16371         runtime
  16372             .lock_state()
  16373             .sqlite_store
  16374             .as_ref()
  16375             .expect("sqlite store")
  16376             .connection()
  16377             .execute_batch(&format!(
  16378                 "update products
  16379                  set availability_window_id = '{fulfillment_window_id}'
  16380                  where id in ('{available_product_id}', '{unavailable_product_id}')"
  16381             ))
  16382             .expect("buyer detail products should attach a fulfillment window");
  16383         assert!(
  16384             runtime
  16385                 .open_personal_product_detail(PersonalSection::Browse, available_product_id)
  16386                 .expect("available buyer detail should open")
  16387         );
  16388         assert!(
  16389             runtime
  16390                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  16391                 .expect("available buyer product should add to cart")
  16392         );
  16393         assert!(
  16394             runtime
  16395                 .open_personal_product_detail(PersonalSection::Browse, unavailable_product_id)
  16396                 .expect("unavailable buyer detail should open")
  16397         );
  16398         assert!(
  16399             runtime
  16400                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  16401                 .expect("second buyer product should add to cart")
  16402         );
  16403         assert!(
  16404             runtime
  16405                 .save_personal_order_review_draft(BuyerOrderReviewDraft {
  16406                     name: "Casey Buyer".to_owned(),
  16407                     email: "casey@example.com".to_owned(),
  16408                     phone: String::new(),
  16409                     order_note: String::new(),
  16410                 })
  16411                 .expect("buyer order review draft should save")
  16412         );
  16413         assert!(
  16414             runtime
  16415                 .place_personal_order()
  16416                 .expect("buyer order should place")
  16417         );
  16418         let order_id = runtime.summary().personal_projection.orders.list.rows[0].order_id;
  16419 
  16420         runtime
  16421             .lock_state()
  16422             .sqlite_store
  16423             .as_ref()
  16424             .expect("sqlite store")
  16425             .connection()
  16426             .execute(
  16427                 "update products set status = 'archived' where id = ?1",
  16428                 [unavailable_product_id.to_string()],
  16429             )
  16430             .expect("product should archive");
  16431 
  16432         assert!(
  16433             runtime
  16434                 .open_personal_order_detail(order_id)
  16435                 .expect("buyer order detail should reopen")
  16436         );
  16437         let detail_summary = runtime.summary();
  16438         let repeat_demand = detail_summary
  16439             .personal_projection
  16440             .orders
  16441             .detail
  16442             .as_ref()
  16443             .and_then(|detail| detail.repeat_demand.as_ref())
  16444             .expect("repeat demand should derive from buyer order detail");
  16445         assert_eq!(repeat_demand.eligibility.storage_key(), "partial");
  16446         assert_eq!(repeat_demand.available_item_count, 1);
  16447         assert_eq!(repeat_demand.unavailable_item_count, 1);
  16448 
  16449         assert!(
  16450             runtime
  16451                 .repeat_personal_order(order_id, false)
  16452                 .expect("repeat demand should add available items to cart")
  16453         );
  16454 
  16455         let summary = runtime.summary();
  16456         assert_eq!(
  16457             summary.shell_projection.selected_section,
  16458             ShellSection::Personal(PersonalSection::Cart)
  16459         );
  16460         assert_eq!(summary.personal_projection.cart.cart.lines.len(), 1);
  16461         assert_eq!(
  16462             summary.personal_projection.cart.cart.lines[0].product_id,
  16463             available_product_id
  16464         );
  16465         assert_eq!(summary.personal_projection.cart.cart.lines[0].quantity, 1);
  16466         assert!(
  16467             summary
  16468                 .personal_projection
  16469                 .cart
  16470                 .cart
  16471                 .replace_confirmation
  16472                 .is_none()
  16473         );
  16474     }
  16475 
  16476     #[test]
  16477     fn runtime_products_queries_refresh_the_repository_backed_projection() {
  16478         let runtime = memory_runtime();
  16479 
  16480         assert!(
  16481             runtime
  16482                 .generate_local_account(Some("Farmer".to_owned()))
  16483                 .expect("account should generate")
  16484         );
  16485         let account_id = runtime
  16486             .summary()
  16487             .settings_account_projection
  16488             .selected_account
  16489             .as_ref()
  16490             .expect("selected account")
  16491             .account
  16492             .account_id
  16493             .clone();
  16494         let farm_id =
  16495             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  16496         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  16497             farm_id,
  16498             display_name: "North field farm".to_owned(),
  16499             readiness: FarmReadiness::Ready,
  16500         });
  16501         runtime
  16502             .lock_state()
  16503             .sqlite_store
  16504             .as_ref()
  16505             .expect("sqlite store")
  16506             .save_farm_summary(
  16507                 farm_setup_projection
  16508                     .saved_farm
  16509                     .as_ref()
  16510                     .expect("saved farm should exist"),
  16511             )
  16512             .expect("farm summary should save");
  16513         runtime
  16514             .lock_state()
  16515             .sqlite_store
  16516             .as_ref()
  16517             .expect("sqlite store")
  16518             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  16519             .expect("farm setup should save");
  16520         seed_product(
  16521             &runtime,
  16522             farm_id,
  16523             "Salad mix",
  16524             "Spring blend",
  16525             "published",
  16526             Some(2),
  16527             "2026-04-18T10:00:00Z",
  16528         );
  16529         seed_product(
  16530             &runtime,
  16531             farm_id,
  16532             "Pea shoots",
  16533             "Tray-grown",
  16534             "draft",
  16535             None,
  16536             "2026-04-18T09:00:00Z",
  16537         );
  16538 
  16539         assert!(
  16540             runtime
  16541                 .select_local_account(account_id.as_str())
  16542                 .expect("account should select")
  16543         );
  16544 
  16545         let summary = runtime.summary();
  16546         assert_eq!(summary.products_projection.list.summary.total_products, 2);
  16547         assert_eq!(summary.products_projection.list.rows[0].title, "Salad mix");
  16548         assert_eq!(
  16549             summary.products_projection.query.filter,
  16550             ProductsFilter::default()
  16551         );
  16552         assert_eq!(
  16553             summary.products_projection.query.sort,
  16554             ProductsSort::default()
  16555         );
  16556 
  16557         assert!(
  16558             runtime
  16559                 .select_products_filter(ProductsFilter::NeedAttention)
  16560                 .expect("filter should apply")
  16561         );
  16562         assert_eq!(runtime.summary().products_projection.list.rows.len(), 2);
  16563 
  16564         assert!(
  16565             runtime
  16566                 .set_products_search_query("pea")
  16567                 .expect("search should apply")
  16568         );
  16569         let searched = runtime.summary();
  16570         assert_eq!(searched.products_projection.list.rows.len(), 1);
  16571         assert_eq!(
  16572             searched.products_projection.list.rows[0].title,
  16573             "Pea shoots"
  16574         );
  16575 
  16576         assert!(
  16577             runtime
  16578                 .select_products_sort(ProductsSort::Name)
  16579                 .expect("sort should apply")
  16580         );
  16581         assert_eq!(
  16582             runtime.summary().products_projection.query.sort,
  16583             ProductsSort::Name
  16584         );
  16585     }
  16586 
  16587     #[test]
  16588     fn runtime_open_products_filter_routes_today_follow_ons_into_products() {
  16589         let runtime = memory_runtime();
  16590 
  16591         assert!(
  16592             runtime
  16593                 .generate_local_account(Some("Farmer".to_owned()))
  16594                 .expect("account should generate")
  16595         );
  16596         let account_id = runtime
  16597             .summary()
  16598             .settings_account_projection
  16599             .selected_account
  16600             .as_ref()
  16601             .expect("selected account")
  16602             .account
  16603             .account_id
  16604             .clone();
  16605         let farm_id =
  16606             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  16607         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  16608             farm_id,
  16609             display_name: "North field farm".to_owned(),
  16610             readiness: FarmReadiness::Ready,
  16611         });
  16612         runtime
  16613             .lock_state()
  16614             .sqlite_store
  16615             .as_ref()
  16616             .expect("sqlite store")
  16617             .save_farm_summary(
  16618                 farm_setup_projection
  16619                     .saved_farm
  16620                     .as_ref()
  16621                     .expect("saved farm should exist"),
  16622             )
  16623             .expect("farm summary should save");
  16624         runtime
  16625             .lock_state()
  16626             .sqlite_store
  16627             .as_ref()
  16628             .expect("sqlite store")
  16629             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  16630             .expect("farm setup should save");
  16631 
  16632         assert!(
  16633             runtime
  16634                 .select_local_account(account_id.as_str())
  16635                 .expect("account should select")
  16636         );
  16637         assert_eq!(
  16638             runtime.summary().shell_projection.selected_section,
  16639             ShellSection::Farmer(FarmerSection::Today)
  16640         );
  16641 
  16642         assert!(
  16643             runtime
  16644                 .open_products_filter(ProductsFilter::Drafts)
  16645                 .expect("products follow-on should route")
  16646         );
  16647         let summary = runtime.summary();
  16648 
  16649         assert_eq!(
  16650             summary.shell_projection.selected_section,
  16651             ShellSection::Farmer(FarmerSection::Products)
  16652         );
  16653         assert_eq!(
  16654             summary.products_projection.query.filter,
  16655             ProductsFilter::Drafts
  16656         );
  16657     }
  16658 
  16659     #[test]
  16660     fn runtime_opens_orders_detail_and_pack_day_through_shared_farmer_routing() {
  16661         let runtime = memory_runtime();
  16662         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  16663         let (fulfillment_window_id, order_id) = seed_order_workspace(&runtime, farm_id);
  16664 
  16665         assert_eq!(
  16666             runtime.summary().orders_projection.query.filter,
  16667             OrdersFilter::NeedsAction
  16668         );
  16669 
  16670         assert!(runtime.open_orders().expect("orders should open"));
  16671         let orders_summary = runtime.summary();
  16672         assert_eq!(
  16673             orders_summary.shell_projection.selected_section,
  16674             ShellSection::Farmer(FarmerSection::Orders)
  16675         );
  16676         assert_eq!(orders_summary.orders_projection.list.rows.len(), 1);
  16677         assert_eq!(
  16678             orders_summary.orders_projection.list.rows[0].order_id,
  16679             order_id
  16680         );
  16681         assert!(orders_summary.orders_projection.detail.is_none());
  16682 
  16683         assert!(
  16684             runtime
  16685                 .open_order_detail(order_id)
  16686                 .expect("order detail should open")
  16687         );
  16688         let detail_summary = runtime.summary();
  16689         assert_eq!(
  16690             detail_summary.shell_projection.selected_section,
  16691             ShellSection::Farmer(FarmerSection::Orders)
  16692         );
  16693         assert_eq!(
  16694             detail_summary
  16695                 .orders_projection
  16696                 .detail
  16697                 .as_ref()
  16698                 .expect("order detail")
  16699                 .order_id,
  16700             order_id
  16701         );
  16702 
  16703         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  16704         let pack_day_summary = runtime.summary();
  16705         assert_eq!(
  16706             pack_day_summary.shell_projection.selected_section,
  16707             ShellSection::Farmer(FarmerSection::PackDay)
  16708         );
  16709         assert_eq!(
  16710             pack_day_summary
  16711                 .pack_day_projection
  16712                 .query
  16713                 .fulfillment_window_id,
  16714             None
  16715         );
  16716         assert_eq!(
  16717             pack_day_summary
  16718                 .pack_day_projection
  16719                 .projection
  16720                 .fulfillment_window
  16721                 .as_ref()
  16722                 .expect("pack day fulfillment window")
  16723                 .fulfillment_window_id,
  16724             fulfillment_window_id
  16725         );
  16726     }
  16727 
  16728     #[test]
  16729     fn runtime_export_pack_day_requires_a_current_window_context() {
  16730         let (runtime, paths) = bootstrapped_runtime("pack_day_export_requires_context");
  16731         let (_, _farm_id) = provision_ready_farmer_account(&runtime);
  16732 
  16733         assert!(
  16734             !runtime
  16735                 .export_pack_day()
  16736                 .expect("missing pack day context should no-op")
  16737         );
  16738         assert_eq!(
  16739             runtime.summary().pack_day_projection.export.status,
  16740             PackDayExportStatus::Idle
  16741         );
  16742 
  16743         cleanup_bootstrapped_runtime_paths(&paths);
  16744     }
  16745 
  16746     #[test]
  16747     fn runtime_export_pack_day_uses_repository_source_truth_and_writes_bundle() {
  16748         let (runtime, paths) = bootstrapped_runtime("pack_day_export_bundle");
  16749         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  16750         let (fulfillment_window_id, order_id) = seed_order_workspace(&runtime, farm_id);
  16751 
  16752         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  16753         let fulfillment_window = runtime
  16754             .summary()
  16755             .pack_day_projection
  16756             .projection
  16757             .fulfillment_window
  16758             .clone()
  16759             .expect("pack day fulfillment window");
  16760         let _ = runtime.lock_state_mut().state_store.apply_in_memory(
  16761             AppStateCommand::replace_pack_day_projection(PackDayProjection {
  16762                 fulfillment_window: Some(fulfillment_window.clone()),
  16763                 reminders: ReminderFeedProjection::default(),
  16764                 totals_by_product: vec![PackDayProductTotalRow {
  16765                     title: "Bogus totals".to_owned(),
  16766                     quantity_display: "999 crates".to_owned(),
  16767                 }],
  16768                 pack_list: vec![PackDayPackListRow {
  16769                     title: "Bogus pack list".to_owned(),
  16770                     quantity_display: "Do not trust screen strings".to_owned(),
  16771                 }],
  16772                 pickup_roster: vec![PackDayRosterRow {
  16773                     order_id: OrderId::new(),
  16774                     order_number: "R-999".to_owned(),
  16775                     customer_display_name: "Bogus".to_owned(),
  16776                 }],
  16777             }),
  16778         );
  16779 
  16780         assert!(
  16781             runtime
  16782                 .export_pack_day()
  16783                 .expect("pack day export should succeed")
  16784         );
  16785 
  16786         let summary = runtime.summary();
  16787         let export = &summary.pack_day_projection.export;
  16788         assert_eq!(export.status, PackDayExportStatus::Succeeded);
  16789         assert_eq!(
  16790             export
  16791                 .request
  16792                 .as_ref()
  16793                 .expect("export request")
  16794                 .fulfillment_window_id,
  16795             fulfillment_window_id
  16796         );
  16797         let bundle = export.bundle.as_ref().expect("export bundle");
  16798         assert_eq!(bundle.fulfillment_window_id, fulfillment_window_id);
  16799         assert_eq!(bundle.artifact_count(), 3);
  16800 
  16801         let pack_sheet_path = PathBuf::from(&bundle.bundle_directory).join("pack_sheet.txt");
  16802         let pickup_roster_path = PathBuf::from(&bundle.bundle_directory).join("pickup_roster.txt");
  16803         let customer_labels_path =
  16804             PathBuf::from(&bundle.bundle_directory).join("customer_labels.txt");
  16805 
  16806         let pack_sheet = fs::read_to_string(&pack_sheet_path).expect("pack sheet should exist");
  16807         let pickup_roster =
  16808             fs::read_to_string(&pickup_roster_path).expect("pickup roster should exist");
  16809         let customer_labels =
  16810             fs::read_to_string(&customer_labels_path).expect("customer labels should exist");
  16811 
  16812         assert!(pack_sheet.contains("Farm: North field farm"));
  16813         assert!(pack_sheet.contains("Casey | R-100 | needs_action | Salad mix | 2 bags"));
  16814         assert!(!pack_sheet.contains("Bogus"));
  16815         assert!(pickup_roster.contains("Casey | R-100 | needs_action"));
  16816         assert!(customer_labels.contains("North field farm"));
  16817         assert!(customer_labels.contains("Casey"));
  16818         assert!(customer_labels.contains("Order: R-100"));
  16819         assert!(!customer_labels.contains("Bogus"));
  16820         assert!(!pickup_roster.contains(&order_id.to_string()));
  16821 
  16822         cleanup_bootstrapped_runtime_paths(&paths);
  16823     }
  16824 
  16825     #[test]
  16826     fn runtime_bootstrap_sweeps_prepared_pack_day_print_assets() {
  16827         let paths = temp_desktop_runtime_paths("pack_day_print_bootstrap_sweep");
  16828         let stale_root = prepared_customer_label_asset_root();
  16829         let stale_directory = stale_root.join(PackDayExportInstanceId::new().to_string());
  16830         let _ = fs::remove_file(&stale_root);
  16831         let _ = fs::remove_dir_all(&stale_root);
  16832         fs::create_dir_all(&stale_directory).expect("stale prepared directory should create");
  16833         fs::write(stale_directory.join("stale.ps"), "stale").expect("stale asset should write");
  16834 
  16835         let _ = restart_runtime(paths.clone());
  16836 
  16837         assert!(!stale_root.exists());
  16838 
  16839         cleanup_bootstrapped_runtime_paths(&paths);
  16840     }
  16841 
  16842     #[test]
  16843     fn runtime_bootstrap_keeps_running_when_prepared_pack_day_print_root_sweep_fails() {
  16844         let paths = temp_desktop_runtime_paths("pack_day_print_bootstrap_best_effort");
  16845         let stale_root = prepared_customer_label_asset_root();
  16846         let _ = fs::remove_file(&stale_root);
  16847         let _ = fs::remove_dir_all(&stale_root);
  16848         if let Some(parent) = stale_root.parent() {
  16849             fs::create_dir_all(parent).expect("prepared asset root parent should create");
  16850         }
  16851         fs::write(&stale_root, "blocked").expect("prepared asset root blocker should write");
  16852 
  16853         let _ = restart_runtime(paths.clone());
  16854 
  16855         assert!(stale_root.is_file());
  16856 
  16857         let _ = fs::remove_file(&stale_root);
  16858         cleanup_bootstrapped_runtime_paths(&paths);
  16859     }
  16860 
  16861     #[test]
  16862     fn runtime_prepare_pack_day_host_handoff_uses_the_current_export_bundle_for_file_actions() {
  16863         let (runtime, paths) = bootstrapped_runtime("pack_day_host_handoff_prepare");
  16864         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  16865 
  16866         seed_order_workspace(&runtime, farm_id);
  16867         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  16868         assert!(
  16869             runtime
  16870                 .export_pack_day()
  16871                 .expect("pack day export should succeed")
  16872         );
  16873 
  16874         for (kind, suffix) in [
  16875             (PackDayHostHandoffKind::OpenPackSheet, "pack_sheet.txt"),
  16876             (
  16877                 PackDayHostHandoffKind::OpenPickupRoster,
  16878                 "pickup_roster.txt",
  16879             ),
  16880             (
  16881                 PackDayHostHandoffKind::OpenCustomerLabels,
  16882                 "customer_labels.txt",
  16883             ),
  16884         ] {
  16885             let prepared = runtime
  16886                 .prepare_pack_day_host_handoff(kind)
  16887                 .expect("host handoff should prepare")
  16888                 .expect("host handoff should produce a plan");
  16889 
  16890             let summary = runtime.summary();
  16891             assert_eq!(
  16892                 summary.pack_day_projection.host_handoff.status,
  16893                 PackDayHostHandoffStatus::Running
  16894             );
  16895             assert_eq!(
  16896                 summary.pack_day_projection.host_handoff.request,
  16897                 Some(prepared.0.clone())
  16898             );
  16899             assert_eq!(prepared.0.kind, kind);
  16900             assert_eq!(
  16901                 prepared.0.bundle_directory,
  16902                 summary
  16903                     .pack_day_projection
  16904                     .export
  16905                     .bundle
  16906                     .as_ref()
  16907                     .expect("pack day export bundle")
  16908                     .bundle_directory
  16909             );
  16910             assert_eq!(prepared.1.kind, kind);
  16911             assert!(prepared.1.target_path.ends_with(suffix));
  16912 
  16913             assert!(
  16914                 runtime
  16915                     .finish_pack_day_host_handoff(prepared.0, Ok(()))
  16916                     .expect("host handoff success should apply")
  16917             );
  16918         }
  16919 
  16920         cleanup_bootstrapped_runtime_paths(&paths);
  16921     }
  16922 
  16923     #[test]
  16924     fn runtime_finish_pack_day_host_handoff_records_failures_in_state() {
  16925         let (runtime, paths) = bootstrapped_runtime("pack_day_host_handoff_failure");
  16926         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  16927 
  16928         seed_order_workspace(&runtime, farm_id);
  16929         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  16930         assert!(
  16931             runtime
  16932                 .export_pack_day()
  16933                 .expect("pack day export should succeed")
  16934         );
  16935 
  16936         let (request, _) = runtime
  16937             .prepare_pack_day_host_handoff(PackDayHostHandoffKind::RevealBundle)
  16938             .expect("host handoff should prepare")
  16939             .expect("host handoff should produce a plan");
  16940 
  16941         let error = runtime
  16942             .finish_pack_day_host_handoff(
  16943                 request.clone(),
  16944                 Err(PackDayHostHandoffError::UnsupportedPlatform),
  16945             )
  16946             .expect_err("host handoff failure should surface");
  16947         assert!(matches!(
  16948             error,
  16949             DesktopAppRuntimeCommandError::PackDayHostHandoff(
  16950                 PackDayHostHandoffError::UnsupportedPlatform
  16951             )
  16952         ));
  16953 
  16954         let summary = runtime.summary();
  16955         assert_eq!(
  16956             summary.pack_day_projection.host_handoff.status,
  16957             PackDayHostHandoffStatus::Failed
  16958         );
  16959         assert_eq!(
  16960             summary.pack_day_projection.host_handoff.request,
  16961             Some(request)
  16962         );
  16963         assert_eq!(
  16964             summary
  16965                 .pack_day_projection
  16966                 .host_handoff
  16967                 .error_message
  16968                 .as_deref(),
  16969             Some("pack day host handoff is only supported on macos")
  16970         );
  16971 
  16972         cleanup_bootstrapped_runtime_paths(&paths);
  16973     }
  16974 
  16975     #[test]
  16976     fn runtime_finish_pack_day_host_handoff_ignores_stale_background_completion() {
  16977         let (runtime, paths) = bootstrapped_runtime("pack_day_host_handoff_stale");
  16978         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  16979 
  16980         seed_order_workspace(&runtime, farm_id);
  16981         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  16982         assert!(
  16983             runtime
  16984                 .export_pack_day()
  16985                 .expect("pack day export should succeed")
  16986         );
  16987 
  16988         let (request, _) = runtime
  16989             .prepare_pack_day_host_handoff(PackDayHostHandoffKind::RevealBundle)
  16990             .expect("host handoff should prepare")
  16991             .expect("host handoff should produce a plan");
  16992 
  16993         let _ = runtime
  16994             .lock_state_mut()
  16995             .state_store
  16996             .apply_in_memory(AppStateCommand::reset_pack_day_host_handoff());
  16997 
  16998         assert!(
  16999             !runtime
  17000                 .finish_pack_day_host_handoff(request, Ok(()))
  17001                 .expect("stale completion should no-op")
  17002         );
  17003         assert_eq!(
  17004             runtime.summary().pack_day_projection.host_handoff.status,
  17005             PackDayHostHandoffStatus::Idle
  17006         );
  17007 
  17008         cleanup_bootstrapped_runtime_paths(&paths);
  17009     }
  17010 
  17011     #[test]
  17012     fn runtime_prepare_pack_day_print_uses_the_current_export_bundle_for_all_v1_documents() {
  17013         let (runtime, paths) = bootstrapped_runtime("pack_day_print_prepare");
  17014         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17015 
  17016         seed_order_workspace(&runtime, farm_id);
  17017         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17018         assert!(
  17019             runtime
  17020                 .export_pack_day()
  17021                 .expect("pack day export should succeed")
  17022         );
  17023 
  17024         for (kind, expected_exported_suffix) in [
  17025             (PackDayPrintKind::PrintPackSheet, Some("pack_sheet.txt")),
  17026             (
  17027                 PackDayPrintKind::PrintPickupRoster,
  17028                 Some("pickup_roster.txt"),
  17029             ),
  17030             (PackDayPrintKind::PrintCustomerLabels, None),
  17031         ] {
  17032             let prepared = runtime
  17033                 .prepare_pack_day_print(kind)
  17034                 .expect("print should prepare")
  17035                 .expect("print should produce a plan");
  17036 
  17037             let summary = runtime.summary();
  17038             assert_eq!(
  17039                 summary.pack_day_projection.print.status,
  17040                 PackDayPrintStatus::Running
  17041             );
  17042             assert_eq!(
  17043                 summary.pack_day_projection.print.request,
  17044                 Some(prepared.0.clone())
  17045             );
  17046             assert_eq!(prepared.0.kind, kind);
  17047             assert_eq!(
  17048                 prepared.0.export_instance_id,
  17049                 summary
  17050                     .pack_day_projection
  17051                     .export
  17052                     .bundle
  17053                     .as_ref()
  17054                     .expect("pack day export bundle")
  17055                     .export_instance_id
  17056             );
  17057             assert_eq!(prepared.0.label_stock, kind.label_stock());
  17058             assert_eq!(prepared.1.kind, kind);
  17059             assert_eq!(prepared.1.command_program, "lp");
  17060             match expected_exported_suffix {
  17061                 Some(suffix) => {
  17062                     assert!(prepared.1.target_path.ends_with(suffix));
  17063                     assert_eq!(
  17064                         prepared.1.command_args,
  17065                         vec![prepared.1.target_path.to_string_lossy().into_owned()]
  17066                     );
  17067                 }
  17068                 None => {
  17069                     let export_bundle = summary
  17070                         .pack_day_projection
  17071                         .export
  17072                         .bundle
  17073                         .as_ref()
  17074                         .expect("pack day export bundle");
  17075                     assert!(
  17076                         prepared
  17077                             .1
  17078                             .target_path
  17079                             .ends_with("customer_labels_avery_5160_letter_30_up.ps")
  17080                     );
  17081                     assert!(
  17082                         !prepared
  17083                             .1
  17084                             .target_path
  17085                             .starts_with(PathBuf::from(&export_bundle.bundle_directory))
  17086                     );
  17087                     assert!(
  17088                         prepared
  17089                             .1
  17090                             .target_path
  17091                             .to_string_lossy()
  17092                             .contains(export_bundle.export_instance_id.to_string().as_str())
  17093                     );
  17094                     assert_eq!(
  17095                         prepared.1.command_args,
  17096                         vec![
  17097                             "-o".to_owned(),
  17098                             "media=Letter".to_owned(),
  17099                             prepared.1.target_path.to_string_lossy().into_owned()
  17100                         ]
  17101                     );
  17102                 }
  17103             }
  17104 
  17105             assert!(
  17106                 runtime
  17107                     .finish_pack_day_print(prepared.0, Ok(()))
  17108                     .expect("print success should apply")
  17109             );
  17110 
  17111             if let PackDayPrintKind::PrintCustomerLabels = kind {
  17112                 if let Some(parent) = prepared.1.target_path.parent() {
  17113                     let _ = fs::remove_dir_all(parent);
  17114                 }
  17115             }
  17116         }
  17117 
  17118         cleanup_bootstrapped_runtime_paths(&paths);
  17119     }
  17120 
  17121     #[test]
  17122     fn runtime_prepare_pack_day_batch_print_uses_the_current_export_bundle_for_all_v1_documents() {
  17123         let (runtime, paths) = bootstrapped_runtime("pack_day_batch_print_prepare");
  17124         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17125 
  17126         seed_order_workspace(&runtime, farm_id);
  17127         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17128         assert!(
  17129             runtime
  17130                 .export_pack_day()
  17131                 .expect("pack day export should succeed")
  17132         );
  17133 
  17134         let (request, plan) = runtime
  17135             .prepare_pack_day_batch_print()
  17136             .expect("batch print should prepare")
  17137             .expect("batch print should produce a plan");
  17138 
  17139         let summary = runtime.summary();
  17140         let bundle = summary
  17141             .pack_day_projection
  17142             .export
  17143             .bundle
  17144             .as_ref()
  17145             .expect("pack day export bundle");
  17146         assert_eq!(
  17147             summary.pack_day_projection.batch_print.status,
  17148             PackDayBatchPrintStatus::Running
  17149         );
  17150         assert_eq!(
  17151             summary.pack_day_projection.batch_print.request,
  17152             Some(request.clone())
  17153         );
  17154         assert_eq!(request.export_instance_id, bundle.export_instance_id);
  17155         assert_eq!(
  17156             request.artifacts,
  17157             Vec::from(PackDayBatchPrintArtifact::all_v1())
  17158         );
  17159         assert_eq!(plan.export_instance_id, bundle.export_instance_id);
  17160         assert_eq!(
  17161             plan.plans
  17162                 .iter()
  17163                 .map(|plan| PackDayBatchPrintArtifact::from_print_kind(plan.kind))
  17164                 .collect::<Vec<_>>(),
  17165             request.artifacts.clone()
  17166         );
  17167         assert!(plan.plans.iter().all(|plan| plan.command_program == "lp"));
  17168 
  17169         assert!(
  17170             runtime
  17171                 .finish_pack_day_batch_print(request, Ok(()))
  17172                 .expect("batch print success should apply")
  17173         );
  17174         assert_eq!(
  17175             runtime.summary().pack_day_projection.batch_print.status,
  17176             PackDayBatchPrintStatus::Succeeded
  17177         );
  17178 
  17179         cleanup_bootstrapped_runtime_paths(&paths);
  17180     }
  17181 
  17182     #[test]
  17183     fn runtime_pack_day_batch_print_blocks_conflicting_pack_day_actions() {
  17184         let (runtime, paths) = bootstrapped_runtime("pack_day_batch_print_conflicts");
  17185         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17186 
  17187         seed_order_workspace(&runtime, farm_id);
  17188         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17189         assert!(
  17190             runtime
  17191                 .export_pack_day()
  17192                 .expect("pack day export should succeed")
  17193         );
  17194 
  17195         let (_, _) = runtime
  17196             .prepare_pack_day_batch_print()
  17197             .expect("batch print should prepare")
  17198             .expect("batch print should produce a plan");
  17199         assert!(
  17200             runtime
  17201                 .prepare_pack_day_print(PackDayPrintKind::PrintPackSheet)
  17202                 .expect("print prepare should not fail")
  17203                 .is_none()
  17204         );
  17205         assert!(
  17206             runtime
  17207                 .prepare_pack_day_host_handoff(PackDayHostHandoffKind::RevealBundle)
  17208                 .expect("host handoff prepare should not fail")
  17209                 .is_none()
  17210         );
  17211 
  17212         let _ = runtime
  17213             .lock_state_mut()
  17214             .state_store
  17215             .apply_in_memory(AppStateCommand::reset_pack_day_batch_print());
  17216 
  17217         let (print_request, _) = runtime
  17218             .prepare_pack_day_print(PackDayPrintKind::PrintPackSheet)
  17219             .expect("print should prepare")
  17220             .expect("print should produce a plan");
  17221         assert!(
  17222             runtime
  17223                 .prepare_pack_day_batch_print()
  17224                 .expect("batch print prepare should not fail")
  17225                 .is_none()
  17226         );
  17227         assert!(
  17228             runtime
  17229                 .finish_pack_day_print(print_request, Ok(()))
  17230                 .expect("print success should apply")
  17231         );
  17232         let _ = runtime
  17233             .lock_state_mut()
  17234             .state_store
  17235             .apply_in_memory(AppStateCommand::reset_pack_day_print());
  17236 
  17237         let (_, _) = runtime
  17238             .prepare_pack_day_host_handoff(PackDayHostHandoffKind::RevealBundle)
  17239             .expect("host handoff should prepare")
  17240             .expect("host handoff should produce a plan");
  17241         assert!(
  17242             runtime
  17243                 .prepare_pack_day_batch_print()
  17244                 .expect("batch print prepare should not fail")
  17245                 .is_none()
  17246         );
  17247 
  17248         cleanup_bootstrapped_runtime_paths(&paths);
  17249     }
  17250 
  17251     #[test]
  17252     fn runtime_finish_pack_day_batch_print_records_failures_and_cleans_prepared_assets() {
  17253         let (runtime, paths) = bootstrapped_runtime("pack_day_batch_print_failure_cleanup");
  17254         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17255 
  17256         seed_order_workspace(&runtime, farm_id);
  17257         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17258         assert!(
  17259             runtime
  17260                 .export_pack_day()
  17261                 .expect("pack day export should succeed")
  17262         );
  17263 
  17264         let (request, plan) = runtime
  17265             .prepare_pack_day_batch_print()
  17266             .expect("batch print should prepare")
  17267             .expect("batch print should produce a plan");
  17268         let prepared_directory = plan
  17269             .plans
  17270             .iter()
  17271             .find(|plan| plan.kind == PackDayPrintKind::PrintCustomerLabels)
  17272             .and_then(|plan| plan.target_path.parent())
  17273             .expect("prepared customer labels parent")
  17274             .to_path_buf();
  17275         assert!(prepared_directory.is_dir());
  17276 
  17277         let failed_artifact =
  17278             PackDayBatchPrintArtifact::from_print_kind(PackDayPrintKind::PrintPickupRoster);
  17279         let error = runtime
  17280             .finish_pack_day_batch_print(
  17281                 request.clone(),
  17282                 Err(PackDayBatchPrintError::QueueExit {
  17283                     submitted_artifacts: vec![PackDayBatchPrintArtifact::from_print_kind(
  17284                         PackDayPrintKind::PrintPackSheet,
  17285                     )],
  17286                     failed_artifact,
  17287                     source: PackDayPrintError::UnsupportedPlatform,
  17288                 }),
  17289             )
  17290             .expect_err("batch print failure should surface");
  17291         assert!(matches!(
  17292             error,
  17293             DesktopAppRuntimeCommandError::PackDayBatchPrint(
  17294                 PackDayBatchPrintError::QueueExit { .. }
  17295             )
  17296         ));
  17297         assert!(!prepared_directory.exists());
  17298 
  17299         let summary = runtime.summary();
  17300         let batch_print = &summary.pack_day_projection.batch_print;
  17301         assert_eq!(batch_print.status, PackDayBatchPrintStatus::Failed);
  17302         assert_eq!(batch_print.request, Some(request));
  17303         assert_eq!(batch_print.failed_artifact, Some(failed_artifact));
  17304         assert_eq!(
  17305             batch_print.failure,
  17306             Some(PackDayBatchPrintFailureKind::QueueExit)
  17307         );
  17308 
  17309         cleanup_bootstrapped_runtime_paths(&paths);
  17310     }
  17311 
  17312     #[test]
  17313     fn runtime_finish_pack_day_batch_print_ignores_stale_background_completion() {
  17314         let (runtime, paths) = bootstrapped_runtime("pack_day_batch_print_stale");
  17315         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17316 
  17317         seed_order_workspace(&runtime, farm_id);
  17318         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17319         assert!(
  17320             runtime
  17321                 .export_pack_day()
  17322                 .expect("pack day export should succeed")
  17323         );
  17324 
  17325         let (request, _) = runtime
  17326             .prepare_pack_day_batch_print()
  17327             .expect("batch print should prepare")
  17328             .expect("batch print should produce a plan");
  17329 
  17330         let _ = runtime
  17331             .lock_state_mut()
  17332             .state_store
  17333             .apply_in_memory(AppStateCommand::reset_pack_day_batch_print());
  17334 
  17335         assert!(
  17336             !runtime
  17337                 .finish_pack_day_batch_print(request, Ok(()))
  17338                 .expect("stale completion should no-op")
  17339         );
  17340         assert_eq!(
  17341             runtime.summary().pack_day_projection.batch_print.status,
  17342             PackDayBatchPrintStatus::Idle
  17343         );
  17344 
  17345         cleanup_bootstrapped_runtime_paths(&paths);
  17346     }
  17347 
  17348     #[test]
  17349     fn pack_day_batch_workflow_success_submits_frozen_v1_and_records_success() {
  17350         let (runtime, paths) = bootstrapped_runtime("pack_day_batch_workflow_success");
  17351         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17352 
  17353         seed_order_workspace(&runtime, farm_id);
  17354         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17355         assert!(
  17356             runtime
  17357                 .export_pack_day()
  17358                 .expect("pack day export should succeed")
  17359         );
  17360 
  17361         let (request, plan) = runtime
  17362             .prepare_pack_day_batch_print()
  17363             .expect("batch print should prepare")
  17364             .expect("batch print should produce a plan");
  17365         let mut submitted = Vec::new();
  17366 
  17367         execute_pack_day_batch_print_plan_with(&plan, |print_plan| {
  17368             submitted.push(PackDayBatchPrintArtifact::from_print_kind(print_plan.kind));
  17369             Ok(PackDayPrintCommandResult::succeeded())
  17370         })
  17371         .expect("batch print execution should succeed");
  17372 
  17373         assert_eq!(submitted, Vec::from(PackDayBatchPrintArtifact::all_v1()));
  17374         assert!(
  17375             runtime
  17376                 .finish_pack_day_batch_print(request.clone(), Ok(()))
  17377                 .expect("batch print success should apply")
  17378         );
  17379 
  17380         let summary = runtime.summary();
  17381         let batch_print = &summary.pack_day_projection.batch_print;
  17382         assert_eq!(batch_print.status, PackDayBatchPrintStatus::Succeeded);
  17383         assert_eq!(batch_print.request, Some(request));
  17384         assert_eq!(batch_print.failed_artifact, None);
  17385         assert_eq!(batch_print.failure, None);
  17386 
  17387         cleanup_bootstrapped_runtime_paths(&paths);
  17388     }
  17389 
  17390     #[test]
  17391     fn pack_day_batch_workflow_queue_failure_records_failed_artifact_state() {
  17392         let (runtime, paths) = bootstrapped_runtime("pack_day_batch_workflow_queue_failure");
  17393         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17394 
  17395         seed_order_workspace(&runtime, farm_id);
  17396         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17397         assert!(
  17398             runtime
  17399                 .export_pack_day()
  17400                 .expect("pack day export should succeed")
  17401         );
  17402 
  17403         let (request, plan) = runtime
  17404             .prepare_pack_day_batch_print()
  17405             .expect("batch print should prepare")
  17406             .expect("batch print should produce a plan");
  17407         let mut submitted = Vec::new();
  17408 
  17409         let execution_error = execute_pack_day_batch_print_plan_with(&plan, |print_plan| {
  17410             submitted.push(PackDayBatchPrintArtifact::from_print_kind(print_plan.kind));
  17411             match print_plan.kind {
  17412                 PackDayPrintKind::PrintPackSheet => Ok(PackDayPrintCommandResult::succeeded()),
  17413                 PackDayPrintKind::PrintPickupRoster => Ok(PackDayPrintCommandResult::failed(
  17414                     Some(2),
  17415                     "lp stopped before submit",
  17416                 )),
  17417                 PackDayPrintKind::PrintCustomerLabels => {
  17418                     panic!("batch should stop before customer labels")
  17419                 }
  17420             }
  17421         })
  17422         .expect_err("batch print execution should fail");
  17423 
  17424         assert_eq!(
  17425             submitted,
  17426             vec![
  17427                 PackDayBatchPrintArtifact::from_print_kind(PackDayPrintKind::PrintPackSheet),
  17428                 PackDayBatchPrintArtifact::from_print_kind(PackDayPrintKind::PrintPickupRoster),
  17429             ]
  17430         );
  17431         let failed_artifact =
  17432             PackDayBatchPrintArtifact::from_print_kind(PackDayPrintKind::PrintPickupRoster);
  17433         let runtime_error = runtime
  17434             .finish_pack_day_batch_print(request.clone(), Err(execution_error))
  17435             .expect_err("batch print failure should surface");
  17436         assert!(matches!(
  17437             runtime_error,
  17438             DesktopAppRuntimeCommandError::PackDayBatchPrint(
  17439                 PackDayBatchPrintError::QueueExit { .. }
  17440             )
  17441         ));
  17442 
  17443         let summary = runtime.summary();
  17444         let batch_print = &summary.pack_day_projection.batch_print;
  17445         assert_eq!(batch_print.status, PackDayBatchPrintStatus::Failed);
  17446         assert_eq!(batch_print.request, Some(request));
  17447         assert_eq!(batch_print.failed_artifact, Some(failed_artifact));
  17448         assert_eq!(
  17449             batch_print.failure,
  17450             Some(PackDayBatchPrintFailureKind::QueueExit)
  17451         );
  17452 
  17453         cleanup_bootstrapped_runtime_paths(&paths);
  17454     }
  17455 
  17456     #[test]
  17457     fn runtime_finish_pack_day_print_records_failures_in_state() {
  17458         let (runtime, paths) = bootstrapped_runtime("pack_day_print_failure");
  17459         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17460 
  17461         seed_order_workspace(&runtime, farm_id);
  17462         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17463         assert!(
  17464             runtime
  17465                 .export_pack_day()
  17466                 .expect("pack day export should succeed")
  17467         );
  17468 
  17469         let (request, _) = runtime
  17470             .prepare_pack_day_print(PackDayPrintKind::PrintPackSheet)
  17471             .expect("print should prepare")
  17472             .expect("print should produce a plan");
  17473 
  17474         let error = runtime
  17475             .finish_pack_day_print(request.clone(), Err(PackDayPrintError::UnsupportedPlatform))
  17476             .expect_err("print failure should surface");
  17477         assert!(matches!(
  17478             error,
  17479             DesktopAppRuntimeCommandError::PackDayPrint(PackDayPrintError::UnsupportedPlatform)
  17480         ));
  17481 
  17482         let summary = runtime.summary();
  17483         assert_eq!(
  17484             summary.pack_day_projection.print.status,
  17485             PackDayPrintStatus::Failed
  17486         );
  17487         assert_eq!(summary.pack_day_projection.print.request, Some(request));
  17488         assert_eq!(summary.pack_day_projection.print.failure, None);
  17489 
  17490         cleanup_bootstrapped_runtime_paths(&paths);
  17491     }
  17492 
  17493     #[test]
  17494     fn runtime_prepare_pack_day_print_surfaces_customer_label_overflow_as_a_typed_failure() {
  17495         let (runtime, paths) = bootstrapped_runtime("pack_day_print_overflow_failure");
  17496         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17497 
  17498         seed_order_workspace(&runtime, farm_id);
  17499         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17500         assert!(
  17501             runtime
  17502                 .export_pack_day()
  17503                 .expect("pack day export should succeed")
  17504         );
  17505 
  17506         let bundle = runtime
  17507             .summary()
  17508             .pack_day_projection
  17509             .export
  17510             .bundle
  17511             .clone()
  17512             .expect("pack day export bundle");
  17513         let customer_labels_path =
  17514             PathBuf::from(&bundle.bundle_directory).join("customer_labels.txt");
  17515         fs::write(
  17516             &customer_labels_path,
  17517             "Willow farm\nCasey\nOrder R-1001\nPickup barn\nThursday\nKeep cold\nOverflow note\n",
  17518         )
  17519         .expect("overflowing customer labels should write");
  17520 
  17521         let error = runtime
  17522             .prepare_pack_day_print(PackDayPrintKind::PrintCustomerLabels)
  17523             .expect_err("overflowing customer labels should fail");
  17524         assert!(matches!(
  17525             error,
  17526             DesktopAppRuntimeCommandError::PackDayPrint(
  17527                 PackDayPrintError::CustomerLabelsAvery5160Overflow
  17528             )
  17529         ));
  17530 
  17531         let summary = runtime.summary();
  17532         let print = &summary.pack_day_projection.print;
  17533         assert_eq!(print.status, PackDayPrintStatus::Failed);
  17534         assert_eq!(
  17535             print.request.as_ref().map(|request| request.kind),
  17536             Some(PackDayPrintKind::PrintCustomerLabels)
  17537         );
  17538         assert_eq!(
  17539             print
  17540                 .request
  17541                 .as_ref()
  17542                 .map(|request| request.export_instance_id),
  17543             Some(bundle.export_instance_id)
  17544         );
  17545         assert_eq!(
  17546             print.failure,
  17547             Some(PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow)
  17548         );
  17549 
  17550         cleanup_bootstrapped_runtime_paths(&paths);
  17551     }
  17552 
  17553     #[test]
  17554     fn runtime_finish_pack_day_print_cleans_customer_label_assets_and_keeps_cleanup_failures_best_effort()
  17555      {
  17556         let (runtime, paths) = bootstrapped_runtime("pack_day_print_cleanup");
  17557         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17558 
  17559         seed_order_workspace(&runtime, farm_id);
  17560         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17561         assert!(
  17562             runtime
  17563                 .export_pack_day()
  17564                 .expect("pack day export should succeed")
  17565         );
  17566 
  17567         let (success_request, success_plan) = runtime
  17568             .prepare_pack_day_print(PackDayPrintKind::PrintCustomerLabels)
  17569             .expect("customer labels should prepare")
  17570             .expect("customer labels plan should exist");
  17571         let success_directory = success_plan
  17572             .target_path
  17573             .parent()
  17574             .expect("prepared asset parent")
  17575             .to_path_buf();
  17576         assert!(success_directory.is_dir());
  17577 
  17578         assert!(
  17579             runtime
  17580                 .finish_pack_day_print(success_request, Ok(()))
  17581                 .expect("print success should apply")
  17582         );
  17583         assert!(!success_directory.exists());
  17584 
  17585         let (failure_request, failure_plan) = runtime
  17586             .prepare_pack_day_print(PackDayPrintKind::PrintCustomerLabels)
  17587             .expect("customer labels should prepare again")
  17588             .expect("customer labels plan should exist again");
  17589         let failure_directory = failure_plan
  17590             .target_path
  17591             .parent()
  17592             .expect("prepared asset parent")
  17593             .to_path_buf();
  17594         fs::remove_file(&failure_plan.target_path).expect("prepared asset should remove");
  17595         fs::remove_dir_all(&failure_directory).expect("prepared asset directory should remove");
  17596         fs::write(&failure_directory, "blocked").expect("cleanup blocker should write");
  17597 
  17598         let error = runtime
  17599             .finish_pack_day_print(
  17600                 failure_request.clone(),
  17601                 Err(PackDayPrintError::UnsupportedPlatform),
  17602             )
  17603             .expect_err("print failure should surface");
  17604         assert!(matches!(
  17605             error,
  17606             DesktopAppRuntimeCommandError::PackDayPrint(PackDayPrintError::UnsupportedPlatform)
  17607         ));
  17608         assert!(failure_directory.is_file());
  17609 
  17610         let summary = runtime.summary();
  17611         assert_eq!(
  17612             summary.pack_day_projection.print.status,
  17613             PackDayPrintStatus::Failed
  17614         );
  17615         assert_eq!(
  17616             summary.pack_day_projection.print.request,
  17617             Some(failure_request)
  17618         );
  17619 
  17620         let _ = fs::remove_file(&failure_directory);
  17621         cleanup_bootstrapped_runtime_paths(&paths);
  17622     }
  17623 
  17624     #[test]
  17625     fn runtime_reexport_pack_day_cleans_previous_customer_label_prepared_assets() {
  17626         let (runtime, paths) = bootstrapped_runtime("pack_day_print_reexport_cleanup");
  17627         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17628 
  17629         seed_order_workspace(&runtime, farm_id);
  17630         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17631         assert!(
  17632             runtime
  17633                 .export_pack_day()
  17634                 .expect("initial pack day export should succeed")
  17635         );
  17636         let first_bundle = runtime
  17637             .summary()
  17638             .pack_day_projection
  17639             .export
  17640             .bundle
  17641             .clone()
  17642             .expect("initial export bundle");
  17643 
  17644         let (_request, plan) = runtime
  17645             .prepare_pack_day_print(PackDayPrintKind::PrintCustomerLabels)
  17646             .expect("customer labels should prepare")
  17647             .expect("customer labels plan should exist");
  17648         let prepared_directory = plan
  17649             .target_path
  17650             .parent()
  17651             .expect("prepared asset parent")
  17652             .to_path_buf();
  17653         assert!(prepared_directory.is_dir());
  17654         let _ = runtime
  17655             .lock_state_mut()
  17656             .state_store
  17657             .apply_in_memory(AppStateCommand::reset_pack_day_print());
  17658 
  17659         assert!(
  17660             runtime
  17661                 .export_pack_day()
  17662                 .expect("replacement pack day export should succeed")
  17663         );
  17664 
  17665         let summary = runtime.summary();
  17666         let replacement_bundle = summary
  17667             .pack_day_projection
  17668             .export
  17669             .bundle
  17670             .as_ref()
  17671             .expect("replacement export bundle");
  17672         assert_ne!(
  17673             replacement_bundle.export_instance_id,
  17674             first_bundle.export_instance_id
  17675         );
  17676         assert!(!prepared_directory.exists());
  17677 
  17678         cleanup_bootstrapped_runtime_paths(&paths);
  17679     }
  17680 
  17681     #[test]
  17682     fn runtime_pack_day_window_change_cleans_previous_customer_label_prepared_assets() {
  17683         let (runtime, paths) = bootstrapped_runtime("pack_day_print_window_cleanup");
  17684         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17685         let (fulfillment_window_id, _) = seed_order_workspace(&runtime, farm_id);
  17686         let (other_fulfillment_window_id, _) =
  17687             seed_second_order_workspace(&runtime, farm_id, fulfillment_window_id);
  17688 
  17689         assert!(
  17690             runtime
  17691                 .open_pack_day(Some(fulfillment_window_id))
  17692                 .expect("first pack day window should open")
  17693         );
  17694         assert!(
  17695             runtime
  17696                 .export_pack_day()
  17697                 .expect("initial pack day export should succeed")
  17698         );
  17699 
  17700         let (_request, plan) = runtime
  17701             .prepare_pack_day_print(PackDayPrintKind::PrintCustomerLabels)
  17702             .expect("customer labels should prepare")
  17703             .expect("customer labels plan should exist");
  17704         let prepared_directory = plan
  17705             .target_path
  17706             .parent()
  17707             .expect("prepared asset parent")
  17708             .to_path_buf();
  17709         assert!(prepared_directory.is_dir());
  17710         let _ = runtime
  17711             .lock_state_mut()
  17712             .state_store
  17713             .apply_in_memory(AppStateCommand::reset_pack_day_print());
  17714 
  17715         assert!(
  17716             runtime
  17717                 .open_pack_day(Some(other_fulfillment_window_id))
  17718                 .expect("second pack day window should open")
  17719         );
  17720 
  17721         let summary = runtime.summary();
  17722         assert_eq!(
  17723             summary.pack_day_projection.query.fulfillment_window_id,
  17724             Some(other_fulfillment_window_id)
  17725         );
  17726         assert_eq!(
  17727             summary.pack_day_projection.export.status,
  17728             PackDayExportStatus::Idle
  17729         );
  17730         assert!(!prepared_directory.exists());
  17731 
  17732         cleanup_bootstrapped_runtime_paths(&paths);
  17733     }
  17734 
  17735     #[test]
  17736     fn runtime_finish_pack_day_print_ignores_stale_background_completion() {
  17737         let (runtime, paths) = bootstrapped_runtime("pack_day_print_stale");
  17738         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17739 
  17740         seed_order_workspace(&runtime, farm_id);
  17741         assert!(runtime.open_pack_day(None).expect("pack day should open"));
  17742         assert!(
  17743             runtime
  17744                 .export_pack_day()
  17745                 .expect("pack day export should succeed")
  17746         );
  17747 
  17748         let (request, _) = runtime
  17749             .prepare_pack_day_print(PackDayPrintKind::PrintPickupRoster)
  17750             .expect("print should prepare")
  17751             .expect("print should produce a plan");
  17752 
  17753         let _ = runtime
  17754             .lock_state_mut()
  17755             .state_store
  17756             .apply_in_memory(AppStateCommand::reset_pack_day_print());
  17757 
  17758         assert!(
  17759             !runtime
  17760                 .finish_pack_day_print(request, Ok(()))
  17761                 .expect("stale completion should no-op")
  17762         );
  17763         assert_eq!(
  17764             runtime.summary().pack_day_projection.print.status,
  17765             PackDayPrintStatus::Idle
  17766         );
  17767 
  17768         cleanup_bootstrapped_runtime_paths(&paths);
  17769     }
  17770 
  17771     #[test]
  17772     fn runtime_threads_canonical_seller_reminders_across_today_orders_and_pack_day() {
  17773         let runtime = memory_runtime();
  17774         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17775         seed_order_workspace(&runtime, farm_id);
  17776 
  17777         assert!(runtime.open_orders().expect("orders should open"));
  17778         let summary = runtime.summary();
  17779 
  17780         assert_eq!(summary.today_projection.reminders.items.len(), 1);
  17781         assert_eq!(
  17782             summary.today_projection.reminders.items[0].kind,
  17783             ReminderKind::FulfillmentWindow
  17784         );
  17785         assert_eq!(summary.orders_projection.reminders.items.len(), 1);
  17786         assert_eq!(
  17787             summary.orders_projection.reminders.items[0].kind,
  17788             ReminderKind::OrderAction
  17789         );
  17790         assert_eq!(
  17791             summary.pack_day_projection.projection.reminders.items.len(),
  17792             1
  17793         );
  17794         assert_eq!(
  17795             summary.pack_day_projection.projection.reminders.items[0].kind,
  17796             ReminderKind::FulfillmentWindow
  17797         );
  17798     }
  17799 
  17800     #[test]
  17801     fn runtime_sync_refresh_threads_sync_reminders_into_orders_projection() {
  17802         let runtime = memory_runtime();
  17803         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17804         let (_, order_id) = seed_order_workspace(&runtime, farm_id);
  17805 
  17806         assert!(runtime.open_orders().expect("orders should open"));
  17807         assert!(
  17808             runtime
  17809                 .lock_state_mut()
  17810                 .enqueue_selected_account_sync_operations(vec![pending_sync_upsert(
  17811                     SyncAggregateRef::Order(order_id),
  17812                     json!({
  17813                         "aggregate_kind": "order",
  17814                         "order_id": order_id.to_string(),
  17815                         "source": "test_pending_order_sync",
  17816                     })
  17817                     .to_string(),
  17818                 )])
  17819                 .expect("pending order sync should enqueue")
  17820         );
  17821         let summary = runtime.summary();
  17822 
  17823         assert_eq!(summary.sync_status.pending_write_count, 1);
  17824         assert!(
  17825             summary
  17826                 .orders_projection
  17827                 .reminders
  17828                 .items
  17829                 .iter()
  17830                 .any(|item| item.kind == ReminderKind::SyncImpact
  17831                     && item.title == "Pending local changes")
  17832         );
  17833     }
  17834 
  17835     #[test]
  17836     fn runtime_refresh_promotes_blocking_sync_reminders_into_presented_log_entries() {
  17837         let runtime = memory_runtime();
  17838         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  17839 
  17840         runtime
  17841             .lock_state()
  17842             .sqlite_store
  17843             .as_ref()
  17844             .expect("sqlite store")
  17845             .record_sync_conflict(
  17846                 account_id.as_str(),
  17847                 &SyncConflict {
  17848                     aggregate: SyncAggregateRef::Farm(farm_id),
  17849                     kind: SyncConflictKind::RevisionMismatch,
  17850                     severity: SyncConflictSeverity::Blocking,
  17851                     resolution: SyncConflictResolutionStatus::Unresolved,
  17852                     local_payload_json: "{\"farm\":\"local\"}".to_owned(),
  17853                     remote_payload_json: Some("{\"farm\":\"remote\"}".to_owned()),
  17854                     detected_at: "2026-04-20T20:10:00Z".to_owned(),
  17855                     resolved_at: None,
  17856                 },
  17857             )
  17858             .expect("blocking conflict should save");
  17859 
  17860         assert!(
  17861             runtime
  17862                 .lock_state_mut()
  17863                 .refresh_selected_account_sync()
  17864                 .expect("sync status should refresh")
  17865         );
  17866 
  17867         let summary = runtime.summary();
  17868         let reminder = summary
  17869             .orders_projection
  17870             .reminders
  17871             .items
  17872             .iter()
  17873             .find(|item| item.kind == ReminderKind::SyncImpact)
  17874             .expect("sync reminder");
  17875 
  17876         assert_eq!(reminder.delivery_state, ReminderDeliveryState::Presented);
  17877         assert!(summary.reminder_log.entries.iter().any(|entry| {
  17878             entry.reminder_id == reminder.reminder_id
  17879                 && entry.delivery_state == ReminderDeliveryState::Presented
  17880         }));
  17881     }
  17882 
  17883     #[test]
  17884     fn runtime_resolving_an_acknowledged_reminder_records_the_resolved_log_entry() {
  17885         let runtime = memory_runtime();
  17886         let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
  17887 
  17888         let conflict_id = runtime
  17889             .lock_state()
  17890             .sqlite_store
  17891             .as_ref()
  17892             .expect("sqlite store")
  17893             .record_sync_conflict(
  17894                 account_id.as_str(),
  17895                 &SyncConflict {
  17896                     aggregate: SyncAggregateRef::Farm(farm_id),
  17897                     kind: SyncConflictKind::RevisionMismatch,
  17898                     severity: SyncConflictSeverity::Blocking,
  17899                     resolution: SyncConflictResolutionStatus::Unresolved,
  17900                     local_payload_json: "{\"farm\":\"local\"}".to_owned(),
  17901                     remote_payload_json: Some("{\"farm\":\"remote\"}".to_owned()),
  17902                     detected_at: "2026-04-20T20:15:00Z".to_owned(),
  17903                     resolved_at: None,
  17904                 },
  17905             )
  17906             .expect("blocking conflict should save");
  17907         assert!(
  17908             runtime
  17909                 .lock_state_mut()
  17910                 .refresh_selected_account_sync()
  17911                 .expect("sync status should refresh")
  17912         );
  17913 
  17914         let reminder_id = runtime
  17915             .summary()
  17916             .orders_projection
  17917             .reminders
  17918             .items
  17919             .iter()
  17920             .find(|item| item.kind == ReminderKind::SyncImpact)
  17921             .expect("sync reminder")
  17922             .reminder_id;
  17923         assert!(
  17924             runtime
  17925                 .acknowledge_reminder(reminder_id)
  17926                 .expect("reminder should acknowledge")
  17927         );
  17928 
  17929         let acknowledged_summary = runtime.summary();
  17930         assert!(
  17931             acknowledged_summary
  17932                 .orders_projection
  17933                 .reminders
  17934                 .items
  17935                 .iter()
  17936                 .any(|item| {
  17937                     item.reminder_id == reminder_id
  17938                         && item.delivery_state == ReminderDeliveryState::Acknowledged
  17939                 })
  17940         );
  17941         assert!(
  17942             acknowledged_summary
  17943                 .reminder_log
  17944                 .entries
  17945                 .iter()
  17946                 .any(|entry| {
  17947                     entry.reminder_id == reminder_id
  17948                         && entry.delivery_state == ReminderDeliveryState::Acknowledged
  17949                 })
  17950         );
  17951 
  17952         assert!(
  17953             runtime
  17954                 .resolve_sync_conflict(
  17955                     conflict_id.as_str(),
  17956                     SyncConflictResolutionStatus::AcceptedLocal,
  17957                 )
  17958                 .expect("conflict resolution should succeed")
  17959         );
  17960 
  17961         let resolved_summary = runtime.summary();
  17962         assert!(
  17963             resolved_summary
  17964                 .orders_projection
  17965                 .reminders
  17966                 .items
  17967                 .iter()
  17968                 .all(|item| { item.reminder_id != reminder_id })
  17969         );
  17970         assert!(resolved_summary.reminder_log.entries.iter().any(|entry| {
  17971             entry.reminder_id == reminder_id
  17972                 && entry.delivery_state == ReminderDeliveryState::Resolved
  17973         }));
  17974     }
  17975 
  17976     #[test]
  17977     fn reminder_urgency_marks_due_soon_and_overdue_deadlines() {
  17978         let due_soon = (Utc::now() + Duration::hours(24))
  17979             .format("%Y-%m-%dT%H:%M:%SZ")
  17980             .to_string();
  17981         let overdue = (Utc::now() - Duration::hours(2))
  17982             .format("%Y-%m-%dT%H:%M:%SZ")
  17983             .to_string();
  17984 
  17985         assert_eq!(
  17986             super::reminder_urgency(due_soon.as_str()),
  17987             super::ReminderUrgency::DueSoon
  17988         );
  17989         assert_eq!(
  17990             super::reminder_urgency(overdue.as_str()),
  17991             super::ReminderUrgency::Overdue
  17992         );
  17993     }
  17994 
  17995     #[test]
  17996     fn runtime_open_orders_resets_to_default_queue_and_clears_detail() {
  17997         let runtime = memory_runtime();
  17998         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  17999         let (_, order_id) = seed_order_workspace(&runtime, farm_id);
  18000 
  18001         assert!(
  18002             runtime
  18003                 .select_orders_filter(OrdersFilter::Packed)
  18004                 .expect("orders filter should update")
  18005         );
  18006         assert!(
  18007             runtime
  18008                 .open_order_detail(order_id)
  18009                 .expect("order detail should open")
  18010         );
  18011 
  18012         assert!(runtime.open_orders().expect("orders should reopen"));
  18013         let summary = runtime.summary();
  18014 
  18015         assert_eq!(
  18016             summary.shell_projection.selected_section,
  18017             ShellSection::Farmer(FarmerSection::Orders)
  18018         );
  18019         assert_eq!(
  18020             summary.orders_projection.query.filter,
  18021             OrdersFilter::NeedsAction
  18022         );
  18023         assert_eq!(summary.orders_projection.list.rows.len(), 1);
  18024         assert!(summary.orders_projection.detail.is_none());
  18025     }
  18026 
  18027     #[test]
  18028     fn runtime_open_orders_fulfillment_window_filters_the_queue_to_one_window() {
  18029         let runtime = memory_runtime();
  18030         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  18031         let (fulfillment_window_id, order_id) = seed_order_workspace(&runtime, farm_id);
  18032         let other_fulfillment_window_id = FulfillmentWindowId::new();
  18033         let other_order_id = OrderId::new();
  18034         let sql = format!(
  18035             "insert into fulfillment_windows (
  18036                 id,
  18037                 farm_id,
  18038                 starts_at,
  18039                 ends_at,
  18040                 capacity_limit,
  18041                 created_at,
  18042                 updated_at,
  18043                 pickup_location_id,
  18044                 label,
  18045                 order_cutoff_at
  18046              )
  18047              select
  18048                 '{other_fulfillment_window_id}',
  18049                 farm_id,
  18050                 '2099-04-19T16:00:00Z',
  18051                 '2099-04-19T18:00:00Z',
  18052                 capacity_limit,
  18053                 '2099-04-19T16:00:00Z',
  18054                 '2099-04-19T16:00:00Z',
  18055                 pickup_location_id,
  18056                 'Saturday pickup',
  18057                 '2099-04-18T18:00:00Z'
  18058              from fulfillment_windows
  18059              where id = '{fulfillment_window_id}' and farm_id = '{farm_id}';
  18060              insert into orders (
  18061                 id,
  18062                 farm_id,
  18063                 fulfillment_window_id,
  18064                 order_number,
  18065                 customer_display_name,
  18066                 status,
  18067                 updated_at
  18068              ) values (
  18069                 '{other_order_id}',
  18070                 '{farm_id}',
  18071                 '{other_fulfillment_window_id}',
  18072                 'R-101',
  18073                 'Robin',
  18074                 'scheduled',
  18075                 '2026-04-17T11:00:00Z'
  18076              )"
  18077         );
  18078         runtime
  18079             .lock_state()
  18080             .sqlite_store
  18081             .as_ref()
  18082             .expect("sqlite store")
  18083             .connection()
  18084             .execute_batch(&sql)
  18085             .expect("second orders workspace should seed");
  18086 
  18087         assert!(
  18088             runtime
  18089                 .open_orders_fulfillment_window(fulfillment_window_id)
  18090                 .expect("orders window follow-on should route")
  18091         );
  18092         let summary = runtime.summary();
  18093 
  18094         assert_eq!(
  18095             summary.shell_projection.selected_section,
  18096             ShellSection::Farmer(FarmerSection::Orders)
  18097         );
  18098         assert_eq!(summary.orders_projection.query.filter, OrdersFilter::All);
  18099         assert_eq!(
  18100             summary.orders_projection.query.fulfillment_window_id,
  18101             Some(fulfillment_window_id)
  18102         );
  18103         assert_eq!(summary.orders_projection.list.rows.len(), 1);
  18104         assert_eq!(summary.orders_projection.list.rows[0].order_id, order_id);
  18105         assert!(summary.orders_projection.detail.is_none());
  18106     }
  18107 
  18108     #[test]
  18109     fn runtime_order_filters_refresh_repository_backed_orders_projection() {
  18110         let runtime = memory_runtime();
  18111         let (_, farm_id) = provision_ready_farmer_account(&runtime);
  18112         let (fulfillment_window_id, scheduled_order_id) = seed_order_workspace(&runtime, farm_id);
  18113         let packed_order_id = OrderId::new();
  18114         let completed_order_id = OrderId::new();
  18115 
  18116         let sql = format!(
  18117             "update orders
  18118              set status = 'scheduled', updated_at = '2026-04-17T12:00:00Z'
  18119              where id = '{scheduled_order_id}' and farm_id = '{farm_id}';
  18120              insert into orders (
  18121                 id,
  18122                 farm_id,
  18123                 fulfillment_window_id,
  18124                 order_number,
  18125                 customer_display_name,
  18126                 status,
  18127                 updated_at
  18128              ) values (
  18129                 '{packed_order_id}',
  18130                 '{farm_id}',
  18131                 '{fulfillment_window_id}',
  18132                 'R-101',
  18133                 'Taylor',
  18134                 'packed',
  18135                 '2026-04-17T12:30:00Z'
  18136              );
  18137              insert into orders (
  18138                 id,
  18139                 farm_id,
  18140                 fulfillment_window_id,
  18141                 order_number,
  18142                 customer_display_name,
  18143                 status,
  18144                 updated_at
  18145              ) values (
  18146                 '{completed_order_id}',
  18147                 '{farm_id}',
  18148                 '{fulfillment_window_id}',
  18149                 'R-102',
  18150                 'Morgan',
  18151                 'completed',
  18152                 '2026-04-17T13:00:00Z'
  18153              )"
  18154         );
  18155         runtime
  18156             .lock_state()
  18157             .sqlite_store
  18158             .as_ref()
  18159             .expect("sqlite store")
  18160             .connection()
  18161             .execute_batch(&sql)
  18162             .expect("order should update to scheduled");
  18163 
  18164         assert!(
  18165             runtime
  18166                 .select_orders_filter(OrdersFilter::Scheduled)
  18167                 .expect("scheduled filter should apply")
  18168         );
  18169         assert_eq!(runtime.summary().orders_projection.list.rows.len(), 1);
  18170         assert_eq!(
  18171             runtime.summary().orders_projection.list.rows[0].status,
  18172             OrderStatus::Scheduled
  18173         );
  18174 
  18175         assert!(
  18176             runtime
  18177                 .open_order_detail(scheduled_order_id)
  18178                 .expect("order detail should open")
  18179         );
  18180         let scheduled_detail_summary = runtime.summary();
  18181         assert_eq!(
  18182             scheduled_detail_summary
  18183                 .orders_projection
  18184                 .detail
  18185                 .as_ref()
  18186                 .expect("scheduled detail")
  18187                 .status,
  18188             OrderStatus::Scheduled
  18189         );
  18190         assert_eq!(
  18191             scheduled_detail_summary
  18192                 .orders_projection
  18193                 .list
  18194                 .summary
  18195                 .scheduled_orders,
  18196             1
  18197         );
  18198         assert_eq!(
  18199             scheduled_detail_summary
  18200                 .orders_projection
  18201                 .list
  18202                 .summary
  18203                 .packed_orders,
  18204             1
  18205         );
  18206 
  18207         assert!(
  18208             runtime
  18209                 .select_orders_filter(OrdersFilter::Packed)
  18210                 .expect("packed filter should apply")
  18211         );
  18212         assert_eq!(runtime.summary().orders_projection.list.rows.len(), 1);
  18213         assert_eq!(
  18214             runtime.summary().orders_projection.list.rows[0].status,
  18215             OrderStatus::Packed
  18216         );
  18217 
  18218         assert!(
  18219             runtime
  18220                 .open_order_detail(packed_order_id)
  18221                 .expect("packed detail should open")
  18222         );
  18223         let packed_detail_summary = runtime.summary();
  18224         assert_eq!(
  18225             packed_detail_summary
  18226                 .orders_projection
  18227                 .detail
  18228                 .as_ref()
  18229                 .expect("packed detail")
  18230                 .status,
  18231             OrderStatus::Packed
  18232         );
  18233 
  18234         assert!(
  18235             runtime
  18236                 .select_orders_filter(OrdersFilter::Completed)
  18237                 .expect("completed filter should apply")
  18238         );
  18239         assert_eq!(runtime.summary().orders_projection.list.rows.len(), 1);
  18240         assert_eq!(
  18241             runtime.summary().orders_projection.list.rows[0].status,
  18242             OrderStatus::Completed
  18243         );
  18244 
  18245         assert!(
  18246             runtime
  18247                 .open_order_detail(completed_order_id)
  18248                 .expect("completed detail should open")
  18249         );
  18250         assert_eq!(
  18251             runtime
  18252                 .summary()
  18253                 .orders_projection
  18254                 .detail
  18255                 .as_ref()
  18256                 .expect("completed detail")
  18257                 .status,
  18258             OrderStatus::Completed
  18259         );
  18260     }
  18261 
  18262     #[test]
  18263     fn runtime_stock_updates_refresh_today_and_products_projections() {
  18264         let runtime = memory_runtime();
  18265 
  18266         assert!(
  18267             runtime
  18268                 .generate_local_account(Some("Farmer".to_owned()))
  18269                 .expect("account should generate")
  18270         );
  18271         let account_id = runtime
  18272             .summary()
  18273             .settings_account_projection
  18274             .selected_account
  18275             .as_ref()
  18276             .expect("selected account")
  18277             .account
  18278             .account_id
  18279             .clone();
  18280         let farm_id =
  18281             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  18282         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  18283             farm_id,
  18284             display_name: "North field farm".to_owned(),
  18285             readiness: FarmReadiness::Ready,
  18286         });
  18287         runtime
  18288             .lock_state()
  18289             .sqlite_store
  18290             .as_ref()
  18291             .expect("sqlite store")
  18292             .save_farm_summary(
  18293                 farm_setup_projection
  18294                     .saved_farm
  18295                     .as_ref()
  18296                     .expect("saved farm should exist"),
  18297             )
  18298             .expect("farm summary should save");
  18299         runtime
  18300             .lock_state()
  18301             .sqlite_store
  18302             .as_ref()
  18303             .expect("sqlite store")
  18304             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  18305             .expect("farm setup should save");
  18306         seed_product(
  18307             &runtime,
  18308             farm_id,
  18309             "Salad mix",
  18310             "Spring blend",
  18311             "published",
  18312             Some(2),
  18313             "2026-04-18T10:00:00Z",
  18314         );
  18315 
  18316         assert!(
  18317             runtime
  18318                 .select_local_account(account_id.as_str())
  18319                 .expect("account should select")
  18320         );
  18321         let product_id = runtime.summary().products_projection.list.rows[0].product_id;
  18322 
  18323         assert_eq!(
  18324             runtime.summary().today_projection.low_stock_products.len(),
  18325             1
  18326         );
  18327         assert!(
  18328             runtime
  18329                 .update_product_stock(product_id, 12)
  18330                 .expect("stock update should succeed")
  18331         );
  18332 
  18333         let summary = runtime.summary();
  18334         assert_eq!(
  18335             summary.products_projection.list.rows[0].stock.quantity,
  18336             Some(12)
  18337         );
  18338         assert!(summary.today_projection.low_stock_products.is_empty());
  18339     }
  18340 
  18341     #[test]
  18342     fn runtime_open_new_product_editor_creates_a_local_draft_and_opens_it() {
  18343         let runtime = memory_runtime();
  18344 
  18345         assert!(
  18346             runtime
  18347                 .generate_local_account(Some("Farmer".to_owned()))
  18348                 .expect("account should generate")
  18349         );
  18350         let account_id = runtime
  18351             .summary()
  18352             .settings_account_projection
  18353             .selected_account
  18354             .as_ref()
  18355             .expect("selected account")
  18356             .account
  18357             .account_id
  18358             .clone();
  18359         let farm_id =
  18360             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  18361         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  18362             farm_id,
  18363             display_name: "North field farm".to_owned(),
  18364             readiness: FarmReadiness::Ready,
  18365         });
  18366         runtime
  18367             .lock_state()
  18368             .sqlite_store
  18369             .as_ref()
  18370             .expect("sqlite store")
  18371             .save_farm_summary(
  18372                 farm_setup_projection
  18373                     .saved_farm
  18374                     .as_ref()
  18375                     .expect("saved farm should exist"),
  18376             )
  18377             .expect("farm summary should save");
  18378         runtime
  18379             .lock_state()
  18380             .sqlite_store
  18381             .as_ref()
  18382             .expect("sqlite store")
  18383             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  18384             .expect("farm setup should save");
  18385 
  18386         assert!(
  18387             runtime
  18388                 .select_local_account(account_id.as_str())
  18389                 .expect("account should select")
  18390         );
  18391         assert_eq!(
  18392             runtime
  18393                 .summary()
  18394                 .products_projection
  18395                 .list
  18396                 .summary
  18397                 .total_products,
  18398             0
  18399         );
  18400 
  18401         assert!(
  18402             runtime
  18403                 .open_new_product_editor()
  18404                 .expect("new product editor should open")
  18405         );
  18406 
  18407         let summary = runtime.summary();
  18408         assert_eq!(summary.products_projection.list.summary.total_products, 1);
  18409         assert!(matches!(
  18410             summary.products_projection.editor,
  18411             radroots_app_state::ProductEditorState::Open(_)
  18412         ));
  18413         assert_eq!(
  18414             summary.products_projection.list.rows[0].status,
  18415             ProductStatus::Draft
  18416         );
  18417     }
  18418 
  18419     #[test]
  18420     fn runtime_open_existing_and_save_product_editor_refreshes_products_projection() {
  18421         let runtime = memory_runtime();
  18422 
  18423         assert!(
  18424             runtime
  18425                 .generate_local_account(Some("Farmer".to_owned()))
  18426                 .expect("account should generate")
  18427         );
  18428         let account_id = runtime
  18429             .summary()
  18430             .settings_account_projection
  18431             .selected_account
  18432             .as_ref()
  18433             .expect("selected account")
  18434             .account
  18435             .account_id
  18436             .clone();
  18437         let farm_id =
  18438             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  18439         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  18440             farm_id,
  18441             display_name: "North field farm".to_owned(),
  18442             readiness: FarmReadiness::Ready,
  18443         });
  18444         runtime
  18445             .lock_state()
  18446             .sqlite_store
  18447             .as_ref()
  18448             .expect("sqlite store")
  18449             .save_farm_summary(
  18450                 farm_setup_projection
  18451                     .saved_farm
  18452                     .as_ref()
  18453                     .expect("saved farm should exist"),
  18454             )
  18455             .expect("farm summary should save");
  18456         runtime
  18457             .lock_state()
  18458             .sqlite_store
  18459             .as_ref()
  18460             .expect("sqlite store")
  18461             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  18462             .expect("farm setup should save");
  18463         let product_id = seed_product(
  18464             &runtime,
  18465             farm_id,
  18466             "Salad mix",
  18467             "Spring blend",
  18468             "draft",
  18469             Some(2),
  18470             "2026-04-18T10:00:00Z",
  18471         );
  18472 
  18473         assert!(
  18474             runtime
  18475                 .select_local_account(account_id.as_str())
  18476                 .expect("account should select")
  18477         );
  18478         assert!(
  18479             runtime
  18480                 .open_existing_product_editor(product_id)
  18481                 .expect("existing product editor should open")
  18482         );
  18483 
  18484         let saved_draft = ProductEditorDraft {
  18485             title: "Salad mix".to_owned(),
  18486             subtitle: "Washed and boxed".to_owned(),
  18487             category: "greens".to_owned(),
  18488             unit_label: "box".to_owned(),
  18489             price_minor_units: Some(900),
  18490             price_currency: "usd".to_owned(),
  18491             stock_quantity: Some(14),
  18492             availability_window_id: None,
  18493             status: radroots_app_view::ProductStatus::Published,
  18494         };
  18495 
  18496         assert!(
  18497             runtime
  18498                 .save_product_editor_draft(saved_draft.clone())
  18499                 .expect("product editor draft should save")
  18500         );
  18501 
  18502         let summary = runtime.summary();
  18503         assert_eq!(
  18504             summary.products_projection.list.rows[0].subtitle.as_deref(),
  18505             Some("Washed and boxed")
  18506         );
  18507         assert_eq!(
  18508             summary.products_projection.list.rows[0]
  18509                 .price
  18510                 .as_ref()
  18511                 .map(|price| price.amount_minor_units),
  18512             Some(900)
  18513         );
  18514         assert_eq!(
  18515             summary.products_projection.list.rows[0].stock.quantity,
  18516             Some(14)
  18517         );
  18518         assert_eq!(
  18519             runtime
  18520                 .lock_state()
  18521                 .sqlite_store
  18522                 .as_ref()
  18523                 .expect("sqlite store")
  18524                 .load_product_editor_draft(product_id)
  18525                 .expect("saved draft should load"),
  18526             Some(ProductEditorDraft {
  18527                 price_currency: "USD".to_owned(),
  18528                 ..saved_draft
  18529             })
  18530         );
  18531     }
  18532 
  18533     #[test]
  18534     fn runtime_account_commands_refresh_identity_projection() {
  18535         let runtime = memory_runtime();
  18536 
  18537         assert!(
  18538             runtime
  18539                 .generate_local_account(Some("First".to_owned()))
  18540                 .expect("first account should generate")
  18541         );
  18542         let first_summary = runtime.summary();
  18543         let first_account_id = first_summary
  18544             .settings_account_projection
  18545             .selected_account
  18546             .as_ref()
  18547             .expect("first selected account")
  18548             .account
  18549             .account_id
  18550             .clone();
  18551 
  18552         assert!(
  18553             runtime
  18554                 .generate_local_account(Some("Second".to_owned()))
  18555                 .expect("second account should generate")
  18556         );
  18557         let second_summary = runtime.summary();
  18558         let second_account_id = second_summary
  18559             .settings_account_projection
  18560             .selected_account
  18561             .as_ref()
  18562             .expect("second selected account")
  18563             .account
  18564             .account_id
  18565             .clone();
  18566         assert_eq!(second_summary.settings_account_projection.roster.len(), 2);
  18567         assert_eq!(
  18568             second_summary
  18569                 .settings_account_projection
  18570                 .selected_account
  18571                 .as_ref()
  18572                 .and_then(|account| account.account.label.as_deref()),
  18573             Some("Second")
  18574         );
  18575 
  18576         save_surface_activation(
  18577             &runtime,
  18578             second_account_id.as_str(),
  18579             ActiveSurface::Farmer,
  18580             true,
  18581         );
  18582         assert!(
  18583             runtime
  18584                 .select_local_account(second_account_id.as_str())
  18585                 .expect("selection should succeed")
  18586         );
  18587         let selected_summary = runtime.summary();
  18588         assert_eq!(selected_summary.startup_gate, AppStartupGate::Farmer);
  18589         assert_eq!(selected_summary.home_route, HomeRoute::FarmSetupOnboarding);
  18590         assert_eq!(
  18591             selected_summary
  18592                 .settings_account_projection
  18593                 .selected_account
  18594                 .as_ref()
  18595                 .map(|account| account.active_surface()),
  18596             Some(ActiveSurface::Farmer)
  18597         );
  18598 
  18599         assert!(
  18600             runtime
  18601                 .remove_selected_local_key()
  18602                 .expect("selected local key should remove")
  18603         );
  18604         let removed_summary = runtime.summary();
  18605         assert_eq!(removed_summary.settings_account_projection.roster.len(), 1);
  18606         assert_eq!(
  18607             removed_summary
  18608                 .settings_account_projection
  18609                 .selected_account
  18610                 .as_ref()
  18611                 .map(|account| account.account.account_id.as_str()),
  18612             Some(first_account_id.as_str())
  18613         );
  18614         assert_eq!(
  18615             runtime
  18616                 .lock_state()
  18617                 .sqlite_store
  18618                 .as_ref()
  18619                 .expect("sqlite store")
  18620                 .load_surface_activation(second_account_id.as_str())
  18621                 .expect("removed activation should load"),
  18622             None
  18623         );
  18624 
  18625         let imported_identity = RadrootsIdentity::generate();
  18626         assert!(
  18627             runtime
  18628                 .import_local_account(DesktopLocalIdentityImportRequest::raw_secret_key(
  18629                     imported_identity.nsec(),
  18630                 ))
  18631                 .expect("raw import should succeed")
  18632         );
  18633         let imported_summary = runtime.summary();
  18634         assert_eq!(imported_summary.settings_account_projection.roster.len(), 2);
  18635         assert_eq!(
  18636             imported_summary
  18637                 .settings_account_projection
  18638                 .selected_account
  18639                 .as_ref()
  18640                 .map(|account| account.account.account_id.as_str()),
  18641             Some(imported_identity.id().as_str())
  18642         );
  18643     }
  18644 
  18645     #[test]
  18646     fn runtime_select_active_surface_persists_selected_surface() {
  18647         let runtime = memory_runtime();
  18648 
  18649         assert!(
  18650             runtime
  18651                 .generate_local_account(Some("Farmer".to_owned()))
  18652                 .expect("account should generate")
  18653         );
  18654         let account_id = runtime
  18655             .summary()
  18656             .settings_account_projection
  18657             .selected_account
  18658             .as_ref()
  18659             .expect("selected account")
  18660             .account
  18661             .account_id
  18662             .clone();
  18663         save_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer, true);
  18664         assert!(
  18665             runtime
  18666                 .select_local_account(account_id.as_str())
  18667                 .expect("account should select")
  18668         );
  18669         assert_eq!(runtime.summary().startup_gate, AppStartupGate::Farmer);
  18670         assert!(runtime.select_account());
  18671         assert_eq!(
  18672             runtime.summary().shell_projection.selected_section,
  18673             ShellSection::Account
  18674         );
  18675 
  18676         assert!(
  18677             runtime
  18678                 .select_active_surface(ActiveSurface::Personal)
  18679                 .expect("surface should select")
  18680         );
  18681         let personal_summary = runtime.summary();
  18682         assert_eq!(personal_summary.startup_gate, AppStartupGate::Personal);
  18683         assert_eq!(
  18684             personal_summary.shell_projection.active_surface,
  18685             ActiveSurface::Personal
  18686         );
  18687         assert_eq!(
  18688             personal_summary.shell_projection.selected_section,
  18689             ShellSection::Personal(PersonalSection::Browse)
  18690         );
  18691         assert_eq!(
  18692             personal_summary
  18693                 .settings_account_projection
  18694                 .selected_account
  18695                 .as_ref()
  18696                 .map(|account| account.active_surface()),
  18697             Some(ActiveSurface::Personal)
  18698         );
  18699         assert_eq!(
  18700             runtime
  18701                 .lock_state()
  18702                 .sqlite_store
  18703                 .as_ref()
  18704                 .expect("sqlite store")
  18705                 .load_surface_activation(account_id.as_str())
  18706                 .expect("surface activation should load")
  18707                 .expect("surface activation should exist")
  18708                 .active_surface(),
  18709             ActiveSurface::Personal
  18710         );
  18711 
  18712         assert!(runtime.select_account());
  18713         assert_eq!(
  18714             runtime.summary().shell_projection.selected_section,
  18715             ShellSection::Account
  18716         );
  18717         assert!(
  18718             runtime
  18719                 .select_active_surface(ActiveSurface::Farmer)
  18720                 .expect("surface should reselect")
  18721         );
  18722         let farmer_summary = runtime.summary();
  18723         assert_eq!(farmer_summary.startup_gate, AppStartupGate::Farmer);
  18724         assert_eq!(
  18725             farmer_summary.shell_projection.active_surface,
  18726             ActiveSurface::Farmer
  18727         );
  18728         assert_eq!(
  18729             farmer_summary.shell_projection.selected_section,
  18730             ShellSection::Farmer(FarmerSection::Today)
  18731         );
  18732         assert_eq!(
  18733             farmer_summary
  18734                 .settings_account_projection
  18735                 .selected_account
  18736                 .as_ref()
  18737                 .map(|account| account.active_surface()),
  18738             Some(ActiveSurface::Farmer)
  18739         );
  18740         assert_eq!(
  18741             runtime
  18742                 .lock_state()
  18743                 .sqlite_store
  18744                 .as_ref()
  18745                 .expect("sqlite store")
  18746                 .load_surface_activation(account_id.as_str())
  18747                 .expect("surface activation should load")
  18748                 .expect("surface activation should exist")
  18749                 .active_surface(),
  18750             ActiveSurface::Farmer
  18751         );
  18752     }
  18753 
  18754     #[test]
  18755     fn selecting_farmer_account_loads_persisted_farm_setup_draft() {
  18756         let runtime = memory_runtime();
  18757 
  18758         assert!(
  18759             runtime
  18760                 .generate_local_account(Some("Farmer".to_owned()))
  18761                 .expect("account should generate")
  18762         );
  18763         let account_id = runtime
  18764             .summary()
  18765             .settings_account_projection
  18766             .selected_account
  18767             .as_ref()
  18768             .expect("selected account")
  18769             .account
  18770             .account_id
  18771             .clone();
  18772         let projection = FarmSetupProjection::from_draft(FarmSetupDraft::new(
  18773             "North field farm",
  18774             "Stockholm County",
  18775             [FarmOrderMethod::Pickup],
  18776         ));
  18777         runtime
  18778             .lock_state()
  18779             .sqlite_store
  18780             .as_ref()
  18781             .expect("sqlite store")
  18782             .save_farm_setup(account_id.as_str(), &projection)
  18783             .expect("farm setup should save");
  18784         save_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer, true);
  18785 
  18786         assert!(
  18787             runtime
  18788                 .select_local_account(account_id.as_str())
  18789                 .expect("account should select")
  18790         );
  18791         let summary = runtime.summary();
  18792 
  18793         assert_eq!(summary.startup_gate, AppStartupGate::Farmer);
  18794         assert_eq!(summary.home_route, HomeRoute::FarmSetupForm);
  18795         assert_eq!(summary.farm_setup_projection, projection);
  18796     }
  18797 
  18798     #[test]
  18799     fn finishing_farm_setup_persists_saved_farm_and_today_projection() {
  18800         let runtime = memory_runtime();
  18801 
  18802         assert!(
  18803             runtime
  18804                 .generate_local_account(Some("Farmer".to_owned()))
  18805                 .expect("account should generate")
  18806         );
  18807         let account_id = runtime
  18808             .summary()
  18809             .settings_account_projection
  18810             .selected_account
  18811             .as_ref()
  18812             .expect("selected account")
  18813             .account
  18814             .account_id
  18815             .clone();
  18816         let farm_id =
  18817             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  18818         assert!(
  18819             runtime
  18820                 .select_local_account(account_id.as_str())
  18821                 .expect("account should select")
  18822         );
  18823         assert_eq!(runtime.summary().home_route, HomeRoute::FarmSetupOnboarding);
  18824 
  18825         let draft = FarmSetupDraft::new(
  18826             "North field farm",
  18827             "Stockholm County",
  18828             [FarmOrderMethod::Pickup, FarmOrderMethod::Delivery],
  18829         );
  18830         assert_eq!(
  18831             runtime
  18832                 .save_farm_setup_draft(draft.clone())
  18833                 .expect("draft should save")
  18834                 .draft,
  18835             draft
  18836         );
  18837         assert_eq!(runtime.summary().home_route, HomeRoute::FarmSetupForm);
  18838 
  18839         let finished_projection = runtime
  18840             .finish_farm_setup()
  18841             .expect("farm setup should finish");
  18842         let summary = runtime.summary();
  18843 
  18844         assert_eq!(summary.home_route, HomeRoute::Today);
  18845         assert_eq!(
  18846             finished_projection.saved_farm,
  18847             Some(FarmSummary {
  18848                 farm_id,
  18849                 display_name: "North field farm".to_owned(),
  18850                 readiness: FarmReadiness::Incomplete,
  18851             })
  18852         );
  18853         assert_eq!(
  18854             summary.today_projection.farm,
  18855             finished_projection.saved_farm.clone()
  18856         );
  18857         assert_eq!(summary.today_projection.setup_checklist.len(), 6);
  18858         assert_eq!(
  18859             runtime
  18860                 .lock_state()
  18861                 .sqlite_store
  18862                 .as_ref()
  18863                 .expect("sqlite store")
  18864                 .load_farm_setup(account_id.as_str())
  18865                 .expect("farm setup should load"),
  18866             finished_projection
  18867         );
  18868         assert_eq!(
  18869             runtime
  18870                 .lock_state()
  18871                 .sqlite_store
  18872                 .as_ref()
  18873                 .expect("sqlite store")
  18874                 .load_today_agenda(Some(farm_id))
  18875                 .expect("today agenda should load")
  18876                 .farm,
  18877             finished_projection.saved_farm
  18878         );
  18879     }
  18880 
  18881     #[test]
  18882     fn loading_farm_rules_projection_seeds_profile_from_saved_farm() {
  18883         let runtime = memory_runtime();
  18884 
  18885         assert!(
  18886             runtime
  18887                 .generate_local_account(Some("Farmer".to_owned()))
  18888                 .expect("account should generate")
  18889         );
  18890         let account_id = runtime
  18891             .summary()
  18892             .settings_account_projection
  18893             .selected_account
  18894             .as_ref()
  18895             .expect("selected account")
  18896             .account
  18897             .account_id
  18898             .clone();
  18899         let farm_id =
  18900             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  18901         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  18902             farm_id,
  18903             display_name: "North field farm".to_owned(),
  18904             readiness: FarmReadiness::Incomplete,
  18905         });
  18906         runtime
  18907             .lock_state()
  18908             .sqlite_store
  18909             .as_ref()
  18910             .expect("sqlite store")
  18911             .save_farm_summary(
  18912                 farm_setup_projection
  18913                     .saved_farm
  18914                     .as_ref()
  18915                     .expect("saved farm should exist"),
  18916             )
  18917             .expect("farm summary should save");
  18918         runtime
  18919             .lock_state()
  18920             .sqlite_store
  18921             .as_ref()
  18922             .expect("sqlite store")
  18923             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  18924             .expect("farm setup should save");
  18925 
  18926         assert!(
  18927             runtime
  18928                 .select_local_account(account_id.as_str())
  18929                 .expect("account should select")
  18930         );
  18931 
  18932         let projection = runtime
  18933             .load_farm_rules_projection()
  18934             .expect("farm rules projection should load");
  18935 
  18936         assert_eq!(
  18937             projection.farm_profile,
  18938             Some(FarmProfileRecord {
  18939                 farm_id,
  18940                 display_name: "North field farm".to_owned(),
  18941                 timezone: "UTC".to_owned(),
  18942                 currency_code: "USD".to_owned(),
  18943             })
  18944         );
  18945         assert_eq!(
  18946             projection.readiness.blockers,
  18947             vec![
  18948                 FarmReadinessBlocker::MissingPickupLocation,
  18949                 FarmReadinessBlocker::MissingOperatingRules,
  18950                 FarmReadinessBlocker::MissingFulfillmentWindow,
  18951             ]
  18952         );
  18953     }
  18954 
  18955     #[test]
  18956     fn saving_farm_rules_projection_refreshes_saved_farm_summary_and_pickup_defaults() {
  18957         let runtime = memory_runtime();
  18958 
  18959         assert!(
  18960             runtime
  18961                 .generate_local_account(Some("Farmer".to_owned()))
  18962                 .expect("account should generate")
  18963         );
  18964         let account_id = runtime
  18965             .summary()
  18966             .settings_account_projection
  18967             .selected_account
  18968             .as_ref()
  18969             .expect("selected account")
  18970             .account
  18971             .account_id
  18972             .clone();
  18973         let farm_id =
  18974             save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
  18975         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  18976             farm_id,
  18977             display_name: "North field farm".to_owned(),
  18978             readiness: FarmReadiness::Incomplete,
  18979         });
  18980         runtime
  18981             .lock_state()
  18982             .sqlite_store
  18983             .as_ref()
  18984             .expect("sqlite store")
  18985             .save_farm_summary(
  18986                 farm_setup_projection
  18987                     .saved_farm
  18988                     .as_ref()
  18989                     .expect("saved farm should exist"),
  18990             )
  18991             .expect("farm summary should save");
  18992         runtime
  18993             .lock_state()
  18994             .sqlite_store
  18995             .as_ref()
  18996             .expect("sqlite store")
  18997             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  18998             .expect("farm setup should save");
  18999 
  19000         assert!(
  19001             runtime
  19002                 .select_local_account(account_id.as_str())
  19003                 .expect("account should select")
  19004         );
  19005 
  19006         let default_pickup_location_id = PickupLocationId::new();
  19007         let market_pickup_location_id = PickupLocationId::new();
  19008         let fulfillment_window_id = FulfillmentWindowId::new();
  19009         let blackout_period_id = BlackoutPeriodId::new();
  19010 
  19011         let saved_projection = runtime
  19012             .save_farm_rules_projection(radroots_app_view::FarmRulesProjection {
  19013                 farm_profile: Some(FarmProfileRecord {
  19014                     farm_id,
  19015                     display_name: "Harbor farm".to_owned(),
  19016                     timezone: "Europe/Stockholm".to_owned(),
  19017                     currency_code: "sek".to_owned(),
  19018                 }),
  19019                 pickup_locations: vec![
  19020                     PickupLocationRecord {
  19021                         pickup_location_id: default_pickup_location_id,
  19022                         farm_id,
  19023                         label: "   Barn pickup   ".to_owned(),
  19024                         address_line: " 14 Orchard Lane ".to_owned(),
  19025                         directions: Some("  Drive to the red barn.  ".to_owned()),
  19026                         is_default: false,
  19027                     },
  19028                     PickupLocationRecord {
  19029                         pickup_location_id: market_pickup_location_id,
  19030                         farm_id,
  19031                         label: "Market stall".to_owned(),
  19032                         address_line: "2 Harbor Road".to_owned(),
  19033                         directions: None,
  19034                         is_default: false,
  19035                     },
  19036                 ],
  19037                 operating_rules: Some(FarmOperatingRulesRecord {
  19038                     farm_id,
  19039                     promise_lead_hours: 24,
  19040                     substitution_policy: "  ask_customer  ".to_owned(),
  19041                 }),
  19042                 fulfillment_windows: vec![FulfillmentWindowRecord {
  19043                     fulfillment_window_id,
  19044                     farm_id,
  19045                     pickup_location_id: default_pickup_location_id,
  19046                     label: "  Friday pickup  ".to_owned(),
  19047                     starts_at: " 2026-04-25T14:00:00Z ".to_owned(),
  19048                     ends_at: " 2026-04-25T18:00:00Z ".to_owned(),
  19049                     order_cutoff_at: " 2026-04-24T18:00:00Z ".to_owned(),
  19050                 }],
  19051                 blackout_periods: vec![BlackoutPeriodRecord {
  19052                     blackout_period_id,
  19053                     farm_id,
  19054                     label: "  Spring break  ".to_owned(),
  19055                     starts_at: " 2026-05-01T00:00:00Z ".to_owned(),
  19056                     ends_at: " 2026-05-03T23:59:59Z ".to_owned(),
  19057                 }],
  19058                 ..runtime
  19059                     .load_farm_rules_projection()
  19060                     .expect("farm rules projection should load")
  19061             })
  19062             .expect("farm rules projection should save");
  19063 
  19064         assert_eq!(
  19065             saved_projection.farm_profile,
  19066             Some(FarmProfileRecord {
  19067                 farm_id,
  19068                 display_name: "Harbor farm".to_owned(),
  19069                 timezone: "Europe/Stockholm".to_owned(),
  19070                 currency_code: "SEK".to_owned(),
  19071             })
  19072         );
  19073         assert_eq!(saved_projection.pickup_locations.len(), 2);
  19074         assert!(saved_projection.pickup_locations[0].is_default);
  19075         assert_eq!(saved_projection.pickup_locations[0].label, "Barn pickup");
  19076         assert_eq!(
  19077             saved_projection.pickup_locations[0].address_line,
  19078             "14 Orchard Lane"
  19079         );
  19080         assert_eq!(
  19081             saved_projection.pickup_locations[0].directions.as_deref(),
  19082             Some("Drive to the red barn.")
  19083         );
  19084         assert_eq!(
  19085             saved_projection.operating_rules,
  19086             Some(FarmOperatingRulesRecord {
  19087                 farm_id,
  19088                 promise_lead_hours: 24,
  19089                 substitution_policy: "ask_customer".to_owned(),
  19090             })
  19091         );
  19092         assert_eq!(
  19093             saved_projection.fulfillment_windows,
  19094             vec![FulfillmentWindowRecord {
  19095                 fulfillment_window_id,
  19096                 farm_id,
  19097                 pickup_location_id: default_pickup_location_id,
  19098                 label: "Friday pickup".to_owned(),
  19099                 starts_at: "2026-04-25T14:00:00Z".to_owned(),
  19100                 ends_at: "2026-04-25T18:00:00Z".to_owned(),
  19101                 order_cutoff_at: "2026-04-24T18:00:00Z".to_owned(),
  19102             }]
  19103         );
  19104         assert_eq!(
  19105             saved_projection.blackout_periods,
  19106             vec![BlackoutPeriodRecord {
  19107                 blackout_period_id,
  19108                 farm_id,
  19109                 label: "Spring break".to_owned(),
  19110                 starts_at: "2026-05-01T00:00:00Z".to_owned(),
  19111                 ends_at: "2026-05-03T23:59:59Z".to_owned(),
  19112             }]
  19113         );
  19114 
  19115         let summary = runtime.summary();
  19116         assert_eq!(
  19117             summary.farm_setup_projection.saved_farm,
  19118             Some(FarmSummary {
  19119                 farm_id,
  19120                 display_name: "Harbor farm".to_owned(),
  19121                 readiness: FarmReadiness::Ready,
  19122             })
  19123         );
  19124         assert_eq!(summary.farm_setup_projection.draft.farm_name, "Harbor farm");
  19125         assert_eq!(
  19126             summary.today_projection.farm,
  19127             summary.farm_setup_projection.saved_farm
  19128         );
  19129     }
  19130 
  19131     #[test]
  19132     fn runtime_reset_local_device_state_clears_store_file_and_projection() {
  19133         let (runtime, paths) = file_backed_runtime("reset");
  19134 
  19135         assert!(
  19136             runtime
  19137                 .generate_local_account(Some("First".to_owned()))
  19138                 .expect("first account should generate")
  19139         );
  19140         let first_account_id = runtime
  19141             .summary()
  19142             .settings_account_projection
  19143             .selected_account
  19144             .as_ref()
  19145             .expect("first selected account")
  19146             .account
  19147             .account_id
  19148             .clone();
  19149         assert!(
  19150             runtime
  19151                 .generate_local_account(Some("Second".to_owned()))
  19152                 .expect("second account should generate")
  19153         );
  19154         let second_account_id = runtime
  19155             .summary()
  19156             .settings_account_projection
  19157             .selected_account
  19158             .as_ref()
  19159             .expect("second selected account")
  19160             .account
  19161             .account_id
  19162             .clone();
  19163         save_surface_activation(
  19164             &runtime,
  19165             first_account_id.as_str(),
  19166             ActiveSurface::Farmer,
  19167             true,
  19168         );
  19169         save_surface_activation(
  19170             &runtime,
  19171             second_account_id.as_str(),
  19172             ActiveSurface::Farmer,
  19173             true,
  19174         );
  19175         assert!(paths.store_path.exists());
  19176 
  19177         assert!(
  19178             runtime
  19179                 .reset_local_device_state()
  19180                 .expect("device state should reset")
  19181         );
  19182         let summary = runtime.summary();
  19183 
  19184         assert_eq!(summary.startup_gate, AppStartupGate::SetupRequired);
  19185         assert!(summary.settings_account_projection.roster.is_empty());
  19186         assert!(
  19187             summary
  19188                 .settings_account_projection
  19189                 .selected_account
  19190                 .is_none()
  19191         );
  19192         assert!(!paths.store_path.exists());
  19193         assert_eq!(
  19194             runtime
  19195                 .lock_state()
  19196                 .sqlite_store
  19197                 .as_ref()
  19198                 .expect("sqlite store")
  19199                 .load_surface_activation(first_account_id.as_str())
  19200                 .expect("first activation should load"),
  19201             None
  19202         );
  19203         assert_eq!(
  19204             runtime
  19205                 .lock_state()
  19206                 .sqlite_store
  19207                 .as_ref()
  19208                 .expect("sqlite store")
  19209                 .load_surface_activation(second_account_id.as_str())
  19210                 .expect("second activation should load"),
  19211             None
  19212         );
  19213 
  19214         cleanup_paths(&paths);
  19215     }
  19216 
  19217     #[test]
  19218     fn runtime_account_commands_fail_closed_without_accounts_manager() {
  19219         let paths = temp_shared_accounts_paths("blocked");
  19220         let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  19221             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  19222                 .expect("in-memory state store should load"),
  19223             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  19224             shared_accounts_paths: Some(paths),
  19225             remote_signer_paths: None,
  19226             accounts_manager: None,
  19227             sqlite_store: Some(
  19228                 AppSqliteStore::open(DatabaseTarget::InMemory)
  19229                     .expect("in-memory sqlite store should open"),
  19230             ),
  19231             sdk_runtime: None,
  19232             sync_transport: default_sync_transport(),
  19233             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  19234             selected_account_pending_sync_write_count: 0,
  19235             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  19236             selected_account_sync_conflicts: Vec::new(),
  19237             startup_issue: None,
  19238         });
  19239 
  19240         let error = runtime
  19241             .generate_local_account(Some("Blocked".to_owned()))
  19242             .expect_err("blocked runtime should fail closed");
  19243 
  19244         assert!(matches!(
  19245             error,
  19246             DesktopAppRuntimeCommandError::RuntimeUnavailable
  19247         ));
  19248     }
  19249 
  19250     fn memory_runtime() -> DesktopAppRuntime {
  19251         DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  19252             state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  19253                 .expect("in-memory state store should load"),
  19254             nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  19255             shared_accounts_paths: None,
  19256             remote_signer_paths: None,
  19257             accounts_manager: Some(
  19258                 RadrootsNostrAccountsManager::new(
  19259                     Arc::new(RadrootsNostrMemoryAccountStore::new()),
  19260                     Arc::new(RadrootsNostrSecretVaultMemory::new()),
  19261                 )
  19262                 .expect("memory manager should build"),
  19263             ),
  19264             sqlite_store: Some(
  19265                 AppSqliteStore::open(DatabaseTarget::InMemory)
  19266                     .expect("in-memory sqlite store should open"),
  19267             ),
  19268             sdk_runtime: None,
  19269             sync_transport: default_sync_transport(),
  19270             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  19271             selected_account_pending_sync_write_count: 0,
  19272             selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  19273             selected_account_sync_conflicts: Vec::new(),
  19274             startup_issue: None,
  19275         })
  19276     }
  19277 
  19278     fn file_backed_runtime(label: &str) -> (DesktopAppRuntime, AppSharedAccountsPaths) {
  19279         let paths = temp_shared_accounts_paths(label);
  19280         fs::create_dir_all(paths.data_root.as_path()).expect("data root should create");
  19281         fs::create_dir_all(paths.secrets_root.as_path()).expect("secrets root should create");
  19282 
  19283         (
  19284             DesktopAppRuntime::from_state(DesktopAppRuntimeState {
  19285                 state_store: AppStateStore::load(AppStatePersistenceRepository::in_memory())
  19286                     .expect("in-memory state store should load"),
  19287                 nostr_relay_urls: vec!["ws://127.0.0.1:8080".to_owned()],
  19288                 shared_accounts_paths: Some(paths.clone()),
  19289                 remote_signer_paths: None,
  19290                 accounts_manager: Some(
  19291                     RadrootsNostrAccountsManager::new(
  19292                         Arc::new(RadrootsNostrFileAccountStore::new(
  19293                             paths.store_path.as_path(),
  19294                         )),
  19295                         Arc::new(RadrootsNostrSecretVaultMemory::new()),
  19296                     )
  19297                     .expect("file-backed manager should build"),
  19298                 ),
  19299                 sqlite_store: Some(
  19300                     AppSqliteStore::open(DatabaseTarget::InMemory)
  19301                         .expect("in-memory sqlite store should open"),
  19302                 ),
  19303                 sdk_runtime: None,
  19304                 sync_transport: default_sync_transport(),
  19305                 runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  19306                 selected_account_pending_sync_write_count: 0,
  19307                 selected_account_relay_ingest_freshness: AppRelayIngestScopeFreshness::default(),
  19308                 selected_account_sync_conflicts: Vec::new(),
  19309                 startup_issue: None,
  19310             }),
  19311             paths,
  19312         )
  19313     }
  19314 
  19315     fn bootstrapped_runtime(label: &str) -> (DesktopAppRuntime, AppDesktopRuntimePaths) {
  19316         let paths = temp_desktop_runtime_paths(label);
  19317         let runtime = restart_runtime(paths.clone());
  19318         (runtime, paths)
  19319     }
  19320 
  19321     fn restart_runtime(paths: AppDesktopRuntimePaths) -> DesktopAppRuntime {
  19322         DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
  19323             paths.clone(),
  19324             vec!["ws://127.0.0.1:8080".to_owned()],
  19325             super::default_runtime_snapshot(),
  19326         )
  19327     }
  19328 
  19329     fn restore_sdk_runtime(runtime: &DesktopAppRuntime, paths: &AppDesktopRuntimePaths) {
  19330         let sdk_runtime =
  19331             super::start_desktop_sdk_runtime(paths, vec!["ws://127.0.0.1:8080".to_owned()])
  19332                 .expect("sdk runtime should restart");
  19333         {
  19334             let mut handle = runtime.sdk_runtime.lock().expect("sdk runtime lock");
  19335             *handle = Some(sdk_runtime);
  19336         }
  19337         let status = runtime
  19338             .wait_for_sdk_startup(StdDuration::from_secs(5))
  19339             .expect("sdk runtime should be present after restart");
  19340         assert_eq!(status.state, AppSdkLifecycleState::Ready);
  19341     }
  19342 
  19343     fn temp_shared_accounts_paths(label: &str) -> AppSharedAccountsPaths {
  19344         let suffix = SystemTime::now()
  19345             .duration_since(UNIX_EPOCH)
  19346             .expect("clock")
  19347             .as_nanos();
  19348         let base = std::env::temp_dir().join(format!("radroots_runtime_accounts_{label}_{suffix}"));
  19349 
  19350         AppSharedAccountsPaths {
  19351             data_root: base.join("data/shared/accounts"),
  19352             secrets_root: base.join("secrets/shared/accounts"),
  19353             store_path: base.join("data/shared/accounts/store.json"),
  19354         }
  19355     }
  19356 
  19357     fn temp_desktop_runtime_paths(label: &str) -> AppDesktopRuntimePaths {
  19358         let suffix = SystemTime::now()
  19359             .duration_since(UNIX_EPOCH)
  19360             .expect("clock")
  19361             .as_nanos();
  19362         let home_dir = std::env::temp_dir().join(format!("radroots_runtime_home_{label}_{suffix}"));
  19363         AppDesktopRuntimePaths::for_desktop(
  19364             AppRuntimePlatform::Macos,
  19365             AppRuntimeHostEnvironment {
  19366                 home_dir: Some(home_dir),
  19367                 ..AppRuntimeHostEnvironment::default()
  19368             },
  19369         )
  19370         .expect("desktop runtime paths should resolve")
  19371     }
  19372 
  19373     fn temp_remote_signer_paths(label: &str) -> DesktopRemoteSignerPaths {
  19374         let suffix = SystemTime::now()
  19375             .duration_since(UNIX_EPOCH)
  19376             .expect("clock")
  19377             .as_nanos();
  19378         let base =
  19379             std::env::temp_dir().join(format!("radroots_runtime_remote_signer_{label}_{suffix}"));
  19380         DesktopRemoteSignerPaths {
  19381             sessions_path: base.join("data/apps/app/nostr/remote-signer-sessions.json"),
  19382             client_secret_root: base.join("secrets/shared/accounts/remote_signer"),
  19383         }
  19384     }
  19385 
  19386     fn cleanup_remote_signer_paths(paths: &DesktopRemoteSignerPaths) {
  19387         if let Some(base) = paths.sessions_path.ancestors().nth(5) {
  19388             let _ = fs::remove_dir_all(base);
  19389         }
  19390     }
  19391 
  19392     fn cleanup_bootstrapped_runtime_paths(paths: &AppDesktopRuntimePaths) {
  19393         if let Some(home_dir) = paths.app.data.ancestors().nth(4) {
  19394             let _ = fs::remove_dir_all(home_dir);
  19395         }
  19396     }
  19397 
  19398     fn append_cli_local_listing_records(paths: &AppDesktopRuntimePaths, account_id: &str) {
  19399         let database_path = paths
  19400             .shared_local_events_database_path()
  19401             .expect("shared local events path");
  19402         if let Some(parent) = database_path.parent() {
  19403             fs::create_dir_all(parent).expect("shared local events directory should create");
  19404         }
  19405         let executor =
  19406             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  19407         let store = LocalEventsStore::new(executor);
  19408         store.migrate_up().expect("migrate shared local events");
  19409         let farm_key = "AAAAAAAAAAAAAAAAAAAAAA";
  19410         let listing_key = "BBBBBBBBBBBBBBBBBBBBBB";
  19411         store
  19412             .append_record(&local_work_record(
  19413                 "cli:local_work:farm",
  19414                 account_id,
  19415                 farm_key,
  19416                 None,
  19417                 json!({
  19418                     "record_kind": "farm_config_v1",
  19419                     "document": {
  19420                         "selection": {
  19421                             "account": account_id,
  19422                             "farm_d_tag": farm_key
  19423                         },
  19424                         "profile": {
  19425                             "name": "Green Farm",
  19426                             "display_name": "Green Farm"
  19427                         },
  19428                         "farm": {
  19429                             "d_tag": farm_key,
  19430                             "name": "Green Farm",
  19431                             "location": {
  19432                                 "primary": "farmstand"
  19433                             }
  19434                         },
  19435                         "listing_defaults": {
  19436                             "delivery_method": "pickup",
  19437                             "location": {
  19438                                 "primary": "farmstand"
  19439                             }
  19440                         }
  19441                     }
  19442                 }),
  19443             ))
  19444             .expect("append farm local work");
  19445         store
  19446             .append_record(&local_work_record(
  19447                 "cli:local_work:listing",
  19448                 account_id,
  19449                 farm_key,
  19450                 Some(format!("30402:seller-pubkey:{listing_key}")),
  19451                 json!({
  19452                     "record_kind": "listing_draft_v1",
  19453                     "document": {
  19454                         "listing": {
  19455                             "d_tag": listing_key,
  19456                             "farm_d_tag": farm_key
  19457                         },
  19458                         "seller_actor": {
  19459                             "account_id": account_id,
  19460                             "pubkey": "seller-pubkey"
  19461                         },
  19462                         "product": {
  19463                             "key": "eggs",
  19464                             "title": "Eggs",
  19465                             "summary": "Fresh eggs"
  19466                         },
  19467                         "primary_bin": {
  19468                             "quantity_unit": "each",
  19469                             "price_amount": "6",
  19470                             "price_currency": "USD"
  19471                         },
  19472                         "inventory": {
  19473                             "available": "10"
  19474                         }
  19475                     }
  19476                 }),
  19477             ))
  19478             .expect("append listing local work");
  19479     }
  19480 
  19481     fn append_cli_signed_buyer_listing_record(paths: &AppDesktopRuntimePaths) {
  19482         append_cli_signed_buyer_listing_record_with(
  19483             paths,
  19484             "buyer-visible-listing",
  19485             "DDDDDDDDDDDDDDDDDDDDDD",
  19486             "Buyer Visible Eggs",
  19487             1100,
  19488         );
  19489     }
  19490 
  19491     fn append_cli_signed_buyer_listing_record_with(
  19492         paths: &AppDesktopRuntimePaths,
  19493         record_suffix: &str,
  19494         listing_key: &str,
  19495         title: &str,
  19496         created_at_ms: i64,
  19497     ) {
  19498         append_cli_signed_buyer_listing_record_with_bin(
  19499             paths,
  19500             record_suffix,
  19501             listing_key,
  19502             title,
  19503             created_at_ms,
  19504             "bin-1",
  19505         );
  19506     }
  19507 
  19508     fn append_cli_signed_buyer_listing_record_with_bin(
  19509         paths: &AppDesktopRuntimePaths,
  19510         record_suffix: &str,
  19511         listing_key: &str,
  19512         title: &str,
  19513         created_at_ms: i64,
  19514         bin_id: &str,
  19515     ) {
  19516         let database_path = paths
  19517             .shared_local_events_database_path()
  19518             .expect("shared local events path");
  19519         if let Some(parent) = database_path.parent() {
  19520             fs::create_dir_all(parent).expect("shared local events directory should create");
  19521         }
  19522         let executor =
  19523             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  19524         let store = LocalEventsStore::new(executor);
  19525         store.migrate_up().expect("migrate shared local events");
  19526         let farm_key = "CCCCCCCCCCCCCCCCCCCCCC";
  19527         let owner_pubkey = BUYER_VISIBLE_SELLER_PUBKEY;
  19528         let record_id = format!("cli:signed_event:{record_suffix}");
  19529         let content = json!({
  19530             "d_tag": listing_key,
  19531             "status": "active",
  19532             "farm": {
  19533                 "pubkey": owner_pubkey,
  19534                 "d_tag": farm_key
  19535             },
  19536             "product": {
  19537                 "key": listing_key,
  19538                 "title": title,
  19539                 "summary": "Published local eggs"
  19540             },
  19541             "availability": {
  19542                 "kind": "window",
  19543                 "amount": {
  19544                     "start": 4_102_444_800u64,
  19545                     "end": 4_102_531_200u64
  19546                 }
  19547             },
  19548             "delivery_method": {
  19549                 "kind": "pickup"
  19550             },
  19551             "location": {
  19552                 "primary": "North barn pickup"
  19553             }
  19554         });
  19555         let event_id = signed_event_id(record_id.as_str());
  19556         store
  19557             .append_record(&LocalEventRecordInput {
  19558                 record_id: record_id.to_owned(),
  19559                 family: LocalRecordFamily::SignedEvent,
  19560                 status: LocalRecordStatus::Published,
  19561                 source_runtime: SourceRuntime::Cli,
  19562                 created_at_ms,
  19563                 inserted_at_ms: created_at_ms + 1,
  19564                 owner_account_id: Some("seller-account".to_owned()),
  19565                 owner_pubkey: Some(owner_pubkey.to_owned()),
  19566                 farm_id: Some(farm_key.to_owned()),
  19567                 listing_addr: Some(format!("30402:{owner_pubkey}:{listing_key}")),
  19568                 local_work_json: None,
  19569                 event_id: Some(event_id.clone()),
  19570                 event_kind: Some(30402),
  19571                 event_pubkey: Some(owner_pubkey.to_owned()),
  19572                 event_created_at: Some(created_at_ms),
  19573                 event_tags_json: Some(json!([
  19574                     ["d", listing_key],
  19575                     ["a", format!("30340:{owner_pubkey}:{farm_key}")],
  19576                     ["key", listing_key],
  19577                     ["title", title],
  19578                     ["summary", "Published local eggs"],
  19579                     ["radroots:bin", bin_id, "1", "each"],
  19580                     ["radroots:price", bin_id, "8", "USD", "1", "each"],
  19581                     ["inventory", "9"],
  19582                     ["status", "active"],
  19583                     ["radroots:availability_start", "4102444800"],
  19584                     ["expires_at", "4102531200"],
  19585                     ["delivery", "pickup"],
  19586                     ["location", "North barn pickup"]
  19587                 ])),
  19588                 event_content: Some(content.to_string()),
  19589                 event_sig: Some("signature".to_owned()),
  19590                 raw_event_json: Some(json!({
  19591                     "id": event_id,
  19592                     "kind": 30402,
  19593                     "pubkey": owner_pubkey,
  19594                     "content": content.to_string()
  19595                 })),
  19596                 outbox_status: PublishOutboxStatus::Acknowledged,
  19597                 relay_set_fingerprint: Some("relay-set".to_owned()),
  19598                 relay_delivery_json: Some(json!({
  19599                     "state": "acknowledged",
  19600                     "acknowledged_relays": ["ws://127.0.0.1:1234/"]
  19601                 })),
  19602             })
  19603             .expect("append signed buyer listing");
  19604     }
  19605 
  19606     struct LinkedBuyerLifecycleFixture {
  19607         runtime: DesktopAppRuntime,
  19608         paths: AppDesktopRuntimePaths,
  19609         order_id: OrderId,
  19610         trade_order_id: String,
  19611         request_event_id: String,
  19612         decision_event_id: String,
  19613         listing_addr: String,
  19614         buyer_pubkey: String,
  19615         seller_pubkey: String,
  19616     }
  19617 
  19618     fn linked_buyer_lifecycle_runtime(label: &str) -> LinkedBuyerLifecycleFixture {
  19619         linked_buyer_lifecycle_runtime_with_seller_pubkey(label, SDK_TEST_SELLER_PUBLIC_KEY_HEX)
  19620     }
  19621 
  19622     fn linked_buyer_request_runtime(label: &str) -> LinkedBuyerLifecycleFixture {
  19623         linked_buyer_runtime_with_seller_pubkey(label, SDK_TEST_SELLER_PUBLIC_KEY_HEX, false)
  19624     }
  19625 
  19626     fn linked_buyer_lifecycle_runtime_with_seller_pubkey(
  19627         label: &str,
  19628         seller_pubkey: &str,
  19629     ) -> LinkedBuyerLifecycleFixture {
  19630         linked_buyer_runtime_with_seller_pubkey(label, seller_pubkey, true)
  19631     }
  19632 
  19633     fn linked_buyer_runtime_with_seller_pubkey(
  19634         label: &str,
  19635         seller_pubkey: &str,
  19636         append_decision: bool,
  19637     ) -> LinkedBuyerLifecycleFixture {
  19638         let (runtime, paths) = bootstrapped_runtime(label);
  19639         assert!(
  19640             runtime
  19641                 .import_local_account(DesktopLocalIdentityImportRequest::raw_secret_key(
  19642                     SDK_TEST_BUYER_SECRET_KEY_HEX,
  19643                 ))
  19644                 .expect("buyer account should import")
  19645         );
  19646         assert!(
  19647             runtime
  19648                 .select_active_surface(ActiveSurface::Personal)
  19649                 .expect("buyer surface should select")
  19650         );
  19651         let buyer_account_id = runtime
  19652             .summary()
  19653             .settings_account_projection
  19654             .selected_account
  19655             .as_ref()
  19656             .expect("selected buyer account")
  19657             .account
  19658             .account_id
  19659             .clone();
  19660         let buyer_pubkey = runtime
  19661             .lock_state()
  19662             .accounts_manager
  19663             .as_ref()
  19664             .expect("accounts manager")
  19665             .resolve_account_selector(buyer_account_id.as_str())
  19666             .expect("selected buyer account should resolve")
  19667             .public_identity
  19668             .public_key_hex;
  19669         let farm_key = super::d_tag_from_uuid(FarmId::new().as_uuid());
  19670         let listing_key = super::d_tag_from_uuid(ProductId::new().as_uuid());
  19671         let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
  19672         let listing_event_id = signed_listing_event_id(label);
  19673         let trade_order_id = format!("{label}-trade-order");
  19674         let order_id =
  19675             projected_order_id_from_trade_request(trade_order_id.as_str(), buyer_pubkey.as_str());
  19676         append_app_signed_listing_record(
  19677             &paths,
  19678             "linked-seller-account",
  19679             seller_pubkey,
  19680             farm_key.as_str(),
  19681             listing_key.as_str(),
  19682             listing_event_id.as_str(),
  19683             6,
  19684         );
  19685         let request_event_id = append_signed_order_request_record(
  19686             &paths,
  19687             trade_order_id.as_str(),
  19688             listing_addr.as_str(),
  19689             listing_event_id.as_str(),
  19690             buyer_pubkey.as_str(),
  19691             seller_pubkey,
  19692             2,
  19693         );
  19694         let decision_event_id = if append_decision {
  19695             append_signed_order_decision_record(
  19696                 &paths,
  19697                 trade_order_id.as_str(),
  19698                 request_event_id.as_str(),
  19699                 listing_addr.as_str(),
  19700                 buyer_pubkey.as_str(),
  19701                 seller_pubkey,
  19702                 2,
  19703             )
  19704         } else {
  19705             String::new()
  19706         };
  19707         LinkedBuyerLifecycleFixture {
  19708             runtime,
  19709             paths,
  19710             order_id,
  19711             trade_order_id,
  19712             request_event_id,
  19713             decision_event_id,
  19714             listing_addr,
  19715             buyer_pubkey,
  19716             seller_pubkey: seller_pubkey.to_owned(),
  19717         }
  19718     }
  19719 
  19720     fn seller_order_decision_runtime(
  19721         label: &str,
  19722         stock_count: u32,
  19723         order_quantity: u32,
  19724     ) -> (
  19725         DesktopAppRuntime,
  19726         AppDesktopRuntimePaths,
  19727         OrderId,
  19728         ProductId,
  19729         String,
  19730         String,
  19731     ) {
  19732         let (runtime, paths) = bootstrapped_runtime(label);
  19733         let (account_id, farm_id) =
  19734             provision_ready_farmer_account_from_secret(&runtime, SDK_TEST_SELLER_SECRET_KEY_HEX);
  19735         runtime.lock_state_mut().nostr_relay_urls = vec!["wss://relay.example".to_owned()];
  19736         let seller_pubkey = runtime
  19737             .lock_state()
  19738             .accounts_manager
  19739             .as_ref()
  19740             .expect("accounts manager")
  19741             .resolve_account_selector(account_id.as_str())
  19742             .expect("selected seller account should resolve")
  19743             .public_identity
  19744             .public_key_hex;
  19745         let buyer_pubkey = SDK_TEST_BUYER_PUBLIC_KEY_HEX.to_owned();
  19746         let product_id = ProductId::new();
  19747         let trade_order_id = "seller-order-decision-1";
  19748         let order_id = projected_order_id_from_trade_request(trade_order_id, buyer_pubkey.as_str());
  19749         let farm_key = super::d_tag_from_uuid(farm_id.as_uuid());
  19750         let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
  19751         let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
  19752         let listing_event_id = signed_listing_event_id("seller-order-decision");
  19753         append_app_signed_listing_record(
  19754             &paths,
  19755             account_id.as_str(),
  19756             seller_pubkey.as_str(),
  19757             farm_key.as_str(),
  19758             listing_key.as_str(),
  19759             listing_event_id.as_str(),
  19760             stock_count,
  19761         );
  19762         append_signed_order_request_record(
  19763             &paths,
  19764             trade_order_id,
  19765             listing_addr.as_str(),
  19766             listing_event_id.as_str(),
  19767             buyer_pubkey.as_str(),
  19768             seller_pubkey.as_str(),
  19769             order_quantity,
  19770         );
  19771 
  19772         (
  19773             runtime,
  19774             paths,
  19775             order_id,
  19776             product_id,
  19777             seller_pubkey,
  19778             buyer_pubkey,
  19779         )
  19780     }
  19781 
  19782     fn seller_order_decision_sdk_runtime(
  19783         label: &str,
  19784         stock_count: u32,
  19785         order_quantity: u32,
  19786     ) -> (
  19787         DesktopAppRuntime,
  19788         AppDesktopRuntimePaths,
  19789         OrderId,
  19790         ProductId,
  19791         String,
  19792         String,
  19793     ) {
  19794         let (runtime, paths) = bootstrapped_runtime(label);
  19795         let (account_id, farm_id) =
  19796             provision_ready_farmer_account_from_secret(&runtime, SDK_TEST_SELLER_SECRET_KEY_HEX);
  19797         runtime.lock_state_mut().nostr_relay_urls = vec!["wss://relay.example".to_owned()];
  19798         let seller_pubkey = runtime
  19799             .lock_state()
  19800             .accounts_manager
  19801             .as_ref()
  19802             .expect("accounts manager")
  19803             .resolve_account_selector(account_id.as_str())
  19804             .expect("selected seller account should resolve")
  19805             .public_identity
  19806             .public_key_hex;
  19807         let buyer_pubkey = SDK_TEST_BUYER_PUBLIC_KEY_HEX.to_owned();
  19808         let product_id = ProductId::new();
  19809         let trade_order_id = "seller-order-decision-1";
  19810         let order_id = projected_order_id_from_trade_request(trade_order_id, buyer_pubkey.as_str());
  19811         let farm_key = super::d_tag_from_uuid(farm_id.as_uuid());
  19812         let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
  19813         let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
  19814         let listing_event_id = signed_listing_event_id("seller-order-decision");
  19815         append_app_signed_listing_record(
  19816             &paths,
  19817             account_id.as_str(),
  19818             seller_pubkey.as_str(),
  19819             farm_key.as_str(),
  19820             listing_key.as_str(),
  19821             listing_event_id.as_str(),
  19822             stock_count,
  19823         );
  19824         append_verified_signed_order_request_record(
  19825             &paths,
  19826             trade_order_id,
  19827             listing_addr.as_str(),
  19828             listing_event_id.as_str(),
  19829             buyer_pubkey.as_str(),
  19830             seller_pubkey.as_str(),
  19831             order_quantity,
  19832         );
  19833 
  19834         (
  19835             runtime,
  19836             paths,
  19837             order_id,
  19838             product_id,
  19839             seller_pubkey,
  19840             buyer_pubkey,
  19841         )
  19842     }
  19843 
  19844     fn publish_prior_relay_seller_order_accept(
  19845         runtime: &DesktopAppRuntime,
  19846         relay: &ThreadedAckRelay,
  19847         order_id: OrderId,
  19848         product_id: ProductId,
  19849         seller_pubkey: &str,
  19850         buyer_pubkey: &str,
  19851     ) {
  19852         let (account_id, farm_id, accounts_manager) = {
  19853             let state = runtime.lock_state();
  19854             let selected_account = state
  19855                 .state_store
  19856                 .identity_projection()
  19857                 .selected_account
  19858                 .as_ref()
  19859                 .expect("selected seller account");
  19860             (
  19861                 selected_account.account.account_id.clone(),
  19862                 state.selected_farm_id().expect("selected farm"),
  19863                 state
  19864                     .accounts_manager
  19865                     .as_ref()
  19866                     .expect("accounts manager")
  19867                     .clone(),
  19868             )
  19869         };
  19870         let listing_key = super::d_tag_from_uuid(product_id.as_uuid());
  19871         let request_event_id = runtime
  19872             .lock_state()
  19873             .resolve_seller_order_request_evidence(order_id)
  19874             .expect("seller request evidence should resolve")
  19875             .request_event
  19876             .id;
  19877         let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload {
  19878             context: AppPublishContext::new(account_id.clone(), "seller_order_decision"),
  19879             app_order_id: order_id,
  19880             farm_id,
  19881             trade_order_id: "seller-order-decision-1".to_owned(),
  19882             request_event_id,
  19883             listing_event_id: Some(signed_listing_event_id("seller-order-decision")),
  19884             listing_addr: format!("30402:{seller_pubkey}:{listing_key}"),
  19885             buyer_pubkey: buyer_pubkey.to_owned(),
  19886             seller_pubkey: seller_pubkey.to_owned(),
  19887             decision: AppOrderDecisionPayload::Accepted {
  19888                 inventory_commitments: vec![AppOrderDecisionInventoryCommitment {
  19889                     bin_id: "seller-order-primary-bin".to_owned(),
  19890                     bin_count: 2,
  19891                 }],
  19892             },
  19893         });
  19894         let operation = PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z")
  19895             .expect("prior order decision publish work should serialize");
  19896         let payload = operation
  19897             .publish_payload()
  19898             .expect("prior order decision operation should carry payload");
  19899         let AppPublishPayload::OrderDecision(payload) = payload else {
  19900             panic!("prior order decision operation should carry order decision payload")
  19901         };
  19902         let account_id =
  19903             RadrootsIdentityId::parse(account_id.as_str()).expect("selected account id");
  19904         let identity = accounts_manager
  19905             .get_signing_identity(&account_id)
  19906             .expect("seller signer lookup should succeed")
  19907             .expect("seller account should have local signer");
  19908         let request_event_id = test_event_id(payload.request_event_id.as_str());
  19909         let decision = order_decision_publish_payload_to_sdk_decision(&payload)
  19910             .expect("order decision payload should convert to SDK decision");
  19911         let parts = radroots_sdk::protocol::order::build_order_decision_draft(
  19912             &request_event_id,
  19913             &request_event_id,
  19914             &decision,
  19915         )
  19916         .expect("order decision draft should build")
  19917         .into_wire_parts();
  19918         let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
  19919             .expect("order decision event builder should build")
  19920             .sign_with_keys(identity.keys())
  19921             .expect("order decision event should sign");
  19922         publish_signed_test_event_to_relay(relay, &event);
  19923         assert_eq!(relay.event_count(), 1);
  19924     }
  19925 
  19926     fn append_app_signed_listing_record(
  19927         paths: &AppDesktopRuntimePaths,
  19928         account_id: &str,
  19929         seller_pubkey: &str,
  19930         farm_key: &str,
  19931         listing_key: &str,
  19932         listing_event_id: &str,
  19933         stock_count: u32,
  19934     ) {
  19935         let database_path = paths
  19936             .shared_local_events_database_path()
  19937             .expect("shared local events path");
  19938         if let Some(parent) = database_path.parent() {
  19939             fs::create_dir_all(parent).expect("shared local events directory should create");
  19940         }
  19941         let executor =
  19942             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  19943         let store = LocalEventsStore::new(executor);
  19944         store.migrate_up().expect("migrate shared local events");
  19945         let listing_addr = format!("30402:{seller_pubkey}:{listing_key}");
  19946         let listing_event_id = test_event_id_seed(listing_event_id);
  19947         let content = json!({
  19948             "d_tag": listing_key,
  19949             "status": "active",
  19950             "farm": {
  19951                 "pubkey": seller_pubkey,
  19952                 "d_tag": farm_key
  19953             },
  19954             "product": {
  19955                 "key": listing_key,
  19956                 "title": "Seller decision lettuce",
  19957                 "summary": "Signed listing for seller decision tests"
  19958             },
  19959             "availability": {
  19960                 "kind": "window",
  19961                 "amount": {
  19962                     "start": 4_102_444_800u64,
  19963                     "end": 4_102_531_200u64
  19964                 }
  19965             },
  19966             "delivery_method": {
  19967                 "kind": "pickup"
  19968             },
  19969             "location": {
  19970                 "primary": "North barn pickup"
  19971             }
  19972         });
  19973         store
  19974             .append_record(&LocalEventRecordInput {
  19975                 record_id: "app:signed_event:listing:seller-order-decision".to_owned(),
  19976                 family: LocalRecordFamily::SignedEvent,
  19977                 status: LocalRecordStatus::Published,
  19978                 source_runtime: SourceRuntime::App,
  19979                 created_at_ms: 1_774_000_000_000,
  19980                 inserted_at_ms: 1_774_000_000_001,
  19981                 owner_account_id: Some(account_id.to_owned()),
  19982                 owner_pubkey: Some(seller_pubkey.to_owned()),
  19983                 farm_id: Some(farm_key.to_owned()),
  19984                 listing_addr: Some(listing_addr),
  19985                 local_work_json: None,
  19986                 event_id: Some(listing_event_id.clone()),
  19987                 event_kind: Some(30402),
  19988                 event_pubkey: Some(seller_pubkey.to_owned()),
  19989                 event_created_at: Some(1_774_000_000),
  19990                 event_tags_json: Some(json!([
  19991                     ["d", listing_key],
  19992                     ["a", format!("30340:{seller_pubkey}:{farm_key}")],
  19993                     ["key", listing_key],
  19994                     ["title", "Seller decision lettuce"],
  19995                     ["summary", "Signed listing for seller decision tests"],
  19996                     ["radroots:bin", "seller-order-primary-bin", "1", "each"],
  19997                     [
  19998                         "radroots:price",
  19999                         "seller-order-primary-bin",
  20000                         "8",
  20001                         "USD",
  20002                         "1",
  20003                         "each"
  20004                     ],
  20005                     ["inventory", stock_count.to_string()],
  20006                     ["status", "active"],
  20007                     ["radroots:availability_start", "4102444800"],
  20008                     ["expires_at", "4102531200"],
  20009                     ["delivery", "pickup"],
  20010                     ["location", "North barn pickup"]
  20011                 ])),
  20012                 event_content: Some(content.to_string()),
  20013                 event_sig: Some("signature".to_owned()),
  20014                 raw_event_json: Some(json!({
  20015                     "id": listing_event_id,
  20016                     "kind": 30402,
  20017                     "pubkey": seller_pubkey,
  20018                     "content": content.to_string()
  20019                 })),
  20020                 outbox_status: PublishOutboxStatus::Acknowledged,
  20021                 relay_set_fingerprint: Some("relay-set".to_owned()),
  20022                 relay_delivery_json: Some(json!({
  20023                     "state": "acknowledged",
  20024                     "target_relays": ["wss://relay.example"],
  20025                     "connected_relays": ["wss://relay.example"],
  20026                     "acknowledged_relays": ["wss://relay.example"]
  20027                 })),
  20028             })
  20029             .expect("append app signed listing");
  20030     }
  20031 
  20032     fn append_signed_order_request_record(
  20033         paths: &AppDesktopRuntimePaths,
  20034         trade_order_id: &str,
  20035         listing_addr: &str,
  20036         listing_event_id: &str,
  20037         buyer_pubkey: &str,
  20038         seller_pubkey: &str,
  20039         order_quantity: u32,
  20040     ) -> String {
  20041         let database_path = paths
  20042             .shared_local_events_database_path()
  20043             .expect("shared local events path");
  20044         if let Some(parent) = database_path.parent() {
  20045             fs::create_dir_all(parent).expect("shared local events directory should create");
  20046         }
  20047         let executor =
  20048             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  20049         let store = LocalEventsStore::new(executor);
  20050         store.migrate_up().expect("migrate shared local events");
  20051         let order = RadrootsOrderRequest {
  20052             order_id: test_order_id(trade_order_id),
  20053             listing_addr: test_listing_addr(listing_addr),
  20054             buyer_pubkey: test_pubkey(buyer_pubkey),
  20055             seller_pubkey: test_pubkey(seller_pubkey),
  20056             items: vec![RadrootsOrderItem {
  20057                 bin_id: test_bin_id("seller-order-primary-bin"),
  20058                 bin_count: order_quantity,
  20059             }],
  20060             economics: signed_order_request_economics(trade_order_id, order_quantity),
  20061         };
  20062         let parts = radroots_sdk::protocol::order::build_order_request_draft(
  20063             &RadrootsNostrEventPtr {
  20064                 id: test_event_id_seed(listing_event_id),
  20065                 relays: Some("wss://relay.example".to_owned()),
  20066             },
  20067             &order,
  20068         )
  20069         .expect("order request draft should build")
  20070         .into_wire_parts();
  20071         let record_id = format!("app:signed_event:order-request:{trade_order_id}");
  20072         let event_id = signed_event_id(record_id.as_str());
  20073         let event = test_event_from_parts(
  20074             record_id.as_str(),
  20075             event_id,
  20076             buyer_pubkey,
  20077             1_774_000_010,
  20078             parts,
  20079         );
  20080         let stored_event_id = event.id.clone();
  20081         let relay_delivery_json = RelayDeliveryEvidence::acknowledged(
  20082             ["wss://relay.example"],
  20083             ["wss://relay.example"],
  20084             ["wss://relay.example"],
  20085             Vec::new(),
  20086         )
  20087         .expect("acknowledged relay delivery evidence")
  20088         .to_json_value()
  20089         .expect("acknowledged relay delivery json");
  20090         store
  20091             .append_record(&LocalEventRecordInput {
  20092                 record_id,
  20093                 family: LocalRecordFamily::SignedEvent,
  20094                 status: LocalRecordStatus::Published,
  20095                 source_runtime: SourceRuntime::Test,
  20096                 created_at_ms: 1_774_000_010_000,
  20097                 inserted_at_ms: 1_774_000_010_001,
  20098                 owner_account_id: None,
  20099                 owner_pubkey: Some(event.author.clone()),
  20100                 farm_id: None,
  20101                 listing_addr: Some(listing_addr.to_owned()),
  20102                 local_work_json: None,
  20103                 event_id: Some(event.id.clone()),
  20104                 event_kind: Some(i64::from(event.kind)),
  20105                 event_pubkey: Some(event.author.clone()),
  20106                 event_created_at: Some(i64::from(event.created_at)),
  20107                 event_tags_json: Some(json!(event.tags.clone())),
  20108                 event_content: Some(event.content.clone()),
  20109                 event_sig: Some(event.sig.clone()),
  20110                 raw_event_json: Some(json!({
  20111                     "id": event.id.clone(),
  20112                     "kind": event.kind,
  20113                     "pubkey": event.author.clone(),
  20114                     "tags": event.tags.clone(),
  20115                     "content": event.content.clone(),
  20116                     "sig": event.sig.clone()
  20117                 })),
  20118                 outbox_status: PublishOutboxStatus::Acknowledged,
  20119                 relay_set_fingerprint: Some("relay-set".to_owned()),
  20120                 relay_delivery_json: Some(relay_delivery_json),
  20121             })
  20122             .expect("append signed order request");
  20123         stored_event_id
  20124     }
  20125 
  20126     fn append_verified_signed_order_request_record(
  20127         paths: &AppDesktopRuntimePaths,
  20128         trade_order_id: &str,
  20129         listing_addr: &str,
  20130         listing_event_id: &str,
  20131         buyer_pubkey: &str,
  20132         seller_pubkey: &str,
  20133         order_quantity: u32,
  20134     ) {
  20135         assert_eq!(buyer_pubkey, SDK_TEST_BUYER_PUBLIC_KEY_HEX);
  20136         let database_path = paths
  20137             .shared_local_events_database_path()
  20138             .expect("shared local events path");
  20139         if let Some(parent) = database_path.parent() {
  20140             fs::create_dir_all(parent).expect("shared local events directory should create");
  20141         }
  20142         let executor =
  20143             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  20144         let store = LocalEventsStore::new(executor);
  20145         store.migrate_up().expect("migrate shared local events");
  20146         let order = RadrootsOrderRequest {
  20147             order_id: test_order_id(trade_order_id),
  20148             listing_addr: test_listing_addr(listing_addr),
  20149             buyer_pubkey: test_pubkey(buyer_pubkey),
  20150             seller_pubkey: test_pubkey(seller_pubkey),
  20151             items: vec![RadrootsOrderItem {
  20152                 bin_id: test_bin_id("seller-order-primary-bin"),
  20153                 bin_count: order_quantity,
  20154             }],
  20155             economics: signed_order_request_economics(trade_order_id, order_quantity),
  20156         };
  20157         let parts = radroots_sdk::protocol::order::build_order_request_draft(
  20158             &RadrootsNostrEventPtr {
  20159                 id: test_event_id_seed(listing_event_id),
  20160                 relays: Some("wss://relay.example".to_owned()),
  20161             },
  20162             &order,
  20163         )
  20164         .expect("order request draft should build")
  20165         .into_wire_parts();
  20166         let secret_key = RadrootsNostrSecretKey::from_hex(SDK_TEST_BUYER_SECRET_KEY_HEX)
  20167             .expect("SDK test buyer secret key should parse");
  20168         let keys = RadrootsNostrKeys::new(secret_key);
  20169         let signed_event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags)
  20170             .expect("order request event should build")
  20171             .custom_created_at(RadrootsNostrTimestamp::from_secs(1_774_000_010))
  20172             .sign_with_keys(&keys)
  20173             .expect("order request event should sign");
  20174         let event = radroots_event_from_nostr(&signed_event);
  20175         let record_id = format!("app:signed_event:order-request:{trade_order_id}");
  20176         let relay_delivery_json = RelayDeliveryEvidence::acknowledged(
  20177             ["wss://relay.example"],
  20178             ["wss://relay.example"],
  20179             ["wss://relay.example"],
  20180             Vec::new(),
  20181         )
  20182         .expect("acknowledged relay delivery evidence")
  20183         .to_json_value()
  20184         .expect("acknowledged relay delivery json");
  20185         store
  20186             .append_record(&LocalEventRecordInput {
  20187                 record_id,
  20188                 family: LocalRecordFamily::SignedEvent,
  20189                 status: LocalRecordStatus::Published,
  20190                 source_runtime: SourceRuntime::Test,
  20191                 created_at_ms: 1_774_000_010_000,
  20192                 inserted_at_ms: 1_774_000_010_001,
  20193                 owner_account_id: None,
  20194                 owner_pubkey: Some(event.author.clone()),
  20195                 farm_id: None,
  20196                 listing_addr: Some(listing_addr.to_owned()),
  20197                 local_work_json: None,
  20198                 event_id: Some(event.id.clone()),
  20199                 event_kind: Some(i64::from(event.kind)),
  20200                 event_pubkey: Some(event.author.clone()),
  20201                 event_created_at: Some(i64::from(event.created_at)),
  20202                 event_tags_json: Some(json!(event.tags.clone())),
  20203                 event_content: Some(event.content.clone()),
  20204                 event_sig: Some(event.sig.clone()),
  20205                 raw_event_json: Some(
  20206                     serde_json::to_value(&event).expect("SDK test event should serialize"),
  20207                 ),
  20208                 outbox_status: PublishOutboxStatus::Acknowledged,
  20209                 relay_set_fingerprint: Some("relay-set".to_owned()),
  20210                 relay_delivery_json: Some(relay_delivery_json),
  20211             })
  20212             .expect("append verified signed order request");
  20213     }
  20214 
  20215     fn signed_order_request_economics(
  20216         trade_order_id: &str,
  20217         order_quantity: u32,
  20218     ) -> RadrootsOrderEconomics {
  20219         let currency = RadrootsCoreCurrency::USD;
  20220         let unit_price_minor_units = 800_u32;
  20221         let total_minor_units = unit_price_minor_units
  20222             .checked_mul(order_quantity)
  20223             .expect("order total should fit");
  20224 
  20225         RadrootsOrderEconomics {
  20226             quote_id: test_quote_id(format!("{trade_order_id}-quote").as_str()),
  20227             quote_version: 1,
  20228             pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
  20229             currency,
  20230             items: vec![RadrootsOrderEconomicItem {
  20231                 bin_id: test_bin_id("seller-order-primary-bin"),
  20232                 bin_count: order_quantity,
  20233                 quantity_amount: RadrootsCoreDecimal::from(1u32),
  20234                 quantity_unit: RadrootsCoreUnit::Each,
  20235                 unit_price_amount: RadrootsCoreDecimal::from(8u32),
  20236                 unit_price_currency: currency,
  20237                 line_subtotal: RadrootsCoreMoney::from_minor_units_u32(total_minor_units, currency),
  20238             }],
  20239             discounts: Vec::new(),
  20240             adjustments: Vec::new(),
  20241             subtotal: RadrootsCoreMoney::from_minor_units_u32(total_minor_units, currency),
  20242             discount_total: RadrootsCoreMoney::zero(currency),
  20243             adjustment_total: RadrootsCoreMoney::zero(currency),
  20244             total: RadrootsCoreMoney::from_minor_units_u32(total_minor_units, currency),
  20245         }
  20246     }
  20247 
  20248     fn append_signed_order_decision_record(
  20249         paths: &AppDesktopRuntimePaths,
  20250         trade_order_id: &str,
  20251         request_event_id: &str,
  20252         listing_addr: &str,
  20253         buyer_pubkey: &str,
  20254         seller_pubkey: &str,
  20255         order_quantity: u32,
  20256     ) -> String {
  20257         let payload = RadrootsOrderDecision {
  20258             order_id: test_order_id(trade_order_id),
  20259             listing_addr: test_listing_addr(listing_addr),
  20260             buyer_pubkey: test_pubkey(buyer_pubkey),
  20261             seller_pubkey: test_pubkey(seller_pubkey),
  20262             decision: RadrootsOrderDecisionOutcome::Accepted {
  20263                 inventory_commitments: vec![RadrootsOrderInventoryCommitment {
  20264                     bin_id: test_bin_id("seller-order-primary-bin"),
  20265                     bin_count: order_quantity,
  20266                 }],
  20267             },
  20268         };
  20269         let request_event_id = test_event_id(request_event_id);
  20270         let parts = radroots_sdk::protocol::order::build_order_decision_draft(
  20271             &request_event_id,
  20272             &request_event_id,
  20273             &payload,
  20274         )
  20275         .expect("order decision draft should build")
  20276         .into_wire_parts();
  20277         let record_id = format!("app:signed_event:order-decision:{trade_order_id}");
  20278         append_trade_signed_event_record(
  20279             paths,
  20280             record_id.as_str(),
  20281             seller_pubkey,
  20282             listing_addr,
  20283             parts,
  20284         )
  20285     }
  20286 
  20287     fn append_signed_order_cancellation_record_with_prev(
  20288         paths: &AppDesktopRuntimePaths,
  20289         trade_order_id: &str,
  20290         event_key: &str,
  20291         request_event_id: &str,
  20292         prev_event_id: &str,
  20293         listing_addr: &str,
  20294         buyer_pubkey: &str,
  20295         seller_pubkey: &str,
  20296     ) -> String {
  20297         let request_event_id = test_event_id(request_event_id);
  20298         let prev_event_id = test_event_id(prev_event_id);
  20299         let payload = RadrootsOrderCancellation {
  20300             order_id: test_order_id(trade_order_id),
  20301             listing_addr: test_listing_addr(listing_addr),
  20302             buyer_pubkey: test_pubkey(buyer_pubkey),
  20303             seller_pubkey: test_pubkey(seller_pubkey),
  20304             reason: "buyer cancelled order".to_owned(),
  20305         };
  20306         let parts = radroots_sdk::protocol::order::build_order_cancellation_draft(
  20307             &request_event_id,
  20308             &prev_event_id,
  20309             &payload,
  20310         )
  20311         .expect("order cancellation draft should build")
  20312         .into_wire_parts();
  20313         let record_id = format!("app:signed_event:cancellation:{event_key}");
  20314         append_trade_signed_event_record(
  20315             paths,
  20316             record_id.as_str(),
  20317             buyer_pubkey,
  20318             listing_addr,
  20319             parts,
  20320         )
  20321     }
  20322 
  20323     fn append_signed_order_revision_proposal_record_with_prev(
  20324         paths: &AppDesktopRuntimePaths,
  20325         trade_order_id: &str,
  20326         event_key: &str,
  20327         request_event_id: &str,
  20328         prev_event_id: &str,
  20329         listing_addr: &str,
  20330         buyer_pubkey: &str,
  20331         seller_pubkey: &str,
  20332     ) -> String {
  20333         let request_event_id = test_event_id(request_event_id);
  20334         let prev_event_id = test_event_id(prev_event_id);
  20335         let payload = RadrootsOrderRevisionProposal {
  20336             revision_id: test_revision_id(format!("revision-{event_key}").as_str()),
  20337             order_id: test_order_id(trade_order_id),
  20338             listing_addr: test_listing_addr(listing_addr),
  20339             buyer_pubkey: test_pubkey(buyer_pubkey),
  20340             seller_pubkey: test_pubkey(seller_pubkey),
  20341             root_event_id: request_event_id.clone(),
  20342             prev_event_id: prev_event_id.clone(),
  20343             items: revision_test_order_items(),
  20344             economics: revision_test_order_economics(),
  20345             reason: "harvest count updated".to_owned(),
  20346         };
  20347         let parts = radroots_sdk::protocol::order::build_order_revision_proposal_draft(
  20348             &request_event_id,
  20349             &prev_event_id,
  20350             &payload,
  20351         )
  20352         .expect("order revision proposal draft should build")
  20353         .into_wire_parts();
  20354         let record_id = format!("app:signed_event:revision-proposal:{event_key}");
  20355         append_trade_signed_event_record(
  20356             paths,
  20357             record_id.as_str(),
  20358             seller_pubkey,
  20359             listing_addr,
  20360             parts,
  20361         )
  20362     }
  20363 
  20364     fn revision_test_order_items() -> Vec<RadrootsOrderItem> {
  20365         vec![RadrootsOrderItem {
  20366             bin_id: test_bin_id("seller-order-primary-bin"),
  20367             bin_count: 3,
  20368         }]
  20369     }
  20370 
  20371     fn revision_test_order_economics() -> RadrootsOrderEconomics {
  20372         RadrootsOrderEconomics {
  20373             quote_id: test_quote_id("quote-revision-test"),
  20374             quote_version: 2,
  20375             pricing_basis: RadrootsOrderPricingBasis::ListingEvent,
  20376             currency: RadrootsCoreCurrency::USD,
  20377             items: vec![RadrootsOrderEconomicItem {
  20378                 bin_id: test_bin_id("seller-order-primary-bin"),
  20379                 bin_count: 3,
  20380                 quantity_amount: RadrootsCoreDecimal::from(1u32),
  20381                 quantity_unit: RadrootsCoreUnit::Each,
  20382                 unit_price_amount: RadrootsCoreDecimal::from(8u32),
  20383                 unit_price_currency: RadrootsCoreCurrency::USD,
  20384                 line_subtotal: RadrootsCoreMoney::from_minor_units_u32(
  20385                     2400,
  20386                     RadrootsCoreCurrency::USD,
  20387                 ),
  20388             }],
  20389             discounts: Vec::new(),
  20390             adjustments: Vec::new(),
  20391             subtotal: RadrootsCoreMoney::from_minor_units_u32(2400, RadrootsCoreCurrency::USD),
  20392             discount_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
  20393             adjustment_total: RadrootsCoreMoney::zero(RadrootsCoreCurrency::USD),
  20394             total: RadrootsCoreMoney::from_minor_units_u32(2400, RadrootsCoreCurrency::USD),
  20395         }
  20396     }
  20397 
  20398     fn assert_order_lifecycle_evidence_invalid(error: AppSqliteError) {
  20399         assert!(
  20400             matches!(
  20401                 error,
  20402                 AppSqliteError::InvalidProjection {
  20403                     reason: "order lifecycle evidence is invalid"
  20404                 }
  20405             ),
  20406             "{error:?}"
  20407         );
  20408     }
  20409 
  20410     fn assert_invalid_projection_reason(error: AppSqliteError, expected_reason: &'static str) {
  20411         assert!(
  20412             matches!(
  20413                 error,
  20414                 AppSqliteError::InvalidProjection { reason } if reason == expected_reason
  20415             ),
  20416             "{error:?}"
  20417         );
  20418     }
  20419 
  20420     fn append_trade_signed_event_record(
  20421         paths: &AppDesktopRuntimePaths,
  20422         record_id: &str,
  20423         event_pubkey: &str,
  20424         listing_addr: &str,
  20425         parts: WireEventParts,
  20426     ) -> String {
  20427         let database_path = paths
  20428             .shared_local_events_database_path()
  20429             .expect("shared local events path");
  20430         if let Some(parent) = database_path.parent() {
  20431             fs::create_dir_all(parent).expect("shared local events directory should create");
  20432         }
  20433         let executor =
  20434             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  20435         let store = LocalEventsStore::new(executor);
  20436         store.migrate_up().expect("migrate shared local events");
  20437         let created_at = test_event_created_at(record_id, 1_774_000_020);
  20438         let event = test_event_from_parts(
  20439             record_id,
  20440             signed_event_id(record_id),
  20441             event_pubkey,
  20442             created_at,
  20443             parts,
  20444         );
  20445         let stored_event_id = event.id.clone();
  20446         let relay_delivery_json = RelayDeliveryEvidence::acknowledged(
  20447             ["wss://relay.example"],
  20448             ["wss://relay.example"],
  20449             ["wss://relay.example"],
  20450             Vec::new(),
  20451         )
  20452         .expect("acknowledged relay delivery evidence")
  20453         .to_json_value()
  20454         .expect("acknowledged relay delivery json");
  20455         store
  20456             .append_record(&LocalEventRecordInput {
  20457                 record_id: record_id.to_owned(),
  20458                 family: LocalRecordFamily::SignedEvent,
  20459                 status: LocalRecordStatus::Published,
  20460                 source_runtime: SourceRuntime::Test,
  20461                 created_at_ms: i64::from(created_at) * 1000,
  20462                 inserted_at_ms: i64::from(created_at) * 1000 + 1,
  20463                 owner_account_id: None,
  20464                 owner_pubkey: Some(event.author.clone()),
  20465                 farm_id: None,
  20466                 listing_addr: Some(listing_addr.to_owned()),
  20467                 local_work_json: None,
  20468                 event_id: Some(event.id.clone()),
  20469                 event_kind: Some(i64::from(event.kind)),
  20470                 event_pubkey: Some(event.author.clone()),
  20471                 event_created_at: Some(i64::from(event.created_at)),
  20472                 event_tags_json: Some(json!(event.tags.clone())),
  20473                 event_content: Some(event.content.clone()),
  20474                 event_sig: Some(event.sig.clone()),
  20475                 raw_event_json: Some(json!({
  20476                     "id": event.id.clone(),
  20477                     "kind": event.kind,
  20478                     "pubkey": event.author.clone(),
  20479                     "tags": event.tags.clone(),
  20480                     "content": event.content.clone(),
  20481                     "sig": event.sig.clone()
  20482                 })),
  20483                 outbox_status: PublishOutboxStatus::Acknowledged,
  20484                 relay_set_fingerprint: Some("relay-set".to_owned()),
  20485                 relay_delivery_json: Some(relay_delivery_json),
  20486             })
  20487             .expect("append signed trade event");
  20488         stored_event_id
  20489     }
  20490 
  20491     fn mark_shared_seller_order_request_evidence_pending(paths: &AppDesktopRuntimePaths) {
  20492         let database_path = paths
  20493             .shared_local_events_database_path()
  20494             .expect("shared local events path");
  20495         let executor =
  20496             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  20497         executor
  20498             .exec(
  20499                 "UPDATE local_event_record
  20500                  SET status = 'pending_publish',
  20501                      outbox_status = 'pending',
  20502                      relay_set_fingerprint = NULL,
  20503                      relay_delivery_json = NULL
  20504                  WHERE record_id = 'app:signed_event:order-request:seller-order-decision-1'",
  20505                 "[]",
  20506             )
  20507             .expect("mark shared order request evidence pending");
  20508     }
  20509 
  20510     fn append_unrelated_signed_event_records(paths: &AppDesktopRuntimePaths, count: usize) {
  20511         let database_path = paths
  20512             .shared_local_events_database_path()
  20513             .expect("shared local events path");
  20514         let executor =
  20515             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  20516         let store = LocalEventsStore::new(executor);
  20517         store.migrate_up().expect("migrate shared local events");
  20518         let pubkey = "2222222222222222222222222222222222222222222222222222222222222222";
  20519 
  20520         for index in 0..count {
  20521             let record_id = format!("app:signed_event:unrelated:{index}");
  20522             let event_id = signed_event_id(record_id.as_str());
  20523             store
  20524                 .append_record(&LocalEventRecordInput {
  20525                     record_id,
  20526                     family: LocalRecordFamily::SignedEvent,
  20527                     status: LocalRecordStatus::Published,
  20528                     source_runtime: SourceRuntime::Test,
  20529                     created_at_ms: 1_774_000_100_000 + i64::try_from(index).unwrap_or_default(),
  20530                     inserted_at_ms: 1_774_000_100_001 + i64::try_from(index).unwrap_or_default(),
  20531                     owner_account_id: None,
  20532                     owner_pubkey: Some(pubkey.to_owned()),
  20533                     farm_id: None,
  20534                     listing_addr: None,
  20535                     local_work_json: None,
  20536                     event_id: Some(event_id.clone()),
  20537                     event_kind: Some(1),
  20538                     event_pubkey: Some(pubkey.to_owned()),
  20539                     event_created_at: Some(
  20540                         1_774_000_100 + i64::try_from(index).unwrap_or_default(),
  20541                     ),
  20542                     event_tags_json: Some(json!([])),
  20543                     event_content: Some("{}".to_owned()),
  20544                     event_sig: Some("signature".to_owned()),
  20545                     raw_event_json: Some(json!({
  20546                         "id": event_id,
  20547                         "kind": 1,
  20548                         "pubkey": pubkey,
  20549                         "content": "{}"
  20550                     })),
  20551                     outbox_status: PublishOutboxStatus::Acknowledged,
  20552                     relay_set_fingerprint: Some("relay-set".to_owned()),
  20553                     relay_delivery_json: Some(json!({
  20554                         "state": "acknowledged",
  20555                         "acknowledged_relays": ["wss://relay.example"]
  20556                     })),
  20557                 })
  20558                 .expect("append unrelated signed event");
  20559         }
  20560     }
  20561 
  20562     fn deterministic_cli_listing_product_id(
  20563         owner_pubkey: Option<&str>,
  20564         listing_key: &str,
  20565     ) -> ProductId {
  20566         let seed = format!(
  20567             "radroots-cli-listing:{}:{}",
  20568             owner_pubkey.unwrap_or("unknown-owner"),
  20569             listing_key.trim()
  20570         );
  20571 
  20572         ProductId::from(uuid::Uuid::new_v5(
  20573             &uuid::Uuid::NAMESPACE_URL,
  20574             seed.as_bytes(),
  20575         ))
  20576     }
  20577 
  20578     fn assert_detail_open_imports_shared_local_events_before_lookup(
  20579         label: &str,
  20580         section: PersonalSection,
  20581     ) {
  20582         let (runtime, paths) = bootstrapped_runtime(label);
  20583         assert!(
  20584             runtime
  20585                 .generate_local_account(Some("Buyer".to_owned()))
  20586                 .expect("account should generate")
  20587         );
  20588         assert_eq!(
  20589             runtime
  20590                 .summary()
  20591                 .personal_projection
  20592                 .browse
  20593                 .listings
  20594                 .rows
  20595                 .len(),
  20596             0
  20597         );
  20598 
  20599         let listing_key = "DDDDDDDDDDDDDDDDDDDDDD";
  20600         append_cli_signed_buyer_listing_record_with(
  20601             &paths,
  20602             "detail-open-pending-listing",
  20603             listing_key,
  20604             "Buyer Visible Eggs",
  20605             1100,
  20606         );
  20607         let product_id =
  20608             deterministic_cli_listing_product_id(Some(BUYER_VISIBLE_SELLER_PUBKEY), listing_key);
  20609 
  20610         assert!(
  20611             runtime
  20612                 .open_personal_product_detail(section, product_id)
  20613                 .expect("buyer detail should import before lookup")
  20614         );
  20615         let summary = runtime.summary();
  20616         let detail = match section {
  20617             PersonalSection::Browse => summary.personal_projection.browse.detail,
  20618             PersonalSection::Search => summary.personal_projection.search.detail,
  20619             _ => None,
  20620         }
  20621         .expect("buyer detail should open from imported shared local events");
  20622 
  20623         assert_eq!(detail.listing.product_id, product_id);
  20624         assert_eq!(detail.listing.title, "Buyer Visible Eggs");
  20625 
  20626         cleanup_bootstrapped_runtime_paths(&paths);
  20627     }
  20628 
  20629     fn local_work_record(
  20630         record_id: &str,
  20631         account_id: &str,
  20632         farm_key: &str,
  20633         listing_addr: Option<String>,
  20634         payload: serde_json::Value,
  20635     ) -> LocalEventRecordInput {
  20636         LocalEventRecordInput {
  20637             record_id: record_id.to_owned(),
  20638             family: LocalRecordFamily::LocalWork,
  20639             status: LocalRecordStatus::LocalSaved,
  20640             source_runtime: SourceRuntime::Cli,
  20641             created_at_ms: 1000,
  20642             inserted_at_ms: 1001,
  20643             owner_account_id: Some(account_id.to_owned()),
  20644             owner_pubkey: Some("seller-pubkey".to_owned()),
  20645             farm_id: Some(farm_key.to_owned()),
  20646             listing_addr,
  20647             local_work_json: Some(payload),
  20648             event_id: None,
  20649             event_kind: None,
  20650             event_pubkey: None,
  20651             event_created_at: None,
  20652             event_tags_json: None,
  20653             event_content: None,
  20654             event_sig: None,
  20655             raw_event_json: None,
  20656             outbox_status: PublishOutboxStatus::None,
  20657             relay_set_fingerprint: None,
  20658             relay_delivery_json: None,
  20659         }
  20660     }
  20661 
  20662     fn published_operation_receipt_fixture(
  20663         source_account_id: String,
  20664         source_local_event_id: Option<String>,
  20665         event_id: &str,
  20666     ) -> AppPublishedOperationReceipt {
  20667         let event_pubkey = "1111111111111111111111111111111111111111111111111111111111111111";
  20668         AppPublishedOperationReceipt {
  20669             operation_key: "farm:upsert".to_owned(),
  20670             source_account_id,
  20671             source_local_event_id,
  20672             listing_addr: None,
  20673             event_id: event_id.to_owned(),
  20674             event_kind: 30340,
  20675             event_pubkey: event_pubkey.to_owned(),
  20676             event_created_at: 1_774_000_000,
  20677             event_tags_json: json!([["d", "farm-key"]]),
  20678             event_content: "{}".to_owned(),
  20679             event_sig: "signature".to_owned(),
  20680             raw_event_json: json!({
  20681                 "id": event_id,
  20682                 "kind": 30340,
  20683                 "pubkey": event_pubkey,
  20684                 "content": "{}"
  20685             }),
  20686             relay_set_fingerprint: "relay-set".to_owned(),
  20687             relay_delivery_json: json!({
  20688                 "state": "acknowledged",
  20689                 "acknowledged_relays": ["ws://127.0.0.1:1234/"]
  20690             }),
  20691         }
  20692     }
  20693 
  20694     fn shared_local_event_records(paths: &AppDesktopRuntimePaths) -> Vec<LocalEventRecord> {
  20695         let database_path = paths
  20696             .shared_local_events_database_path()
  20697             .expect("shared local events path");
  20698         let executor =
  20699             SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
  20700         let store = LocalEventsStore::new(executor);
  20701         store
  20702             .list_records_after_seq(0, 100)
  20703             .expect("shared local records should list")
  20704     }
  20705 
  20706     fn shared_order_request_event_id(
  20707         paths: &AppDesktopRuntimePaths,
  20708         trade_order_id: &str,
  20709     ) -> String {
  20710         let record_id = format!("app:signed_event:order-request:{trade_order_id}");
  20711         shared_local_event_records(paths)
  20712             .into_iter()
  20713             .find(|record| record.record_id == record_id)
  20714             .and_then(|record| record.event_id)
  20715             .expect("signed order request event id should exist")
  20716     }
  20717 
  20718     fn shared_order_events_by_kind(
  20719         paths: &AppDesktopRuntimePaths,
  20720         kind: i64,
  20721         pubkey: &str,
  20722     ) -> Vec<SdkRadrootsNostrEvent> {
  20723         shared_local_event_records(paths)
  20724             .into_iter()
  20725             .filter(|record| {
  20726                 record.family == LocalRecordFamily::SignedEvent
  20727                     && record.event_kind == Some(kind)
  20728                     && record.event_pubkey.as_deref() == Some(pubkey)
  20729             })
  20730             .filter_map(|record| {
  20731                 signed_event_from_local_record(&record)
  20732                     .expect("shared signed event record should decode")
  20733             })
  20734             .collect()
  20735     }
  20736 
  20737     fn persisted_order_status(runtime: &DesktopAppRuntime, order_id: OrderId) -> String {
  20738         runtime
  20739             .lock_state()
  20740             .sqlite_store
  20741             .as_ref()
  20742             .expect("sqlite store")
  20743             .connection()
  20744             .query_row(
  20745                 "select status from orders where id = ?1 limit 1",
  20746                 [order_id.to_string()],
  20747                 |row| row.get::<_, String>(0),
  20748             )
  20749             .expect("order status should load")
  20750     }
  20751 
  20752     fn pending_order_sync_payloads(
  20753         runtime: &DesktopAppRuntime,
  20754         account_id: &str,
  20755         order_id: OrderId,
  20756     ) -> Vec<String> {
  20757         runtime
  20758             .lock_state()
  20759             .sqlite_store
  20760             .as_ref()
  20761             .expect("sqlite store")
  20762             .load_pending_sync_operations(account_id)
  20763             .expect("pending sync operations should load")
  20764             .into_iter()
  20765             .filter(|pending| {
  20766                 pending.operation.operation == SyncOperationKind::Upsert
  20767                     && matches!(pending.operation.aggregate, SyncAggregateRef::Order(id) if id == order_id)
  20768             })
  20769             .map(|pending| pending.operation.payload_json)
  20770             .collect()
  20771     }
  20772 
  20773     fn assert_no_order_request_pending_sync_payloads(
  20774         runtime: &DesktopAppRuntime,
  20775         account_id: &str,
  20776         order_id: OrderId,
  20777     ) {
  20778         assert!(pending_order_sync_payloads(runtime, account_id, order_id).is_empty());
  20779     }
  20780 
  20781     fn assert_order_request_sdk_migration_receipt(
  20782         runtime: &DesktopAppRuntime,
  20783         order_id: OrderId,
  20784         expected_state: AppSdkMigrationState,
  20785     ) {
  20786         let source_record_id = format!("app:local_work:order_request:{order_id}");
  20787         let receipt = runtime
  20788             .lock_state()
  20789             .sqlite_store
  20790             .as_ref()
  20791             .expect("sqlite store")
  20792             .sdk_migration_receipt_repository()
  20793             .load_receipt(
  20794                 AppSdkMigrationReceiptSourceKind::SharedLocalEvent,
  20795                 source_record_id.as_str(),
  20796             )
  20797             .expect("SDK migration receipt should load")
  20798             .expect("SDK migration receipt should exist");
  20799         assert_eq!(receipt.source_record_id, source_record_id);
  20800         assert_eq!(receipt.sdk_operation_kind, ORDER_SUBMIT_OPERATION_KIND);
  20801         assert_eq!(
  20802             receipt.migration_state, expected_state,
  20803             "receipt detail: {}",
  20804             receipt.detail_json
  20805         );
  20806         if expected_state == AppSdkMigrationState::Enqueued {
  20807             assert!(receipt.expected_event_id.is_some());
  20808             assert!(receipt.actor_pubkey.as_deref().is_some_and(is_hex_64));
  20809             assert!(!receipt.sdk_outbox_event_ids.is_empty());
  20810         }
  20811     }
  20812 
  20813     fn assert_order_decision_sdk_migration_receipt(
  20814         runtime: &DesktopAppRuntime,
  20815         order_id: OrderId,
  20816         expected_state: AppSdkMigrationState,
  20817     ) {
  20818         let source_record_id = format!("app:order_decision:{order_id}");
  20819         let receipt = runtime
  20820             .lock_state()
  20821             .sqlite_store
  20822             .as_ref()
  20823             .expect("sqlite store")
  20824             .sdk_migration_receipt_repository()
  20825             .load_receipt(
  20826                 AppSdkMigrationReceiptSourceKind::LocalOutbox,
  20827                 source_record_id.as_str(),
  20828             )
  20829             .expect("SDK migration receipt should load")
  20830             .expect("SDK migration receipt should exist");
  20831         assert_eq!(receipt.source_record_id, source_record_id);
  20832         assert_eq!(receipt.sdk_operation_kind, ORDER_DECISION_OPERATION_KIND);
  20833         assert_eq!(
  20834             receipt.migration_state, expected_state,
  20835             "receipt detail: {}",
  20836             receipt.detail_json
  20837         );
  20838         if expected_state == AppSdkMigrationState::Enqueued {
  20839             assert!(receipt.expected_event_id.is_some());
  20840             assert!(receipt.actor_pubkey.as_deref().is_some_and(is_hex_64));
  20841             assert!(!receipt.sdk_outbox_event_ids.is_empty());
  20842         }
  20843     }
  20844 
  20845     fn assert_order_revision_decision_sdk_migration_receipt(
  20846         runtime: &DesktopAppRuntime,
  20847         order_id: OrderId,
  20848         revision_id: &str,
  20849         expected_state: AppSdkMigrationState,
  20850     ) {
  20851         assert_order_sdk_migration_receipt(
  20852             runtime,
  20853             format!("app:order_revision_decision:{order_id}:{revision_id}").as_str(),
  20854             ORDER_REVISION_DECISION_OPERATION_KIND,
  20855             expected_state,
  20856         );
  20857     }
  20858 
  20859     fn assert_order_cancellation_sdk_migration_receipt(
  20860         runtime: &DesktopAppRuntime,
  20861         order_id: OrderId,
  20862         expected_state: AppSdkMigrationState,
  20863     ) {
  20864         assert_order_sdk_migration_receipt(
  20865             runtime,
  20866             format!("app:order_cancellation:{order_id}").as_str(),
  20867             ORDER_CANCELLATION_OPERATION_KIND,
  20868             expected_state,
  20869         );
  20870     }
  20871 
  20872     fn assert_order_sdk_migration_receipt(
  20873         runtime: &DesktopAppRuntime,
  20874         source_record_id: &str,
  20875         operation_kind: &str,
  20876         expected_state: AppSdkMigrationState,
  20877     ) {
  20878         let receipt = runtime
  20879             .lock_state()
  20880             .sqlite_store
  20881             .as_ref()
  20882             .expect("sqlite store")
  20883             .sdk_migration_receipt_repository()
  20884             .load_receipt(
  20885                 AppSdkMigrationReceiptSourceKind::LocalOutbox,
  20886                 source_record_id,
  20887             )
  20888             .expect("SDK migration receipt should load")
  20889             .expect("SDK migration receipt should exist");
  20890         assert_eq!(receipt.source_record_id, source_record_id);
  20891         assert_eq!(receipt.sdk_operation_kind, operation_kind);
  20892         assert_eq!(
  20893             receipt.migration_state, expected_state,
  20894             "receipt detail: {}",
  20895             receipt.detail_json
  20896         );
  20897         if expected_state == AppSdkMigrationState::Enqueued {
  20898             assert!(receipt.expected_event_id.is_some());
  20899             assert!(receipt.actor_pubkey.as_deref().is_some_and(is_hex_64));
  20900             assert!(!receipt.sdk_outbox_event_ids.is_empty());
  20901         }
  20902     }
  20903 
  20904     fn buyer_order_local_work_record_ids(paths: &AppDesktopRuntimePaths) -> Vec<String> {
  20905         shared_local_event_records(paths)
  20906             .into_iter()
  20907             .filter(|record| {
  20908                 record.source_runtime == SourceRuntime::App
  20909                     && record
  20910                         .local_work_json
  20911                         .as_ref()
  20912                         .and_then(|payload| payload["record_kind"].as_str())
  20913                         == Some(BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND)
  20914             })
  20915             .map(|record| record.record_id)
  20916             .collect()
  20917     }
  20918 
  20919     fn blocked_buyer_order_runtime(
  20920         label: &str,
  20921     ) -> (DesktopAppRuntime, AppDesktopRuntimePaths, String, OrderId) {
  20922         let (runtime, paths) = bootstrapped_runtime(label);
  20923         let _ = install_recorded_sync_transport(
  20924             &runtime,
  20925             RecordedAppSyncTransport::fail(AppSyncTransportError::unavailable(
  20926                 "test sync unavailable",
  20927             )),
  20928         );
  20929         assert!(
  20930             runtime
  20931                 .generate_local_account(Some("Buyer".to_owned()))
  20932                 .expect("account should generate")
  20933         );
  20934         let buyer_account_id = runtime
  20935             .summary()
  20936             .settings_account_projection
  20937             .selected_account
  20938             .as_ref()
  20939             .expect("selected account")
  20940             .account
  20941             .account_id
  20942             .clone();
  20943         assert!(
  20944             runtime
  20945                 .select_active_surface(ActiveSurface::Personal)
  20946                 .expect("surface should switch into marketplace")
  20947         );
  20948         let listing_key = "DDDDDDDDDDDDDDDDDDDDDD";
  20949         append_cli_signed_buyer_listing_record_with(
  20950             &paths,
  20951             "buyer-order-append-failure-listing",
  20952             listing_key,
  20953             "Buyer Visible Eggs",
  20954             1100,
  20955         );
  20956         let product_id =
  20957             deterministic_cli_listing_product_id(Some(BUYER_VISIBLE_SELLER_PUBKEY), listing_key);
  20958         assert!(
  20959             runtime
  20960                 .open_personal_product_detail(PersonalSection::Browse, product_id)
  20961                 .expect("buyer detail should import before lookup")
  20962         );
  20963         assert!(
  20964             runtime
  20965                 .add_personal_product_to_cart(PersonalSection::Browse, false)
  20966                 .expect("buyer product should add to cart")
  20967         );
  20968         assert!(
  20969             runtime
  20970                 .save_personal_order_review_draft(BuyerOrderReviewDraft {
  20971                     name: "Casey Buyer".to_owned(),
  20972                     email: "casey@example.com".to_owned(),
  20973                     phone: String::new(),
  20974                     order_note: String::new(),
  20975                 })
  20976                 .expect("buyer order review draft should save")
  20977         );
  20978         block_shared_local_events_database(&paths);
  20979 
  20980         let error = runtime
  20981             .place_personal_order()
  20982             .expect_err("blocked local events should fail order completion");
  20983 
  20984         assert!(matches!(error, AppSqliteError::LocalEventsSql { .. }));
  20985         let summary = runtime.summary();
  20986         assert_eq!(
  20987             summary.shell_projection.selected_section,
  20988             ShellSection::Personal(PersonalSection::Orders)
  20989         );
  20990         assert!(
  20991             summary
  20992                 .personal_projection
  20993                 .orders
  20994                 .has_recoverable_coordination
  20995         );
  20996         assert!(summary.personal_projection.cart.cart.lines.is_empty());
  20997         assert!(
  20998             !summary
  20999                 .personal_projection
  21000                 .cart
  21001                 .order_review
  21002                 .can_place_order
  21003         );
  21004         assert_eq!(summary.personal_projection.orders.list.rows.len(), 1);
  21005         let visible_order_id = summary.personal_projection.orders.list.rows[0].order_id;
  21006         let order_detail = summary
  21007             .personal_projection
  21008             .orders
  21009             .detail
  21010             .as_ref()
  21011             .expect("buyer order detail should remain visible after coordination failure");
  21012         assert_eq!(order_detail.order_id, visible_order_id);
  21013         {
  21014             let state = runtime.lock_state_mut();
  21015             let buyer_context = state.state_store.identity_projection().buyer_context();
  21016             let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store");
  21017             let buyer_orders = sqlite_store
  21018                 .load_buyer_orders(&buyer_context)
  21019                 .expect("buyer order should persist after coordination failure");
  21020             assert_eq!(buyer_orders.rows.len(), 1);
  21021             let order_id = buyer_orders.rows[0].order_id;
  21022             assert_eq!(order_id, visible_order_id);
  21023             let coordination = sqlite_store
  21024                 .load_buyer_order_coordination_record(&buyer_context, order_id)
  21025                 .expect("buyer order coordination should load")
  21026                 .expect("buyer order coordination should exist");
  21027             assert_eq!(coordination.state, BuyerOrderCoordinationState::Failed);
  21028             assert_eq!(coordination.attempt_count, 1);
  21029             assert!(coordination.record_id.is_some());
  21030             assert!(coordination.payload_json.is_some());
  21031             assert!(coordination.last_error_message.is_some());
  21032         }
  21033         assert!(
  21034             pending_order_sync_payloads(&runtime, buyer_account_id.as_str(), visible_order_id)
  21035                 .is_empty()
  21036         );
  21037 
  21038         (runtime, paths, buyer_account_id, visible_order_id)
  21039     }
  21040 
  21041     fn block_shared_local_events_database(paths: &AppDesktopRuntimePaths) {
  21042         let database_path = paths
  21043             .shared_local_events_database_path()
  21044             .expect("shared local events path");
  21045         if let Some(parent) = database_path.parent() {
  21046             fs::create_dir_all(parent).expect("shared local events directory should create");
  21047         }
  21048         if database_path.is_file() {
  21049             fs::remove_file(&database_path).expect("shared local events file should remove");
  21050         } else if database_path.is_dir() {
  21051             fs::remove_dir_all(&database_path)
  21052                 .expect("shared local events directory should remove");
  21053         }
  21054         fs::create_dir(&database_path).expect("blocking directory should create");
  21055     }
  21056 
  21057     fn unblock_shared_local_events_database(paths: &AppDesktopRuntimePaths) {
  21058         let database_path = paths
  21059             .shared_local_events_database_path()
  21060             .expect("shared local events path");
  21061         if database_path.is_dir() {
  21062             fs::remove_dir_all(&database_path).expect("blocking directory should remove");
  21063         }
  21064     }
  21065 
  21066     fn fixture_pending_session() -> RadrootsAppRemoteSignerPendingSession {
  21067         let signer_identity = RadrootsIdentity::from_secret_key_str(
  21068             "1111111111111111111111111111111111111111111111111111111111111111",
  21069         )
  21070         .expect("signer identity");
  21071         let client_identity = RadrootsIdentity::from_secret_key_str(
  21072             "3333333333333333333333333333333333333333333333333333333333333333",
  21073         )
  21074         .expect("client identity");
  21075 
  21076         RadrootsAppRemoteSignerPendingSession {
  21077             record: RadrootsAppRemoteSignerSessionRecord::pending(
  21078                 client_identity.to_public(),
  21079                 signer_identity.to_public(),
  21080                 vec!["ws://127.0.0.1:8080".to_owned()],
  21081             ),
  21082             client_secret_key_hex: client_identity.secret_key_hex(),
  21083         }
  21084     }
  21085 
  21086     fn save_surface_activation(
  21087         runtime: &DesktopAppRuntime,
  21088         account_id: &str,
  21089         active_surface: ActiveSurface,
  21090         farmer_active: bool,
  21091     ) {
  21092         let activation = AccountSurfaceActivationProjection::new(
  21093             account_id,
  21094             SelectedSurfaceProjection::new(active_surface),
  21095             if farmer_active {
  21096                 FarmerActivationProjection::active(FarmId::new())
  21097             } else {
  21098                 FarmerActivationProjection::inactive()
  21099             },
  21100         );
  21101         runtime
  21102             .lock_state()
  21103             .sqlite_store
  21104             .as_ref()
  21105             .expect("sqlite store")
  21106             .save_surface_activation(&activation)
  21107             .expect("surface activation should save");
  21108     }
  21109 
  21110     fn save_farmer_surface_activation(
  21111         runtime: &DesktopAppRuntime,
  21112         account_id: &str,
  21113         active_surface: ActiveSurface,
  21114     ) -> FarmId {
  21115         let farm_id = FarmId::new();
  21116         let activation = AccountSurfaceActivationProjection::new(
  21117             account_id,
  21118             SelectedSurfaceProjection::new(active_surface),
  21119             FarmerActivationProjection::active(farm_id),
  21120         );
  21121         runtime
  21122             .lock_state()
  21123             .sqlite_store
  21124             .as_ref()
  21125             .expect("sqlite store")
  21126             .save_surface_activation(&activation)
  21127             .expect("surface activation should save");
  21128         farm_id
  21129     }
  21130 
  21131     fn seed_product(
  21132         runtime: &DesktopAppRuntime,
  21133         farm_id: FarmId,
  21134         title: &str,
  21135         subtitle: &str,
  21136         status: &str,
  21137         stock_count: Option<u32>,
  21138         updated_at: &str,
  21139     ) -> radroots_app_view::ProductId {
  21140         let product_id = radroots_app_view::ProductId::new();
  21141         let stock_count = stock_count
  21142             .map(|value| value.to_string())
  21143             .unwrap_or_else(|| "null".to_owned());
  21144         let sql = format!(
  21145             "insert into products (
  21146                 id,
  21147                 farm_id,
  21148                 title,
  21149                 subtitle,
  21150                 status,
  21151                 unit_label,
  21152                 price_minor_units,
  21153                 price_currency,
  21154                 stock_count,
  21155                 availability_window_id,
  21156                 updated_at
  21157             ) values (
  21158                 '{product_id}',
  21159                 '{farm_id}',
  21160                 '{title}',
  21161                 '{subtitle}',
  21162                 '{status}',
  21163                 'box',
  21164                 600,
  21165                 'USD',
  21166                 {stock_count},
  21167                 null,
  21168                 '{updated_at}'
  21169             )",
  21170             product_id = product_id,
  21171             farm_id = farm_id,
  21172             title = title,
  21173             subtitle = subtitle,
  21174             status = status,
  21175             stock_count = stock_count,
  21176             updated_at = updated_at,
  21177         );
  21178         runtime
  21179             .lock_state()
  21180             .sqlite_store
  21181             .as_ref()
  21182             .expect("sqlite store")
  21183             .connection()
  21184             .execute_batch(&sql)
  21185             .expect("product should seed");
  21186 
  21187         product_id
  21188     }
  21189 
  21190     fn seed_buyer_marketplace_support(
  21191         runtime: &DesktopAppRuntime,
  21192         account_id: &str,
  21193         farm_id: FarmId,
  21194         farm_display_name: &str,
  21195         fulfillment_label: &str,
  21196     ) -> FulfillmentWindowId {
  21197         let pickup_location_id = PickupLocationId::new();
  21198         let fulfillment_window_id = FulfillmentWindowId::new();
  21199         let sql = format!(
  21200             "insert into pickup_locations (
  21201                 id,
  21202                 farm_id,
  21203                 label,
  21204                 address_line,
  21205                 directions,
  21206                 is_default,
  21207                 created_at,
  21208                 updated_at
  21209              ) values (
  21210                 '{pickup_location_id}',
  21211                 '{farm_id}',
  21212                 'North barn',
  21213                 '14 County Road',
  21214                 null,
  21215                 1,
  21216                 '2026-04-20T08:00:00Z',
  21217                 '2026-04-20T08:00:00Z'
  21218              );
  21219              insert into fulfillment_windows (
  21220                 id,
  21221                 farm_id,
  21222                 starts_at,
  21223                 ends_at,
  21224                 capacity_limit,
  21225                 created_at,
  21226                 updated_at,
  21227                 pickup_location_id,
  21228                 label,
  21229                 order_cutoff_at
  21230              ) values (
  21231                 '{fulfillment_window_id}',
  21232                 '{farm_id}',
  21233                 '2099-04-18T16:00:00Z',
  21234                 '2099-04-18T18:00:00Z',
  21235                 null,
  21236                 '2099-04-18T16:00:00Z',
  21237                 '2099-04-18T16:00:00Z',
  21238                 '{pickup_location_id}',
  21239                 '{fulfillment_label}',
  21240                 '2099-04-17T18:00:00Z'
  21241              );
  21242              insert into account_farm_setups (
  21243                 account_id,
  21244                 farm_name,
  21245                 location_or_service_area,
  21246                 pickup_enabled,
  21247                 delivery_enabled,
  21248                 shipping_enabled,
  21249                 saved_farm_id,
  21250                 saved_farm_display_name,
  21251                 saved_farm_readiness,
  21252                 updated_at
  21253              ) values (
  21254                 '{account_id}',
  21255                 '{farm_display_name}',
  21256                 'County Road',
  21257                 1,
  21258                 0,
  21259                 0,
  21260                 '{farm_id}',
  21261                 '{farm_display_name}',
  21262                 'ready',
  21263                 '2026-04-20T08:00:00Z'
  21264              )
  21265              on conflict(account_id) do update set
  21266                 farm_name = excluded.farm_name,
  21267                 location_or_service_area = excluded.location_or_service_area,
  21268                 pickup_enabled = excluded.pickup_enabled,
  21269                 delivery_enabled = excluded.delivery_enabled,
  21270                 shipping_enabled = excluded.shipping_enabled,
  21271                 saved_farm_id = excluded.saved_farm_id,
  21272                 saved_farm_display_name = excluded.saved_farm_display_name,
  21273                 saved_farm_readiness = excluded.saved_farm_readiness,
  21274                 updated_at = excluded.updated_at;"
  21275         );
  21276         runtime
  21277             .lock_state()
  21278             .sqlite_store
  21279             .as_ref()
  21280             .expect("sqlite store")
  21281             .connection()
  21282             .execute_batch(&sql)
  21283             .expect("buyer marketplace support should seed");
  21284 
  21285         fulfillment_window_id
  21286     }
  21287 
  21288     fn provision_ready_farmer_account(runtime: &DesktopAppRuntime) -> (String, FarmId) {
  21289         assert!(
  21290             runtime
  21291                 .generate_local_account(Some("Farmer".to_owned()))
  21292                 .expect("account should generate")
  21293         );
  21294         provision_selected_farmer_account(runtime)
  21295     }
  21296 
  21297     fn provision_ready_farmer_account_from_secret(
  21298         runtime: &DesktopAppRuntime,
  21299         secret_key_hex: &str,
  21300     ) -> (String, FarmId) {
  21301         assert!(
  21302             runtime
  21303                 .import_local_account(DesktopLocalIdentityImportRequest::raw_secret_key(
  21304                     secret_key_hex,
  21305                 ))
  21306                 .expect("account should import")
  21307         );
  21308         provision_selected_farmer_account(runtime)
  21309     }
  21310 
  21311     fn provision_selected_farmer_account(runtime: &DesktopAppRuntime) -> (String, FarmId) {
  21312         let account_id = runtime
  21313             .summary()
  21314             .settings_account_projection
  21315             .selected_account
  21316             .as_ref()
  21317             .expect("selected account")
  21318             .account
  21319             .account_id
  21320             .clone();
  21321         let farm_id =
  21322             save_farmer_surface_activation(runtime, account_id.as_str(), ActiveSurface::Farmer);
  21323         let farm_setup_projection = FarmSetupProjection::from_saved_farm(FarmSummary {
  21324             farm_id,
  21325             display_name: "North field farm".to_owned(),
  21326             readiness: FarmReadiness::Ready,
  21327         });
  21328         runtime
  21329             .lock_state()
  21330             .sqlite_store
  21331             .as_ref()
  21332             .expect("sqlite store")
  21333             .save_farm_summary(
  21334                 farm_setup_projection
  21335                     .saved_farm
  21336                     .as_ref()
  21337                     .expect("saved farm should exist"),
  21338             )
  21339             .expect("farm summary should save");
  21340         runtime
  21341             .lock_state()
  21342             .sqlite_store
  21343             .as_ref()
  21344             .expect("sqlite store")
  21345             .save_farm_setup(account_id.as_str(), &farm_setup_projection)
  21346             .expect("farm setup should save");
  21347         assert!(
  21348             runtime
  21349                 .select_local_account(account_id.as_str())
  21350                 .expect("account should select")
  21351         );
  21352 
  21353         (account_id, farm_id)
  21354     }
  21355 
  21356     fn seed_order_workspace(
  21357         runtime: &DesktopAppRuntime,
  21358         farm_id: FarmId,
  21359     ) -> (FulfillmentWindowId, OrderId) {
  21360         let pickup_location_id = PickupLocationId::new();
  21361         let fulfillment_window_id = FulfillmentWindowId::new();
  21362         let order_id = OrderId::new();
  21363         let sql = format!(
  21364             "insert into pickup_locations (
  21365                 id,
  21366                 farm_id,
  21367                 label,
  21368                 address_line,
  21369                 directions,
  21370                 is_default,
  21371                 created_at,
  21372                 updated_at
  21373              ) values (
  21374                 '{pickup_location_id}',
  21375                 '{farm_id}',
  21376                 'North barn',
  21377                 '14 County Road',
  21378                 null,
  21379                 1,
  21380                 '2026-04-17T08:00:00Z',
  21381                 '2026-04-17T08:00:00Z'
  21382              );
  21383              insert into fulfillment_windows (
  21384                 id,
  21385                 farm_id,
  21386                 starts_at,
  21387                 ends_at,
  21388                 capacity_limit,
  21389                 created_at,
  21390                 updated_at,
  21391                 pickup_location_id,
  21392                 label,
  21393                 order_cutoff_at
  21394              ) values (
  21395                 '{fulfillment_window_id}',
  21396                 '{farm_id}',
  21397                 '2099-04-18T16:00:00Z',
  21398                 '2099-04-18T18:00:00Z',
  21399                 null,
  21400                 '2099-04-18T16:00:00Z',
  21401                 '2099-04-18T16:00:00Z',
  21402                 '{pickup_location_id}',
  21403                 'Friday pickup',
  21404                 '2099-04-17T18:00:00Z'
  21405              );
  21406              insert into orders (
  21407                 id,
  21408                 farm_id,
  21409                 fulfillment_window_id,
  21410                 order_number,
  21411                 customer_display_name,
  21412                 status,
  21413                 updated_at
  21414              ) values (
  21415                 '{order_id}',
  21416                 '{farm_id}',
  21417                 '{fulfillment_window_id}',
  21418                 'R-100',
  21419                 'Casey',
  21420                 'needs_action',
  21421                 '2026-04-17T10:00:00Z'
  21422              );
  21423              insert into order_lines (
  21424                 id,
  21425                 order_id,
  21426                 title,
  21427                 quantity_value,
  21428                 quantity_unit_label,
  21429                 quantity_display,
  21430                 sort_index
  21431              ) values (
  21432                 'line-1',
  21433                 '{order_id}',
  21434                 'Salad mix',
  21435                 2,
  21436                 'bags',
  21437                 '2 bags',
  21438                 0
  21439              )",
  21440         );
  21441         runtime
  21442             .lock_state()
  21443             .sqlite_store
  21444             .as_ref()
  21445             .expect("sqlite store")
  21446             .connection()
  21447             .execute_batch(&sql)
  21448             .expect("orders workspace should seed");
  21449 
  21450         (fulfillment_window_id, order_id)
  21451     }
  21452 
  21453     fn seed_second_order_workspace(
  21454         runtime: &DesktopAppRuntime,
  21455         farm_id: FarmId,
  21456         source_fulfillment_window_id: FulfillmentWindowId,
  21457     ) -> (FulfillmentWindowId, OrderId) {
  21458         let fulfillment_window_id = FulfillmentWindowId::new();
  21459         let order_id = OrderId::new();
  21460         let sql = format!(
  21461             "insert into fulfillment_windows (
  21462                 id,
  21463                 farm_id,
  21464                 starts_at,
  21465                 ends_at,
  21466                 capacity_limit,
  21467                 created_at,
  21468                 updated_at,
  21469                 pickup_location_id,
  21470                 label,
  21471                 order_cutoff_at
  21472              )
  21473              select
  21474                 '{fulfillment_window_id}',
  21475                 farm_id,
  21476                 '2099-04-19T16:00:00Z',
  21477                 '2099-04-19T18:00:00Z',
  21478                 capacity_limit,
  21479                 '2099-04-19T16:00:00Z',
  21480                 '2099-04-19T16:00:00Z',
  21481                 pickup_location_id,
  21482                 'Saturday pickup',
  21483                 '2099-04-18T18:00:00Z'
  21484              from fulfillment_windows
  21485              where id = '{source_fulfillment_window_id}' and farm_id = '{farm_id}';
  21486              insert into orders (
  21487                 id,
  21488                 farm_id,
  21489                 fulfillment_window_id,
  21490                 order_number,
  21491                 customer_display_name,
  21492                 status,
  21493                 updated_at
  21494              ) values (
  21495                 '{order_id}',
  21496                 '{farm_id}',
  21497                 '{fulfillment_window_id}',
  21498                 'R-101',
  21499                 'Robin',
  21500                 'scheduled',
  21501                 '2026-04-17T11:00:00Z'
  21502              )"
  21503         );
  21504         runtime
  21505             .lock_state()
  21506             .sqlite_store
  21507             .as_ref()
  21508             .expect("sqlite store")
  21509             .connection()
  21510             .execute_batch(&sql)
  21511             .expect("second orders workspace should seed");
  21512 
  21513         (fulfillment_window_id, order_id)
  21514     }
  21515 
  21516     fn cleanup_paths(paths: &AppSharedAccountsPaths) {
  21517         let Some(base) = paths.data_root.ancestors().nth(3).map(PathBuf::from) else {
  21518             return;
  21519         };
  21520         let _ = fs::remove_dir_all(base);
  21521     }
  21522 }