app

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

lib.rs (26064B)


      1 #![forbid(unsafe_code)]
      2 
      3 use serde::{Deserialize, Serialize};
      4 use std::{fmt, str::FromStr};
      5 use uuid::Uuid;
      6 
      7 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
      8 #[serde(rename_all = "snake_case")]
      9 pub enum SettingsSection {
     10     #[default]
     11     Account,
     12     Farm,
     13     Settings,
     14     About,
     15 }
     16 
     17 impl SettingsSection {
     18     pub const fn storage_key(self) -> &'static str {
     19         match self {
     20             Self::Account => "settings.account",
     21             Self::Farm => "settings.farm",
     22             Self::Settings => "settings.settings",
     23             Self::About => "settings.about",
     24         }
     25     }
     26 }
     27 
     28 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
     29 #[serde(rename_all = "snake_case")]
     30 pub enum SettingsPreference {
     31     AllowRelayConnections,
     32     UseMediaServers,
     33     UseNip05,
     34     LaunchAtLogin,
     35 }
     36 
     37 impl SettingsPreference {
     38     pub const fn storage_key(self) -> &'static str {
     39         match self {
     40             Self::AllowRelayConnections => "allow_relay_connections",
     41             Self::UseMediaServers => "use_media_servers",
     42             Self::UseNip05 => "use_nip05",
     43             Self::LaunchAtLogin => "launch_at_login",
     44         }
     45     }
     46 }
     47 
     48 macro_rules! typed_id {
     49     ($name:ident) => {
     50         #[derive(
     51             Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize,
     52         )]
     53         #[serde(transparent)]
     54         pub struct $name(Uuid);
     55 
     56         impl $name {
     57             pub fn new() -> Self {
     58                 Self(Uuid::now_v7())
     59             }
     60 
     61             pub fn as_uuid(self) -> Uuid {
     62                 self.0
     63             }
     64         }
     65 
     66         impl From<Uuid> for $name {
     67             fn from(value: Uuid) -> Self {
     68                 Self(value)
     69             }
     70         }
     71 
     72         impl From<$name> for Uuid {
     73             fn from(value: $name) -> Self {
     74                 value.0
     75             }
     76         }
     77 
     78         impl fmt::Display for $name {
     79             fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     80                 self.0.fmt(formatter)
     81             }
     82         }
     83 
     84         impl FromStr for $name {
     85             type Err = uuid::Error;
     86 
     87             fn from_str(value: &str) -> Result<Self, Self::Err> {
     88                 Uuid::parse_str(value).map(Self)
     89             }
     90         }
     91 
     92         impl TryFrom<&str> for $name {
     93             type Error = uuid::Error;
     94 
     95             fn try_from(value: &str) -> Result<Self, Self::Error> {
     96                 value.parse()
     97             }
     98         }
     99     };
    100 }
    101 
    102 typed_id!(FarmId);
    103 
    104 typed_id!(PickupLocationId);
    105 
    106 typed_id!(BlackoutPeriodId);
    107 
    108 typed_id!(ProductId);
    109 
    110 typed_id!(OrderId);
    111 
    112 typed_id!(FulfillmentWindowId);
    113 
    114 typed_id!(PackDayExportInstanceId);
    115 
    116 typed_id!(ActivityEventId);
    117 
    118 typed_id!(ReminderId);
    119 
    120 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    121 #[serde(rename_all = "snake_case")]
    122 pub enum AccountCustody {
    123     LocalManaged,
    124     BrowserSigner,
    125     RemoteSigner,
    126 }
    127 
    128 impl AccountCustody {
    129     pub const fn storage_key(self) -> &'static str {
    130         match self {
    131             Self::LocalManaged => "local_managed",
    132             Self::BrowserSigner => "browser_signer",
    133             Self::RemoteSigner => "remote_signer",
    134         }
    135     }
    136 }
    137 
    138 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    139 #[serde(rename_all = "snake_case")]
    140 pub enum FarmReadiness {
    141     Incomplete,
    142     Ready,
    143 }
    144 
    145 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    146 pub struct FarmProfileRecord {
    147     pub farm_id: FarmId,
    148     pub display_name: String,
    149     pub timezone: String,
    150     pub currency_code: String,
    151 }
    152 
    153 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    154 pub struct FarmOperatingRulesRecord {
    155     pub farm_id: FarmId,
    156     pub promise_lead_hours: u16,
    157     pub substitution_policy: String,
    158 }
    159 
    160 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    161 pub struct PickupLocationRecord {
    162     pub pickup_location_id: PickupLocationId,
    163     pub farm_id: FarmId,
    164     pub label: String,
    165     pub address_line: String,
    166     pub directions: Option<String>,
    167     pub is_default: bool,
    168 }
    169 
    170 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    171 pub struct FulfillmentWindowRecord {
    172     pub fulfillment_window_id: FulfillmentWindowId,
    173     pub farm_id: FarmId,
    174     pub pickup_location_id: PickupLocationId,
    175     pub label: String,
    176     pub starts_at: String,
    177     pub ends_at: String,
    178     pub order_cutoff_at: String,
    179 }
    180 
    181 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    182 pub struct BlackoutPeriodRecord {
    183     pub blackout_period_id: BlackoutPeriodId,
    184     pub farm_id: FarmId,
    185     pub label: String,
    186     pub starts_at: String,
    187     pub ends_at: String,
    188 }
    189 
    190 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    191 #[serde(rename_all = "snake_case")]
    192 pub enum FarmReadinessBlocker {
    193     MissingProfileBasics,
    194     MissingPickupLocation,
    195     MissingFulfillmentWindow,
    196     MissingOperatingRules,
    197 }
    198 
    199 impl FarmReadinessBlocker {
    200     pub const fn storage_key(self) -> &'static str {
    201         match self {
    202             Self::MissingProfileBasics => "missing_profile_basics",
    203             Self::MissingPickupLocation => "missing_pickup_location",
    204             Self::MissingFulfillmentWindow => "missing_fulfillment_window",
    205             Self::MissingOperatingRules => "missing_operating_rules",
    206         }
    207     }
    208 }
    209 
    210 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    211 #[serde(rename_all = "snake_case")]
    212 pub enum FarmTimingConflictKind {
    213     FulfillmentWindowEndsBeforeStart,
    214     FulfillmentWindowCutoffAfterStart,
    215     BlackoutPeriodEndsBeforeStart,
    216     BlackoutOverlapsFulfillmentWindow,
    217 }
    218 
    219 impl FarmTimingConflictKind {
    220     pub const fn storage_key(self) -> &'static str {
    221         match self {
    222             Self::FulfillmentWindowEndsBeforeStart => "fulfillment_window_ends_before_start",
    223             Self::FulfillmentWindowCutoffAfterStart => "fulfillment_window_cutoff_after_start",
    224             Self::BlackoutPeriodEndsBeforeStart => "blackout_period_ends_before_start",
    225             Self::BlackoutOverlapsFulfillmentWindow => "blackout_overlaps_fulfillment_window",
    226         }
    227     }
    228 }
    229 
    230 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    231 pub struct FarmTimingConflict {
    232     pub kind: FarmTimingConflictKind,
    233     pub fulfillment_window_id: Option<FulfillmentWindowId>,
    234     pub blackout_period_id: Option<BlackoutPeriodId>,
    235 }
    236 
    237 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    238 pub struct FarmRulesReadiness {
    239     pub blockers: Vec<FarmReadinessBlocker>,
    240     pub timing_conflicts: Vec<FarmTimingConflict>,
    241 }
    242 
    243 impl FarmRulesReadiness {
    244     pub fn ready() -> Self {
    245         Self {
    246             blockers: Vec::new(),
    247             timing_conflicts: Vec::new(),
    248         }
    249     }
    250 
    251     pub fn missing_v1_basics() -> Self {
    252         Self {
    253             blockers: vec![
    254                 FarmReadinessBlocker::MissingProfileBasics,
    255                 FarmReadinessBlocker::MissingPickupLocation,
    256                 FarmReadinessBlocker::MissingFulfillmentWindow,
    257                 FarmReadinessBlocker::MissingOperatingRules,
    258             ],
    259             timing_conflicts: Vec::new(),
    260         }
    261     }
    262 
    263     pub fn is_ready(&self) -> bool {
    264         self.blockers.is_empty() && self.timing_conflicts.is_empty()
    265     }
    266 }
    267 
    268 impl Default for FarmRulesReadiness {
    269     fn default() -> Self {
    270         Self::ready()
    271     }
    272 }
    273 
    274 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
    275 #[serde(rename_all = "snake_case")]
    276 pub enum ProductStatus {
    277     #[default]
    278     Draft,
    279     Published,
    280     Paused,
    281     Archived,
    282 }
    283 
    284 impl ProductStatus {
    285     pub const fn storage_key(self) -> &'static str {
    286         match self {
    287             Self::Draft => "draft",
    288             Self::Published => "published",
    289             Self::Paused => "paused",
    290             Self::Archived => "archived",
    291         }
    292     }
    293 
    294     pub const fn is_live(self) -> bool {
    295         matches!(self, Self::Published)
    296     }
    297 }
    298 
    299 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    300 #[serde(rename_all = "snake_case")]
    301 pub enum ProductPublishBlocker {
    302     AddProductName,
    303     ChooseCategory,
    304     ChooseUnit,
    305     SetPrice,
    306     SetStock,
    307     AttachAvailability,
    308     CompleteFarmProfile,
    309     AddPickupLocation,
    310     AddOperatingRules,
    311     AddFulfillmentWindow,
    312     ResolveAvailabilityConflicts,
    313 }
    314 
    315 impl ProductPublishBlocker {
    316     pub const fn storage_key(self) -> &'static str {
    317         match self {
    318             Self::AddProductName => "add_product_name",
    319             Self::ChooseCategory => "choose_category",
    320             Self::ChooseUnit => "choose_unit",
    321             Self::SetPrice => "set_price",
    322             Self::SetStock => "set_stock",
    323             Self::AttachAvailability => "attach_availability",
    324             Self::CompleteFarmProfile => "complete_farm_profile",
    325             Self::AddPickupLocation => "add_pickup_location",
    326             Self::AddOperatingRules => "add_operating_rules",
    327             Self::AddFulfillmentWindow => "add_fulfillment_window",
    328             Self::ResolveAvailabilityConflicts => "resolve_availability_conflicts",
    329         }
    330     }
    331 }
    332 
    333 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    334 #[serde(rename_all = "snake_case")]
    335 pub enum OrderStatus {
    336     NeedsAction,
    337     Scheduled,
    338     Packed,
    339     Completed,
    340     Declined,
    341     NeedsReview,
    342 }
    343 
    344 impl OrderStatus {
    345     pub const fn storage_key(self) -> &'static str {
    346         match self {
    347             Self::NeedsAction => "needs_action",
    348             Self::Scheduled => "scheduled",
    349             Self::Packed => "packed",
    350             Self::Completed => "completed",
    351             Self::Declined => "declined",
    352             Self::NeedsReview => "needs_review",
    353         }
    354     }
    355 }
    356 
    357 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    358 #[serde(rename_all = "snake_case")]
    359 pub enum BuyerOrderStatus {
    360     Placed,
    361     Scheduled,
    362     Ready,
    363     Completed,
    364     Declined,
    365     NeedsReview,
    366 }
    367 
    368 impl BuyerOrderStatus {
    369     pub const fn storage_key(self) -> &'static str {
    370         match self {
    371             Self::Placed => "placed",
    372             Self::Scheduled => "scheduled",
    373             Self::Ready => "ready",
    374             Self::Completed => "completed",
    375             Self::Declined => "declined",
    376             Self::NeedsReview => "needs_review",
    377         }
    378     }
    379 }
    380 
    381 impl From<OrderStatus> for BuyerOrderStatus {
    382     fn from(value: OrderStatus) -> Self {
    383         match value {
    384             OrderStatus::NeedsAction => Self::Placed,
    385             OrderStatus::Scheduled => Self::Scheduled,
    386             OrderStatus::Packed => Self::Ready,
    387             OrderStatus::Completed => Self::Completed,
    388             OrderStatus::Declined => Self::Declined,
    389             OrderStatus::NeedsReview => Self::NeedsReview,
    390         }
    391     }
    392 }
    393 
    394 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    395 #[serde(rename_all = "snake_case")]
    396 pub enum PackDayExportArtifactKind {
    397     PackSheet,
    398     PickupRoster,
    399     CustomerLabels,
    400 }
    401 
    402 impl PackDayExportArtifactKind {
    403     pub const fn all_v1() -> [Self; 3] {
    404         [Self::PackSheet, Self::PickupRoster, Self::CustomerLabels]
    405     }
    406 
    407     pub const fn storage_key(self) -> &'static str {
    408         match self {
    409             Self::PackSheet => "pack_sheet",
    410             Self::PickupRoster => "pickup_roster",
    411             Self::CustomerLabels => "customer_labels",
    412         }
    413     }
    414 
    415     pub const fn file_name(self) -> &'static str {
    416         match self {
    417             Self::PackSheet => "pack_sheet.txt",
    418             Self::PickupRoster => "pickup_roster.txt",
    419             Self::CustomerLabels => "customer_labels.txt",
    420         }
    421     }
    422 }
    423 
    424 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    425 #[serde(rename_all = "snake_case")]
    426 pub enum PackDayPrintKind {
    427     PrintPackSheet,
    428     PrintPickupRoster,
    429     PrintCustomerLabels,
    430 }
    431 
    432 impl PackDayPrintKind {
    433     pub const fn all_v1() -> [Self; 3] {
    434         [
    435             Self::PrintPackSheet,
    436             Self::PrintPickupRoster,
    437             Self::PrintCustomerLabels,
    438         ]
    439     }
    440 
    441     pub const fn storage_key(self) -> &'static str {
    442         match self {
    443             Self::PrintPackSheet => "print_pack_sheet",
    444             Self::PrintPickupRoster => "print_pickup_roster",
    445             Self::PrintCustomerLabels => "print_customer_labels",
    446         }
    447     }
    448 
    449     pub const fn artifact_kind(self) -> PackDayExportArtifactKind {
    450         match self {
    451             Self::PrintPackSheet => PackDayExportArtifactKind::PackSheet,
    452             Self::PrintPickupRoster => PackDayExportArtifactKind::PickupRoster,
    453             Self::PrintCustomerLabels => PackDayExportArtifactKind::CustomerLabels,
    454         }
    455     }
    456 
    457     pub const fn label_stock(self) -> Option<PackDayPrintLabelStock> {
    458         match self {
    459             Self::PrintPackSheet | Self::PrintPickupRoster => None,
    460             Self::PrintCustomerLabels => Some(PackDayPrintLabelStock::Avery5160Letter30Up),
    461         }
    462     }
    463 }
    464 
    465 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    466 #[serde(rename_all = "snake_case")]
    467 pub enum PackDayPrintLabelStock {
    468     Avery5160Letter30Up,
    469 }
    470 
    471 impl PackDayPrintLabelStock {
    472     pub const fn all_v1() -> [Self; 1] {
    473         [Self::Avery5160Letter30Up]
    474     }
    475 
    476     pub const fn storage_key(self) -> &'static str {
    477         match self {
    478             Self::Avery5160Letter30Up => "avery_5160_letter_30_up",
    479         }
    480     }
    481 }
    482 
    483 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    484 #[serde(rename_all = "snake_case")]
    485 pub enum PackDayPrintFailureKind {
    486     CustomerLabelsAvery5160Overflow,
    487 }
    488 
    489 impl PackDayPrintFailureKind {
    490     pub const fn storage_key(self) -> &'static str {
    491         match self {
    492             Self::CustomerLabelsAvery5160Overflow => "customer_labels_avery_5160_overflow",
    493         }
    494     }
    495 }
    496 
    497 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    498 pub struct PackDayBatchPrintArtifact {
    499     pub print_kind: PackDayPrintKind,
    500     pub artifact_kind: PackDayExportArtifactKind,
    501     pub label_stock: Option<PackDayPrintLabelStock>,
    502 }
    503 
    504 impl PackDayBatchPrintArtifact {
    505     pub const fn all_v1() -> [Self; 3] {
    506         [
    507             Self::from_print_kind(PackDayPrintKind::PrintPackSheet),
    508             Self::from_print_kind(PackDayPrintKind::PrintPickupRoster),
    509             Self::from_print_kind(PackDayPrintKind::PrintCustomerLabels),
    510         ]
    511     }
    512 
    513     pub const fn from_print_kind(print_kind: PackDayPrintKind) -> Self {
    514         Self {
    515             print_kind,
    516             artifact_kind: print_kind.artifact_kind(),
    517             label_stock: print_kind.label_stock(),
    518         }
    519     }
    520 }
    521 
    522 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    523 #[serde(rename_all = "snake_case")]
    524 pub enum PackDayBatchPrintFailureKind {
    525     Preflight,
    526     QueueLaunch,
    527     QueueExit,
    528     CustomerLabelsAvery5160Overflow,
    529 }
    530 
    531 impl PackDayBatchPrintFailureKind {
    532     pub const fn storage_key(self) -> &'static str {
    533         match self {
    534             Self::Preflight => "preflight",
    535             Self::QueueLaunch => "queue_launch",
    536             Self::QueueExit => "queue_exit",
    537             Self::CustomerLabelsAvery5160Overflow => "customer_labels_avery_5160_overflow",
    538         }
    539     }
    540 }
    541 
    542 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
    543 #[serde(rename_all = "snake_case")]
    544 pub enum PackDayBatchPrintStatus {
    545     #[default]
    546     Idle,
    547     Running,
    548     Succeeded,
    549     Failed,
    550 }
    551 
    552 impl PackDayBatchPrintStatus {
    553     pub const fn storage_key(self) -> &'static str {
    554         match self {
    555             Self::Idle => "idle",
    556             Self::Running => "running",
    557             Self::Succeeded => "succeeded",
    558             Self::Failed => "failed",
    559         }
    560     }
    561 }
    562 
    563 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
    564 #[serde(rename_all = "snake_case")]
    565 pub enum PackDayPrintStatus {
    566     #[default]
    567     Idle,
    568     Running,
    569     Succeeded,
    570     Failed,
    571 }
    572 
    573 impl PackDayPrintStatus {
    574     pub const fn storage_key(self) -> &'static str {
    575         match self {
    576             Self::Idle => "idle",
    577             Self::Running => "running",
    578             Self::Succeeded => "succeeded",
    579             Self::Failed => "failed",
    580         }
    581     }
    582 }
    583 
    584 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    585 #[serde(rename_all = "snake_case")]
    586 pub enum PackDayHostHandoffKind {
    587     RevealBundle,
    588     OpenPackSheet,
    589     OpenPickupRoster,
    590     OpenCustomerLabels,
    591 }
    592 
    593 impl PackDayHostHandoffKind {
    594     pub const fn all_v1() -> [Self; 4] {
    595         [
    596             Self::RevealBundle,
    597             Self::OpenPackSheet,
    598             Self::OpenPickupRoster,
    599             Self::OpenCustomerLabels,
    600         ]
    601     }
    602 
    603     pub const fn storage_key(self) -> &'static str {
    604         match self {
    605             Self::RevealBundle => "reveal_bundle",
    606             Self::OpenPackSheet => "open_pack_sheet",
    607             Self::OpenPickupRoster => "open_pickup_roster",
    608             Self::OpenCustomerLabels => "open_customer_labels",
    609         }
    610     }
    611 
    612     pub const fn artifact_kind(self) -> Option<PackDayExportArtifactKind> {
    613         match self {
    614             Self::RevealBundle => None,
    615             Self::OpenPackSheet => Some(PackDayExportArtifactKind::PackSheet),
    616             Self::OpenPickupRoster => Some(PackDayExportArtifactKind::PickupRoster),
    617             Self::OpenCustomerLabels => Some(PackDayExportArtifactKind::CustomerLabels),
    618         }
    619     }
    620 }
    621 
    622 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
    623 #[serde(rename_all = "snake_case")]
    624 pub enum PackDayExportStatus {
    625     #[default]
    626     Idle,
    627     Running,
    628     Succeeded,
    629     Failed,
    630 }
    631 
    632 impl PackDayExportStatus {
    633     pub const fn storage_key(self) -> &'static str {
    634         match self {
    635             Self::Idle => "idle",
    636             Self::Running => "running",
    637             Self::Succeeded => "succeeded",
    638             Self::Failed => "failed",
    639         }
    640     }
    641 }
    642 
    643 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
    644 #[serde(rename_all = "snake_case")]
    645 pub enum PackDayHostHandoffStatus {
    646     #[default]
    647     Idle,
    648     Running,
    649     Succeeded,
    650     Failed,
    651 }
    652 
    653 impl PackDayHostHandoffStatus {
    654     pub const fn storage_key(self) -> &'static str {
    655         match self {
    656             Self::Idle => "idle",
    657             Self::Running => "running",
    658             Self::Succeeded => "succeeded",
    659             Self::Failed => "failed",
    660         }
    661     }
    662 }
    663 
    664 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    665 #[serde(rename_all = "snake_case")]
    666 pub enum PackDayOutputOrderState {
    667     NeedsAction,
    668     Scheduled,
    669     Packed,
    670 }
    671 
    672 impl PackDayOutputOrderState {
    673     pub const fn all_v1() -> [Self; 3] {
    674         [Self::NeedsAction, Self::Scheduled, Self::Packed]
    675     }
    676 
    677     pub const fn storage_key(self) -> &'static str {
    678         match self {
    679             Self::NeedsAction => "needs_action",
    680             Self::Scheduled => "scheduled",
    681             Self::Packed => "packed",
    682         }
    683     }
    684 
    685     pub const fn from_order_status(status: OrderStatus) -> Option<Self> {
    686         match status {
    687             OrderStatus::NeedsAction => Some(Self::NeedsAction),
    688             OrderStatus::Scheduled => Some(Self::Scheduled),
    689             OrderStatus::Packed => Some(Self::Packed),
    690             OrderStatus::Completed | OrderStatus::Declined | OrderStatus::NeedsReview => None,
    691         }
    692     }
    693 }
    694 
    695 impl From<PackDayOutputOrderState> for OrderStatus {
    696     fn from(value: PackDayOutputOrderState) -> Self {
    697         match value {
    698             PackDayOutputOrderState::NeedsAction => Self::NeedsAction,
    699             PackDayOutputOrderState::Scheduled => Self::Scheduled,
    700             PackDayOutputOrderState::Packed => Self::Packed,
    701         }
    702     }
    703 }
    704 
    705 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    706 pub struct PackDayOutputQuantity {
    707     pub value: u32,
    708     pub unit_label: String,
    709 }
    710 
    711 impl PackDayOutputQuantity {
    712     pub fn new(value: u32, unit_label: impl Into<String>) -> Self {
    713         Self {
    714             value,
    715             unit_label: unit_label.into(),
    716         }
    717     }
    718 }
    719 
    720 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    721 pub struct PackDayOutputWindow {
    722     pub fulfillment_window_id: FulfillmentWindowId,
    723     pub farm_id: FarmId,
    724     pub farm_display_name: String,
    725     pub pickup_location_label: Option<String>,
    726     pub starts_at: String,
    727     pub ends_at: String,
    728 }
    729 
    730 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    731 pub struct PackDayOutputProductTotal {
    732     pub title: String,
    733     pub quantity: PackDayOutputQuantity,
    734 }
    735 
    736 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    737 pub struct PackDayOutputPackListEntry {
    738     pub order_id: OrderId,
    739     pub order_number: String,
    740     pub customer_display_name: String,
    741     pub order_state: PackDayOutputOrderState,
    742     pub title: String,
    743     pub quantity: PackDayOutputQuantity,
    744 }
    745 
    746 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    747 pub struct PackDayOutputCustomerOrder {
    748     pub order_id: OrderId,
    749     pub order_number: String,
    750     pub customer_display_name: String,
    751     pub order_state: PackDayOutputOrderState,
    752 }
    753 
    754 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    755 pub struct PackDayOutputSource {
    756     pub fulfillment_window: PackDayOutputWindow,
    757     pub totals_by_product: Vec<PackDayOutputProductTotal>,
    758     pub pack_list: Vec<PackDayOutputPackListEntry>,
    759     pub pickup_roster: Vec<PackDayOutputCustomerOrder>,
    760 }
    761 
    762 impl PackDayOutputSource {
    763     pub fn is_empty(&self) -> bool {
    764         self.totals_by_product.is_empty()
    765             && self.pack_list.is_empty()
    766             && self.pickup_roster.is_empty()
    767     }
    768 }
    769 
    770 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    771 pub struct PackDayExportArtifact {
    772     pub kind: PackDayExportArtifactKind,
    773     pub relative_path: String,
    774 }
    775 
    776 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    777 pub struct PackDayExportBundle {
    778     pub fulfillment_window_id: FulfillmentWindowId,
    779     pub export_instance_id: PackDayExportInstanceId,
    780     pub generated_at_utc: String,
    781     pub bundle_directory: String,
    782     pub artifacts: Vec<PackDayExportArtifact>,
    783 }
    784 
    785 impl PackDayExportBundle {
    786     pub fn artifact_count(&self) -> usize {
    787         self.artifacts.len()
    788     }
    789 
    790     pub fn includes_artifact(&self, kind: PackDayExportArtifactKind) -> bool {
    791         self.artifacts.iter().any(|artifact| artifact.kind == kind)
    792     }
    793 }
    794 
    795 #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
    796 #[serde(rename_all = "snake_case")]
    797 pub enum FarmOrderMethod {
    798     Pickup,
    799     Delivery,
    800     Shipping,
    801 }
    802 
    803 impl FarmOrderMethod {
    804     pub const fn storage_key(self) -> &'static str {
    805         match self {
    806             Self::Pickup => "pickup",
    807             Self::Delivery => "delivery",
    808             Self::Shipping => "shipping",
    809         }
    810     }
    811 }
    812 
    813 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    814 #[serde(rename_all = "snake_case")]
    815 pub enum ReminderSurface {
    816     Today,
    817     Orders,
    818     PackDay,
    819 }
    820 
    821 impl ReminderSurface {
    822     pub const fn storage_key(self) -> &'static str {
    823         match self {
    824             Self::Today => "today",
    825             Self::Orders => "orders",
    826             Self::PackDay => "pack_day",
    827         }
    828     }
    829 }
    830 
    831 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    832 #[serde(rename_all = "snake_case")]
    833 pub enum ReminderKind {
    834     FulfillmentWindow,
    835     OrderAction,
    836     SyncImpact,
    837 }
    838 
    839 impl ReminderKind {
    840     pub const fn storage_key(self) -> &'static str {
    841         match self {
    842             Self::FulfillmentWindow => "fulfillment_window",
    843             Self::OrderAction => "order_action",
    844             Self::SyncImpact => "sync_impact",
    845         }
    846     }
    847 }
    848 
    849 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    850 #[serde(rename_all = "snake_case")]
    851 pub enum ReminderUrgency {
    852     Upcoming,
    853     DueSoon,
    854     Overdue,
    855     Blocking,
    856 }
    857 
    858 impl ReminderUrgency {
    859     pub const fn storage_key(self) -> &'static str {
    860         match self {
    861             Self::Upcoming => "upcoming",
    862             Self::DueSoon => "due_soon",
    863             Self::Overdue => "overdue",
    864             Self::Blocking => "blocking",
    865         }
    866     }
    867 }
    868 
    869 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    870 #[serde(rename_all = "snake_case")]
    871 pub enum ReminderDeliveryState {
    872     Scheduled,
    873     Presented,
    874     Acknowledged,
    875     Resolved,
    876 }
    877 
    878 impl ReminderDeliveryState {
    879     pub const fn storage_key(self) -> &'static str {
    880         match self {
    881             Self::Scheduled => "scheduled",
    882             Self::Presented => "presented",
    883             Self::Acknowledged => "acknowledged",
    884             Self::Resolved => "resolved",
    885         }
    886     }
    887 }
    888 
    889 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
    890 #[serde(rename_all = "snake_case")]
    891 pub enum RepeatDemandEligibility {
    892     Eligible,
    893     Partial,
    894     Unavailable,
    895 }
    896 
    897 impl RepeatDemandEligibility {
    898     pub const fn storage_key(self) -> &'static str {
    899         match self {
    900             Self::Eligible => "eligible",
    901             Self::Partial => "partial",
    902             Self::Unavailable => "unavailable",
    903         }
    904     }
    905 }
    906 
    907 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    908 pub enum AppActivityKind {
    909     HomeOpened,
    910     SettingsOpened {
    911         section: SettingsSection,
    912     },
    913     SettingsSectionSelected {
    914         section: SettingsSection,
    915     },
    916     SettingsPreferenceUpdated {
    917         preference: SettingsPreference,
    918         enabled: bool,
    919     },
    920 }
    921 
    922 impl AppActivityKind {
    923     pub const fn storage_key(&self) -> &'static str {
    924         match self {
    925             Self::HomeOpened => "home_opened",
    926             Self::SettingsOpened { .. } => "settings_opened",
    927             Self::SettingsSectionSelected { .. } => "settings_section_selected",
    928             Self::SettingsPreferenceUpdated { .. } => "settings_preference_updated",
    929         }
    930     }
    931 }
    932 
    933 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    934 pub struct AppActivityEvent {
    935     pub activity_event_id: ActivityEventId,
    936     pub recorded_at: String,
    937     pub kind: AppActivityKind,
    938 }
    939 
    940 #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
    941 pub struct AppActivityContext {
    942     pub recent_events: Vec<AppActivityEvent>,
    943 }
    944 
    945 impl AppActivityContext {
    946     pub fn from_recent_events(recent_events: Vec<AppActivityEvent>) -> Self {
    947         Self { recent_events }
    948     }
    949 }