app

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

commit 73c6e71560a3f3ece67546960a1ffb82d80466ee
parent a4c8f33021f25909d5afe3d4520b2d2cccd5e809
Author: triesap <tyson@radroots.org>
Date:   Fri, 19 Jun 2026 21:21:06 -0700

app: align order negotiation workflow

- remove missed-pickup recovery state, storage, reminders, and UI
- drop missed-pickup policy from farm operating rules and localized copy
- align buyer cancellation and seller revision paths with pre-agreement SDK lifecycle
- validate full desktop, view, sqlite, i18n, state, source-guard, and check gates

Diffstat:
Mcrates/desktop/src/runtime.rs | 738++++++++++++++++++-------------------------------------------------------------
Mcrates/desktop/src/source_guards.rs | 24------------------------
Mcrates/desktop/src/window.rs | 389+++----------------------------------------------------------------------------
Mcrates/i18n/src/keys.rs | 12------------
Mcrates/i18n/src/lib.rs | 14--------------
Mcrates/state/src/lib.rs | 36+++---------------------------------
Mcrates/store/migrations/0006_farm_rules_workspace.sql | 1-
Acrates/store/migrations/0011_reminders.sql | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/store/migrations/0011_reminders_and_recovery.sql | 102-------------------------------------------------------------------------------
Mcrates/store/src/lib.rs | 47+++--------------------------------------------
Mcrates/store/src/migrations.rs | 2+-
Mcrates/store/src/repo/farm_rules.rs | 35+++++++++++------------------------
Mcrates/store/src/repo/orders.rs | 1-
Mcrates/store/src/repo/reminders.rs | 274+++----------------------------------------------------------------------------
Mcrates/store/src/repo/today.rs | 1-
Mcrates/types/src/lib.rs | 37-------------------------------------
Mcrates/view/src/lib.rs | 101+++++++++++++++++++++----------------------------------------------------------
Mi18n/locales/en/messages.json | 12------------
18 files changed, 302 insertions(+), 1596 deletions(-)

diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -45,24 +45,25 @@ use radroots_app_sync::{ AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload, AppPublishContext, AppPublishPayload, AppPublishedOperationReceipt, AppRelayIngestScopeFreshness, AppSyncProjection, AppSyncRequest, AppSyncResult, - AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, PendingSyncOperation, - SyncAggregateRef, SyncCheckpointStatus, SyncConflictSeverity, SyncOperationKind, SyncTrigger, + AppSyncRunStatus, AppSyncTransport, AppSyncTransportError, SyncCheckpointStatus, + SyncConflictSeverity, SyncTrigger, }; +#[cfg(test)] +use radroots_app_sync::{PendingSyncOperation, SyncAggregateRef, SyncOperationKind}; use radroots_app_view::{ ActiveSurface, AppActivityContext, AppActivityKind, AppIdentityProjection, AppStartupGate, BuyerCartLineProjection, BuyerCartProjection, BuyerCartReplaceConfirmationProjection, BuyerContext, BuyerOrderDetailProjection, BuyerOrderReviewDraft, BuyerOrderStatus, BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmProfileRecord, FarmReadiness, FarmRulesProjection, FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection, - FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderId, - OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListProjection, - OrdersScreenQueryState, PackDayBatchPrintStatus, PackDayExportBundle, PackDayExportInstanceId, - PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPrintKind, - PackDayPrintStatus, PackDayProjection, PackDayScreenQueryState, PersonalSection, - PickupLocationRecord, ProductEditorDraft, ProductId, ProductStatus, ProductsFilter, - ProductsListProjection, ProductsSort, RecoveryKind, RecoveryQueueProjection, RecoveryRecordId, - RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, - ReminderId, ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, + FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrderStatus, + OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayBatchPrintStatus, + PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, + PackDayHostHandoffStatus, PackDayPrintKind, PackDayPrintStatus, PackDayProjection, + PackDayScreenQueryState, PersonalSection, PickupLocationRecord, ProductEditorDraft, ProductId, + ProductStatus, ProductsFilter, ProductsListProjection, ProductsSort, + ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, ReminderId, + ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, }; @@ -243,6 +244,7 @@ struct ResolvedAppOrderRevisionDecisionEvidence { #[derive(Clone, Debug, Eq, PartialEq)] struct ResolvedAppOrderLifecycleEvidence { evidence_events: Vec<SdkRadrootsNostrEvent>, + request_event_id: String, status: RadrootsOrderStatus, agreement_event_id: Option<String>, last_event_id: Option<String>, @@ -868,38 +870,6 @@ impl DesktopAppRuntime { .publish_buyer_order_revision_decline(order_id) } - pub fn start_order_recovery( - &self, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<bool, AppSqliteError> { - self.lock_state_mut().start_order_recovery(order_id, kind) - } - - pub fn review_order_recovery( - &self, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<bool, AppSqliteError> { - self.lock_state_mut().review_order_recovery(order_id, kind) - } - - pub fn reopen_order_recovery( - &self, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<bool, AppSqliteError> { - self.lock_state_mut().reopen_order_recovery(order_id, kind) - } - - pub fn resolve_order_recovery( - &self, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<bool, AppSqliteError> { - self.lock_state_mut().resolve_order_recovery(order_id, kind) - } - pub fn open_pack_day( &self, fulfillment_window_id: Option<FulfillmentWindowId>, @@ -1479,7 +1449,6 @@ struct DesktopSelectedAccountContext { orders_query: OrdersScreenQueryState, orders_list: OrdersListProjection, orders_reminders: ReminderFeedProjection, - recovery_queue: RecoveryQueueProjection, order_detail: Option<OrderDetailProjection>, pack_day_query: PackDayScreenQueryState, pack_day_projection: PackDayProjection, @@ -1492,10 +1461,7 @@ struct DesktopSellerReminderContext { today_feed: ReminderFeedProjection, orders_feed: ReminderFeedProjection, pack_day_feed: ReminderFeedProjection, - recovery_queue: RecoveryQueueProjection, - selected_order_recoveries: Vec<OrderRecoveryProjection>, due_soon_count: u32, - recovery_actions_open: u32, reminder_log: ReminderLogProjection, } @@ -2768,22 +2734,14 @@ impl DesktopAppRuntimeState { }); } let lifecycle = self.resolve_order_lifecycle_evidence(&request)?; - let Some(decision) = lifecycle.decision.as_ref() else { - return Err(AppSqliteError::InvalidProjection { - reason: "seller order revision requires accepted order decision evidence", - }); - }; - if !matches!( - decision.payload.decision, - RadrootsOrderDecisionOutcome::Accepted { .. } - ) { + if lifecycle.decision.is_some() || lifecycle.status != RadrootsOrderStatus::Requested { return Err(AppSqliteError::InvalidProjection { - reason: "seller order revision requires accepted order decision evidence", + reason: "seller order revision requires an undecided order", }); } if lifecycle.cancellation_event_id.is_some() { return Err(AppSqliteError::InvalidProjection { - reason: "seller order revision requires an active order", + reason: "seller order revision requires an undecided order", }); } let Some(order_detail) = sqlite_store.load_order_detail(farm_id, order_id)? else { @@ -2791,16 +2749,16 @@ impl DesktopAppRuntimeState { reason: "seller order revision requires a visible seller order", }); }; - if order_detail.status != OrderStatus::Scheduled { + if order_detail.status != OrderStatus::NeedsAction { return Err(AppSqliteError::InvalidProjection { - reason: "seller order revision requires a scheduled order", + reason: "seller order revision requires an undecided order", }); } - let Some(prev_event_id) = active_order_revision_parent_event_id(&lifecycle) else { + if active_order_pending_revision_proposal(&lifecycle).is_some() { return Err(AppSqliteError::InvalidProjection { reason: "seller order revision requires no pending revision proposal", }); - }; + } let reason = reason.trim(); if reason.is_empty() { return Err(AppSqliteError::InvalidProjection { @@ -2813,7 +2771,7 @@ impl DesktopAppRuntimeState { farm_id, trade_order_id: request.payload.order_id.to_string(), request_event_id: request.request_event_id, - prev_event_id, + prev_event_id: lifecycle.request_event_id, revision_id: format!("app-revision-{}", d_tag_from_uuid(Uuid::now_v7())), listing_addr: request.payload.listing_addr.to_string(), buyer_pubkey: request.payload.buyer_pubkey.to_string(), @@ -2895,9 +2853,12 @@ impl DesktopAppRuntimeState { reason: "buyer order revision requires a visible buyer order", }); }; - if detail.status != BuyerOrderStatus::Scheduled { + if matches!( + detail.status, + BuyerOrderStatus::Ready | BuyerOrderStatus::Completed | BuyerOrderStatus::Declined + ) { return Err(AppSqliteError::InvalidProjection { - reason: "buyer order revision requires a scheduled order", + reason: "buyer order revision requires an active negotiated order", }); } let request = self.resolve_seller_order_request_evidence(order_id)?; @@ -2907,22 +2868,14 @@ impl DesktopAppRuntimeState { }); } let lifecycle = self.resolve_order_lifecycle_evidence(&request)?; - let Some(order_decision) = lifecycle.decision.as_ref() else { - return Err(AppSqliteError::InvalidProjection { - reason: "buyer order revision requires accepted order decision evidence", - }); - }; - if !matches!( - order_decision.payload.decision, - RadrootsOrderDecisionOutcome::Accepted { .. } - ) { + if lifecycle.decision.is_some() || lifecycle.status != RadrootsOrderStatus::Requested { return Err(AppSqliteError::InvalidProjection { - reason: "buyer order revision requires accepted order decision evidence", + reason: "buyer order revision requires active pre-agreement negotiation", }); } if lifecycle.cancellation_event_id.is_some() { return Err(AppSqliteError::InvalidProjection { - reason: "buyer order revision requires an active order", + reason: "buyer order revision requires active pre-agreement negotiation", }); } let Some(proposal) = active_order_pending_revision_proposal(&lifecycle) else { @@ -3049,7 +3002,14 @@ impl DesktopAppRuntimeState { }); } let prev_event_id = match lifecycle.status { - RadrootsOrderStatus::Requested => request.request_event_id.clone(), + RadrootsOrderStatus::Requested => { + if active_order_pending_revision_proposal(&lifecycle).is_some() { + return Err(AppSqliteError::InvalidProjection { + reason: "buyer order cancellation requires no pending seller proposal", + }); + } + request.request_event_id.clone() + } RadrootsOrderStatus::Accepted => { return Err(AppSqliteError::InvalidProjection { reason: "buyer order cancellation requires an open pre-agreement order", @@ -3099,116 +3059,6 @@ impl DesktopAppRuntimeState { Ok(true) } - fn start_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<bool, AppSqliteError> { - self.upsert_order_recovery(order_id, kind, RecoveryState::Open, "start_order_recovery") - } - - fn review_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<bool, AppSqliteError> { - self.upsert_order_recovery( - order_id, - kind, - RecoveryState::InReview, - "review_order_recovery", - ) - } - - fn reopen_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<bool, AppSqliteError> { - self.upsert_order_recovery(order_id, kind, RecoveryState::Open, "reopen_order_recovery") - } - - fn resolve_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<bool, AppSqliteError> { - self.upsert_order_recovery( - order_id, - kind, - RecoveryState::Resolved, - "resolve_order_recovery", - ) - } - - fn upsert_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - state: RecoveryState, - source: &str, - ) -> Result<bool, AppSqliteError> { - let Some(sqlite_store) = self.sqlite_store.as_ref() else { - return Ok(false); - }; - let Some(selected_account) = self - .state_store - .identity_projection() - .selected_account - .as_ref() - else { - return Ok(false); - }; - let Some(farm_id) = self.selected_farm_id() else { - return Ok(false); - }; - let Some(_) = sqlite_store.load_order_detail(farm_id, order_id)? else { - return Ok(false); - }; - - let account_id = selected_account.account.account_id.as_str(); - let last_updated_at = current_utc_timestamp(); - let summary = order_recovery_summary(kind, state).to_owned(); - let note = Some(order_recovery_note(kind, state).to_owned()); - let mut record = sqlite_store - .load_recovery_record(account_id, order_id, kind)? - .unwrap_or(OrderRecoveryProjection { - recovery_record_id: RecoveryRecordId::new(), - order_id, - kind, - state, - summary: summary.clone(), - note: note.clone(), - last_updated_at: last_updated_at.clone(), - }); - - if record.state == state && record.summary == summary && record.note == note { - return Ok(false); - } - - record.state = state; - record.summary = summary; - record.note = note; - record.last_updated_at = last_updated_at; - sqlite_store.save_recovery_record(account_id, farm_id, &record)?; - - let continuity_state = self - .continuity_state_with_order_detail(self.selected_order_detail_id().or(Some(order_id))); - let selected_account_context = load_selected_account_context( - sqlite_store, - self.state_store.identity_projection(), - &continuity_state, - )?; - let context_changed = self.apply_selected_account_context(&selected_account_context); - let pending_changed = - self.enqueue_selected_account_sync_operations(vec![pending_sync_upsert( - SyncAggregateRef::Order(order_id), - order_recovery_sync_payload(order_id, farm_id, kind, state, source), - )])?; - - Ok(context_changed || pending_changed) - } - fn open_pack_day( &mut self, fulfillment_window_id: Option<FulfillmentWindowId>, @@ -3932,11 +3782,6 @@ impl DesktopAppRuntimeState { .apply_in_memory(AppStateCommand::replace_orders_reminders( context.orders_reminders.clone(), )); - let recovery_queue_changed = - self.state_store - .apply_in_memory(AppStateCommand::replace_orders_recovery_queue( - context.recovery_queue.clone(), - )); let reminder_log_changed = self.state_store .apply_in_memory(AppStateCommand::replace_reminder_log( @@ -3982,7 +3827,6 @@ impl DesktopAppRuntimeState { || orders_query_changed || orders_changed || orders_reminders_changed - || recovery_queue_changed || reminder_log_changed || order_detail_changed || pack_day_query_changed @@ -4524,6 +4368,7 @@ impl DesktopAppRuntimeState { self.refresh_selected_account_sync() } + #[cfg(test)] fn enqueue_selected_account_sync_operations( &mut self, operations: Vec<PendingSyncOperation>, @@ -5879,6 +5724,7 @@ impl DesktopAppRuntimeState { .transpose()?; Ok(ResolvedAppOrderLifecycleEvidence { evidence_events, + request_event_id: request.request_event_id.clone(), status: projection.status, agreement_event_id: projection .agreement_event_id @@ -8548,7 +8394,6 @@ fn load_selected_account_context_with_options( orders_list: OrdersListProjection::default(), orders_query: OrdersScreenQueryState::default(), orders_reminders: ReminderFeedProjection::default(), - recovery_queue: RecoveryQueueProjection::default(), pack_day_query: PackDayScreenQueryState::default(), product_editor_draft: None, reminder_log: ReminderLogProjection::default(), @@ -8566,7 +8411,7 @@ fn load_selected_account_context_with_options( orders_query, orders_list, canonical_orders_list, - mut order_detail, + order_detail, pack_day_query, mut pack_day_projection, product_editor_draft, @@ -8645,7 +8490,7 @@ fn load_selected_account_context_with_options( None, ), }; - let (orders_reminders, recovery_queue, reminder_log) = match today_farm_id { + let (orders_reminders, reminder_log) = match today_farm_id { Some(farm_id) => { let reminder_context = load_selected_account_reminder_context_with_options( sqlite_store, @@ -8660,22 +8505,13 @@ fn load_selected_account_context_with_options( today_projection.reminders = reminder_context.today_feed; if let Some(summary) = today_projection.summary.as_mut() { summary.reminders_due_soon = reminder_context.due_soon_count; - summary.recovery_actions_open = reminder_context.recovery_actions_open; - } - if let Some(detail) = order_detail.as_mut() { - detail.recoveries = reminder_context.selected_order_recoveries; } pack_day_projection.reminders = reminder_context.pack_day_feed; - ( - reminder_context.orders_feed, - reminder_context.recovery_queue, - reminder_context.reminder_log, - ) + (reminder_context.orders_feed, reminder_context.reminder_log) } None => ( ReminderFeedProjection::default(), - RecoveryQueueProjection::default(), ReminderLogProjection::default(), ), }; @@ -8690,7 +8526,6 @@ fn load_selected_account_context_with_options( orders_query, orders_list, orders_reminders, - recovery_queue, reminder_log, order_detail, pack_day_query, @@ -8757,18 +8592,16 @@ fn load_selected_account_reminder_context_with_options( today_projection: &TodayAgendaProjection, canonical_orders_list: &OrdersListProjection, pack_day_projection: &PackDayProjection, - selected_order_detail: Option<&OrderDetailProjection>, + _selected_order_detail: Option<&OrderDetailProjection>, allow_auto_present: bool, ) -> Result<DesktopSellerReminderContext, AppSqliteError> { let existing_schedule = sqlite_store.load_reminder_schedule(account_id, farm_id)?; - let recovery_queue = sqlite_store.load_recovery_queue(account_id, farm_id)?; let sync_truth = load_selected_account_reminder_sync_truth(sqlite_store, account_id)?; let mut schedule = derive_selected_account_reminder_schedule( farm_id, today_projection, canonical_orders_list, pack_day_projection, - &recovery_queue, &sync_truth, &existing_schedule, ); @@ -8789,34 +8622,22 @@ fn load_selected_account_reminder_context_with_options( } let reminder_log = sqlite_store.load_reminder_log(account_id, farm_id, 8)?; - let selected_order_recoveries = selected_order_detail - .map(|detail| ordered_order_recoveries_for_detail(&recovery_queue, detail.order_id)) - .unwrap_or_default(); let due_soon_count = schedule .items .iter() .filter(|item| { - !matches!(item.kind, ReminderKind::MissedPickupRecovery) - && matches!( - item.urgency, - ReminderUrgency::DueSoon | ReminderUrgency::Overdue | ReminderUrgency::Blocking - ) + matches!( + item.urgency, + ReminderUrgency::DueSoon | ReminderUrgency::Overdue | ReminderUrgency::Blocking + ) }) .count() as u32; - let recovery_actions_open = recovery_queue - .items - .iter() - .filter(|record| record.state != RecoveryState::Resolved) - .count() as u32; Ok(DesktopSellerReminderContext { today_feed: filter_reminder_surface(&schedule, ReminderSurface::Today), orders_feed: filter_reminder_surface(&schedule, ReminderSurface::Orders), pack_day_feed: filter_reminder_surface(&schedule, ReminderSurface::PackDay), - recovery_queue, - selected_order_recoveries, due_soon_count, - recovery_actions_open, reminder_log, }) } @@ -8853,7 +8674,6 @@ fn derive_selected_account_reminder_schedule( today_projection: &TodayAgendaProjection, canonical_orders_list: &OrdersListProjection, pack_day_projection: &PackDayProjection, - recovery_queue: &RecoveryQueueProjection, sync_truth: &DesktopReminderSyncTruth, existing_schedule: &ReminderFeedProjection, ) -> ReminderFeedProjection { @@ -8949,37 +8769,6 @@ fn derive_selected_account_reminder_schedule( items.push(sync_reminder); } - for record in recovery_queue - .items - .iter() - .filter(|record| record.state != RecoveryState::Resolved) - { - let kind = match record.kind { - RecoveryKind::MissedPickup => ReminderKind::MissedPickupRecovery, - }; - items.push(build_reminder_projection( - farm_id, - format!( - "reminder:orders:recovery:{}:{}", - record.kind.storage_key(), - record.order_id - ), - Some(record.order_id), - None, - kind, - ReminderSurface::Orders, - record.summary.clone(), - record - .note - .clone() - .unwrap_or_else(|| "Recovery follow-up is still open.".to_owned()), - record.last_updated_at.clone(), - Some("Review".to_owned()), - None, - existing_schedule, - )); - } - items.sort_by(|left, right| { left.deadline_at.cmp(&right.deadline_at).then_with(|| { left.reminder_id @@ -9247,75 +9036,6 @@ fn build_reminder_log_entry( } } -fn ordered_order_recoveries_for_detail( - recovery_queue: &RecoveryQueueProjection, - order_id: OrderId, -) -> Vec<OrderRecoveryProjection> { - let mut items = recovery_queue - .items - .iter() - .filter(|record| record.order_id == order_id) - .cloned() - .collect::<Vec<_>>(); - items.sort_by(|left, right| { - order_recovery_kind_rank(left.kind) - .cmp(&order_recovery_kind_rank(right.kind)) - .then_with(|| right.last_updated_at.cmp(&left.last_updated_at)) - .then_with(|| left.recovery_record_id.cmp(&right.recovery_record_id)) - }); - items -} - -fn order_recovery_kind_rank(kind: RecoveryKind) -> u8 { - match kind { - RecoveryKind::MissedPickup => 0, - } -} - -fn order_recovery_summary(kind: RecoveryKind, state: RecoveryState) -> &'static str { - match (kind, state) { - (RecoveryKind::MissedPickup, RecoveryState::Open) => "Missed pickup follow-up is open", - (RecoveryKind::MissedPickup, RecoveryState::InReview) => { - "Missed pickup follow-up is in review" - } - (RecoveryKind::MissedPickup, RecoveryState::Resolved) => { - "Missed pickup follow-up is resolved" - } - } -} - -fn order_recovery_note(kind: RecoveryKind, state: RecoveryState) -> &'static str { - match (kind, state) { - (RecoveryKind::MissedPickup, RecoveryState::Open) => { - "Check in with the buyer and agree on the next step." - } - (RecoveryKind::MissedPickup, RecoveryState::InReview) => { - "Use notes outside the app to confirm a new pickup or another resolution." - } - (RecoveryKind::MissedPickup, RecoveryState::Resolved) => { - "The seller and buyer have agreed on the next step." - } - } -} - -fn order_recovery_sync_payload( - order_id: OrderId, - farm_id: FarmId, - kind: RecoveryKind, - state: RecoveryState, - source: &str, -) -> String { - json!({ - "aggregate_kind": "order_recovery", - "order_id": order_id.to_string(), - "farm_id": farm_id.to_string(), - "recovery_kind": kind.storage_key(), - "recovery_state": state.storage_key(), - "source": source, - }) - .to_string() -} - fn load_selected_account_sync_context( sqlite_store: &AppSqliteStore, identity_projection: &AppIdentityProjection, @@ -9586,8 +9306,6 @@ fn normalize_farm_rules_projection( if let Some(operating_rules) = projection.operating_rules.as_mut() { operating_rules.farm_id = fallback_profile.farm_id; operating_rules.substitution_policy = operating_rules.substitution_policy.trim().to_owned(); - operating_rules.missed_pickup_policy = - operating_rules.missed_pickup_policy.trim().to_owned(); } for fulfillment_window in &mut projection.fulfillment_windows { @@ -9752,21 +9470,10 @@ fn active_order_event_record_context( Ok((context.counterparty_pubkey, root_event_id, prev_event_id)) } -fn active_order_revision_parent_event_id( - lifecycle: &ResolvedAppOrderLifecycleEvidence, -) -> Option<String> { - if active_order_pending_revision_proposal(lifecycle).is_some() { - None - } else { - lifecycle.last_event_id.clone() - } -} - fn active_order_pending_revision_proposal( lifecycle: &ResolvedAppOrderLifecycleEvidence, ) -> Option<&ResolvedAppOrderRevisionProposalEvidence> { - let decision = lifecycle.decision.as_ref()?; - let mut parent_event_id = decision.event_id.as_str(); + let mut parent_event_id = lifecycle.request_event_id.as_str(); loop { let proposals = lifecycle .revision_proposals @@ -9977,6 +9684,7 @@ fn order_cancellation_publish_payload_to_sdk_cancellation( }) } +#[cfg(test)] fn pending_sync_upsert(aggregate: SyncAggregateRef, payload_json: String) -> PendingSyncOperation { let created_at = current_utc_timestamp(); @@ -10084,10 +9792,9 @@ mod tests { PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PersonalSection, PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductStatus, - ProductsFilter, ProductsSort, RecoveryKind, RecoveryRecordId, ReminderDeliveryState, - ReminderFeedProjection, ReminderKind, SelectedAccountProjection, SelectedSurfaceProjection, - SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, - TodaySetupTaskKind, TodaySummary, + ProductsFilter, ProductsSort, ReminderDeliveryState, ReminderFeedProjection, ReminderKind, + SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection, + ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, }; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, @@ -10119,7 +9826,7 @@ mod tests { RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest, - RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, + RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, }; use radroots_sdk::{ LISTING_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND, @@ -11946,7 +11653,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id, @@ -12094,7 +11800,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id, @@ -12265,7 +11970,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id, @@ -12411,7 +12115,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id: active_window_id, @@ -14469,7 +14172,6 @@ mod tests { low_stock_products: 1, draft_products: 3, reminders_due_soon: 0, - recovery_actions_open: 0, }), setup_checklist: vec![TodaySetupTask { kind: TodaySetupTaskKind::AddFulfillmentWindow, @@ -16078,7 +15780,7 @@ mod tests { assert!( runtime .retry_pending_personal_order_coordination() - .expect("same-session buyer order recovery retry should sync") + .expect("same-session buyer order coordination retry should sync") ); let summary_after_retry = runtime.summary(); assert!( @@ -16147,7 +15849,7 @@ mod tests { assert!( !runtime .retry_pending_personal_order_coordination() - .expect("same-session synced buyer order recovery retry should be idempotent") + .expect("same-session synced buyer order coordination retry should be idempotent") ); assert_no_order_request_pending_sync_payloads( &runtime, @@ -16216,7 +15918,7 @@ mod tests { assert!( !restarted_runtime .retry_pending_personal_order_coordination() - .expect("synced buyer order recovery retry should be idempotent") + .expect("synced buyer order coordination retry should be idempotent") ); assert_no_order_request_pending_sync_payloads( &restarted_runtime, @@ -16423,7 +16125,7 @@ mod tests { #[test] fn runtime_publishes_linked_buyer_cancellation_from_selected_account_nostr_scope() { let relay = ThreadedAckRelay::spawn(); - let fixture = linked_buyer_lifecycle_runtime("linked_buyer_order_cancel"); + let fixture = linked_buyer_request_runtime("linked_buyer_order_cancel"); install_direct_relay_sync_transport(&fixture.runtime, &relay); fixture .runtime @@ -16445,7 +16147,7 @@ mod tests { assert_eq!( persisted_order_status(&fixture.runtime, fixture.order_id), - "scheduled" + "needs_action" ); assert_eq!(relay.event_count(), 0); let cancellation_events = @@ -16461,76 +16163,80 @@ mod tests { } #[test] - fn runtime_publishes_linked_buyer_cancellation_from_revision_parent() { - for (label, revision_decision) in [ - ("accepted", RadrootsOrderRevisionOutcome::Accepted), - ( - "declined", - RadrootsOrderRevisionOutcome::Declined { - reason: "keep original order".to_owned(), - }, - ), - ] { - let relay = ThreadedAckRelay::spawn(); - let fixture_label = format!("linked_buyer_order_cancel_revision_{label}"); - let fixture = linked_buyer_lifecycle_runtime(fixture_label.as_str()); - let proposal_key = format!("linked-buyer-order-cancel-revision-{label}-proposal"); - let proposal_event_id = append_signed_order_revision_proposal_record_with_prev( - &fixture.paths, - fixture.trade_order_id.as_str(), - proposal_key.as_str(), - fixture.request_event_id.as_str(), - fixture.decision_event_id.as_str(), - fixture.listing_addr.as_str(), - fixture.buyer_pubkey.as_str(), - fixture.seller_pubkey.as_str(), - ); - let revision_id = format!("revision-{proposal_key}"); - let _revision_decision_event_id = - append_signed_order_revision_decision_record_with_prev( - &fixture.paths, - fixture.trade_order_id.as_str(), - format!("linked-buyer-order-cancel-revision-{label}-decision").as_str(), - fixture.request_event_id.as_str(), - proposal_event_id.as_str(), - revision_id.as_str(), - fixture.listing_addr.as_str(), - fixture.buyer_pubkey.as_str(), - fixture.seller_pubkey.as_str(), - revision_decision, - ); - install_direct_relay_sync_transport(&fixture.runtime, &relay); + fn runtime_rejects_linked_buyer_cancellation_from_pending_revision_proposal() { + let relay = ThreadedAckRelay::spawn(); + let fixture = linked_buyer_request_runtime("linked_buyer_order_cancel_revision"); + let proposal_key = "linked-buyer-order-cancel-revision-proposal"; + append_signed_order_revision_proposal_record_with_prev( + &fixture.paths, + fixture.trade_order_id.as_str(), + proposal_key, + fixture.request_event_id.as_str(), + fixture.request_event_id.as_str(), + fixture.listing_addr.as_str(), + fixture.buyer_pubkey.as_str(), + fixture.seller_pubkey.as_str(), + ); + install_direct_relay_sync_transport(&fixture.runtime, &relay); + fixture + .runtime + .refresh_shared_local_events() + .expect("linked buyer revision proposal should import"); + assert!( fixture .runtime - .refresh_shared_local_events() - .expect("linked buyer revision events should import"); - assert!( - fixture - .runtime - .open_personal_order_detail(fixture.order_id) - .expect("linked buyer order detail should open") - ); - set_persisted_order_status(&fixture.runtime, fixture.order_id, "scheduled"); + .open_personal_order_detail(fixture.order_id) + .expect("linked buyer order detail should open") + ); - assert!( - fixture - .runtime - .publish_buyer_order_cancel(fixture.order_id) - .expect("linked buyer cancellation should publish from revision parent") - ); + let error = fixture + .runtime + .publish_buyer_order_cancel(fixture.order_id) + .expect_err("linked buyer cancellation should reject from pending proposal"); - assert_eq!(relay.event_count(), 0); - let cancellation_events = - shared_order_events_by_kind(&fixture.paths, 3432, fixture.buyer_pubkey.as_str()); - assert!(cancellation_events.is_empty()); - assert_order_cancellation_sdk_migration_receipt( - &fixture.runtime, - fixture.order_id, - AppSdkMigrationState::Enqueued, - ); + assert_invalid_projection_reason( + error, + "buyer order cancellation requires no pending seller proposal", + ); + assert_eq!(relay.event_count(), 0); + let cancellation_events = + shared_order_events_by_kind(&fixture.paths, 3432, fixture.buyer_pubkey.as_str()); + assert!(cancellation_events.is_empty()); - cleanup_bootstrapped_runtime_paths(&fixture.paths); - } + cleanup_bootstrapped_runtime_paths(&fixture.paths); + } + + #[test] + fn runtime_rejects_linked_buyer_cancellation_after_agreement() { + let relay = ThreadedAckRelay::spawn(); + let fixture = linked_buyer_lifecycle_runtime("linked_buyer_order_cancel_after_agreement"); + install_direct_relay_sync_transport(&fixture.runtime, &relay); + fixture + .runtime + .refresh_shared_local_events() + .expect("linked buyer local events should import"); + assert!( + fixture + .runtime + .open_personal_order_detail(fixture.order_id) + .expect("linked buyer order detail should open") + ); + + let error = fixture + .runtime + .publish_buyer_order_cancel(fixture.order_id) + .expect_err("post-agreement buyer cancellation should reject"); + + assert_invalid_projection_reason( + error, + "buyer order cancellation requires an open pre-agreement order", + ); + assert_eq!(relay.event_count(), 0); + let cancellation_events = + shared_order_events_by_kind(&fixture.paths, 3432, fixture.buyer_pubkey.as_str()); + assert!(cancellation_events.is_empty()); + + cleanup_bootstrapped_runtime_paths(&fixture.paths); } #[test] @@ -16582,14 +16288,14 @@ mod tests { #[test] fn runtime_publishes_linked_buyer_revision_decision_from_reducer_valid_parent() { let relay = ThreadedAckRelay::spawn(); - let fixture = linked_buyer_lifecycle_runtime("linked_buyer_order_revision"); + let fixture = linked_buyer_request_runtime("linked_buyer_order_revision"); let proposal_key = "linked-buyer-order-revision-proposal"; let _proposal_event_id = append_signed_order_revision_proposal_record_with_prev( &fixture.paths, fixture.trade_order_id.as_str(), proposal_key, fixture.request_event_id.as_str(), - fixture.decision_event_id.as_str(), + fixture.request_event_id.as_str(), fixture.listing_addr.as_str(), fixture.buyer_pubkey.as_str(), fixture.seller_pubkey.as_str(), @@ -18089,15 +17795,6 @@ mod tests { summary.pack_day_projection.projection.reminders.items[0].kind, ReminderKind::FulfillmentWindow ); - assert_eq!( - summary - .today_projection - .summary - .as_ref() - .expect("today summary") - .recovery_actions_open, - 0 - ); } #[test] @@ -18277,81 +17974,6 @@ mod tests { } #[test] - fn runtime_threads_recovery_queue_into_today_counts_and_order_detail() { - let runtime = memory_runtime(); - let (_, farm_id) = provision_ready_farmer_account(&runtime); - let (_, order_id) = seed_order_workspace(&runtime, farm_id); - let recovery_record_id = RecoveryRecordId::new(); - let sql = format!( - "insert into order_recovery_records ( - recovery_record_id, - account_id, - farm_id, - order_id, - recovery_kind, - recovery_state, - summary, - note, - last_updated_at - ) values ( - '{recovery_record_id}', - '{}', - '{farm_id}', - '{order_id}', - 'missed_pickup', - 'open', - 'Follow up on the missed pickup', - 'Confirm a new pickup time.', - '2026-04-18T18:30:00Z' - )", - runtime - .summary() - .settings_account_projection - .selected_account - .as_ref() - .expect("selected account") - .account - .account_id - ); - runtime - .lock_state() - .sqlite_store - .as_ref() - .expect("sqlite store") - .connection() - .execute_batch(&sql) - .expect("recovery record should seed"); - - assert!( - runtime - .open_order_detail(order_id) - .expect("order detail should open") - ); - let summary = runtime.summary(); - - assert_eq!(summary.orders_projection.recovery_queue.items.len(), 1); - assert_eq!( - summary - .today_projection - .summary - .as_ref() - .expect("today summary") - .recovery_actions_open, - 1 - ); - assert_eq!( - summary - .orders_projection - .detail - .as_ref() - .and_then(|detail| detail.recoveries.first()) - .expect("order recovery") - .kind, - RecoveryKind::MissedPickup - ); - } - - #[test] fn reminder_urgency_marks_due_soon_and_overdue_deadlines() { let due_soon = (Utc::now() + Duration::hours(24)) .format("%Y-%m-%dT%H:%M:%SZ") @@ -19416,7 +19038,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: " ask_customer ".to_owned(), - missed_pickup_policy: " hold_next_window ".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id, @@ -19466,7 +19087,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }) ); assert_eq!( @@ -19999,10 +19619,22 @@ mod tests { linked_buyer_lifecycle_runtime_with_seller_pubkey(label, SDK_TEST_SELLER_PUBLIC_KEY_HEX) } + fn linked_buyer_request_runtime(label: &str) -> LinkedBuyerLifecycleFixture { + linked_buyer_runtime_with_seller_pubkey(label, SDK_TEST_SELLER_PUBLIC_KEY_HEX, false) + } + fn linked_buyer_lifecycle_runtime_with_seller_pubkey( label: &str, seller_pubkey: &str, ) -> LinkedBuyerLifecycleFixture { + linked_buyer_runtime_with_seller_pubkey(label, seller_pubkey, true) + } + + fn linked_buyer_runtime_with_seller_pubkey( + label: &str, + seller_pubkey: &str, + append_decision: bool, + ) -> LinkedBuyerLifecycleFixture { let (runtime, paths) = bootstrapped_runtime(label); assert!( runtime @@ -20059,15 +19691,19 @@ mod tests { seller_pubkey, 2, ); - let decision_event_id = append_signed_order_decision_record( - &paths, - trade_order_id.as_str(), - request_event_id.as_str(), - listing_addr.as_str(), - buyer_pubkey.as_str(), - seller_pubkey, - 2, - ); + let decision_event_id = if append_decision { + append_signed_order_decision_record( + &paths, + trade_order_id.as_str(), + request_event_id.as_str(), + listing_addr.as_str(), + buyer_pubkey.as_str(), + seller_pubkey, + 2, + ) + } else { + String::new() + }; LinkedBuyerLifecycleFixture { runtime, paths, @@ -20725,47 +20361,6 @@ mod tests { ) } - fn append_signed_order_revision_decision_record_with_prev( - paths: &AppDesktopRuntimePaths, - trade_order_id: &str, - event_key: &str, - request_event_id: &str, - proposal_event_id: &str, - revision_id: &str, - listing_addr: &str, - buyer_pubkey: &str, - seller_pubkey: &str, - decision: RadrootsOrderRevisionOutcome, - ) -> String { - let request_event_id = test_event_id(request_event_id); - let proposal_event_id = test_event_id(proposal_event_id); - let payload = RadrootsOrderRevisionDecision { - revision_id: test_revision_id(revision_id), - order_id: test_order_id(trade_order_id), - listing_addr: test_listing_addr(listing_addr), - buyer_pubkey: test_pubkey(buyer_pubkey), - seller_pubkey: test_pubkey(seller_pubkey), - root_event_id: request_event_id.clone(), - prev_event_id: proposal_event_id.clone(), - decision, - }; - let parts = radroots_sdk::protocol::order::build_order_revision_decision_draft( - &request_event_id, - &proposal_event_id, - &payload, - ) - .expect("order revision decision draft should build") - .into_wire_parts(); - let record_id = format!("app:signed_event:revision-decision:{event_key}"); - append_trade_signed_event_record( - paths, - record_id.as_str(), - buyer_pubkey, - listing_addr, - parts, - ) - } - fn revision_test_order_items() -> Vec<RadrootsOrderItem> { vec![RadrootsOrderItem { bin_id: test_bin_id("seller-order-primary-bin"), @@ -20812,6 +20407,16 @@ mod tests { ); } + fn assert_invalid_projection_reason(error: AppSqliteError, expected_reason: &'static str) { + assert!( + matches!( + error, + AppSqliteError::InvalidProjection { reason } if reason == expected_reason + ), + "{error:?}" + ); + } + fn append_trade_signed_event_record( paths: &AppDesktopRuntimePaths, record_id: &str, @@ -21144,21 +20749,6 @@ mod tests { .expect("order status should load") } - fn set_persisted_order_status(runtime: &DesktopAppRuntime, order_id: OrderId, status: &str) { - let order_id = order_id.to_string(); - runtime - .lock_state() - .sqlite_store - .as_ref() - .expect("sqlite store") - .connection() - .execute( - "update orders set status = ?1 where id = ?2", - [status, order_id.as_str()], - ) - .expect("order status should update"); - } - fn pending_order_sync_payloads( runtime: &DesktopAppRuntime, account_id: &str, diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -150,10 +150,6 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "failed to switch into farm mode", "failed to switch into marketplace mode", "failed to update orders filter", - "failed to start order recovery", - "failed to review order recovery", - "failed to reopen order recovery", - "failed to resolve order recovery", "failed to route into products view", "failed to update product stock", "failed to update products filter", @@ -240,18 +236,10 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "orders-filter-needs-action", "orders-filter-packed", "orders-filter-scheduled", - "orders-recovery-open", - "orders-recovery-review", - "orders-recovery-reopen", - "orders-recovery-resolve", "orders-row-action-review", "orders-row-open", "orders.detail_open_failed", "orders.filter_update_failed", - "orders.recovery_reopen_failed", - "orders.recovery_resolve_failed", - "orders.recovery_review_failed", - "orders.recovery_start_failed", "orders.route_failed", "outbox.sqlite", "preview", @@ -742,17 +730,6 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::TradeWorkflowProvenanceRelay", "AppTextKey::TradeWorkflowProvenanceLocalEvents", "AppTextKey::TradeWorkflowProvenanceUnknown", - "AppTextKey::OrdersRecoverySectionTitle", - "AppTextKey::OrdersRecoveryMissedPickupTitle", - "AppTextKey::OrdersRecoveryMissedPickupBody", - "AppTextKey::OrdersRecoveryLastUpdatedLabel", - "AppTextKey::OrdersRecoveryActionOpenFollowUp", - "AppTextKey::OrdersRecoveryActionStartReview", - "AppTextKey::OrdersRecoveryActionMarkOpen", - "AppTextKey::OrdersRecoveryActionResolve", - "AppTextKey::OrdersRecoveryStateOpen", - "AppTextKey::OrdersRecoveryStateInReview", - "AppTextKey::OrdersRecoveryStateResolved", "AppTextKey::OrdersRemindersTitle", "AppTextKey::OrdersReminderLogTitle", "AppTextKey::OrdersReminderLogEmptyBody", @@ -926,7 +903,6 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::SettingsOperatingRulesSectionLabel", "AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime", "AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy", - "AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy", "AppTextKey::SettingsOperatingRulesInvalidPromiseLeadTime", "AppTextKey::SettingsFulfillmentWindowsSectionLabel", "AppTextKey::SettingsFulfillmentWindowsEmptyBody", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -69,18 +69,17 @@ use radroots_app_view::{ FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, LoggedOutStartupPhase, OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction, - OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, - PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, PackDayExportBundle, - PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, - PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, - PackDayRosterRow, PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord, - ProductAttentionState, ProductEditorDraft, ProductId, ProductListRow, ProductPricePresentation, - ProductPublishBlocker, ProductStatus, ProductsFilter, ProductsListRow, ProductsSort, - RecoveryKind, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, - ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency, - RepeatDemandEligibility, RepeatDemandHandoffProjection, SettingsAccountProjection, - ShellSection, TodayAgendaProjection, TodaySetupTaskKind, TradeAgreementStatus, - TradeEconomicsProjection, TradeInventoryStatus, TradeRevisionStatus, + OrderStatus, OrdersFilter, OrdersListRow, PackDayBatchPrintFailureKind, + PackDayBatchPrintStatus, PackDayExportBundle, PackDayExportStatus, PackDayHostHandoffKind, + PackDayHostHandoffStatus, PackDayPackListRow, PackDayPrintFailureKind, PackDayPrintKind, + PackDayPrintStatus, PackDayProductTotalRow, PackDayRosterRow, PersonalEntryState, + PersonalSection, PickupLocationId, PickupLocationRecord, ProductAttentionState, + ProductEditorDraft, ProductId, ProductListRow, ProductPricePresentation, ProductPublishBlocker, + ProductStatus, ProductsFilter, ProductsListRow, ProductsSort, ReminderDeadlineProjection, + ReminderDeliveryState, ReminderId, ReminderLogEntryProjection, ReminderLogProjection, + ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection, + SettingsAccountProjection, ShellSection, TodayAgendaProjection, TodaySetupTaskKind, + TradeAgreementStatus, TradeEconomicsProjection, TradeInventoryStatus, TradeRevisionStatus, TradeValidationReceiptProjection, TradeValidationReceiptResult, TradeValidationReceiptType, TradeWorkflowProjection, TradeWorkflowSource, }; @@ -2913,94 +2912,6 @@ impl HomeView { } } - fn start_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - cx: &mut Context<Self>, - ) { - match self.runtime.start_order_recovery(order_id, kind) { - Ok(true) => cx.notify(), - Ok(false) => {} - Err(runtime_error) => { - error!( - target: "orders", - event = "orders.recovery_start_failed", - error = %runtime_error, - order_id = %order_id, - recovery_kind = kind.storage_key(), - "failed to start order recovery" - ); - } - } - } - - fn review_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - cx: &mut Context<Self>, - ) { - match self.runtime.review_order_recovery(order_id, kind) { - Ok(true) => cx.notify(), - Ok(false) => {} - Err(runtime_error) => { - error!( - target: "orders", - event = "orders.recovery_review_failed", - error = %runtime_error, - order_id = %order_id, - recovery_kind = kind.storage_key(), - "failed to review order recovery" - ); - } - } - } - - fn reopen_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - cx: &mut Context<Self>, - ) { - match self.runtime.reopen_order_recovery(order_id, kind) { - Ok(true) => cx.notify(), - Ok(false) => {} - Err(runtime_error) => { - error!( - target: "orders", - event = "orders.recovery_reopen_failed", - error = %runtime_error, - order_id = %order_id, - recovery_kind = kind.storage_key(), - "failed to reopen order recovery" - ); - } - } - } - - fn resolve_order_recovery( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - cx: &mut Context<Self>, - ) { - match self.runtime.resolve_order_recovery(order_id, kind) { - Ok(true) => cx.notify(), - Ok(false) => {} - Err(runtime_error) => { - error!( - target: "orders", - event = "orders.recovery_resolve_failed", - error = %runtime_error, - order_id = %order_id, - recovery_kind = kind.storage_key(), - "failed to resolve order recovery" - ); - } - } - } - fn open_products_stock_editor( &mut self, product_id: ProductId, @@ -4928,8 +4839,7 @@ impl HomeView { .when(detail.items.is_empty(), |this| { this.child(home_body_text(app_shared_text(AppTextKey::ValueNone))) }), - )) - .child(self.render_order_recovery_section(detail, cx)), + )), text_button( "orders-detail-back", app_shared_text(AppTextKey::PersonalDetailBackAction), @@ -4939,198 +4849,6 @@ impl HomeView { ) } - fn render_order_recovery_section( - &mut self, - detail: &OrderDetailProjection, - cx: &mut Context<Self>, - ) -> AnyElement { - app_form_section( - app_shared_text(AppTextKey::OrdersRecoverySectionTitle), - div() - .w_full() - .flex() - .flex_col() - .gap(px(APP_UI_THEME.foundation.spacing.medium_px)) - .child( - self.render_order_recovery_card( - detail.order_id, - RecoveryKind::MissedPickup, - detail - .recoveries - .iter() - .find(|record| record.kind == RecoveryKind::MissedPickup), - cx, - ), - ), - ) - .into_any_element() - } - - fn render_order_recovery_card( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - recovery: Option<&OrderRecoveryProjection>, - cx: &mut Context<Self>, - ) -> AnyElement { - let title_key = order_recovery_title_key(kind); - let body = recovery.map_or_else( - || { - home_body_text(app_shared_text(order_recovery_empty_body_key(kind))) - .into_any_element() - }, - |record| { - app_stack_v(APP_UI_THEME.foundation.spacing.tight_px) - .w_full() - .child(home_body_text(record.summary.clone())) - .when_some( - record - .note - .as_ref() - .map(|note| note.trim()) - .filter(|note| !note.is_empty()), - |this, note| this.child(home_body_text(note.to_owned())), - ) - .child( - div() - .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) - .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) - .child(format!( - "{}: {}", - app_text(AppTextKey::OrdersRecoveryLastUpdatedLabel), - record.last_updated_at - )), - ) - .into_any_element() - }, - ); - - app_surface_card( - app_stack_v(APP_UI_THEME.foundation.spacing.medium_px) - .w_full() - .child( - div() - .w_full() - .min_w_0() - .flex() - .items_start() - .justify_between() - .gap(px(APP_UI_THEME.foundation.spacing.tight_px)) - .child( - div() - .flex_1() - .min_w_0() - .text_size(px(APP_UI_THEME.foundation.typography.body_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .line_height(relative(1.2)) - .text_color(rgb(APP_UI_THEME.foundation.text.primary)) - .child(app_shared_text(title_key)), - ) - .when_some( - recovery.map(|record| order_recovery_state_badge(record.state)), - |this, badge| this.child(badge), - ), - ) - .child(body) - .when_some( - self.render_order_recovery_actions(order_id, kind, recovery, cx), - |this, actions| { - this.child( - div() - .w_full() - .flex() - .items_center() - .justify_end() - .gap(px(APP_UI_THEME.foundation.spacing.tight_px)) - .child(actions), - ) - }, - ), - ) - .into_any_element() - } - - fn render_order_recovery_actions( - &mut self, - order_id: OrderId, - kind: RecoveryKind, - recovery: Option<&OrderRecoveryProjection>, - cx: &mut Context<Self>, - ) -> Option<AnyElement> { - let index = order_recovery_kind_index(kind); - - match recovery.map(|record| record.state) { - None => Some( - action_button_primary( - ("orders-recovery-open", index), - app_shared_text(AppTextKey::OrdersRecoveryActionOpenFollowUp), - cx.listener(move |this, _, _, cx| { - this.start_order_recovery(order_id, kind, cx) - }), - cx, - ) - .into_any_element(), - ), - Some(RecoveryState::Open) => Some( - div() - .flex() - .items_center() - .gap(px(APP_UI_THEME.foundation.spacing.tight_px)) - .child(action_button_compact( - ("orders-recovery-review", index), - app_shared_text(AppTextKey::OrdersRecoveryActionStartReview), - cx.listener(move |this, _, _, cx| { - this.review_order_recovery(order_id, kind, cx) - }), - cx, - )) - .child(action_button_primary( - ("orders-recovery-resolve", index), - app_shared_text(AppTextKey::OrdersRecoveryActionResolve), - cx.listener(move |this, _, _, cx| { - this.resolve_order_recovery(order_id, kind, cx) - }), - cx, - )) - .into_any_element(), - ), - Some(RecoveryState::InReview) => Some( - div() - .flex() - .items_center() - .gap(px(APP_UI_THEME.foundation.spacing.tight_px)) - .child(action_button_compact( - ("orders-recovery-reopen", index), - app_shared_text(AppTextKey::OrdersRecoveryActionMarkOpen), - cx.listener(move |this, _, _, cx| { - this.reopen_order_recovery(order_id, kind, cx) - }), - cx, - )) - .child(action_button_primary( - ("orders-recovery-resolve", index), - app_shared_text(AppTextKey::OrdersRecoveryActionResolve), - cx.listener(move |this, _, _, cx| { - this.resolve_order_recovery(order_id, kind, cx) - }), - cx, - )) - .into_any_element(), - ), - Some(RecoveryState::Resolved) => Some( - action_button_compact( - ("orders-recovery-reopen", index), - app_shared_text(AppTextKey::OrdersRecoveryActionMarkOpen), - cx.listener(move |this, _, _, cx| { - this.reopen_order_recovery(order_id, kind, cx) - }), - cx, - ) - .into_any_element(), - ), - } - } - fn render_products_table_entry( &mut self, index: usize, @@ -6164,10 +5882,8 @@ impl SettingsPickupLocationDraft { struct SettingsOperatingRulesFormState { promise_lead_hours_input: Entity<InputState>, substitution_policy_input: Entity<InputState>, - missed_pickup_policy_input: Entity<InputState>, _promise_lead_hours_subscription: Subscription, _substitution_policy_subscription: Subscription, - _missed_pickup_policy_subscription: Subscription, } impl SettingsOperatingRulesFormState { @@ -6190,13 +5906,6 @@ impl SettingsOperatingRulesFormState { .unwrap_or_default(), ) }); - let missed_pickup_policy_input = cx.new(|cx| { - InputState::new(window, cx).default_value( - record - .map(|record| record.missed_pickup_policy.clone()) - .unwrap_or_default(), - ) - }); let promise_lead_hours_subscription = cx.subscribe_in( &promise_lead_hours_input, window, @@ -6207,19 +5916,12 @@ impl SettingsOperatingRulesFormState { window, SettingsWindowView::handle_farm_rules_input_event, ); - let missed_pickup_policy_subscription = cx.subscribe_in( - &missed_pickup_policy_input, - window, - SettingsWindowView::handle_farm_rules_input_event, - ); Self { promise_lead_hours_input, substitution_policy_input, - missed_pickup_policy_input, _promise_lead_hours_subscription: promise_lead_hours_subscription, _substitution_policy_subscription: substitution_policy_subscription, - _missed_pickup_policy_subscription: missed_pickup_policy_subscription, } } @@ -6227,7 +5929,6 @@ impl SettingsOperatingRulesFormState { SettingsOperatingRulesDraft { promise_lead_hours: self.promise_lead_hours_input.read(cx).value().to_string(), substitution_policy: self.substitution_policy_input.read(cx).value().to_string(), - missed_pickup_policy: self.missed_pickup_policy_input.read(cx).value().to_string(), } } } @@ -6236,7 +5937,6 @@ impl SettingsOperatingRulesFormState { struct SettingsOperatingRulesDraft { promise_lead_hours: String, substitution_policy: String, - missed_pickup_policy: String, } impl SettingsOperatingRulesDraft { @@ -6248,16 +5948,11 @@ impl SettingsOperatingRulesDraft { substitution_policy: record .map(|record| record.substitution_policy.clone()) .unwrap_or_default(), - missed_pickup_policy: record - .map(|record| record.missed_pickup_policy.clone()) - .unwrap_or_default(), } } fn is_empty(&self) -> bool { - self.promise_lead_hours.trim().is_empty() - && self.substitution_policy.trim().is_empty() - && self.missed_pickup_policy.trim().is_empty() + self.promise_lead_hours.trim().is_empty() && self.substitution_policy.trim().is_empty() } } @@ -6795,7 +6490,6 @@ impl SettingsFarmPanelState { farm_id: self.farm_id, promise_lead_hours, substitution_policy: draft.operating_rules.substitution_policy.trim().to_owned(), - missed_pickup_policy: draft.operating_rules.missed_pickup_policy.trim().to_owned(), }) }; let mut fulfillment_windows = Vec::new(); @@ -7736,16 +7430,6 @@ impl SettingsWindowView { &form.operating_rules.substitution_policy_input, false, )) - .child(app_form_input_text( - AppFormFieldSpec::new( - app_shared_text( - AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy, - ), - Option::<SharedString>::None, - ), - &form.operating_rules.missed_pickup_policy_input, - false, - )) .children( evaluation .operating_rules_validation_keys @@ -9294,7 +8978,6 @@ const SETTINGS_PICKUP_LOCATIONS_SECTION_FIELDS: &[AppTextKey] = &[ const SETTINGS_OPERATING_RULES_SECTION_FIELDS: &[AppTextKey] = &[ AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime, AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy, - AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy, ]; #[cfg(test)] @@ -13669,49 +13352,6 @@ fn orders_status_color(status: OrderStatus) -> u32 { } } -fn order_recovery_title_key(kind: RecoveryKind) -> AppTextKey { - match kind { - RecoveryKind::MissedPickup => AppTextKey::OrdersRecoveryMissedPickupTitle, - } -} - -fn order_recovery_empty_body_key(kind: RecoveryKind) -> AppTextKey { - match kind { - RecoveryKind::MissedPickup => AppTextKey::OrdersRecoveryMissedPickupBody, - } -} - -fn order_recovery_state_key(state: RecoveryState) -> AppTextKey { - match state { - RecoveryState::Open => AppTextKey::OrdersRecoveryStateOpen, - RecoveryState::InReview => AppTextKey::OrdersRecoveryStateInReview, - RecoveryState::Resolved => AppTextKey::OrdersRecoveryStateResolved, - } -} - -fn order_recovery_state_color(state: RecoveryState) -> u32 { - match state { - RecoveryState::Open => APP_UI_THEME.components.app_status_indicator.attention, - RecoveryState::InReview => APP_UI_THEME.foundation.text.accent, - RecoveryState::Resolved => APP_UI_THEME.components.app_status_indicator.online, - } -} - -fn order_recovery_state_badge(state: RecoveryState) -> AnyElement { - div() - .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(rgb(order_recovery_state_color(state))) - .child(app_shared_text(order_recovery_state_key(state))) - .into_any_element() -} - -fn order_recovery_kind_index(kind: RecoveryKind) -> usize { - match kind { - RecoveryKind::MissedPickup => 0, - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct PackDayExportStatusPresentation { indicator_color: u32, @@ -16870,7 +16510,6 @@ mod tests { workflow: TradeWorkflowProjection::from_order_status(order_id, OrderStatus::Scheduled), validation_receipts: Vec::new(), primary_action: None, - recoveries: Vec::new(), }); assert_eq!( @@ -17041,7 +16680,6 @@ mod tests { field_keys: &[ AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime, AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy, - AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy, ], }, SettingsInventorySectionSpec { @@ -17541,7 +17179,6 @@ mod tests { ), validation_receipts: Vec::new(), primary_action: None, - recoveries: Vec::new(), }); assert_eq!( home_auto_focus_target(&orders, HomeAutoFocusState::default()), diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs @@ -427,17 +427,6 @@ define_app_text_keys! { TradeValidationReceiptProofSp1Compressed => "trade.validation.proof.sp1_compressed", TradeValidationReceiptProofSp1Groth16 => "trade.validation.proof.sp1_groth16", TradeValidationReceiptProofSp1Plonk => "trade.validation.proof.sp1_plonk", - OrdersRecoverySectionTitle => "orders.recovery.section.title", - OrdersRecoveryMissedPickupTitle => "orders.recovery.missed_pickup.title", - OrdersRecoveryMissedPickupBody => "orders.recovery.missed_pickup.body", - OrdersRecoveryLastUpdatedLabel => "orders.recovery.last_updated.label", - OrdersRecoveryActionOpenFollowUp => "orders.recovery.action.open_follow_up", - OrdersRecoveryActionStartReview => "orders.recovery.action.start_review", - OrdersRecoveryActionMarkOpen => "orders.recovery.action.mark_open", - OrdersRecoveryActionResolve => "orders.recovery.action.resolve", - OrdersRecoveryStateOpen => "orders.recovery.state.open", - OrdersRecoveryStateInReview => "orders.recovery.state.in_review", - OrdersRecoveryStateResolved => "orders.recovery.state.resolved", TradeWorkflowAxisAgreement => "trade.workflow.axis.agreement", TradeWorkflowAxisRevision => "trade.workflow.axis.revision", TradeWorkflowAxisInventory => "trade.workflow.axis.inventory", @@ -679,7 +668,6 @@ define_app_text_keys! { SettingsOperatingRulesSectionLabel => "settings.operating_rules.section.label", SettingsOperatingRulesFieldPromiseLeadTime => "settings.operating_rules.field.promise_lead_time", SettingsOperatingRulesFieldSubstitutionPolicy => "settings.operating_rules.field.substitution_policy", - SettingsOperatingRulesFieldMissedPickupPolicy => "settings.operating_rules.field.missed_pickup_policy", SettingsOperatingRulesInvalidPromiseLeadTime => "settings.operating_rules.invalid_promise_lead_time", SettingsFulfillmentWindowsSectionLabel => "settings.fulfillment_windows.section.label", SettingsFulfillmentWindowsEmptyBody => "settings.fulfillment_windows.empty.body", diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs @@ -495,19 +495,6 @@ mod tests { "Needs review" ); assert_eq!(app_text(AppTextKey::OrdersDetailTitle), "Order detail"); - assert_eq!(app_text(AppTextKey::OrdersRecoverySectionTitle), "Recovery"); - assert_eq!( - app_text(AppTextKey::OrdersRecoveryMissedPickupTitle), - "Missed pickup" - ); - assert_eq!( - app_text(AppTextKey::OrdersRecoveryActionResolve), - "Mark resolved" - ); - assert_eq!( - app_text(AppTextKey::OrdersRecoveryStateInReview), - "In review" - ); } #[test] @@ -573,7 +560,6 @@ mod tests { assert!(action_keys.contains(&AppTextKey::PersonalCartReviewOrderAction)); assert!(action_keys.contains(&AppTextKey::PersonalOrderReviewPlaceOrderAction)); - assert!(action_keys.contains(&AppTextKey::OrdersRecoveryActionResolve)); for key in action_keys { let copy = app_text(key).to_lowercase(); diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs @@ -24,9 +24,9 @@ use radroots_app_view::{ PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, PackDayProjection, PackDayScreenQueryState, PersonalEntryProjection, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductsFilter, ProductsListProjection, ProductsSort, - RecoveryQueueProjection, ReminderFeedProjection, ReminderLogProjection, - SelectedSurfaceProjection, SettingsAccountProjection, SettingsPreference, SettingsSection, - ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + ReminderFeedProjection, ReminderLogProjection, SelectedSurfaceProjection, + SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection, + TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -306,7 +306,6 @@ pub struct OrdersScreenProjection { pub list: OrdersListProjection, pub query: OrdersScreenQueryState, pub reminders: ReminderFeedProjection, - pub recovery_queue: RecoveryQueueProjection, pub detail: Option<OrderDetailProjection>, } @@ -1052,7 +1051,6 @@ pub enum AppStateCommand { SelectOrdersFulfillmentWindow(Option<FulfillmentWindowId>), ReplaceOrdersList(OrdersListProjection), ReplaceOrdersReminders(ReminderFeedProjection), - ReplaceOrdersRecoveryQueue(RecoveryQueueProjection), ReplaceReminderLog(ReminderLogProjection), ReplaceOrderDetail(Option<OrderDetailProjection>), SetPackDayFulfillmentWindow(Option<FulfillmentWindowId>), @@ -1190,10 +1188,6 @@ impl AppStateCommand { Self::ReplaceOrdersReminders(projection) } - pub fn replace_orders_recovery_queue(projection: RecoveryQueueProjection) -> Self { - Self::ReplaceOrdersRecoveryQueue(projection) - } - pub fn replace_reminder_log(projection: ReminderLogProjection) -> Self { Self::ReplaceReminderLog(projection) } @@ -1775,9 +1769,6 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap AppStateCommand::ReplaceOrdersReminders(reminders_projection) => { projection.orders.reminders = reminders_projection; } - AppStateCommand::ReplaceOrdersRecoveryQueue(recovery_queue_projection) => { - projection.orders.recovery_queue = recovery_queue_projection; - } AppStateCommand::ReplaceReminderLog(reminder_log_projection) => { projection.reminder_log = reminder_log_projection; } @@ -2555,7 +2546,6 @@ mod tests { .with_economics(order_economics), validation_receipts: Vec::new(), primary_action: Some(OrderPrimaryAction::Review), - recoveries: Vec::new(), }; let orders_reminders = ReminderFeedProjection { items: vec![radroots_app_view::ReminderDeadlineProjection { @@ -2573,17 +2563,6 @@ mod tests { delivery_state: radroots_app_view::ReminderDeliveryState::Scheduled, }], }; - let recovery_queue = radroots_app_view::RecoveryQueueProjection { - items: vec![radroots_app_view::OrderRecoveryProjection { - recovery_record_id: radroots_app_view::RecoveryRecordId::new(), - order_id, - kind: radroots_app_view::RecoveryKind::MissedPickup, - state: radroots_app_view::RecoveryState::Open, - summary: "Follow up on pickup".to_owned(), - note: None, - last_updated_at: "2026-04-18T19:00:00Z".to_owned(), - }], - }; let reminder_log = ReminderLogProjection { entries: vec![ReminderLogEntryProjection { reminder_id: orders_reminders.items[0].reminder_id, @@ -2647,12 +2626,6 @@ mod tests { Ok(true) ); assert_eq!( - store.apply(AppStateCommand::replace_orders_recovery_queue( - recovery_queue.clone() - )), - Ok(true) - ); - assert_eq!( store.apply(AppStateCommand::replace_reminder_log(reminder_log.clone())), Ok(true) ); @@ -2683,7 +2656,6 @@ mod tests { ); assert_eq!(store.projection().orders.list, orders_list); assert_eq!(store.projection().orders.reminders, orders_reminders); - assert_eq!(store.projection().orders.recovery_queue, recovery_queue); assert_eq!(store.projection().reminder_log, reminder_log); assert_eq!(store.projection().orders.detail, Some(order_detail)); assert_eq!( @@ -3638,7 +3610,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id: active_window_id, @@ -4363,7 +4334,6 @@ mod tests { assert!(projection.today.reminders.is_empty()); assert!(projection.orders.reminders.is_empty()); - assert!(projection.orders.recovery_queue.is_empty()); assert!(projection.reminder_log.is_empty()); assert!(projection.pack_day.projection.reminders.is_empty()); assert_eq!( diff --git a/crates/store/migrations/0006_farm_rules_workspace.sql b/crates/store/migrations/0006_farm_rules_workspace.sql @@ -5,7 +5,6 @@ CREATE TABLE farm_operating_rules ( farm_id TEXT PRIMARY KEY NOT NULL REFERENCES farms(id) ON DELETE CASCADE, promise_lead_hours INTEGER NOT NULL CHECK (promise_lead_hours >= 0), substitution_policy TEXT NOT NULL, - missed_pickup_policy TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); diff --git a/crates/store/migrations/0011_reminders.sql b/crates/store/migrations/0011_reminders.sql @@ -0,0 +1,72 @@ +CREATE TABLE reminder_schedules ( + reminder_id TEXT NOT NULL, + account_id TEXT NOT NULL, + farm_id TEXT NOT NULL, + order_id TEXT, + fulfillment_window_id TEXT, + reminder_kind TEXT NOT NULL CHECK ( + reminder_kind IN ( + 'fulfillment_window', + 'order_action', + 'sync_impact' + ) + ), + reminder_surface TEXT NOT NULL CHECK ( + reminder_surface IN ('today', 'orders', 'pack_day') + ), + reminder_urgency TEXT NOT NULL CHECK ( + reminder_urgency IN ('upcoming', 'due_soon', 'overdue', 'blocking') + ), + title TEXT NOT NULL, + detail TEXT NOT NULL, + deadline_at TEXT NOT NULL, + action_label TEXT, + delivery_state TEXT NOT NULL CHECK ( + delivery_state IN ('scheduled', 'presented', 'acknowledged', 'resolved') + ), + PRIMARY KEY (account_id, farm_id, reminder_id) +); + +CREATE TABLE reminder_log_entries ( + log_entry_id TEXT PRIMARY KEY NOT NULL, + account_id TEXT NOT NULL, + farm_id TEXT NOT NULL, + reminder_id TEXT NOT NULL, + reminder_kind TEXT NOT NULL CHECK ( + reminder_kind IN ( + 'fulfillment_window', + 'order_action', + 'sync_impact' + ) + ), + title TEXT NOT NULL, + recorded_at TEXT NOT NULL, + delivery_state TEXT NOT NULL CHECK ( + delivery_state IN ('scheduled', 'presented', 'acknowledged', 'resolved') + ), + detail TEXT +); + +CREATE INDEX idx_reminder_schedules_account_farm_deadline ON reminder_schedules( + account_id, + farm_id, + deadline_at, + reminder_id +); +CREATE INDEX idx_reminder_schedules_account_farm_surface ON reminder_schedules( + account_id, + farm_id, + reminder_surface, + deadline_at +); +CREATE INDEX idx_reminder_log_entries_account_farm_recorded_at ON reminder_log_entries( + account_id, + farm_id, + recorded_at, + log_entry_id +); +CREATE INDEX idx_reminder_log_entries_account_farm_reminder ON reminder_log_entries( + account_id, + farm_id, + reminder_id +); diff --git a/crates/store/migrations/0011_reminders_and_recovery.sql b/crates/store/migrations/0011_reminders_and_recovery.sql @@ -1,102 +0,0 @@ -CREATE TABLE reminder_schedules ( - reminder_id TEXT NOT NULL, - account_id TEXT NOT NULL, - farm_id TEXT NOT NULL, - order_id TEXT, - fulfillment_window_id TEXT, - reminder_kind TEXT NOT NULL CHECK ( - reminder_kind IN ( - 'fulfillment_window', - 'order_action', - 'missed_pickup_recovery', - 'sync_impact' - ) - ), - reminder_surface TEXT NOT NULL CHECK ( - reminder_surface IN ('today', 'orders', 'pack_day') - ), - reminder_urgency TEXT NOT NULL CHECK ( - reminder_urgency IN ('upcoming', 'due_soon', 'overdue', 'blocking') - ), - title TEXT NOT NULL, - detail TEXT NOT NULL, - deadline_at TEXT NOT NULL, - action_label TEXT, - delivery_state TEXT NOT NULL CHECK ( - delivery_state IN ('scheduled', 'presented', 'acknowledged', 'resolved') - ), - PRIMARY KEY (account_id, farm_id, reminder_id) -); - -CREATE TABLE reminder_log_entries ( - log_entry_id TEXT PRIMARY KEY NOT NULL, - account_id TEXT NOT NULL, - farm_id TEXT NOT NULL, - reminder_id TEXT NOT NULL, - reminder_kind TEXT NOT NULL CHECK ( - reminder_kind IN ( - 'fulfillment_window', - 'order_action', - 'missed_pickup_recovery', - 'sync_impact' - ) - ), - title TEXT NOT NULL, - recorded_at TEXT NOT NULL, - delivery_state TEXT NOT NULL CHECK ( - delivery_state IN ('scheduled', 'presented', 'acknowledged', 'resolved') - ), - detail TEXT -); - -CREATE TABLE order_recovery_records ( - recovery_record_id TEXT PRIMARY KEY NOT NULL, - account_id TEXT NOT NULL, - farm_id TEXT NOT NULL, - order_id TEXT NOT NULL, - recovery_kind TEXT NOT NULL CHECK ( - recovery_kind IN ('missed_pickup') - ), - recovery_state TEXT NOT NULL CHECK ( - recovery_state IN ('open', 'in_review', 'resolved') - ), - summary TEXT NOT NULL, - note TEXT, - last_updated_at TEXT NOT NULL, - UNIQUE(account_id, order_id, recovery_kind) -); - -CREATE INDEX idx_reminder_schedules_account_farm_deadline ON reminder_schedules( - account_id, - farm_id, - deadline_at, - reminder_id -); -CREATE INDEX idx_reminder_schedules_account_farm_surface ON reminder_schedules( - account_id, - farm_id, - reminder_surface, - deadline_at -); -CREATE INDEX idx_reminder_log_entries_account_farm_recorded_at ON reminder_log_entries( - account_id, - farm_id, - recorded_at, - log_entry_id -); -CREATE INDEX idx_reminder_log_entries_account_farm_reminder ON reminder_log_entries( - account_id, - farm_id, - reminder_id -); -CREATE INDEX idx_order_recovery_records_account_farm_updated_at ON order_recovery_records( - account_id, - farm_id, - last_updated_at, - recovery_record_id -); -CREATE INDEX idx_order_recovery_records_account_order_kind ON order_recovery_records( - account_id, - order_id, - recovery_kind -); diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs @@ -20,10 +20,9 @@ use radroots_app_view::{ BuyerOrderReviewDraft, BuyerOrderReviewProjection, BuyerOrdersProjection, BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmRulesProjection, FarmSetupProjection, FarmSummary, FulfillmentWindowId, OrderDetailProjection, OrderId, - OrderRecoveryProjection, OrdersListProjection, OrdersScreenQueryState, PackDayOutputSource, - PackDayProjection, PackDayScreenQueryState, ProductEditorDraft, ProductId, - ProductPublishBlocker, ProductsFilter, ProductsListProjection, ProductsSort, RecoveryKind, - RecoveryQueueProjection, ReminderFeedProjection, ReminderLogEntryProjection, + OrdersListProjection, OrdersScreenQueryState, PackDayOutputSource, PackDayProjection, + PackDayScreenQueryState, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductsFilter, + ProductsListProjection, ProductsSort, ReminderFeedProjection, ReminderLogEntryProjection, ReminderLogProjection, TodayAgendaProjection, }; use rusqlite::Connection; @@ -328,35 +327,6 @@ impl AppSqliteStore { .load_reminder_log(account_id, farm_id, limit) } - pub fn load_recovery_queue( - &self, - account_id: &str, - farm_id: FarmId, - ) -> Result<RecoveryQueueProjection, AppSqliteError> { - self.reminders_repository() - .load_recovery_queue(account_id, farm_id) - } - - pub fn load_recovery_record( - &self, - account_id: &str, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<Option<OrderRecoveryProjection>, AppSqliteError> { - self.reminders_repository() - .load_recovery_record(account_id, order_id, kind) - } - - pub fn save_recovery_record( - &self, - account_id: &str, - farm_id: FarmId, - record: &OrderRecoveryProjection, - ) -> Result<(), AppSqliteError> { - self.reminders_repository() - .save_recovery_record(account_id, farm_id, record) - } - pub fn save_product_editor_draft( &self, product_id: ProductId, @@ -865,7 +835,6 @@ mod tests { assert!(table_exists(connection, "buyer_cart_lines")); assert!(table_exists(connection, "reminder_schedules")); assert!(table_exists(connection, "reminder_log_entries")); - assert!(table_exists(connection, "order_recovery_records")); assert!(table_exists(connection, "buyer_order_coordination_records")); assert!(table_exists(connection, "order_validation_receipts")); assert!(table_exists(connection, "app_sdk_migration_receipts")); @@ -986,11 +955,6 @@ mod tests { )); assert!(column_exists( connection, - "order_recovery_records", - "recovery_kind" - )); - assert!(column_exists( - connection, "buyer_order_coordination_records", "state" )); @@ -1041,11 +1005,6 @@ mod tests { )); assert!(column_exists( connection, - "order_recovery_records", - "recovery_state" - )); - assert!(column_exists( - connection, "app_sdk_migration_receipts", "source_record_id" )); diff --git a/crates/store/src/migrations.rs b/crates/store/src/migrations.rs @@ -46,7 +46,7 @@ const MIGRATIONS: &[Migration] = &[ }, Migration { version: 11, - sql: include_str!("../migrations/0011_reminders_and_recovery.sql"), + sql: include_str!("../migrations/0011_reminders.sql"), }, Migration { version: 12, diff --git a/crates/store/src/repo/farm_rules.rs b/crates/store/src/repo/farm_rules.rs @@ -202,7 +202,7 @@ impl<'a> AppFarmRulesRepository<'a> { let row = self .connection .query_row( - "select farm_id, promise_lead_hours, substitution_policy, missed_pickup_policy + "select farm_id, promise_lead_hours, substitution_policy from farm_operating_rules where farm_id = ?1 limit 1", @@ -212,7 +212,6 @@ impl<'a> AppFarmRulesRepository<'a> { row.get::<_, String>(0)?, row.get::<_, i64>(1)?, row.get::<_, String>(2)?, - row.get::<_, String>(3)?, )) }, ) @@ -222,19 +221,16 @@ impl<'a> AppFarmRulesRepository<'a> { source, })?; - row.map( - |(farm_id, promise_lead_hours, substitution_policy, missed_pickup_policy)| { - Ok(FarmOperatingRulesRecord { - farm_id: parse_typed_id("farm_operating_rules.farm_id", farm_id)?, - promise_lead_hours: parse_u16( - "farm_operating_rules.promise_lead_hours", - promise_lead_hours, - )?, - substitution_policy, - missed_pickup_policy, - }) - }, - ) + row.map(|(farm_id, promise_lead_hours, substitution_policy)| { + Ok(FarmOperatingRulesRecord { + farm_id: parse_typed_id("farm_operating_rules.farm_id", farm_id)?, + promise_lead_hours: parse_u16( + "farm_operating_rules.promise_lead_hours", + promise_lead_hours, + )?, + substitution_policy, + }) + }) .transpose() } @@ -417,27 +413,23 @@ impl<'a> AppFarmRulesRepository<'a> { farm_id, promise_lead_hours, substitution_policy, - missed_pickup_policy, created_at, updated_at ) values ( ?1, ?2, ?3, - ?4, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now') ) on conflict(farm_id) do update set promise_lead_hours = excluded.promise_lead_hours, substitution_policy = excluded.substitution_policy, - missed_pickup_policy = excluded.missed_pickup_policy, updated_at = excluded.updated_at", params![ operating_rules.farm_id.to_string(), i64::from(operating_rules.promise_lead_hours), operating_rules.substitution_policy, - operating_rules.missed_pickup_policy, ], ) .map_err(|source| AppSqliteError::Query { @@ -778,7 +770,6 @@ fn derive_farm_rules_readiness_parts( if operating_rules.is_none_or(|operating_rules| { operating_rules.promise_lead_hours == 0 || operating_rules.substitution_policy.trim().is_empty() - || operating_rules.missed_pickup_policy.trim().is_empty() }) { blockers.push(FarmReadinessBlocker::MissingOperatingRules); } @@ -988,7 +979,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id, @@ -1121,7 +1111,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: Vec::new(), blackout_periods: Vec::new(), @@ -1163,7 +1152,6 @@ mod tests { farm_id, promise_lead_hours: 0, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id: FulfillmentWindowId::new(), @@ -1208,7 +1196,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { fulfillment_window_id: FulfillmentWindowId::new(), diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs @@ -167,7 +167,6 @@ impl<'a> AppOrdersRepository<'a> { validation_receipts, primary_action: primary_action_for_order(status, &workflow), workflow, - recoveries: Vec::new(), }) }, ) diff --git a/crates/store/src/repo/reminders.rs b/crates/store/src/repo/reminders.rs @@ -1,9 +1,9 @@ use radroots_app_view::{ - FarmId, OrderId, OrderRecoveryProjection, RecoveryKind, RecoveryQueueProjection, RecoveryState, - ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, ReminderKind, - ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency, + FarmId, ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, + ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, + ReminderUrgency, }; -use rusqlite::{Connection, OptionalExtension, params}; +use rusqlite::{Connection, params}; use std::str::FromStr; use uuid::Uuid; @@ -343,199 +343,12 @@ impl<'a> AppRemindersRepository<'a> { Ok(ReminderLogProjection { entries }) } - - pub fn load_recovery_queue( - &self, - account_id: &str, - farm_id: FarmId, - ) -> Result<RecoveryQueueProjection, AppSqliteError> { - let mut statement = self - .connection - .prepare( - "SELECT - recovery_record_id, - order_id, - recovery_kind, - recovery_state, - summary, - note, - last_updated_at - FROM order_recovery_records - WHERE account_id = ?1 AND farm_id = ?2 - ORDER BY last_updated_at DESC, recovery_record_id DESC", - ) - .map_err(|source| AppSqliteError::Query { - operation: "prepare recovery queue query", - source, - })?; - let rows = statement - .query_map(params![account_id, farm_id.to_string()], |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, Option<String>>(5)?, - row.get::<_, String>(6)?, - )) - }) - .map_err(|source| AppSqliteError::Query { - operation: "query recovery queue", - source, - })?; - - let items = rows - .map(|row| { - let ( - recovery_record_id, - order_id, - recovery_kind, - recovery_state, - summary, - note, - last_updated_at, - ) = row.map_err(|source| AppSqliteError::Query { - operation: "read recovery queue row", - source, - })?; - - Ok(OrderRecoveryProjection { - recovery_record_id: parse_typed_id( - "order_recovery_records.recovery_record_id", - recovery_record_id, - )?, - order_id: parse_typed_id("order_recovery_records.order_id", order_id)?, - kind: parse_recovery_kind(recovery_kind)?, - state: parse_recovery_state(recovery_state)?, - summary, - note, - last_updated_at, - }) - }) - .collect::<Result<Vec<_>, AppSqliteError>>()?; - - Ok(RecoveryQueueProjection { items }) - } - - pub fn load_recovery_record( - &self, - account_id: &str, - order_id: OrderId, - kind: RecoveryKind, - ) -> Result<Option<OrderRecoveryProjection>, AppSqliteError> { - let row = self - .connection - .query_row( - "SELECT - recovery_record_id, - order_id, - recovery_kind, - recovery_state, - summary, - note, - last_updated_at - FROM order_recovery_records - WHERE account_id = ?1 AND order_id = ?2 AND recovery_kind = ?3 - LIMIT 1", - params![account_id, order_id.to_string(), kind.storage_key()], - |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, Option<String>>(5)?, - row.get::<_, String>(6)?, - )) - }, - ) - .optional() - .map_err(|source| AppSqliteError::Query { - operation: "load recovery record", - source, - })?; - - row.map_or_else( - || Ok(None), - |( - recovery_record_id, - order_id, - recovery_kind, - recovery_state, - summary, - note, - last_updated_at, - )| { - Ok(Some(OrderRecoveryProjection { - recovery_record_id: parse_typed_id( - "order_recovery_records.recovery_record_id", - recovery_record_id, - )?, - order_id: parse_typed_id("order_recovery_records.order_id", order_id)?, - kind: parse_recovery_kind(recovery_kind)?, - state: parse_recovery_state(recovery_state)?, - summary, - note, - last_updated_at, - })) - }, - ) - } - - pub fn save_recovery_record( - &self, - account_id: &str, - farm_id: FarmId, - record: &OrderRecoveryProjection, - ) -> Result<(), AppSqliteError> { - self.connection - .execute( - "INSERT INTO order_recovery_records ( - recovery_record_id, - account_id, - farm_id, - order_id, - recovery_kind, - recovery_state, - summary, - note, - last_updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) - ON CONFLICT(account_id, order_id, recovery_kind) DO UPDATE SET - recovery_record_id = excluded.recovery_record_id, - farm_id = excluded.farm_id, - recovery_state = excluded.recovery_state, - summary = excluded.summary, - note = excluded.note, - last_updated_at = excluded.last_updated_at", - params![ - record.recovery_record_id.to_string(), - account_id, - farm_id.to_string(), - record.order_id.to_string(), - record.kind.storage_key(), - record.state.storage_key(), - record.summary, - record.note, - record.last_updated_at, - ], - ) - .map_err(|source| AppSqliteError::Query { - operation: "save recovery record", - source, - })?; - - Ok(()) - } } fn parse_reminder_kind(value: String) -> Result<ReminderKind, AppSqliteError> { match value.as_str() { "fulfillment_window" => Ok(ReminderKind::FulfillmentWindow), "order_action" => Ok(ReminderKind::OrderAction), - "missed_pickup_recovery" => Ok(ReminderKind::MissedPickupRecovery), "sync_impact" => Ok(ReminderKind::SyncImpact), _ => Err(AppSqliteError::DecodeEnum { field: "reminder_schedules.reminder_kind", @@ -582,28 +395,6 @@ fn parse_reminder_delivery_state(value: String) -> Result<ReminderDeliveryState, } } -fn parse_recovery_kind(value: String) -> Result<RecoveryKind, AppSqliteError> { - match value.as_str() { - "missed_pickup" => Ok(RecoveryKind::MissedPickup), - _ => Err(AppSqliteError::DecodeEnum { - field: "order_recovery_records.recovery_kind", - value, - }), - } -} - -fn parse_recovery_state(value: String) -> Result<RecoveryState, AppSqliteError> { - match value.as_str() { - "open" => Ok(RecoveryState::Open), - "in_review" => Ok(RecoveryState::InReview), - "resolved" => Ok(RecoveryState::Resolved), - _ => Err(AppSqliteError::DecodeEnum { - field: "order_recovery_records.recovery_state", - value, - }), - } -} - fn parse_typed_id<T>(field: &'static str, value: String) -> Result<T, AppSqliteError> where T: FromStr<Err = uuid::Error>, @@ -626,9 +417,8 @@ mod tests { use super::AppRemindersRepository; use crate::{AppSqliteStore, DatabaseTarget}; use radroots_app_view::{ - FarmId, OrderId, OrderRecoveryProjection, RecoveryKind, RecoveryRecordId, RecoveryState, - ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, ReminderId, - ReminderKind, ReminderLogEntryProjection, ReminderSurface, ReminderUrgency, + FarmId, OrderId, ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, + ReminderId, ReminderKind, ReminderLogEntryProjection, ReminderSurface, ReminderUrgency, }; #[test] @@ -716,11 +506,11 @@ mod tests { farm_id, &ReminderLogEntryProjection { reminder_id: second_reminder_id, - kind: ReminderKind::MissedPickupRecovery, - title: "Pickup follow-up pending".to_owned(), + kind: ReminderKind::SyncImpact, + title: "Sync attention needed".to_owned(), recorded_at: "2026-04-25T13:00:00Z".to_owned(), delivery_state: ReminderDeliveryState::Acknowledged, - detail: Some("Customer requested a callback.".to_owned()), + detail: Some("A local sync issue needs review.".to_owned()), }, ) .expect("second log entry should save"); @@ -736,50 +526,4 @@ mod tests { ReminderDeliveryState::Acknowledged ); } - - #[test] - fn recovery_records_round_trip_and_upsert_by_account_order_and_kind() { - let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); - let repository = AppRemindersRepository::new(store.connection()); - let farm_id = FarmId::new(); - let order_id = OrderId::new(); - - let first = OrderRecoveryProjection { - recovery_record_id: RecoveryRecordId::new(), - order_id, - kind: RecoveryKind::MissedPickup, - state: RecoveryState::Open, - summary: "Customer missed pickup".to_owned(), - note: Some("Hold until Friday".to_owned()), - last_updated_at: "2026-04-25T17:00:00Z".to_owned(), - }; - let updated = OrderRecoveryProjection { - recovery_record_id: RecoveryRecordId::new(), - order_id, - kind: RecoveryKind::MissedPickup, - state: RecoveryState::InReview, - summary: "Pickup follow-up underway".to_owned(), - note: Some("Customer will confirm by tonight".to_owned()), - last_updated_at: "2026-04-25T18:00:00Z".to_owned(), - }; - - repository - .save_recovery_record("acct_farmer", farm_id, &first) - .expect("first recovery should save"); - repository - .save_recovery_record("acct_farmer", farm_id, &updated) - .expect("updated recovery should save"); - - let loaded = repository - .load_recovery_queue("acct_farmer", farm_id) - .expect("recovery queue should load"); - let one = repository - .load_recovery_record("acct_farmer", order_id, RecoveryKind::MissedPickup) - .expect("recovery record should load") - .expect("recovery record should exist"); - - assert_eq!(loaded.items.len(), 1); - assert_eq!(loaded.items[0], updated); - assert_eq!(one, updated); - } } diff --git a/crates/store/src/repo/today.rs b/crates/store/src/repo/today.rs @@ -137,7 +137,6 @@ impl<'a> AppTodayAgendaRepository<'a> { params![farm_id.to_string()], )?, reminders_due_soon: 0, - recovery_actions_open: 0, }) } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs @@ -117,8 +117,6 @@ typed_id!(ActivityEventId); typed_id!(ReminderId); -typed_id!(RecoveryRecordId); - #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AccountCustody { @@ -157,7 +155,6 @@ pub struct FarmOperatingRulesRecord { pub farm_id: FarmId, pub promise_lead_hours: u16, pub substitution_policy: String, - pub missed_pickup_policy: String, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -836,7 +833,6 @@ impl ReminderSurface { pub enum ReminderKind { FulfillmentWindow, OrderAction, - MissedPickupRecovery, SyncImpact, } @@ -845,7 +841,6 @@ impl ReminderKind { match self { Self::FulfillmentWindow => "fulfillment_window", Self::OrderAction => "order_action", - Self::MissedPickupRecovery => "missed_pickup_recovery", Self::SyncImpact => "sync_impact", } } @@ -893,38 +888,6 @@ impl ReminderDeliveryState { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum RecoveryKind { - MissedPickup, -} - -impl RecoveryKind { - pub const fn storage_key(self) -> &'static str { - match self { - Self::MissedPickup => "missed_pickup", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RecoveryState { - Open, - InReview, - Resolved, -} - -impl RecoveryState { - pub const fn storage_key(self) -> &'static str { - match self { - Self::Open => "open", - Self::InReview => "in_review", - Self::Resolved => "resolved", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] pub enum RepeatDemandEligibility { Eligible, Partial, diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs @@ -1682,7 +1682,6 @@ pub struct OrderDetailProjection { pub workflow: TradeWorkflowProjection, pub validation_receipts: Vec<TradeValidationReceiptProjection>, pub primary_action: Option<OrderPrimaryAction>, - pub recoveries: Vec<OrderRecoveryProjection>, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -1959,7 +1958,6 @@ pub struct TodaySummary { pub low_stock_products: u32, pub draft_products: u32, pub reminders_due_soon: u32, - pub recovery_actions_open: u32, } impl TodaySummary { @@ -1968,7 +1966,6 @@ impl TodaySummary { || self.low_stock_products > 0 || self.draft_products > 0 || self.reminders_due_soon > 0 - || self.recovery_actions_open > 0 } } @@ -2033,28 +2030,6 @@ impl ReminderLogProjection { } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct OrderRecoveryProjection { - pub recovery_record_id: RecoveryRecordId, - pub order_id: OrderId, - pub kind: RecoveryKind, - pub state: RecoveryState, - pub summary: String, - pub note: Option<String>, - pub last_updated_at: String, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub struct RecoveryQueueProjection { - pub items: Vec<OrderRecoveryProjection>, -} - -impl RecoveryQueueProjection { - pub fn is_empty(&self) -> bool { - self.items.is_empty() - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct RepeatDemandHandoffProjection { pub order_id: OrderId, pub farm_id: FarmId, @@ -2158,32 +2133,31 @@ mod tests { FarmTimingConflictKind, FarmerActivationProjection, FarmerSection, FulfillmentWindowId, IdentityBlockedReason, IdentityReadiness, LoggedOutStartupPhase, LoggedOutStartupProjection, OrderDetailItemRow, OrderDetailProjection, OrderId, - OrderListRow, OrderPrimaryAction, OrderRecoveryProjection, OrderStatus, OrdersFilter, - OrdersListProjection, OrdersListRow, OrdersListSummary, OrdersScreenQueryState, - PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, - PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, - PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, - PackDayHostHandoffStatus, PackDayOutputCustomerOrder, PackDayOutputOrderState, - PackDayOutputPackListEntry, PackDayOutputProductTotal, PackDayOutputQuantity, - PackDayOutputSource, PackDayOutputWindow, PackDayPackListRow, PackDayPrintFailureKind, - PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, PackDayProductTotalRow, - PackDayProjection, PackDayRosterRow, PackDayScreenQueryState, - ParseStartupSignerSourceError, PersonalEntryProjection, PersonalEntryState, - PersonalSection, PickupLocationId, ProductAttentionState, ProductAvailabilityState, - ProductAvailabilitySummary, ProductEditorDraft, ProductListRow, ProductPricePresentation, - ProductPublishBlocker, ProductStatus, ProductStockState, ProductStockSummary, - ProductsFilter, ProductsListProjection, ProductsListRow, ProductsListSummary, ProductsSort, - RecoveryKind, RecoveryQueueProjection, RecoveryRecordId, RecoveryState, - ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, ReminderId, - ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, - ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection, - SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection, - ShellSection, StartupSignerEntryProjection, StartupSignerSource, StartupSignerSourceKind, - TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, - TradeAgreementStatus, TradeEconomicsProjection, TradeInventoryStatus, - TradeProvenanceProjection, TradeRevisionStatus, TradeValidationReceiptProofSystem, - TradeValidationReceiptResult, TradeValidationReceiptType, TradeWorkflowProjection, - TradeWorkflowSource, order_status_from_active_order_projection, + OrderListRow, OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersListProjection, + OrdersListRow, OrdersListSummary, OrdersScreenQueryState, PackDayBatchPrintArtifact, + PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, PackDayExportArtifact, + PackDayExportArtifactKind, PackDayExportBundle, PackDayExportInstanceId, + PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, + PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry, + PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow, + PackDayPackListRow, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintLabelStock, + PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, + PackDayScreenQueryState, ParseStartupSignerSourceError, PersonalEntryProjection, + PersonalEntryState, PersonalSection, PickupLocationId, ProductAttentionState, + ProductAvailabilityState, ProductAvailabilitySummary, ProductEditorDraft, ProductListRow, + ProductPricePresentation, ProductPublishBlocker, ProductStatus, ProductStockState, + ProductStockSummary, ProductsFilter, ProductsListProjection, ProductsListRow, + ProductsListSummary, ProductsSort, ReminderDeadlineProjection, ReminderDeliveryState, + ReminderFeedProjection, ReminderId, ReminderKind, ReminderLogEntryProjection, + ReminderLogProjection, ReminderSurface, ReminderUrgency, RepeatDemandEligibility, + RepeatDemandHandoffProjection, SelectedAccountProjection, SelectedSurfaceProjection, + SettingsPreference, SettingsSection, ShellSection, StartupSignerEntryProjection, + StartupSignerSource, StartupSignerSourceKind, TodayAgendaProjection, TodaySetupTask, + TodaySetupTaskKind, TodaySummary, TradeAgreementStatus, TradeEconomicsProjection, + TradeInventoryStatus, TradeProvenanceProjection, TradeRevisionStatus, + TradeValidationReceiptProofSystem, TradeValidationReceiptResult, + TradeValidationReceiptType, TradeWorkflowProjection, TradeWorkflowSource, + order_status_from_active_order_projection, }; use std::{collections::BTreeSet, str::FromStr}; use uuid::Uuid; @@ -3437,7 +3411,6 @@ mod tests { .with_economics(order_economics), validation_receipts: Vec::new(), primary_action: None, - recoveries: Vec::new(), }; let pack_day = PackDayProjection { fulfillment_window: Some(super::FulfillmentWindowSummary { @@ -3656,7 +3629,6 @@ mod tests { low_stock_products: 0, draft_products: 0, reminders_due_soon: 0, - recovery_actions_open: 0, }; let busy = TodaySummary { farm_id: FarmId::new(), @@ -3664,7 +3636,6 @@ mod tests { low_stock_products: 0, draft_products: 0, reminders_due_soon: 0, - recovery_actions_open: 0, }; assert!(!quiet.has_attention_items()); @@ -3672,7 +3643,7 @@ mod tests { } #[test] - fn reminder_recovery_and_repeat_demand_contracts_are_explicit() { + fn reminder_and_repeat_demand_contracts_are_explicit() { let farm_id = FarmId::new(); let order_id = OrderId::new(); let fulfillment_window_id = FulfillmentWindowId::new(); @@ -3690,15 +3661,6 @@ mod tests { action_label: Some("Open pack day".to_owned()), delivery_state: ReminderDeliveryState::Scheduled, }; - let recovery = OrderRecoveryProjection { - recovery_record_id: RecoveryRecordId::new(), - order_id, - kind: RecoveryKind::MissedPickup, - state: RecoveryState::Open, - summary: "Customer missed pickup".to_owned(), - note: Some("Hold one extra day".to_owned()), - last_updated_at: "2026-04-24T18:00:00Z".to_owned(), - }; let repeat_demand = RepeatDemandHandoffProjection { order_id, farm_id, @@ -3720,29 +3682,19 @@ mod tests { detail: Some(reminder.detail.clone()), }], }; - let recovery_queue = RecoveryQueueProjection { - items: vec![recovery.clone()], - }; assert_eq!(ReminderSurface::PackDay.storage_key(), "pack_day"); - assert_eq!( - ReminderKind::MissedPickupRecovery.storage_key(), - "missed_pickup_recovery" - ); assert_eq!(ReminderUrgency::DueSoon.storage_key(), "due_soon"); assert_eq!( ReminderDeliveryState::Acknowledged.storage_key(), "acknowledged" ); - assert_eq!(RecoveryKind::MissedPickup.storage_key(), "missed_pickup"); - assert_eq!(RecoveryState::InReview.storage_key(), "in_review"); assert_eq!( RepeatDemandEligibility::Unavailable.storage_key(), "unavailable" ); assert_eq!(reminder_feed.due_soon_count(), 1); assert!(!reminder_log.is_empty()); - assert!(!recovery_queue.is_empty()); assert_eq!(repeat_demand.unavailable_item_count, 1); } @@ -3963,7 +3915,6 @@ mod tests { farm_id, promise_lead_hours: 24, substitution_policy: "ask_customer".to_owned(), - missed_pickup_policy: "hold_next_window".to_owned(), }), fulfillment_windows: vec![super::FulfillmentWindowRecord { fulfillment_window_id, diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -407,17 +407,6 @@ "trade.validation.proof.sp1_compressed": "Compressed proof", "trade.validation.proof.sp1_groth16": "Groth16 proof", "trade.validation.proof.sp1_plonk": "Plonk proof", - "orders.recovery.section.title": "Recovery", - "orders.recovery.missed_pickup.title": "Missed pickup", - "orders.recovery.missed_pickup.body": "Use this when a buyer did not collect the order as planned.", - "orders.recovery.last_updated.label": "Last updated", - "orders.recovery.action.open_follow_up": "Open follow-up", - "orders.recovery.action.start_review": "Start review", - "orders.recovery.action.mark_open": "Mark open", - "orders.recovery.action.resolve": "Mark resolved", - "orders.recovery.state.open": "Open", - "orders.recovery.state.in_review": "In review", - "orders.recovery.state.resolved": "Resolved", "trade.workflow.axis.agreement": "Agreement", "trade.workflow.axis.revision": "Change", "trade.workflow.axis.inventory": "Stock", @@ -658,7 +647,6 @@ "settings.operating_rules.section.label": "Operating rules", "settings.operating_rules.field.promise_lead_time": "Promise lead time", "settings.operating_rules.field.substitution_policy": "Substitution policy", - "settings.operating_rules.field.missed_pickup_policy": "Missed pickup policy", "settings.operating_rules.invalid_promise_lead_time": "Enter whole hours, for example 24.", "settings.fulfillment_windows.section.label": "Fulfillment windows", "settings.fulfillment_windows.empty.body": "Add a fulfillment window so customers know when orders are ready.",