commit cd0a000a1ed470cc28fd22689a170ffebbc63d47
parent 2a3c0de2c0d1ae222d5e5457bef89b660145637b
Author: triesap <tyson@radroots.org>
Date: Wed, 22 Apr 2026 20:22:02 +0000
pack_day: add print request contracts
- add export_instance_id to pack day export bundles and freeze v1 print kind plus stock enums
- add restart-ephemeral pack day print state that resets when export truth changes
- add english queued submitted failed and unavailable print contract strings
- prove the contract with pack day model state and UI-facing tests
Diffstat:
8 files changed, 509 insertions(+), 20 deletions(-)
diff --git a/crates/launchers/desktop/src/pack_day_host_handoff.rs b/crates/launchers/desktop/src/pack_day_host_handoff.rs
@@ -347,6 +347,7 @@ mod tests {
fn sample_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle {
PackDayExportBundle {
fulfillment_window_id: radroots_app_models::FulfillmentWindowId::new(),
+ export_instance_id: radroots_app_models::PackDayExportInstanceId::new(),
generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
bundle_directory: bundle_directory.to_string_lossy().into_owned(),
artifacts: vec![
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -12343,6 +12343,7 @@ mod tests {
fn sample_pack_day_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle {
PackDayExportBundle {
fulfillment_window_id: FulfillmentWindowId::new(),
+ export_instance_id: radroots_app_models::PackDayExportInstanceId::new(),
generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
bundle_directory: bundle_directory.to_string_lossy().into_owned(),
artifacts: vec![
@@ -12991,6 +12992,7 @@ mod tests {
let fulfillment_window_id = FulfillmentWindowId::new();
let bundle = PackDayExportBundle {
fulfillment_window_id,
+ export_instance_id: radroots_app_models::PackDayExportInstanceId::new(),
generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(),
artifacts: vec![
diff --git a/crates/shared/core/src/pack_day_export.rs b/crates/shared/core/src/pack_day_export.rs
@@ -5,7 +5,8 @@ use std::{
use chrono::{DateTime, Utc};
use radroots_app_models::{
- PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, PackDayOutputSource,
+ PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, PackDayExportInstanceId,
+ PackDayOutputSource,
};
use thiserror::Error;
@@ -86,6 +87,7 @@ pub fn prepare_pack_day_export_bundle_at_data_root(
.collect::<Vec<_>>();
let bundle = PackDayExportBundle {
fulfillment_window_id: source.fulfillment_window.fulfillment_window_id,
+ export_instance_id: PackDayExportInstanceId::new(),
generated_at_utc: generated_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
bundle_directory: bundle_directory.to_string_lossy().into_owned(),
artifacts,
diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs
@@ -241,6 +241,17 @@ define_app_text_keys! {
PackDayExportFolderLabel => "pack_day.export.folder.label",
PackDayExportFilesLabel => "pack_day.export.files.label",
PackDayExportErrorLabel => "pack_day.export.error.label",
+ PackDayPrintUnavailableTitle => "pack_day.print.unavailable.title",
+ PackDayPrintUnavailableBody => "pack_day.print.unavailable.body",
+ PackDayPrintPackSheetQueuedTitle => "pack_day.print.pack_sheet.queued.title",
+ PackDayPrintPackSheetSubmittedTitle => "pack_day.print.pack_sheet.submitted.title",
+ PackDayPrintPackSheetFailedTitle => "pack_day.print.pack_sheet.failed.title",
+ PackDayPrintPickupRosterQueuedTitle => "pack_day.print.pickup_roster.queued.title",
+ PackDayPrintPickupRosterSubmittedTitle => "pack_day.print.pickup_roster.submitted.title",
+ PackDayPrintPickupRosterFailedTitle => "pack_day.print.pickup_roster.failed.title",
+ PackDayPrintCustomerLabelsQueuedTitle => "pack_day.print.customer_labels.queued.title",
+ PackDayPrintCustomerLabelsSubmittedTitle => "pack_day.print.customer_labels.submitted.title",
+ PackDayPrintCustomerLabelsFailedTitle => "pack_day.print.customer_labels.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
@@ -476,6 +476,50 @@ mod tests {
assert_eq!(app_text(AppTextKey::PackDayExportFilesLabel), "Files");
assert_eq!(app_text(AppTextKey::PackDayExportErrorLabel), "Error");
assert_eq!(
+ app_text(AppTextKey::PackDayPrintUnavailableTitle),
+ "Print not available yet"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintUnavailableBody),
+ "Print actions become available after pack day files are saved locally."
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintPackSheetQueuedTitle),
+ "Queueing pack sheet"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintPackSheetSubmittedTitle),
+ "Sent pack sheet to the printer"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintPackSheetFailedTitle),
+ "Couldn't print pack sheet"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintPickupRosterQueuedTitle),
+ "Queueing pickup roster"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintPickupRosterSubmittedTitle),
+ "Sent pickup roster to the printer"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintPickupRosterFailedTitle),
+ "Couldn't print pickup roster"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintCustomerLabelsQueuedTitle),
+ "Queueing customer labels"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintCustomerLabelsSubmittedTitle),
+ "Sent customer labels to the printer"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayPrintCustomerLabelsFailedTitle),
+ "Couldn't print customer labels"
+ );
+ 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
@@ -238,6 +238,7 @@ typed_id!(BlackoutPeriodId);
typed_id!(ProductId);
typed_id!(OrderId);
typed_id!(FulfillmentWindowId);
+typed_id!(PackDayExportInstanceId);
typed_id!(ActivityEventId);
typed_id!(ReminderId);
typed_id!(RecoveryRecordId);
@@ -1611,6 +1612,86 @@ impl PackDayExportArtifactKind {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
+pub enum PackDayPrintKind {
+ PrintPackSheet,
+ PrintPickupRoster,
+ PrintCustomerLabels,
+}
+
+impl PackDayPrintKind {
+ pub const fn all_v1() -> [Self; 3] {
+ [
+ Self::PrintPackSheet,
+ Self::PrintPickupRoster,
+ Self::PrintCustomerLabels,
+ ]
+ }
+
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::PrintPackSheet => "print_pack_sheet",
+ Self::PrintPickupRoster => "print_pickup_roster",
+ Self::PrintCustomerLabels => "print_customer_labels",
+ }
+ }
+
+ pub const fn artifact_kind(self) -> PackDayExportArtifactKind {
+ match self {
+ Self::PrintPackSheet => PackDayExportArtifactKind::PackSheet,
+ Self::PrintPickupRoster => PackDayExportArtifactKind::PickupRoster,
+ Self::PrintCustomerLabels => PackDayExportArtifactKind::CustomerLabels,
+ }
+ }
+
+ pub const fn label_stock(self) -> Option<PackDayPrintLabelStock> {
+ match self {
+ Self::PrintPackSheet | Self::PrintPickupRoster => None,
+ Self::PrintCustomerLabels => Some(PackDayPrintLabelStock::Avery5160Letter30Up),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum PackDayPrintLabelStock {
+ Avery5160Letter30Up,
+}
+
+impl PackDayPrintLabelStock {
+ pub const fn all_v1() -> [Self; 1] {
+ [Self::Avery5160Letter30Up]
+ }
+
+ pub const fn storage_key(self) -> &'static str {
+ match self {
+ Self::Avery5160Letter30Up => "avery_5160_letter_30_up",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum PackDayPrintStatus {
+ #[default]
+ Idle,
+ Running,
+ Succeeded,
+ Failed,
+}
+
+impl PackDayPrintStatus {
+ 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, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
pub enum PackDayHostHandoffKind {
RevealBundle,
OpenPackSheet,
@@ -1804,6 +1885,7 @@ pub struct PackDayExportArtifact {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PackDayExportBundle {
pub fulfillment_window_id: FulfillmentWindowId,
+ pub export_instance_id: PackDayExportInstanceId,
pub generated_at_utc: String,
pub bundle_directory: String,
pub artifacts: Vec<PackDayExportArtifact>,
@@ -3001,7 +3083,7 @@ mod tests {
}
#[test]
- fn pack_day_export_and_host_handoff_contracts_are_frozen_for_v1() {
+ fn pack_day_export_print_and_host_handoff_contracts_are_frozen_for_v1() {
assert_eq!(
PackDayExportArtifactKind::all_v1(),
[
@@ -3031,6 +3113,56 @@ mod tests {
assert_eq!(PackDayExportStatus::Succeeded.storage_key(), "succeeded");
assert_eq!(PackDayExportStatus::Failed.storage_key(), "failed");
assert_eq!(
+ PackDayPrintKind::all_v1(),
+ [
+ PackDayPrintKind::PrintPackSheet,
+ PackDayPrintKind::PrintPickupRoster,
+ PackDayPrintKind::PrintCustomerLabels,
+ ]
+ );
+ assert_eq!(
+ PackDayPrintKind::PrintPackSheet.storage_key(),
+ "print_pack_sheet"
+ );
+ assert_eq!(
+ PackDayPrintKind::PrintPickupRoster.storage_key(),
+ "print_pickup_roster"
+ );
+ assert_eq!(
+ PackDayPrintKind::PrintCustomerLabels.storage_key(),
+ "print_customer_labels"
+ );
+ assert_eq!(
+ PackDayPrintKind::PrintPackSheet.artifact_kind(),
+ PackDayExportArtifactKind::PackSheet
+ );
+ assert_eq!(
+ PackDayPrintKind::PrintPickupRoster.artifact_kind(),
+ PackDayExportArtifactKind::PickupRoster
+ );
+ assert_eq!(
+ PackDayPrintKind::PrintCustomerLabels.artifact_kind(),
+ PackDayExportArtifactKind::CustomerLabels
+ );
+ assert_eq!(PackDayPrintKind::PrintPackSheet.label_stock(), None);
+ assert_eq!(PackDayPrintKind::PrintPickupRoster.label_stock(), None);
+ assert_eq!(
+ PackDayPrintKind::PrintCustomerLabels.label_stock(),
+ Some(PackDayPrintLabelStock::Avery5160Letter30Up)
+ );
+ assert_eq!(
+ PackDayPrintLabelStock::all_v1(),
+ [PackDayPrintLabelStock::Avery5160Letter30Up]
+ );
+ assert_eq!(
+ PackDayPrintLabelStock::Avery5160Letter30Up.storage_key(),
+ "avery_5160_letter_30_up"
+ );
+ assert_eq!(PackDayPrintStatus::default(), PackDayPrintStatus::Idle);
+ assert_eq!(PackDayPrintStatus::Running.storage_key(), "running");
+ assert_eq!(PackDayPrintStatus::Succeeded.storage_key(), "succeeded");
+ assert_eq!(PackDayPrintStatus::Failed.storage_key(), "failed");
+ assert_eq!(
PackDayHostHandoffKind::all_v1(),
[
PackDayHostHandoffKind::RevealBundle,
@@ -3170,6 +3302,7 @@ mod tests {
let fulfillment_window_id = FulfillmentWindowId::new();
let bundle = PackDayExportBundle {
fulfillment_window_id,
+ export_instance_id: PackDayExportInstanceId::new(),
generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(),
artifacts: vec![
diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs
@@ -14,13 +14,14 @@ use radroots_app_models::{
FarmReadinessBlocker, FarmRulesProjection, FarmSetupBlocker, FarmSetupProjection,
FarmSetupReadiness, FarmTimingConflict, FulfillmentWindowId, LoggedOutStartupPhase,
LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrdersFilter, OrdersListProjection,
- OrdersScreenQueryState, PackDayExportArtifactKind, PackDayExportBundle, PackDayExportStatus,
- PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayProjection, PackDayScreenQueryState,
- PersonalEntryProjection, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductsFilter,
- ProductsListProjection, ProductsSort, RecoveryQueueProjection, ReminderFeedProjection,
- ReminderLogProjection, SelectedSurfaceProjection, SettingsAccountProjection,
- SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask,
- TodaySetupTaskKind,
+ OrdersScreenQueryState, PackDayExportArtifactKind, PackDayExportBundle,
+ PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus,
+ PackDayPrintKind, PackDayPrintLabelStock, PackDayPrintStatus, PackDayProjection,
+ PackDayScreenQueryState, PersonalEntryProjection, ProductEditorDraft, ProductId,
+ ProductPublishBlocker, ProductsFilter, ProductsListProjection, ProductsSort,
+ RecoveryQueueProjection, ReminderFeedProjection, ReminderLogProjection,
+ SelectedSurfaceProjection, SettingsAccountProjection, SettingsPreference, SettingsSection,
+ ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
};
use radroots_app_sync::{
AppSyncProjection, AppSyncRunStatus, SyncCheckpointState, SyncCheckpointStatus, SyncConflict,
@@ -310,6 +311,7 @@ pub struct PackDayScreenProjection {
pub query: PackDayScreenQueryState,
pub projection: PackDayProjection,
pub export: PackDayExportProjection,
+ pub print: PackDayPrintProjection,
pub host_handoff: PackDayHostHandoffProjection,
}
@@ -317,6 +319,7 @@ impl PackDayScreenProjection {
fn select_fulfillment_window(&mut self, fulfillment_window_id: Option<FulfillmentWindowId>) {
if self.query.fulfillment_window_id != fulfillment_window_id {
self.export = PackDayExportProjection::default();
+ self.print = PackDayPrintProjection::default();
self.host_handoff = PackDayHostHandoffProjection::default();
}
self.query.fulfillment_window_id = fulfillment_window_id;
@@ -335,6 +338,7 @@ impl PackDayScreenProjection {
if previous_window_id != next_window_id {
self.export = PackDayExportProjection::default();
+ self.print = PackDayPrintProjection::default();
self.host_handoff = PackDayHostHandoffProjection::default();
}
@@ -343,11 +347,16 @@ impl PackDayScreenProjection {
fn replace_export(&mut self, export: PackDayExportProjection) {
if self.export != export {
+ self.print = PackDayPrintProjection::default();
self.host_handoff = PackDayHostHandoffProjection::default();
}
self.export = export;
}
+ fn replace_print(&mut self, print: PackDayPrintProjection) {
+ self.print = print;
+ }
+
fn replace_host_handoff(&mut self, host_handoff: PackDayHostHandoffProjection) {
self.host_handoff = host_handoff;
}
@@ -406,6 +415,54 @@ impl PackDayExportProjection {
}
#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PackDayPrintRequest {
+ pub fulfillment_window_id: FulfillmentWindowId,
+ pub export_instance_id: PackDayExportInstanceId,
+ pub kind: PackDayPrintKind,
+ pub label_stock: Option<PackDayPrintLabelStock>,
+}
+
+impl PackDayPrintRequest {
+ pub fn for_bundle(kind: PackDayPrintKind, bundle: &PackDayExportBundle) -> Self {
+ Self {
+ fulfillment_window_id: bundle.fulfillment_window_id,
+ export_instance_id: bundle.export_instance_id,
+ kind,
+ label_stock: kind.label_stock(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct PackDayPrintProjection {
+ pub status: PackDayPrintStatus,
+ pub request: Option<PackDayPrintRequest>,
+}
+
+impl PackDayPrintProjection {
+ pub fn running(request: PackDayPrintRequest) -> Self {
+ Self {
+ status: PackDayPrintStatus::Running,
+ request: Some(request),
+ }
+ }
+
+ pub fn succeeded(request: PackDayPrintRequest) -> Self {
+ Self {
+ status: PackDayPrintStatus::Succeeded,
+ request: Some(request),
+ }
+ }
+
+ pub fn failed(request: PackDayPrintRequest) -> Self {
+ Self {
+ status: PackDayPrintStatus::Failed,
+ request: Some(request),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PackDayHostHandoffRequest {
pub fulfillment_window_id: FulfillmentWindowId,
pub kind: PackDayHostHandoffKind,
@@ -906,6 +963,10 @@ pub enum AppStateCommand {
message: String,
},
ResetPackDayExport,
+ BeginPackDayPrint(PackDayPrintRequest),
+ SucceedPackDayPrint(PackDayPrintRequest),
+ FailPackDayPrint(PackDayPrintRequest),
+ ResetPackDayPrint,
BeginPackDayHostHandoff(PackDayHostHandoffRequest),
SucceedPackDayHostHandoff(PackDayHostHandoffRequest),
FailPackDayHostHandoff {
@@ -1057,6 +1118,22 @@ impl AppStateCommand {
Self::ResetPackDayExport
}
+ pub fn begin_pack_day_print(request: PackDayPrintRequest) -> Self {
+ Self::BeginPackDayPrint(request)
+ }
+
+ pub fn succeed_pack_day_print(request: PackDayPrintRequest) -> Self {
+ Self::SucceedPackDayPrint(request)
+ }
+
+ pub fn fail_pack_day_print(request: PackDayPrintRequest) -> Self {
+ Self::FailPackDayPrint(request)
+ }
+
+ pub const fn reset_pack_day_print() -> Self {
+ Self::ResetPackDayPrint
+ }
+
pub fn begin_pack_day_host_handoff(request: PackDayHostHandoffRequest) -> Self {
Self::BeginPackDayHostHandoff(request)
}
@@ -1588,6 +1665,26 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap
.pack_day
.replace_export(PackDayExportProjection::default());
}
+ AppStateCommand::BeginPackDayPrint(request) => {
+ projection
+ .pack_day
+ .replace_print(PackDayPrintProjection::running(request));
+ }
+ AppStateCommand::SucceedPackDayPrint(request) => {
+ projection
+ .pack_day
+ .replace_print(PackDayPrintProjection::succeeded(request));
+ }
+ AppStateCommand::FailPackDayPrint(request) => {
+ projection
+ .pack_day
+ .replace_print(PackDayPrintProjection::failed(request));
+ }
+ AppStateCommand::ResetPackDayPrint => {
+ projection
+ .pack_day
+ .replace_print(PackDayPrintProjection::default());
+ }
AppStateCommand::BeginPackDayHostHandoff(request) => {
projection
.pack_day
@@ -1942,9 +2039,9 @@ mod tests {
AppStateRepositoryError, AppStateStore, AppStateStoreError, FarmSetupFlowStage, HomeRoute,
InMemoryAppStateRepository, OrdersScreenProjection, PackDayExportProjection,
PackDayExportRequest, PackDayHostHandoffProjection, PackDayHostHandoffRequest,
- PackDayScreenProjection, PersistedAppState, ProductEditorState, ProductsScreenProjection,
- ProductsScreenQueryState, SettingsPreference, derive_sync_projection,
- derive_sync_run_status,
+ PackDayPrintProjection, PackDayPrintRequest, PackDayScreenProjection, PersistedAppState,
+ ProductEditorState, ProductsScreenProjection, ProductsScreenQueryState, SettingsPreference,
+ derive_sync_projection, derive_sync_run_status,
};
use radroots_app_models::{
AccountCustody, AccountSummary, ActiveSurface, AppIdentityProjection, AppStartupGate,
@@ -1953,14 +2050,15 @@ mod tests {
LoggedOutStartupProjection, OrderDetailItemRow, OrderDetailProjection, OrderId,
OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersListProjection, OrdersListRow,
OrdersListSummary, OrdersScreenQueryState, PackDayExportArtifact,
- PackDayExportArtifactKind, PackDayExportBundle, PackDayExportStatus,
- PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow,
- PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PackDayScreenQueryState,
- PersonalEntryState, PersonalSection, ProductEditorDraft, ProductId, ProductPublishBlocker,
- ProductsFilter, ProductsListProjection, ProductsSort, ReminderDeliveryState,
- ReminderFeedProjection, ReminderKind, ReminderLogEntryProjection, ReminderLogProjection,
- SelectedAccountProjection, SelectedSurfaceProjection, SettingsSection, ShellSection,
- TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
+ 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,
};
use radroots_app_sync::{
AppSyncProjection, AppSyncRunStatus, SyncCheckpointState, SyncCheckpointStatus,
@@ -2013,11 +2111,20 @@ mod tests {
PackDayHostHandoffRequest::for_bundle(kind, &bundle)
}
+ fn sample_pack_day_print_request(
+ fulfillment_window_id: FulfillmentWindowId,
+ kind: PackDayPrintKind,
+ ) -> PackDayPrintRequest {
+ let bundle = sample_pack_day_export_bundle(fulfillment_window_id);
+ PackDayPrintRequest::for_bundle(kind, &bundle)
+ }
+
fn sample_pack_day_export_bundle(
fulfillment_window_id: FulfillmentWindowId,
) -> PackDayExportBundle {
PackDayExportBundle {
fulfillment_window_id,
+ export_instance_id: PackDayExportInstanceId::new(),
generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(),
artifacts: vec![
@@ -2396,6 +2503,13 @@ mod tests {
}
);
assert_eq!(
+ PackDayScreenProjection::default().print,
+ PackDayPrintProjection {
+ status: PackDayPrintStatus::Idle,
+ request: None,
+ }
+ );
+ assert_eq!(
PackDayScreenProjection::default().host_handoff,
PackDayHostHandoffProjection {
status: PackDayHostHandoffStatus::Idle,
@@ -2461,6 +2575,61 @@ mod tests {
}
#[test]
+ fn pack_day_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_print_request(
+ fulfillment_window_id,
+ PackDayPrintKind::PrintCustomerLabels,
+ );
+
+ assert_eq!(
+ request.label_stock,
+ Some(PackDayPrintLabelStock::Avery5160Letter30Up)
+ );
+ assert_eq!(
+ store.apply(AppStateCommand::begin_pack_day_print(request.clone())),
+ Ok(true)
+ );
+ assert_eq!(
+ store.pack_day_projection().print,
+ PackDayPrintProjection::running(request.clone())
+ );
+ assert_eq!(
+ store.persisted_state().seller.pack_day_query,
+ PackDayScreenQueryState::default()
+ );
+
+ assert_eq!(
+ store.apply(AppStateCommand::succeed_pack_day_print(request.clone())),
+ Ok(true)
+ );
+ assert_eq!(
+ store.pack_day_projection().print,
+ PackDayPrintProjection::succeeded(request.clone())
+ );
+
+ assert_eq!(
+ store.apply(AppStateCommand::fail_pack_day_print(request.clone())),
+ Ok(true)
+ );
+ assert_eq!(
+ store.pack_day_projection().print,
+ PackDayPrintProjection::failed(request)
+ );
+
+ assert_eq!(
+ store.apply(AppStateCommand::reset_pack_day_print()),
+ Ok(true)
+ );
+ assert_eq!(
+ store.pack_day_projection().print,
+ PackDayPrintProjection::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");
@@ -2592,6 +2761,42 @@ mod tests {
}
#[test]
+ fn changing_pack_day_window_clears_stale_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_print_request(fulfillment_window_id, PackDayPrintKind::PrintPackSheet);
+
+ assert_eq!(
+ store.apply(AppStateCommand::begin_pack_day_print(request)),
+ Ok(true)
+ );
+ assert_eq!(
+ store.pack_day_projection().print.status,
+ PackDayPrintStatus::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().print,
+ PackDayPrintProjection::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");
@@ -2633,6 +2838,45 @@ mod tests {
}
#[test]
+ fn changing_pack_day_export_state_clears_stale_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 print_request = sample_pack_day_print_request(
+ fulfillment_window_id,
+ PackDayPrintKind::PrintPickupRoster,
+ );
+
+ assert_eq!(
+ store.apply(AppStateCommand::begin_pack_day_export(
+ export_request.clone(),
+ )),
+ Ok(true)
+ );
+ assert_eq!(
+ store.apply(AppStateCommand::begin_pack_day_print(print_request)),
+ Ok(true)
+ );
+ assert_eq!(
+ store.pack_day_projection().print.status,
+ PackDayPrintStatus::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().print,
+ PackDayPrintProjection::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");
@@ -2676,6 +2920,47 @@ mod tests {
}
#[test]
+ fn replacing_pack_day_projection_with_new_window_clears_stale_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_print_request(current_window_id, PackDayPrintKind::PrintCustomerLabels);
+
+ assert_eq!(
+ store.apply(AppStateCommand::begin_pack_day_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().print,
+ PackDayPrintProjection::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
@@ -221,6 +221,17 @@
"pack_day.export.folder.label": "Folder",
"pack_day.export.files.label": "Files",
"pack_day.export.error.label": "Error",
+ "pack_day.print.unavailable.title": "Print not available yet",
+ "pack_day.print.unavailable.body": "Print actions become available after pack day files are saved locally.",
+ "pack_day.print.pack_sheet.queued.title": "Queueing pack sheet",
+ "pack_day.print.pack_sheet.submitted.title": "Sent pack sheet to the printer",
+ "pack_day.print.pack_sheet.failed.title": "Couldn't print pack sheet",
+ "pack_day.print.pickup_roster.queued.title": "Queueing pickup roster",
+ "pack_day.print.pickup_roster.submitted.title": "Sent pickup roster to the printer",
+ "pack_day.print.pickup_roster.failed.title": "Couldn't print pickup roster",
+ "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.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",