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