app

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

commit 64903b2b7093a4f6510ea0096e9c6e8d389e7968
parent 8a75d36a31d60d7e3e9147059118fb9b4e48ca4c
Author: triesap <tyson@radroots.org>
Date:   Mon, 20 Apr 2026 22:52:09 +0000

app: add reminder and recovery contracts

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 2++
Mcrates/launchers/desktop/src/window.rs | 1+
Mcrates/shared/models/src/lib.rs | 352++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/shared/sqlite/src/buyer.rs | 2++
Mcrates/shared/sqlite/src/orders.rs | 2++
Mcrates/shared/sqlite/src/today.rs | 3+++
Mcrates/shared/state/src/lib.rs | 28++++++++++++++++++++++++----
7 files changed, 374 insertions(+), 16 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -4131,6 +4131,8 @@ mod tests { orders_needing_action: 2, low_stock_products: 1, draft_products: 3, + reminders_due_soon: 0, + recovery_actions_open: 0, }), setup_checklist: vec![TodaySetupTask { kind: TodaySetupTaskKind::AddFulfillmentWindow, diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -10280,6 +10280,7 @@ mod tests { starts_at: String::new(), ends_at: String::new(), }), + reminders: Default::default(), totals_by_product: Vec::new(), pack_list: Vec::new(), pickup_roster: Vec::new(), diff --git a/crates/shared/models/src/lib.rs b/crates/shared/models/src/lib.rs @@ -239,6 +239,8 @@ typed_id!(ProductId); typed_id!(OrderId); typed_id!(FulfillmentWindowId); typed_id!(ActivityEventId); +typed_id!(ReminderId); +typed_id!(RecoveryRecordId); #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -1498,6 +1500,7 @@ pub struct OrderDetailProjection { pub pickup_location_label: Option<String>, pub items: Vec<OrderDetailItemRow>, pub primary_action: Option<OrderPrimaryAction>, + pub recovery: Option<OrderRecoveryProjection>, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -1508,6 +1511,7 @@ pub struct BuyerOrdersListRow { pub farm_display_name: String, pub fulfillment_summary: String, pub status: BuyerOrderStatus, + pub repeat_demand: Option<RepeatDemandHandoffProjection>, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -1531,6 +1535,7 @@ pub struct BuyerOrderDetailProjection { pub status: BuyerOrderStatus, pub items: Vec<OrderDetailItemRow>, pub order_note: Option<String>, + pub repeat_demand: Option<RepeatDemandHandoffProjection>, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -1560,6 +1565,7 @@ pub struct PackDayRosterRow { #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct PackDayProjection { pub fulfillment_window: Option<FulfillmentWindowSummary>, + pub reminders: ReminderFeedProjection, pub totals_by_product: Vec<PackDayProductTotalRow>, pub pack_list: Vec<PackDayPackListRow>, pub pickup_roster: Vec<PackDayRosterRow>, @@ -1784,15 +1790,248 @@ pub struct TodaySummary { pub orders_needing_action: u32, pub low_stock_products: u32, pub draft_products: u32, + pub reminders_due_soon: u32, + pub recovery_actions_open: u32, } impl TodaySummary { pub const fn has_attention_items(&self) -> bool { - self.orders_needing_action > 0 || self.low_stock_products > 0 || self.draft_products > 0 + self.orders_needing_action > 0 + || self.low_stock_products > 0 + || self.draft_products > 0 + || self.reminders_due_soon > 0 + || self.recovery_actions_open > 0 + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ReminderSurface { + Today, + Orders, + PackDay, +} + +impl ReminderSurface { + pub const fn storage_key(self) -> &'static str { + match self { + Self::Today => "today", + Self::Orders => "orders", + Self::PackDay => "pack_day", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ReminderKind { + FulfillmentWindow, + OrderAction, + MissedPickupRecovery, + RefundRecovery, + SyncImpact, +} + +impl ReminderKind { + pub const fn storage_key(self) -> &'static str { + match self { + Self::FulfillmentWindow => "fulfillment_window", + Self::OrderAction => "order_action", + Self::MissedPickupRecovery => "missed_pickup_recovery", + Self::RefundRecovery => "refund_recovery", + Self::SyncImpact => "sync_impact", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ReminderUrgency { + Upcoming, + DueSoon, + Overdue, + Blocking, +} + +impl ReminderUrgency { + pub const fn storage_key(self) -> &'static str { + match self { + Self::Upcoming => "upcoming", + Self::DueSoon => "due_soon", + Self::Overdue => "overdue", + Self::Blocking => "blocking", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ReminderDeliveryState { + Scheduled, + Presented, + Acknowledged, + Resolved, +} + +impl ReminderDeliveryState { + pub const fn storage_key(self) -> &'static str { + match self { + Self::Scheduled => "scheduled", + Self::Presented => "presented", + Self::Acknowledged => "acknowledged", + Self::Resolved => "resolved", + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ReminderDeadlineProjection { + pub reminder_id: ReminderId, + pub farm_id: FarmId, + pub order_id: Option<OrderId>, + pub fulfillment_window_id: Option<FulfillmentWindowId>, + pub kind: ReminderKind, + pub surface: ReminderSurface, + pub urgency: ReminderUrgency, + pub title: String, + pub detail: String, + pub deadline_at: String, + pub action_label: Option<String>, + pub delivery_state: ReminderDeliveryState, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct ReminderFeedProjection { + pub items: Vec<ReminderDeadlineProjection>, +} + +impl ReminderFeedProjection { + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub fn due_soon_count(&self) -> usize { + self.items + .iter() + .filter(|item| { + matches!( + item.urgency, + ReminderUrgency::DueSoon + | ReminderUrgency::Overdue + | ReminderUrgency::Blocking + ) + }) + .count() } } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ReminderLogEntryProjection { + pub reminder_id: ReminderId, + pub kind: ReminderKind, + pub title: String, + pub recorded_at: String, + pub delivery_state: ReminderDeliveryState, + pub detail: Option<String>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct ReminderLogProjection { + pub entries: Vec<ReminderLogEntryProjection>, +} + +impl ReminderLogProjection { + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RecoveryKind { + MissedPickup, + RefundFollowUp, +} + +impl RecoveryKind { + pub const fn storage_key(self) -> &'static str { + match self { + Self::MissedPickup => "missed_pickup", + Self::RefundFollowUp => "refund_follow_up", + } + } +} + +#[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, 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, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RepeatDemandEligibility { + Eligible, + Partial, + Unavailable, +} + +impl RepeatDemandEligibility { + pub const fn storage_key(self) -> &'static str { + match self { + Self::Eligible => "eligible", + Self::Partial => "partial", + Self::Unavailable => "unavailable", + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct RepeatDemandHandoffProjection { + pub order_id: OrderId, + pub farm_id: FarmId, + pub eligibility: RepeatDemandEligibility, + pub available_item_count: u32, + pub unavailable_item_count: u32, + pub action_label: String, + pub note: Option<String>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum AppActivityKind { HomeOpened, SettingsOpened { @@ -1876,6 +2115,7 @@ pub struct TodaySetupTask { pub struct TodayAgendaProjection { pub farm: Option<FarmSummary>, pub summary: Option<TodaySummary>, + pub reminders: ReminderFeedProjection, pub orders_needing_action: Vec<OrderListRow>, pub low_stock_products: Vec<ProductListRow>, pub draft_products: Vec<ProductListRow>, @@ -1888,6 +2128,7 @@ impl TodayAgendaProjection { self.summary .as_ref() .is_some_and(TodaySummary::has_attention_items) + || !self.reminders.is_empty() || !self.orders_needing_action.is_empty() || !self.low_stock_products.is_empty() || !self.draft_products.is_empty() @@ -1912,17 +2153,22 @@ mod tests { FarmSetupSection, FarmTimingConflict, FarmTimingConflictKind, FarmerActivationProjection, FarmerSection, IdentityBlockedReason, IdentityReadiness, LoggedOutStartupPhase, LoggedOutStartupProjection, OrderDetailItemRow, OrderDetailProjection, OrderListRow, - OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersListProjection, OrdersListRow, - OrdersListSummary, OrdersScreenQueryState, PackDayPackListRow, PackDayProductTotalRow, - PackDayProjection, PackDayRosterRow, PackDayScreenQueryState, - ParseStartupSignerSourceError, PersonalEntryProjection, PersonalEntryState, - PersonalSection, PickupLocationId, ProductAttentionState, ProductAvailabilityState, - ProductAvailabilitySummary, ProductEditorDraft, ProductListRow, ProductPricePresentation, - ProductPublishBlocker, ProductStatus, ProductStockState, ProductStockSummary, - ProductsFilter, ProductsListProjection, ProductsListRow, ProductsListSummary, ProductsSort, + OrderId, OrderPrimaryAction, OrderRecoveryProjection, OrderStatus, OrdersFilter, + OrdersListProjection, OrdersListRow, OrdersListSummary, OrdersScreenQueryState, + PackDayPackListRow, 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, + FulfillmentWindowId, }; use std::{collections::BTreeSet, str::FromStr}; use uuid::Uuid; @@ -2553,6 +2799,7 @@ mod tests { quantity_display: "2 bags".to_owned(), }], primary_action: Some(OrderPrimaryAction::MarkPacked), + recovery: None, }; let pack_day = PackDayProjection { fulfillment_window: Some(super::FulfillmentWindowSummary { @@ -2572,9 +2819,10 @@ mod tests { pickup_roster: vec![PackDayRosterRow { order_id, order_number: "R-1001".to_owned(), - customer_display_name: "Casey".to_owned(), - }], - }; + customer_display_name: "Casey".to_owned(), + }], + reminders: ReminderFeedProjection::default(), + }; assert!(orders_list.summary.has_orders()); assert!(!orders_list.is_empty()); @@ -2663,6 +2911,7 @@ mod tests { farm_display_name: "Cedar Grove Farm".to_owned(), fulfillment_summary: "Thursday pickup".to_owned(), status: BuyerOrderStatus::Scheduled, + repeat_demand: None, }], }; let order_detail = BuyerOrderDetailProjection { @@ -2677,6 +2926,7 @@ mod tests { quantity_display: "2 bags".to_owned(), }], order_note: Some("Leave by the cooler".to_owned()), + repeat_demand: None, }; assert!(!listings.is_empty()); @@ -2728,12 +2978,16 @@ mod tests { orders_needing_action: 0, low_stock_products: 0, draft_products: 0, + reminders_due_soon: 0, + recovery_actions_open: 0, }; let busy = TodaySummary { farm_id: FarmId::new(), orders_needing_action: 1, low_stock_products: 0, draft_products: 0, + reminders_due_soon: 0, + recovery_actions_open: 0, }; assert!(!quiet.has_attention_items()); @@ -2741,6 +2995,80 @@ mod tests { } #[test] + fn reminder_recovery_and_repeat_demand_contracts_are_explicit() { + let farm_id = FarmId::new(); + let order_id = OrderId::new(); + let fulfillment_window_id = FulfillmentWindowId::new(); + let reminder = ReminderDeadlineProjection { + reminder_id: ReminderId::new(), + farm_id, + order_id: Some(order_id), + fulfillment_window_id: Some(fulfillment_window_id), + kind: ReminderKind::FulfillmentWindow, + surface: ReminderSurface::Today, + urgency: ReminderUrgency::DueSoon, + title: "Pickup closes soon".to_owned(), + detail: "Pack before the pickup window opens.".to_owned(), + deadline_at: "2026-04-24T15:00:00Z".to_owned(), + 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, + eligibility: RepeatDemandEligibility::Partial, + available_item_count: 2, + unavailable_item_count: 1, + action_label: "Reorder available items".to_owned(), + note: Some("One prior item is no longer listed.".to_owned()), + }; + + let reminder_feed = ReminderFeedProjection { + items: vec![reminder.clone()], + }; + let reminder_log = ReminderLogProjection { + entries: vec![ReminderLogEntryProjection { + reminder_id: reminder.reminder_id, + kind: reminder.kind, + title: reminder.title.clone(), + recorded_at: "2026-04-24T14:00:00Z".to_owned(), + delivery_state: ReminderDeliveryState::Presented, + detail: Some(reminder.detail.clone()), + }], + }; + let recovery_queue = RecoveryQueueProjection { + items: vec![recovery.clone()], + }; + + assert_eq!(ReminderSurface::PackDay.storage_key(), "pack_day"); + assert_eq!(ReminderKind::RefundRecovery.storage_key(), "refund_recovery"); + assert_eq!(ReminderUrgency::DueSoon.storage_key(), "due_soon"); + assert_eq!( + ReminderDeliveryState::Acknowledged.storage_key(), + "acknowledged" + ); + assert_eq!(RecoveryKind::RefundFollowUp.storage_key(), "refund_follow_up"); + 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); + } + + #[test] fn today_agenda_projection_tracks_attention_and_setup_independently() { let calm = TodayAgendaProjection::default(); let with_attention = TodayAgendaProjection { diff --git a/crates/shared/sqlite/src/buyer.rs b/crates/shared/sqlite/src/buyer.rs @@ -465,6 +465,7 @@ impl<'a> AppBuyerRepository<'a> { fulfillment_ends_at, ), status: BuyerOrderStatus::from(parse_order_status("orders.status", status)?), + repeat_demand: None, }); } @@ -545,6 +546,7 @@ impl<'a> AppBuyerRepository<'a> { )?), items: self.load_order_detail_items(order_id)?, order_note: empty_string_to_none(order_note), + repeat_demand: None, }) }, ) diff --git a/crates/shared/sqlite/src/orders.rs b/crates/shared/sqlite/src/orders.rs @@ -109,6 +109,7 @@ impl<'a> AppOrdersRepository<'a> { pickup_location_label: empty_string_to_none(pickup_location_label), items, primary_action: primary_action_for_status(status), + recovery: None, }) }, ) @@ -140,6 +141,7 @@ impl<'a> AppOrdersRepository<'a> { Ok(PackDayProjection { fulfillment_window: Some(fulfillment_window), + reminders: Default::default(), totals_by_product, pack_list, pickup_roster, diff --git a/crates/shared/sqlite/src/today.rs b/crates/shared/sqlite/src/today.rs @@ -27,6 +27,7 @@ impl<'a> AppTodayAgendaRepository<'a> { Ok(TodayAgendaProjection { farm: Some(farm.clone()), summary: Some(self.load_today_summary(farm.farm_id)?), + reminders: Default::default(), orders_needing_action: self.load_orders_needing_action(farm.farm_id)?, low_stock_products: self.load_low_stock_products(farm.farm_id)?, draft_products: self.load_draft_products(farm.farm_id)?, @@ -135,6 +136,8 @@ impl<'a> AppTodayAgendaRepository<'a> { "select count(*) from products where farm_id = ?1 and status = 'draft'", params![farm_id.to_string()], )?, + reminders_due_soon: 0, + recovery_actions_open: 0, }) } diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs @@ -11,8 +11,9 @@ use radroots_app_models::{ LoggedOutStartupProjection, OrderDetailProjection, OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayProjection, PackDayScreenQueryState, PersonalEntryProjection, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductsFilter, ProductsListProjection, - ProductsSort, SelectedSurfaceProjection, SettingsAccountProjection, SettingsPreference, - SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + ProductsSort, RecoveryQueueProjection, ReminderFeedProjection, ReminderLogProjection, + SelectedSurfaceProjection, SettingsAccountProjection, SettingsPreference, SettingsSection, + ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, }; use radroots_app_sync::{ AppSyncProjection, AppSyncRunStatus, SyncCheckpointState, SyncCheckpointStatus, SyncConflict, @@ -274,6 +275,8 @@ pub struct ProductsScreenProjection { pub struct OrdersScreenProjection { pub list: OrdersListProjection, pub query: OrdersScreenQueryState, + pub reminders: ReminderFeedProjection, + pub recovery_queue: RecoveryQueueProjection, pub detail: Option<OrderDetailProjection>, } @@ -450,6 +453,7 @@ pub struct AppProjection { pub products: ProductsScreenProjection, pub orders: OrdersScreenProjection, pub pack_day: PackDayScreenProjection, + pub reminder_log: ReminderLogProjection, pub farm_setup: FarmSetupProjection, pub farm_rules: FarmRulesProjection, pub farm_readiness: FarmWorkspaceReadinessProjection, @@ -482,6 +486,7 @@ impl AppProjection { products: ProductsScreenProjection::default(), orders: OrdersScreenProjection::default(), pack_day: PackDayScreenProjection::default(), + reminder_log: ReminderLogProjection::default(), farm_setup, farm_rules: FarmRulesProjection::default(), farm_readiness: FarmWorkspaceReadinessProjection::default(), @@ -1434,8 +1439,9 @@ mod tests { OrdersListSummary, OrdersScreenQueryState, PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PackDayScreenQueryState, PersonalEntryState, PersonalSection, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductsFilter, - ProductsListProjection, ProductsSort, SelectedAccountProjection, SelectedSurfaceProjection, - SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + ProductsListProjection, ProductsSort, ReminderFeedProjection, SelectedAccountProjection, + SelectedSurfaceProjection, SettingsSection, ShellSection, TodayAgendaProjection, + TodaySetupTask, TodaySetupTaskKind, }; use radroots_app_sync::{ AppSyncProjection, AppSyncRunStatus, SyncCheckpointState, SyncCheckpointStatus, @@ -1641,6 +1647,7 @@ mod tests { quantity_display: "2 bags".to_owned(), }], primary_action: Some(OrderPrimaryAction::Review), + recovery: None, }; let pack_day = PackDayProjection { fulfillment_window: Some(radroots_app_models::FulfillmentWindowSummary { @@ -1662,6 +1669,7 @@ mod tests { order_number: "R-100".to_owned(), customer_display_name: "Casey".to_owned(), }], + reminders: ReminderFeedProjection::default(), }; assert_eq!( @@ -2480,4 +2488,16 @@ mod tests { .allow_relay_connections ); } + + #[test] + fn app_projection_defaults_the_new_reminder_contracts() { + let projection = AppProjection::default(); + + 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!(projection.orders.reminders, ReminderFeedProjection::default()); + } }