app

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

commit c6a3ce4974104d42eb8f3e8d11391f19017c36ff
parent a1a994c8a21ebbe5fac5575e5cdedb8520f5bac3
Author: triesap <tyson@radroots.org>
Date:   Wed, 22 Apr 2026 21:47:55 +0000

app: add pack day print failure contracts

- add a typed avery 5160 overflow failure enum and pack day print state payload
- expose localized overflow failure copy in shared i18n keys and english messages
- map customer-label overflow failures to a dedicated desktop status title and source guard
- extend shared model, state, i18n, and window tests for the hardening contract

Diffstat:
Mcrates/launchers/desktop/src/source_guards.rs | 1+
Mcrates/launchers/desktop/src/window.rs | 60++++++++++++++++++++++++++++++++++++++++++------------------
Mcrates/shared/i18n/src/keys.rs | 1+
Mcrates/shared/i18n/src/lib.rs | 4++++
Mcrates/shared/models/src/lib.rs | 46+++++++++++++++++++++++++++++++++-------------
Mcrates/shared/state/src/lib.rs | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mi18n/locales/en/messages.json | 1+
7 files changed, 143 insertions(+), 42 deletions(-)

diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs @@ -486,6 +486,7 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::PackDayExportFolderLabel", "AppTextKey::PackDayExportFilesLabel", "AppTextKey::PackDayExportErrorLabel", + "AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle", "AppTextKey::PackDayHostHandoffRevealAction", "AppTextKey::PackDayHostHandoffRevealActionRunning", "AppTextKey::PackDayHostHandoffOpenPackSheetAction", diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -21,8 +21,8 @@ use radroots_app_models::{ OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction, OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, PackDayExportBundle, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, - PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, PackDayRosterRow, - PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord, + PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, + PackDayRosterRow, PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord, ProductAttentionState, ProductEditorDraft, ProductId, ProductListRow, ProductPricePresentation, ProductPublishBlocker, ProductStatus, ProductsFilter, ProductsListRow, ProductsSort, RecoveryKind, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, @@ -10419,58 +10419,67 @@ fn pack_day_print_status_presentation( ) -> Option<PackDayPrintStatusPresentation> { let print = &runtime.pack_day_projection.print; let kind = print.request.as_ref()?.kind; + let failure = print.failure; - let status = match (print.status, kind) { - (PackDayPrintStatus::Idle, _) => return None, - (PackDayPrintStatus::Running, PackDayPrintKind::PrintPackSheet) => { + let status = match (print.status, kind, failure) { + (PackDayPrintStatus::Idle, _, _) => return None, + (PackDayPrintStatus::Running, PackDayPrintKind::PrintPackSheet, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.foundation.text.accent, title_key: AppTextKey::PackDayPrintPackSheetQueuedTitle, } } - (PackDayPrintStatus::Running, PackDayPrintKind::PrintPickupRoster) => { + (PackDayPrintStatus::Running, PackDayPrintKind::PrintPickupRoster, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.foundation.text.accent, title_key: AppTextKey::PackDayPrintPickupRosterQueuedTitle, } } - (PackDayPrintStatus::Running, PackDayPrintKind::PrintCustomerLabels) => { + (PackDayPrintStatus::Running, PackDayPrintKind::PrintCustomerLabels, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.foundation.text.accent, title_key: AppTextKey::PackDayPrintCustomerLabelsQueuedTitle, } } - (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintPackSheet) => { + (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintPackSheet, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.components.app_status_indicator.online, title_key: AppTextKey::PackDayPrintPackSheetSubmittedTitle, } } - (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintPickupRoster) => { + (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintPickupRoster, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.components.app_status_indicator.online, title_key: AppTextKey::PackDayPrintPickupRosterSubmittedTitle, } } - (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintCustomerLabels) => { + (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintCustomerLabels, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.components.app_status_indicator.online, title_key: AppTextKey::PackDayPrintCustomerLabelsSubmittedTitle, } } - (PackDayPrintStatus::Failed, PackDayPrintKind::PrintPackSheet) => { + (PackDayPrintStatus::Failed, PackDayPrintKind::PrintPackSheet, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.components.app_status_indicator.attention, title_key: AppTextKey::PackDayPrintPackSheetFailedTitle, } } - (PackDayPrintStatus::Failed, PackDayPrintKind::PrintPickupRoster) => { + (PackDayPrintStatus::Failed, PackDayPrintKind::PrintPickupRoster, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.components.app_status_indicator.attention, title_key: AppTextKey::PackDayPrintPickupRosterFailedTitle, } } - (PackDayPrintStatus::Failed, PackDayPrintKind::PrintCustomerLabels) => { + ( + PackDayPrintStatus::Failed, + PackDayPrintKind::PrintCustomerLabels, + Some(PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow), + ) => PackDayPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle, + }, + (PackDayPrintStatus::Failed, PackDayPrintKind::PrintCustomerLabels, _) => { PackDayPrintStatusPresentation { indicator_color: APP_UI_THEME.components.app_status_indicator.attention, title_key: AppTextKey::PackDayPrintCustomerLabelsFailedTitle, @@ -12619,11 +12628,12 @@ mod tests { FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersListRow, PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, - PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPrintKind, PackDayPrintStatus, - PackDayProductTotalRow, PackDayProjection, PersonalSection, ReminderDeadlineProjection, - ReminderDeliveryState, ReminderId, ReminderKind, ReminderSurface, ReminderUrgency, - RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection, - TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPrintFailureKind, + PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, + PersonalSection, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, + ReminderKind, ReminderSurface, ReminderUrgency, RepeatDemandEligibility, + RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, TodaySetupTask, + TodaySetupTaskKind, }; use radroots_app_remote_signer::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession, @@ -13691,6 +13701,20 @@ mod tests { title_key: AppTextKey::PackDayPrintCustomerLabelsFailedTitle, }) ); + + let overflow_request = + PackDayPrintRequest::for_bundle(PackDayPrintKind::PrintCustomerLabels, &bundle); + runtime.pack_day_projection.print = PackDayPrintProjection::failed_with_kind( + overflow_request, + PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow, + ); + assert_eq!( + pack_day_print_status_presentation(&runtime), + Some(PackDayPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle, + }) + ); } #[test] diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -258,6 +258,7 @@ define_app_text_keys! { PackDayPrintCustomerLabelsQueuedTitle => "pack_day.print.customer_labels.queued.title", PackDayPrintCustomerLabelsSubmittedTitle => "pack_day.print.customer_labels.submitted.title", PackDayPrintCustomerLabelsFailedTitle => "pack_day.print.customer_labels.failed.title", + PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle => "pack_day.print.customer_labels.avery_5160_overflow.failed.title", PackDayHostHandoffRevealAction => "pack_day.host_handoff.reveal.action", PackDayHostHandoffRevealActionRunning => "pack_day.host_handoff.reveal.action.running", PackDayHostHandoffOpenPackSheetAction => "pack_day.host_handoff.open_pack_sheet.action", diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs @@ -544,6 +544,10 @@ mod tests { "Couldn't print customer labels" ); assert_eq!( + app_text(AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle), + "Customer labels do not fit Avery 5160" + ); + assert_eq!( app_text(AppTextKey::PackDayHostHandoffRevealAction), "Show in Finder" ); diff --git a/crates/shared/models/src/lib.rs b/crates/shared/models/src/lib.rs @@ -1669,6 +1669,20 @@ impl PackDayPrintLabelStock { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PackDayPrintFailureKind { + CustomerLabelsAvery5160Overflow, +} + +impl PackDayPrintFailureKind { + pub const fn storage_key(self) -> &'static str { + match self { + Self::CustomerLabelsAvery5160Overflow => "customer_labels_avery_5160_overflow", + } + } +} + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PackDayPrintStatus { @@ -2474,19 +2488,21 @@ mod tests { OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction, OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListProjection, OrdersListRow, OrdersListSummary, OrdersScreenQueryState, PackDayExportArtifact, PackDayExportArtifactKind, - PackDayExportBundle, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, - PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry, - PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow, - 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, + 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, @@ -3158,6 +3174,10 @@ mod tests { PackDayPrintLabelStock::Avery5160Letter30Up.storage_key(), "avery_5160_letter_30_up" ); + assert_eq!( + PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow.storage_key(), + "customer_labels_avery_5160_overflow" + ); assert_eq!(PackDayPrintStatus::default(), PackDayPrintStatus::Idle); assert_eq!(PackDayPrintStatus::Running.storage_key(), "running"); assert_eq!(PackDayPrintStatus::Succeeded.storage_key(), "succeeded"); diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs @@ -16,9 +16,9 @@ use radroots_app_models::{ LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayExportArtifactKind, PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, - PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, PackDayProjection, - PackDayScreenQueryState, PersonalEntryProjection, ProductEditorDraft, ProductId, - ProductPublishBlocker, ProductsFilter, ProductsListProjection, ProductsSort, + PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, + PackDayProjection, PackDayScreenQueryState, PersonalEntryProjection, ProductEditorDraft, + ProductId, ProductPublishBlocker, ProductsFilter, ProductsListProjection, ProductsSort, RecoveryQueueProjection, ReminderFeedProjection, ReminderLogProjection, SelectedSurfaceProjection, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, @@ -437,6 +437,7 @@ impl PackDayPrintRequest { pub struct PackDayPrintProjection { pub status: PackDayPrintStatus, pub request: Option<PackDayPrintRequest>, + pub failure: Option<PackDayPrintFailureKind>, } impl PackDayPrintProjection { @@ -444,6 +445,7 @@ impl PackDayPrintProjection { Self { status: PackDayPrintStatus::Running, request: Some(request), + failure: None, } } @@ -451,13 +453,29 @@ impl PackDayPrintProjection { Self { status: PackDayPrintStatus::Succeeded, request: Some(request), + failure: None, } } pub fn failed(request: PackDayPrintRequest) -> Self { + Self::failed_with_failure(request, None) + } + + pub fn failed_with_kind( + request: PackDayPrintRequest, + failure: PackDayPrintFailureKind, + ) -> Self { + Self::failed_with_failure(request, Some(failure)) + } + + fn failed_with_failure( + request: PackDayPrintRequest, + failure: Option<PackDayPrintFailureKind>, + ) -> Self { Self { status: PackDayPrintStatus::Failed, request: Some(request), + failure, } } } @@ -966,6 +984,10 @@ pub enum AppStateCommand { BeginPackDayPrint(PackDayPrintRequest), SucceedPackDayPrint(PackDayPrintRequest), FailPackDayPrint(PackDayPrintRequest), + FailPackDayPrintWithKind { + request: PackDayPrintRequest, + failure: PackDayPrintFailureKind, + }, ResetPackDayPrint, BeginPackDayHostHandoff(PackDayHostHandoffRequest), SucceedPackDayHostHandoff(PackDayHostHandoffRequest), @@ -1130,6 +1152,13 @@ impl AppStateCommand { Self::FailPackDayPrint(request) } + pub fn fail_pack_day_print_with_kind( + request: PackDayPrintRequest, + failure: PackDayPrintFailureKind, + ) -> Self { + Self::FailPackDayPrintWithKind { request, failure } + } + pub const fn reset_pack_day_print() -> Self { Self::ResetPackDayPrint } @@ -1680,6 +1709,11 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap .pack_day .replace_print(PackDayPrintProjection::failed(request)); } + AppStateCommand::FailPackDayPrintWithKind { request, failure } => { + projection + .pack_day + .replace_print(PackDayPrintProjection::failed_with_kind(request, failure)); + } AppStateCommand::ResetPackDayPrint => { projection .pack_day @@ -2052,13 +2086,13 @@ mod tests { OrdersListSummary, OrdersScreenQueryState, PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, - PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, PackDayProductTotalRow, - PackDayProjection, PackDayRosterRow, PackDayScreenQueryState, PersonalEntryState, - PersonalSection, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductsFilter, - ProductsListProjection, ProductsSort, ReminderDeliveryState, ReminderFeedProjection, - ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, SelectedAccountProjection, - SelectedSurfaceProjection, SettingsSection, ShellSection, TodayAgendaProjection, - TodaySetupTask, TodaySetupTaskKind, + PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, + PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PackDayScreenQueryState, + PersonalEntryState, PersonalSection, ProductEditorDraft, ProductId, ProductPublishBlocker, + ProductsFilter, ProductsListProjection, ProductsSort, ReminderDeliveryState, + ReminderFeedProjection, ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, + SelectedAccountProjection, SelectedSurfaceProjection, SettingsSection, ShellSection, + TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, }; use radroots_app_sync::{ AppSyncProjection, AppSyncRunStatus, SyncCheckpointState, SyncCheckpointStatus, @@ -2507,6 +2541,7 @@ mod tests { PackDayPrintProjection { status: PackDayPrintStatus::Idle, request: None, + failure: None, } ); assert_eq!( @@ -2616,7 +2651,22 @@ mod tests { ); assert_eq!( store.pack_day_projection().print, - PackDayPrintProjection::failed(request) + PackDayPrintProjection::failed(request.clone()) + ); + + assert_eq!( + store.apply(AppStateCommand::fail_pack_day_print_with_kind( + request.clone(), + PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow, + )), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().print, + PackDayPrintProjection::failed_with_kind( + request, + PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow, + ) ); assert_eq!( diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -238,6 +238,7 @@ "pack_day.print.customer_labels.queued.title": "Queueing customer labels", "pack_day.print.customer_labels.submitted.title": "Sent customer labels to the printer", "pack_day.print.customer_labels.failed.title": "Couldn't print customer labels", + "pack_day.print.customer_labels.avery_5160_overflow.failed.title": "Customer labels do not fit Avery 5160", "pack_day.host_handoff.reveal.action": "Show in Finder", "pack_day.host_handoff.reveal.action.running": "Showing in Finder...", "pack_day.host_handoff.open_pack_sheet.action": "Open pack sheet",