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:
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.",