app

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

commit d0a72f6cb9a90bf0b1abe7ce2e2314a4d4aef2fa
parent 17edd1eca917b1d3338bd1ded78e8506f3ca89e2
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Apr 2026 17:33:40 +0000

app: add pack day batch print contracts

- add explicit batch print artifact, status, and failure contracts
- carry restart-ephemeral batch print state through Pack day projection commands
- add localized batch action and outcome copy
- align typed i18n ids with the generated messages namespace

Diffstat:
Mcrates/shared/i18n/src/keys.rs | 11++++++++++-
Mcrates/shared/i18n/src/lib.rs | 33+++++++++++++++++++++++++++++++++
Mcrates/shared/models/src/lib.rs | 123++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/shared/state/src/lib.rs | 357+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mi18n/locales/en/messages.json | 9+++++++++
5 files changed, 525 insertions(+), 8 deletions(-)

diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -12,7 +12,7 @@ macro_rules! define_app_text_keys { pub const fn id(self) -> &'static str { match self { - $(Self::$variant => $id,)+ + $(Self::$variant => concat!("messages.", $id),)+ } } } @@ -259,6 +259,15 @@ define_app_text_keys! { 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", + PackDayBatchPrintAction => "pack_day.batch_print.action", + PackDayBatchPrintActionRunning => "pack_day.batch_print.action.running", + PackDayBatchPrintQueuedTitle => "pack_day.batch_print.queued.title", + PackDayBatchPrintSucceededTitle => "pack_day.batch_print.succeeded.title", + PackDayBatchPrintFailedTitle => "pack_day.batch_print.failed.title", + PackDayBatchPrintFailedPreflightTitle => "pack_day.batch_print.failed.preflight.title", + PackDayBatchPrintFailedQueueLaunchTitle => "pack_day.batch_print.failed.queue_launch.title", + PackDayBatchPrintFailedQueueExitTitle => "pack_day.batch_print.failed.queue_exit.title", + PackDayBatchPrintCustomerLabelsAvery5160OverflowFailedTitle => "pack_day.batch_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 @@ -547,6 +547,39 @@ mod tests { app_text(AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle), "Customer labels do not fit Avery 5160" ); + assert_eq!(app_text(AppTextKey::PackDayBatchPrintAction), "Print all"); + assert_eq!( + app_text(AppTextKey::PackDayBatchPrintActionRunning), + "Printing all..." + ); + assert_eq!( + app_text(AppTextKey::PackDayBatchPrintQueuedTitle), + "Queueing pack day print run" + ); + assert_eq!( + app_text(AppTextKey::PackDayBatchPrintSucceededTitle), + "Sent all pack day files to the printer" + ); + assert_eq!( + app_text(AppTextKey::PackDayBatchPrintFailedTitle), + "Couldn't print all pack day files" + ); + assert_eq!( + app_text(AppTextKey::PackDayBatchPrintFailedPreflightTitle), + "Pack day files are not ready to print" + ); + assert_eq!( + app_text(AppTextKey::PackDayBatchPrintFailedQueueLaunchTitle), + "Couldn't start the print queue" + ); + assert_eq!( + app_text(AppTextKey::PackDayBatchPrintFailedQueueExitTitle), + "Print queue stopped before the run finished" + ); + assert_eq!( + app_text(AppTextKey::PackDayBatchPrintCustomerLabelsAvery5160OverflowFailedTitle), + "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 @@ -1683,6 +1683,72 @@ impl PackDayPrintFailureKind { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct PackDayBatchPrintArtifact { + pub print_kind: PackDayPrintKind, + pub artifact_kind: PackDayExportArtifactKind, + pub label_stock: Option<PackDayPrintLabelStock>, +} + +impl PackDayBatchPrintArtifact { + pub const fn all_v1() -> [Self; 3] { + [ + Self::from_print_kind(PackDayPrintKind::PrintPackSheet), + Self::from_print_kind(PackDayPrintKind::PrintPickupRoster), + Self::from_print_kind(PackDayPrintKind::PrintCustomerLabels), + ] + } + + pub const fn from_print_kind(print_kind: PackDayPrintKind) -> Self { + Self { + print_kind, + artifact_kind: print_kind.artifact_kind(), + label_stock: print_kind.label_stock(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PackDayBatchPrintFailureKind { + Preflight, + QueueLaunch, + QueueExit, + CustomerLabelsAvery5160Overflow, +} + +impl PackDayBatchPrintFailureKind { + pub const fn storage_key(self) -> &'static str { + match self { + Self::Preflight => "preflight", + Self::QueueLaunch => "queue_launch", + Self::QueueExit => "queue_exit", + Self::CustomerLabelsAvery5160Overflow => "customer_labels_avery_5160_overflow", + } + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PackDayBatchPrintStatus { + #[default] + Idle, + Running, + Succeeded, + Failed, +} + +impl PackDayBatchPrintStatus { + pub const fn storage_key(self) -> &'static str { + match self { + Self::Idle => "idle", + Self::Running => "running", + Self::Succeeded => "succeeded", + Self::Failed => "failed", + } + } +} + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PackDayPrintStatus { @@ -2487,7 +2553,8 @@ mod tests { LoggedOutStartupPhase, LoggedOutStartupProjection, OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction, OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListProjection, OrdersListRow, OrdersListSummary, - OrdersScreenQueryState, PackDayExportArtifact, PackDayExportArtifactKind, + OrdersScreenQueryState, PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, + PackDayBatchPrintStatus, PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry, PackDayOutputProductTotal, PackDayOutputQuantity, @@ -3178,6 +3245,60 @@ mod tests { PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow.storage_key(), "customer_labels_avery_5160_overflow" ); + assert_eq!( + PackDayBatchPrintArtifact::all_v1(), + [ + PackDayBatchPrintArtifact { + print_kind: PackDayPrintKind::PrintPackSheet, + artifact_kind: PackDayExportArtifactKind::PackSheet, + label_stock: None, + }, + PackDayBatchPrintArtifact { + print_kind: PackDayPrintKind::PrintPickupRoster, + artifact_kind: PackDayExportArtifactKind::PickupRoster, + label_stock: None, + }, + PackDayBatchPrintArtifact { + print_kind: PackDayPrintKind::PrintCustomerLabels, + artifact_kind: PackDayExportArtifactKind::CustomerLabels, + label_stock: Some(PackDayPrintLabelStock::Avery5160Letter30Up), + }, + ] + ); + assert_eq!( + PackDayBatchPrintArtifact::from_print_kind(PackDayPrintKind::PrintCustomerLabels), + PackDayBatchPrintArtifact { + print_kind: PackDayPrintKind::PrintCustomerLabels, + artifact_kind: PackDayExportArtifactKind::CustomerLabels, + label_stock: Some(PackDayPrintLabelStock::Avery5160Letter30Up), + } + ); + assert_eq!( + PackDayBatchPrintFailureKind::Preflight.storage_key(), + "preflight" + ); + assert_eq!( + PackDayBatchPrintFailureKind::QueueLaunch.storage_key(), + "queue_launch" + ); + assert_eq!( + PackDayBatchPrintFailureKind::QueueExit.storage_key(), + "queue_exit" + ); + assert_eq!( + PackDayBatchPrintFailureKind::CustomerLabelsAvery5160Overflow.storage_key(), + "customer_labels_avery_5160_overflow" + ); + assert_eq!( + PackDayBatchPrintStatus::default(), + PackDayBatchPrintStatus::Idle + ); + assert_eq!(PackDayBatchPrintStatus::Running.storage_key(), "running"); + assert_eq!( + PackDayBatchPrintStatus::Succeeded.storage_key(), + "succeeded" + ); + assert_eq!(PackDayBatchPrintStatus::Failed.storage_key(), "failed"); 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 @@ -14,7 +14,8 @@ use radroots_app_models::{ FarmReadinessBlocker, FarmRulesProjection, FarmSetupBlocker, FarmSetupProjection, FarmSetupReadiness, FarmTimingConflict, FulfillmentWindowId, LoggedOutStartupPhase, LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrdersFilter, OrdersListProjection, - OrdersScreenQueryState, PackDayExportArtifactKind, PackDayExportBundle, + OrdersScreenQueryState, PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, + PackDayBatchPrintStatus, PackDayExportArtifactKind, PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, PackDayProjection, PackDayScreenQueryState, PersonalEntryProjection, ProductEditorDraft, @@ -312,6 +313,7 @@ pub struct PackDayScreenProjection { pub projection: PackDayProjection, pub export: PackDayExportProjection, pub print: PackDayPrintProjection, + pub batch_print: PackDayBatchPrintProjection, pub host_handoff: PackDayHostHandoffProjection, } @@ -320,6 +322,7 @@ impl PackDayScreenProjection { if self.query.fulfillment_window_id != fulfillment_window_id { self.export = PackDayExportProjection::default(); self.print = PackDayPrintProjection::default(); + self.batch_print = PackDayBatchPrintProjection::default(); self.host_handoff = PackDayHostHandoffProjection::default(); } self.query.fulfillment_window_id = fulfillment_window_id; @@ -339,6 +342,7 @@ impl PackDayScreenProjection { if previous_window_id != next_window_id { self.export = PackDayExportProjection::default(); self.print = PackDayPrintProjection::default(); + self.batch_print = PackDayBatchPrintProjection::default(); self.host_handoff = PackDayHostHandoffProjection::default(); } @@ -348,6 +352,7 @@ impl PackDayScreenProjection { fn replace_export(&mut self, export: PackDayExportProjection) { if self.export != export { self.print = PackDayPrintProjection::default(); + self.batch_print = PackDayBatchPrintProjection::default(); self.host_handoff = PackDayHostHandoffProjection::default(); } self.export = export; @@ -357,6 +362,10 @@ impl PackDayScreenProjection { self.print = print; } + fn replace_batch_print(&mut self, batch_print: PackDayBatchPrintProjection) { + self.batch_print = batch_print; + } + fn replace_host_handoff(&mut self, host_handoff: PackDayHostHandoffProjection) { self.host_handoff = host_handoff; } @@ -481,6 +490,64 @@ impl PackDayPrintProjection { } #[derive(Clone, Debug, Eq, PartialEq)] +pub struct PackDayBatchPrintRequest { + pub fulfillment_window_id: FulfillmentWindowId, + pub export_instance_id: PackDayExportInstanceId, + pub artifacts: Vec<PackDayBatchPrintArtifact>, +} + +impl PackDayBatchPrintRequest { + pub fn for_bundle(bundle: &PackDayExportBundle) -> Self { + Self { + fulfillment_window_id: bundle.fulfillment_window_id, + export_instance_id: bundle.export_instance_id, + artifacts: Vec::from(PackDayBatchPrintArtifact::all_v1()), + } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PackDayBatchPrintProjection { + pub status: PackDayBatchPrintStatus, + pub request: Option<PackDayBatchPrintRequest>, + pub failed_artifact: Option<PackDayBatchPrintArtifact>, + pub failure: Option<PackDayBatchPrintFailureKind>, +} + +impl PackDayBatchPrintProjection { + pub fn running(request: PackDayBatchPrintRequest) -> Self { + Self { + status: PackDayBatchPrintStatus::Running, + request: Some(request), + failed_artifact: None, + failure: None, + } + } + + pub fn succeeded(request: PackDayBatchPrintRequest) -> Self { + Self { + status: PackDayBatchPrintStatus::Succeeded, + request: Some(request), + failed_artifact: None, + failure: None, + } + } + + pub fn failed( + request: PackDayBatchPrintRequest, + failed_artifact: Option<PackDayBatchPrintArtifact>, + failure: PackDayBatchPrintFailureKind, + ) -> Self { + Self { + status: PackDayBatchPrintStatus::Failed, + request: Some(request), + failed_artifact, + failure: Some(failure), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] pub struct PackDayHostHandoffRequest { pub fulfillment_window_id: FulfillmentWindowId, pub kind: PackDayHostHandoffKind, @@ -989,6 +1056,14 @@ pub enum AppStateCommand { failure: PackDayPrintFailureKind, }, ResetPackDayPrint, + BeginPackDayBatchPrint(PackDayBatchPrintRequest), + SucceedPackDayBatchPrint(PackDayBatchPrintRequest), + FailPackDayBatchPrint { + request: PackDayBatchPrintRequest, + failed_artifact: Option<PackDayBatchPrintArtifact>, + failure: PackDayBatchPrintFailureKind, + }, + ResetPackDayBatchPrint, BeginPackDayHostHandoff(PackDayHostHandoffRequest), SucceedPackDayHostHandoff(PackDayHostHandoffRequest), FailPackDayHostHandoff { @@ -1163,6 +1238,30 @@ impl AppStateCommand { Self::ResetPackDayPrint } + pub fn begin_pack_day_batch_print(request: PackDayBatchPrintRequest) -> Self { + Self::BeginPackDayBatchPrint(request) + } + + pub fn succeed_pack_day_batch_print(request: PackDayBatchPrintRequest) -> Self { + Self::SucceedPackDayBatchPrint(request) + } + + pub fn fail_pack_day_batch_print( + request: PackDayBatchPrintRequest, + failed_artifact: Option<PackDayBatchPrintArtifact>, + failure: PackDayBatchPrintFailureKind, + ) -> Self { + Self::FailPackDayBatchPrint { + request, + failed_artifact, + failure, + } + } + + pub const fn reset_pack_day_batch_print() -> Self { + Self::ResetPackDayBatchPrint + } + pub fn begin_pack_day_host_handoff(request: PackDayHostHandoffRequest) -> Self { Self::BeginPackDayHostHandoff(request) } @@ -1719,6 +1818,34 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap .pack_day .replace_print(PackDayPrintProjection::default()); } + AppStateCommand::BeginPackDayBatchPrint(request) => { + projection + .pack_day + .replace_batch_print(PackDayBatchPrintProjection::running(request)); + } + AppStateCommand::SucceedPackDayBatchPrint(request) => { + projection + .pack_day + .replace_batch_print(PackDayBatchPrintProjection::succeeded(request)); + } + AppStateCommand::FailPackDayBatchPrint { + request, + failed_artifact, + failure, + } => { + projection + .pack_day + .replace_batch_print(PackDayBatchPrintProjection::failed( + request, + failed_artifact, + failure, + )); + } + AppStateCommand::ResetPackDayBatchPrint => { + projection + .pack_day + .replace_batch_print(PackDayBatchPrintProjection::default()); + } AppStateCommand::BeginPackDayHostHandoff(request) => { projection .pack_day @@ -2071,10 +2198,11 @@ mod tests { use super::{ AppProjection, AppShellProjection, AppStateCommand, AppStateRepository, AppStateRepositoryError, AppStateStore, AppStateStoreError, FarmSetupFlowStage, HomeRoute, - InMemoryAppStateRepository, OrdersScreenProjection, PackDayExportProjection, - PackDayExportRequest, PackDayHostHandoffProjection, PackDayHostHandoffRequest, - PackDayPrintProjection, PackDayPrintRequest, PackDayScreenProjection, PersistedAppState, - ProductEditorState, ProductsScreenProjection, ProductsScreenQueryState, SettingsPreference, + InMemoryAppStateRepository, OrdersScreenProjection, PackDayBatchPrintProjection, + PackDayBatchPrintRequest, PackDayExportProjection, PackDayExportRequest, + PackDayHostHandoffProjection, PackDayHostHandoffRequest, PackDayPrintProjection, + PackDayPrintRequest, PackDayScreenProjection, PersistedAppState, ProductEditorState, + ProductsScreenProjection, ProductsScreenQueryState, SettingsPreference, derive_sync_projection, derive_sync_run_status, }; use radroots_app_models::{ @@ -2083,7 +2211,8 @@ mod tests { FarmerActivationProjection, FarmerSection, FulfillmentWindowId, LoggedOutStartupPhase, LoggedOutStartupProjection, OrderDetailItemRow, OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersListProjection, OrdersListRow, - OrdersListSummary, OrdersScreenQueryState, PackDayExportArtifact, + OrdersListSummary, OrdersScreenQueryState, PackDayBatchPrintArtifact, + PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, @@ -2153,6 +2282,13 @@ mod tests { PackDayPrintRequest::for_bundle(kind, &bundle) } + fn sample_pack_day_batch_print_request( + fulfillment_window_id: FulfillmentWindowId, + ) -> PackDayBatchPrintRequest { + let bundle = sample_pack_day_export_bundle(fulfillment_window_id); + PackDayBatchPrintRequest::for_bundle(&bundle) + } + fn sample_pack_day_export_bundle( fulfillment_window_id: FulfillmentWindowId, ) -> PackDayExportBundle { @@ -2545,6 +2681,15 @@ mod tests { } ); assert_eq!( + PackDayScreenProjection::default().batch_print, + PackDayBatchPrintProjection { + status: PackDayBatchPrintStatus::Idle, + request: None, + failed_artifact: None, + failure: None, + } + ); + assert_eq!( PackDayScreenProjection::default().host_handoff, PackDayHostHandoffProjection { status: PackDayHostHandoffStatus::Idle, @@ -2680,6 +2825,95 @@ mod tests { } #[test] + fn pack_day_batch_print_state_is_restart_ephemeral_and_skips_persistence() { + let mut store = + AppStateStore::load(FailingRepository).expect("failing repository should still load"); + let fulfillment_window_id = FulfillmentWindowId::new(); + let request = sample_pack_day_batch_print_request(fulfillment_window_id); + + assert_eq!( + request.artifacts, + Vec::from(PackDayBatchPrintArtifact::all_v1()) + ); + assert_eq!( + request + .artifacts + .last() + .expect("v1 batch should include customer labels") + .label_stock, + Some(PackDayPrintLabelStock::Avery5160Letter30Up) + ); + assert_eq!( + store.apply(AppStateCommand::begin_pack_day_batch_print(request.clone(),)), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().batch_print, + PackDayBatchPrintProjection::running(request.clone()) + ); + assert_eq!( + store.persisted_state().seller.pack_day_query, + PackDayScreenQueryState::default() + ); + + assert_eq!( + store.apply(AppStateCommand::succeed_pack_day_batch_print( + request.clone(), + )), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().batch_print, + PackDayBatchPrintProjection::succeeded(request.clone()) + ); + + let failed_artifact = + PackDayBatchPrintArtifact::from_print_kind(PackDayPrintKind::PrintCustomerLabels); + assert_eq!( + store.apply(AppStateCommand::fail_pack_day_batch_print( + request.clone(), + Some(failed_artifact), + PackDayBatchPrintFailureKind::CustomerLabelsAvery5160Overflow, + )), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().batch_print, + PackDayBatchPrintProjection::failed( + request.clone(), + Some(failed_artifact), + PackDayBatchPrintFailureKind::CustomerLabelsAvery5160Overflow, + ) + ); + + assert_eq!( + store.apply(AppStateCommand::fail_pack_day_batch_print( + request.clone(), + None, + PackDayBatchPrintFailureKind::Preflight, + )), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().batch_print, + PackDayBatchPrintProjection::failed( + request, + None, + PackDayBatchPrintFailureKind::Preflight, + ) + ); + + assert_eq!( + store.apply(AppStateCommand::reset_pack_day_batch_print()), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().batch_print, + PackDayBatchPrintProjection::default() + ); + } + + #[test] fn pack_day_host_handoff_state_is_restart_ephemeral_and_skips_persistence() { let mut store = AppStateStore::load(FailingRepository).expect("failing repository should still load"); @@ -2847,6 +3081,41 @@ mod tests { } #[test] + fn changing_pack_day_window_clears_stale_batch_print_state() { + let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) + .expect("in-memory repository should load"); + let fulfillment_window_id = FulfillmentWindowId::new(); + let next_window_id = FulfillmentWindowId::new(); + let request = sample_pack_day_batch_print_request(fulfillment_window_id); + + assert_eq!( + store.apply(AppStateCommand::begin_pack_day_batch_print(request)), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().batch_print.status, + PackDayBatchPrintStatus::Running + ); + + assert_eq!( + store.apply(AppStateCommand::set_pack_day_fulfillment_window(Some( + next_window_id, + ))), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().query, + PackDayScreenQueryState { + fulfillment_window_id: Some(next_window_id), + } + ); + assert_eq!( + store.pack_day_projection().batch_print, + PackDayBatchPrintProjection::default() + ); + } + + #[test] fn changing_pack_day_export_state_clears_stale_host_handoff_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); @@ -2927,6 +3196,42 @@ mod tests { } #[test] + fn changing_pack_day_export_state_clears_stale_batch_print_state() { + let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) + .expect("in-memory repository should load"); + let fulfillment_window_id = FulfillmentWindowId::new(); + let export_request = sample_pack_day_export_request(fulfillment_window_id); + let batch_request = sample_pack_day_batch_print_request(fulfillment_window_id); + + assert_eq!( + store.apply(AppStateCommand::begin_pack_day_export( + export_request.clone(), + )), + Ok(true) + ); + assert_eq!( + store.apply(AppStateCommand::begin_pack_day_batch_print(batch_request)), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().batch_print.status, + PackDayBatchPrintStatus::Running + ); + + assert_eq!( + store.apply(AppStateCommand::succeed_pack_day_export( + export_request, + sample_pack_day_export_bundle(fulfillment_window_id), + )), + Ok(true) + ); + assert_eq!( + store.pack_day_projection().batch_print, + PackDayBatchPrintProjection::default() + ); + } + + #[test] fn replacing_pack_day_projection_with_new_window_clears_stale_host_handoff_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); @@ -3011,6 +3316,46 @@ mod tests { } #[test] + fn replacing_pack_day_projection_with_new_window_clears_stale_batch_print_state() { + let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) + .expect("in-memory repository should load"); + let farm_id = FarmId::new(); + let current_window_id = FulfillmentWindowId::new(); + let next_window_id = FulfillmentWindowId::new(); + let request = sample_pack_day_batch_print_request(current_window_id); + + assert_eq!( + store.apply(AppStateCommand::begin_pack_day_batch_print(request)), + Ok(true) + ); + + let next_pack_day = PackDayProjection { + fulfillment_window: Some(radroots_app_models::FulfillmentWindowSummary { + fulfillment_window_id: next_window_id, + farm_id, + starts_at: "2026-04-25T16:00:00Z".to_owned(), + ends_at: "2026-04-25T19:00:00Z".to_owned(), + }), + totals_by_product: Vec::new(), + pack_list: Vec::new(), + pickup_roster: Vec::new(), + reminders: ReminderFeedProjection::default(), + }; + + assert_eq!( + store.apply(AppStateCommand::replace_pack_day_projection( + next_pack_day.clone(), + )), + Ok(true) + ); + assert_eq!(store.pack_day_projection().projection, next_pack_day); + assert_eq!( + store.pack_day_projection().batch_print, + PackDayBatchPrintProjection::default() + ); + } + + #[test] fn startup_identity_choice_flow_is_explicit_and_in_memory_only() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -239,6 +239,15 @@ "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.batch_print.action": "Print all", + "pack_day.batch_print.action.running": "Printing all...", + "pack_day.batch_print.queued.title": "Queueing pack day print run", + "pack_day.batch_print.succeeded.title": "Sent all pack day files to the printer", + "pack_day.batch_print.failed.title": "Couldn't print all pack day files", + "pack_day.batch_print.failed.preflight.title": "Pack day files are not ready to print", + "pack_day.batch_print.failed.queue_launch.title": "Couldn't start the print queue", + "pack_day.batch_print.failed.queue_exit.title": "Print queue stopped before the run finished", + "pack_day.batch_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",