commit 3ad6cf55370f16b6c0ac6f179111b5f45e7db9ec
parent cd0a000a1ed470cc28fd22689a170ffebbc63d47
Author: triesap <tyson@radroots.org>
Date: Wed, 22 Apr 2026 20:32:06 +0000
pack_day: add macos print queue planning
- add launcher-owned lp planning and execution for pack sheet and pickup roster
- keep customer labels deferred behind an explicit unsupported-kind guard until stock prep lands
- thread print prepare and finish through runtime state using the current export bundle only
- prove queue planning failure classification and runtime state transitions with pack day tests
Diffstat:
3 files changed, 755 insertions(+), 14 deletions(-)
diff --git a/crates/launchers/desktop/src/lib.rs b/crates/launchers/desktop/src/lib.rs
@@ -4,6 +4,7 @@ mod accounts;
mod app;
mod menus;
pub mod pack_day_host_handoff;
+pub mod pack_day_print;
mod remote_signer;
mod runtime;
#[cfg(test)]
diff --git a/crates/launchers/desktop/src/pack_day_print.rs b/crates/launchers/desktop/src/pack_day_print.rs
@@ -0,0 +1,527 @@
+use std::io;
+use std::path::{Component, Path, PathBuf};
+#[cfg(target_os = "macos")]
+use std::process::Command;
+
+use radroots_app_models::{PackDayExportArtifactKind, PackDayExportBundle, PackDayPrintKind};
+use thiserror::Error;
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PackDayPrintCommandPlan {
+ pub kind: PackDayPrintKind,
+ pub target_path: PathBuf,
+ pub command_program: &'static str,
+ pub command_args: Vec<String>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct PackDayPrintCommandResult {
+ success: bool,
+ exit_code: Option<i32>,
+ stderr: String,
+}
+
+impl PackDayPrintCommandResult {
+ #[cfg(test)]
+ fn succeeded() -> Self {
+ Self {
+ success: true,
+ exit_code: Some(0),
+ stderr: String::new(),
+ }
+ }
+
+ #[cfg(test)]
+ fn failed(exit_code: Option<i32>, stderr: impl Into<String>) -> Self {
+ Self {
+ success: false,
+ exit_code,
+ stderr: stderr.into(),
+ }
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum PackDayPrintError {
+ #[error("pack day export bundle directory does not exist: {path}")]
+ MissingBundleDirectory { path: PathBuf },
+ #[error("pack day print kind is not supported by this launcher path yet: {kind:?}")]
+ UnsupportedKind { kind: PackDayPrintKind },
+ #[error("pack day export bundle is missing required artifact {artifact_kind:?} for {kind:?}")]
+ MissingArtifactReference {
+ kind: PackDayPrintKind,
+ artifact_kind: PackDayExportArtifactKind,
+ },
+ #[error("pack day export artifact path is invalid for {kind:?}: {relative_path}")]
+ InvalidArtifactRelativePath {
+ kind: PackDayPrintKind,
+ relative_path: String,
+ },
+ #[error("pack day print target does not exist for {kind:?}: {path}")]
+ MissingTargetPath {
+ kind: PackDayPrintKind,
+ path: PathBuf,
+ },
+ #[error("pack day print target must be a file for {kind:?}: {path}")]
+ InvalidTargetFile {
+ kind: PackDayPrintKind,
+ path: PathBuf,
+ },
+ #[error("pack day print is only supported on macos")]
+ UnsupportedPlatform,
+ #[error("failed to launch macos print command {program} for {kind:?}: {source}")]
+ CommandLaunch {
+ kind: PackDayPrintKind,
+ program: String,
+ source: io::Error,
+ },
+ #[error("macos print command {program} for {kind:?} exited with code {exit_code:?}: {stderr}")]
+ CommandFailed {
+ kind: PackDayPrintKind,
+ program: String,
+ exit_code: Option<i32>,
+ stderr: String,
+ },
+}
+
+impl PartialEq for PackDayPrintError {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (
+ Self::MissingBundleDirectory { path: left },
+ Self::MissingBundleDirectory { path: right },
+ ) => left == right,
+ (Self::UnsupportedKind { kind: left }, Self::UnsupportedKind { kind: right }) => {
+ left == right
+ }
+ (
+ Self::MissingArtifactReference {
+ kind: left_kind,
+ artifact_kind: left_artifact,
+ },
+ Self::MissingArtifactReference {
+ kind: right_kind,
+ artifact_kind: right_artifact,
+ },
+ ) => left_kind == right_kind && left_artifact == right_artifact,
+ (
+ Self::InvalidArtifactRelativePath {
+ kind: left_kind,
+ relative_path: left_path,
+ },
+ Self::InvalidArtifactRelativePath {
+ kind: right_kind,
+ relative_path: right_path,
+ },
+ ) => left_kind == right_kind && left_path == right_path,
+ (
+ Self::MissingTargetPath {
+ kind: left_kind,
+ path: left_path,
+ },
+ Self::MissingTargetPath {
+ kind: right_kind,
+ path: right_path,
+ },
+ ) => left_kind == right_kind && left_path == right_path,
+ (
+ Self::InvalidTargetFile {
+ kind: left_kind,
+ path: left_path,
+ },
+ Self::InvalidTargetFile {
+ kind: right_kind,
+ path: right_path,
+ },
+ ) => left_kind == right_kind && left_path == right_path,
+ (Self::UnsupportedPlatform, Self::UnsupportedPlatform) => true,
+ (
+ Self::CommandLaunch {
+ kind: left_kind,
+ program: left_program,
+ source: left_source,
+ },
+ Self::CommandLaunch {
+ kind: right_kind,
+ program: right_program,
+ source: right_source,
+ },
+ ) => {
+ left_kind == right_kind
+ && left_program == right_program
+ && left_source.kind() == right_source.kind()
+ && left_source.to_string() == right_source.to_string()
+ }
+ (
+ Self::CommandFailed {
+ kind: left_kind,
+ program: left_program,
+ exit_code: left_code,
+ stderr: left_stderr,
+ },
+ Self::CommandFailed {
+ kind: right_kind,
+ program: right_program,
+ exit_code: right_code,
+ stderr: right_stderr,
+ },
+ ) => {
+ left_kind == right_kind
+ && left_program == right_program
+ && left_code == right_code
+ && left_stderr == right_stderr
+ }
+ _ => false,
+ }
+ }
+}
+
+impl Eq for PackDayPrintError {}
+
+pub fn plan_pack_day_print(
+ bundle: &PackDayExportBundle,
+ kind: PackDayPrintKind,
+) -> Result<PackDayPrintCommandPlan, PackDayPrintError> {
+ let bundle_directory = PathBuf::from(&bundle.bundle_directory);
+ if !bundle_directory.is_dir() {
+ return Err(PackDayPrintError::MissingBundleDirectory {
+ path: bundle_directory,
+ });
+ }
+
+ let artifact_kind = match kind {
+ PackDayPrintKind::PrintPackSheet => PackDayExportArtifactKind::PackSheet,
+ PackDayPrintKind::PrintPickupRoster => PackDayExportArtifactKind::PickupRoster,
+ PackDayPrintKind::PrintCustomerLabels => {
+ return Err(PackDayPrintError::UnsupportedKind { kind });
+ }
+ };
+ let target_path = resolve_bundle_artifact_path(bundle, artifact_kind, kind)?;
+
+ Ok(PackDayPrintCommandPlan {
+ kind,
+ target_path: target_path.clone(),
+ command_program: "lp",
+ command_args: vec![target_path.to_string_lossy().into_owned()],
+ })
+}
+
+pub fn execute_pack_day_print_plan(
+ plan: &PackDayPrintCommandPlan,
+) -> Result<(), PackDayPrintError> {
+ #[cfg(target_os = "macos")]
+ {
+ execute_pack_day_print_plan_with(plan, run_macos_print_command)
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ {
+ let _ = plan;
+ Err(PackDayPrintError::UnsupportedPlatform)
+ }
+}
+
+fn resolve_bundle_artifact_path(
+ bundle: &PackDayExportBundle,
+ artifact_kind: PackDayExportArtifactKind,
+ kind: PackDayPrintKind,
+) -> Result<PathBuf, PackDayPrintError> {
+ let Some(artifact) = bundle
+ .artifacts
+ .iter()
+ .find(|artifact| artifact.kind == artifact_kind)
+ else {
+ return Err(PackDayPrintError::MissingArtifactReference {
+ kind,
+ artifact_kind,
+ });
+ };
+
+ let relative_path = Path::new(&artifact.relative_path);
+ if relative_path.is_absolute()
+ || relative_path.components().any(|component| {
+ matches!(
+ component,
+ Component::ParentDir | Component::RootDir | Component::Prefix(_)
+ )
+ })
+ {
+ return Err(PackDayPrintError::InvalidArtifactRelativePath {
+ kind,
+ relative_path: artifact.relative_path.clone(),
+ });
+ }
+
+ let path = PathBuf::from(&bundle.bundle_directory).join(relative_path);
+ if !path.exists() {
+ return Err(PackDayPrintError::MissingTargetPath { kind, path });
+ }
+ if !path.is_file() {
+ return Err(PackDayPrintError::InvalidTargetFile { kind, path });
+ }
+
+ Ok(path)
+}
+
+fn execute_pack_day_print_plan_with(
+ plan: &PackDayPrintCommandPlan,
+ run_command: impl FnOnce(&PackDayPrintCommandPlan) -> Result<PackDayPrintCommandResult, io::Error>,
+) -> Result<(), PackDayPrintError> {
+ let result = run_command(plan).map_err(|source| PackDayPrintError::CommandLaunch {
+ kind: plan.kind,
+ program: plan.command_program.to_owned(),
+ source,
+ })?;
+
+ if result.success {
+ return Ok(());
+ }
+
+ Err(PackDayPrintError::CommandFailed {
+ kind: plan.kind,
+ program: plan.command_program.to_owned(),
+ exit_code: result.exit_code,
+ stderr: result.stderr,
+ })
+}
+
+#[cfg(target_os = "macos")]
+fn run_macos_print_command(
+ plan: &PackDayPrintCommandPlan,
+) -> Result<PackDayPrintCommandResult, io::Error> {
+ let output = Command::new(plan.command_program)
+ .args(&plan.command_args)
+ .output()?;
+
+ Ok(PackDayPrintCommandResult {
+ success: output.status.success(),
+ exit_code: output.status.code(),
+ stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ PackDayPrintCommandResult, PackDayPrintError, execute_pack_day_print_plan_with,
+ plan_pack_day_print,
+ };
+ use radroots_app_models::{
+ PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle,
+ PackDayExportInstanceId, PackDayPrintKind,
+ };
+ use std::fs;
+ use std::io;
+ use std::path::PathBuf;
+ use uuid::Uuid;
+
+ struct TestDirectory {
+ path: PathBuf,
+ }
+
+ impl TestDirectory {
+ fn new() -> Self {
+ let path = std::env::temp_dir()
+ .join(format!("radroots_app_pack_day_print_{}", Uuid::new_v4()));
+ fs::create_dir_all(&path).expect("test directory should create");
+ Self { path }
+ }
+
+ fn path(&self) -> &PathBuf {
+ &self.path
+ }
+ }
+
+ impl Drop for TestDirectory {
+ fn drop(&mut self) {
+ let _ = fs::remove_dir_all(&self.path);
+ }
+ }
+
+ fn sample_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle {
+ PackDayExportBundle {
+ fulfillment_window_id: radroots_app_models::FulfillmentWindowId::new(),
+ export_instance_id: PackDayExportInstanceId::new(),
+ generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
+ bundle_directory: bundle_directory.to_string_lossy().into_owned(),
+ artifacts: vec![
+ PackDayExportArtifact {
+ kind: PackDayExportArtifactKind::PackSheet,
+ relative_path: "pack_sheet.txt".to_owned(),
+ },
+ PackDayExportArtifact {
+ kind: PackDayExportArtifactKind::PickupRoster,
+ relative_path: "pickup_roster.txt".to_owned(),
+ },
+ PackDayExportArtifact {
+ kind: PackDayExportArtifactKind::CustomerLabels,
+ relative_path: "customer_labels.txt".to_owned(),
+ },
+ ],
+ }
+ }
+
+ fn write_artifact(bundle_directory: &PathBuf, file_name: &str) -> PathBuf {
+ let path = bundle_directory.join(file_name);
+ fs::write(&path, file_name).expect("artifact should write");
+ path
+ }
+
+ #[test]
+ fn print_pack_sheet_plan_targets_the_exported_file_with_lp() {
+ let temp_dir = TestDirectory::new();
+ let pack_sheet_path = write_artifact(temp_dir.path(), "pack_sheet.txt");
+ let bundle = sample_bundle(temp_dir.path());
+
+ let plan = plan_pack_day_print(&bundle, PackDayPrintKind::PrintPackSheet)
+ .expect("pack sheet print plan should build");
+
+ assert_eq!(plan.kind, PackDayPrintKind::PrintPackSheet);
+ assert_eq!(plan.target_path, pack_sheet_path.clone());
+ assert_eq!(plan.command_program, "lp");
+ assert_eq!(
+ plan.command_args,
+ vec![pack_sheet_path.to_string_lossy().into_owned()]
+ );
+ }
+
+ #[test]
+ fn print_pickup_roster_plan_targets_the_exported_file_with_lp() {
+ let temp_dir = TestDirectory::new();
+ let pickup_roster_path = write_artifact(temp_dir.path(), "pickup_roster.txt");
+ let bundle = sample_bundle(temp_dir.path());
+
+ let plan = plan_pack_day_print(&bundle, PackDayPrintKind::PrintPickupRoster)
+ .expect("pickup roster print plan should build");
+
+ assert_eq!(plan.kind, PackDayPrintKind::PrintPickupRoster);
+ assert_eq!(plan.target_path, pickup_roster_path.clone());
+ assert_eq!(plan.command_program, "lp");
+ assert_eq!(
+ plan.command_args,
+ vec![pickup_roster_path.to_string_lossy().into_owned()]
+ );
+ }
+
+ #[test]
+ fn customer_labels_are_deferred_to_the_stock_preparation_slice() {
+ let temp_dir = TestDirectory::new();
+ let bundle = sample_bundle(temp_dir.path());
+
+ let error = plan_pack_day_print(&bundle, PackDayPrintKind::PrintCustomerLabels)
+ .expect_err("customer labels should remain deferred");
+
+ assert_eq!(
+ error,
+ PackDayPrintError::UnsupportedKind {
+ kind: PackDayPrintKind::PrintCustomerLabels,
+ }
+ );
+ }
+
+ #[test]
+ fn planning_fails_when_pack_sheet_reference_is_missing_on_disk() {
+ let temp_dir = TestDirectory::new();
+ let bundle = sample_bundle(temp_dir.path());
+
+ let error = plan_pack_day_print(&bundle, PackDayPrintKind::PrintPackSheet)
+ .expect_err("missing pack sheet file should fail");
+
+ assert_eq!(
+ error,
+ PackDayPrintError::MissingTargetPath {
+ kind: PackDayPrintKind::PrintPackSheet,
+ path: temp_dir.path().join("pack_sheet.txt"),
+ }
+ );
+ }
+
+ #[test]
+ fn planning_fails_when_pickup_roster_relative_path_is_invalid() {
+ let temp_dir = TestDirectory::new();
+ write_artifact(temp_dir.path(), "pickup_roster.txt");
+ let mut bundle = sample_bundle(temp_dir.path());
+ bundle.artifacts[1].relative_path = "../pickup_roster.txt".to_owned();
+
+ let error = plan_pack_day_print(&bundle, PackDayPrintKind::PrintPickupRoster)
+ .expect_err("invalid relative path should fail");
+
+ assert_eq!(
+ error,
+ PackDayPrintError::InvalidArtifactRelativePath {
+ kind: PackDayPrintKind::PrintPickupRoster,
+ relative_path: "../pickup_roster.txt".to_owned(),
+ }
+ );
+ }
+
+ #[test]
+ fn execution_accepts_successful_lp_runs() {
+ let temp_dir = TestDirectory::new();
+ let pack_sheet_path = write_artifact(temp_dir.path(), "pack_sheet.txt");
+ let bundle = sample_bundle(temp_dir.path());
+ let plan = plan_pack_day_print(&bundle, PackDayPrintKind::PrintPackSheet)
+ .expect("pack sheet print plan should build");
+
+ assert_eq!(plan.target_path, pack_sheet_path);
+ assert!(
+ execute_pack_day_print_plan_with(&plan, |_| {
+ Ok(PackDayPrintCommandResult::succeeded())
+ })
+ .is_ok()
+ );
+ }
+
+ #[test]
+ fn execution_classifies_command_launch_failures() {
+ let temp_dir = TestDirectory::new();
+ write_artifact(temp_dir.path(), "pickup_roster.txt");
+ let bundle = sample_bundle(temp_dir.path());
+ let plan = plan_pack_day_print(&bundle, PackDayPrintKind::PrintPickupRoster)
+ .expect("pickup roster print plan should build");
+
+ let error = execute_pack_day_print_plan_with(&plan, |_| {
+ Err(io::Error::new(
+ io::ErrorKind::PermissionDenied,
+ "lp unavailable",
+ ))
+ })
+ .expect_err("launch failure should surface");
+
+ assert_eq!(
+ error,
+ PackDayPrintError::CommandLaunch {
+ kind: PackDayPrintKind::PrintPickupRoster,
+ program: "lp".to_owned(),
+ source: io::Error::new(io::ErrorKind::PermissionDenied, "lp unavailable"),
+ }
+ );
+ }
+
+ #[test]
+ fn execution_classifies_nonzero_exit_failures() {
+ let temp_dir = TestDirectory::new();
+ write_artifact(temp_dir.path(), "pack_sheet.txt");
+ let bundle = sample_bundle(temp_dir.path());
+ let plan = plan_pack_day_print(&bundle, PackDayPrintKind::PrintPackSheet)
+ .expect("pack sheet print plan should build");
+
+ let error = execute_pack_day_print_plan_with(&plan, |_| {
+ Ok(PackDayPrintCommandResult::failed(
+ Some(1),
+ "lp: printer not found",
+ ))
+ })
+ .expect_err("nonzero exit should surface");
+
+ assert_eq!(
+ error,
+ PackDayPrintError::CommandFailed {
+ kind: PackDayPrintKind::PrintPackSheet,
+ program: "lp".to_owned(),
+ exit_code: Some(1),
+ stderr: "lp: printer not found".to_owned(),
+ }
+ );
+ }
+}
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -17,13 +17,14 @@ use radroots_app_models::{
FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrderRecoveryProjection,
OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayExportBundle,
- PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayProjection,
- PackDayScreenQueryState, PersonalSection, PickupLocationRecord, ProductEditorDraft, ProductId,
- ProductsFilter, ProductsListProjection, ProductsSort, RecoveryKind, RecoveryQueueProjection,
- RecoveryRecordId, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState,
- ReminderFeedProjection, ReminderId, ReminderKind, ReminderLogEntryProjection,
- ReminderLogProjection, ReminderSurface, ReminderUrgency, SettingsAccountProjection,
- SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection,
+ PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPrintKind,
+ PackDayPrintStatus, PackDayProjection, PackDayScreenQueryState, PersonalSection,
+ PickupLocationRecord, ProductEditorDraft, ProductId, ProductsFilter, ProductsListProjection,
+ ProductsSort, RecoveryKind, RecoveryQueueProjection, RecoveryRecordId, RecoveryState,
+ ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, ReminderId,
+ ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface,
+ ReminderUrgency, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection,
+ TodayAgendaProjection,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession,
@@ -37,9 +38,9 @@ use radroots_app_state::{
AppStateStore, AppStateStoreError, BuyerBrowseScreenProjection, BuyerCartScreenProjection,
BuyerOrdersScreenProjection, BuyerSearchScreenProjection, BuyerSearchScreenQueryState,
FarmSetupFlowStage, FarmWorkspaceReadinessProjection, HomeRoute, OrdersScreenProjection,
- PackDayExportRequest, PackDayHostHandoffRequest, PackDayScreenProjection, PersistedAppState,
- PersonalWorkspaceProjection, ProductsScreenProjection, ProductsScreenQueryState,
- derive_sync_projection,
+ PackDayExportRequest, PackDayHostHandoffRequest, PackDayPrintRequest, PackDayScreenProjection,
+ PersistedAppState, PersonalWorkspaceProjection, ProductsScreenProjection,
+ ProductsScreenQueryState, derive_sync_projection,
};
use radroots_app_sync::{
AppSyncProjection, AppSyncRequest, AppSyncResult, AppSyncTransport, AppSyncTransportError,
@@ -61,6 +62,7 @@ use crate::accounts::{
use crate::pack_day_host_handoff::{
PackDayHostHandoffCommandPlan, PackDayHostHandoffError, plan_pack_day_host_handoff,
};
+use crate::pack_day_print::{PackDayPrintCommandPlan, PackDayPrintError, plan_pack_day_print};
use crate::remote_signer::{
DesktopRemoteSignerError, DesktopRemoteSignerPaths, activate_pending_session,
apply_remote_signer_custody, clear_pending_session, load_pending_session, purge_all_state,
@@ -433,6 +435,22 @@ impl DesktopAppRuntime {
.finish_pack_day_host_handoff(request, result)
}
+ pub fn prepare_pack_day_print(
+ &self,
+ kind: PackDayPrintKind,
+ ) -> Result<Option<(PackDayPrintRequest, PackDayPrintCommandPlan)>, DesktopAppRuntimeCommandError>
+ {
+ self.lock_state_mut().prepare_pack_day_print(kind)
+ }
+
+ pub fn finish_pack_day_print(
+ &self,
+ request: PackDayPrintRequest,
+ result: Result<(), PackDayPrintError>,
+ ) -> Result<bool, DesktopAppRuntimeCommandError> {
+ self.lock_state_mut().finish_pack_day_print(request, result)
+ }
+
pub fn update_product_stock(
&self,
product_id: ProductId,
@@ -1824,6 +1842,7 @@ impl DesktopAppRuntimeState {
> {
if self.state_store.pack_day_projection().host_handoff.status
== PackDayHostHandoffStatus::Running
+ || self.state_store.pack_day_projection().print.status == PackDayPrintStatus::Running
{
return Ok(None);
}
@@ -1852,6 +1871,59 @@ impl DesktopAppRuntimeState {
}
}
+ fn prepare_pack_day_print(
+ &mut self,
+ kind: PackDayPrintKind,
+ ) -> Result<Option<(PackDayPrintRequest, PackDayPrintCommandPlan)>, DesktopAppRuntimeCommandError>
+ {
+ if self.state_store.pack_day_projection().print.status == PackDayPrintStatus::Running
+ || self.state_store.pack_day_projection().host_handoff.status
+ == PackDayHostHandoffStatus::Running
+ {
+ return Ok(None);
+ }
+
+ let Some(bundle) = self.current_pack_day_export_bundle() else {
+ return Ok(None);
+ };
+ let request = PackDayPrintRequest::for_bundle(kind, &bundle);
+ let _ = self
+ .state_store
+ .apply_in_memory(AppStateCommand::begin_pack_day_print(request.clone()));
+
+ match plan_pack_day_print(&bundle, kind) {
+ Ok(plan) => Ok(Some((request, plan))),
+ Err(error) => {
+ let _ = self
+ .state_store
+ .apply_in_memory(AppStateCommand::fail_pack_day_print(request));
+ Err(error.into())
+ }
+ }
+ }
+
+ fn finish_pack_day_print(
+ &mut self,
+ request: PackDayPrintRequest,
+ result: Result<(), PackDayPrintError>,
+ ) -> Result<bool, DesktopAppRuntimeCommandError> {
+ if !self.current_pack_day_print_request_matches(&request) {
+ return Ok(false);
+ }
+
+ match result {
+ Ok(()) => Ok(self
+ .state_store
+ .apply_in_memory(AppStateCommand::succeed_pack_day_print(request))),
+ Err(error) => {
+ let _ = self
+ .state_store
+ .apply_in_memory(AppStateCommand::fail_pack_day_print(request));
+ Err(error.into())
+ }
+ }
+ }
+
fn finish_pack_day_host_handoff(
&mut self,
request: PackDayHostHandoffRequest,
@@ -3197,6 +3269,12 @@ impl DesktopAppRuntimeState {
pack_day.host_handoff.status == PackDayHostHandoffStatus::Running
&& pack_day.host_handoff.request.as_ref() == Some(request)
}
+
+ fn current_pack_day_print_request_matches(&self, request: &PackDayPrintRequest) -> bool {
+ let pack_day = self.state_store.pack_day_projection();
+ pack_day.print.status == PackDayPrintStatus::Running
+ && pack_day.print.request.as_ref() == Some(request)
+ }
}
#[derive(Debug, Error)]
@@ -3215,6 +3293,8 @@ pub enum DesktopAppRuntimeCommandError {
PackDayExportWrite(#[from] PackDayExportWriteError),
#[error(transparent)]
PackDayHostHandoff(#[from] PackDayHostHandoffError),
+ #[error(transparent)]
+ PackDayPrint(#[from] PackDayPrintError),
}
#[derive(Debug, Error)]
@@ -4545,10 +4625,10 @@ mod tests {
FarmReadinessBlocker, FarmSetupDraft, FarmSetupProjection, FarmSummary,
FarmerActivationProjection, FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord,
LoggedOutStartupProjection, OrderId, OrderStatus, OrdersFilter, PackDayExportStatus,
- PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow,
- PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PersonalSection,
- PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductStatus, ProductsFilter,
- ProductsSort, RecoveryKind, RecoveryRecordId, ReminderDeliveryState,
+ PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, PackDayPrintKind,
+ PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow,
+ PersonalSection, PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductStatus,
+ ProductsFilter, ProductsSort, RecoveryKind, RecoveryRecordId, ReminderDeliveryState,
ReminderFeedProjection, ReminderKind, SelectedSurfaceProjection, SettingsPreference,
SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
TodaySummary,
@@ -4583,6 +4663,7 @@ mod tests {
default_sync_transport,
};
use crate::pack_day_host_handoff::PackDayHostHandoffError;
+ use crate::pack_day_print::PackDayPrintError;
#[derive(Clone)]
struct SharedRecordedSyncTransport(Arc<Mutex<RecordedAppSyncTransport>>);
@@ -7397,6 +7478,138 @@ mod tests {
}
#[test]
+ fn runtime_prepare_pack_day_print_uses_the_current_export_bundle_for_simple_documents() {
+ let (runtime, paths) = bootstrapped_runtime("pack_day_print_prepare");
+ let (_, farm_id) = provision_ready_farmer_account(&runtime);
+
+ seed_order_workspace(&runtime, farm_id);
+ assert!(runtime.open_pack_day(None).expect("pack day should open"));
+ assert!(
+ runtime
+ .export_pack_day()
+ .expect("pack day export should succeed")
+ );
+
+ for (kind, suffix) in [
+ (PackDayPrintKind::PrintPackSheet, "pack_sheet.txt"),
+ (PackDayPrintKind::PrintPickupRoster, "pickup_roster.txt"),
+ ] {
+ let prepared = runtime
+ .prepare_pack_day_print(kind)
+ .expect("print should prepare")
+ .expect("print should produce a plan");
+
+ let summary = runtime.summary();
+ assert_eq!(
+ summary.pack_day_projection.print.status,
+ PackDayPrintStatus::Running
+ );
+ assert_eq!(
+ summary.pack_day_projection.print.request,
+ Some(prepared.0.clone())
+ );
+ assert_eq!(prepared.0.kind, kind);
+ assert_eq!(
+ prepared.0.export_instance_id,
+ summary
+ .pack_day_projection
+ .export
+ .bundle
+ .as_ref()
+ .expect("pack day export bundle")
+ .export_instance_id
+ );
+ assert_eq!(prepared.1.kind, kind);
+ assert_eq!(prepared.1.command_program, "lp");
+ assert!(prepared.1.target_path.ends_with(suffix));
+ assert_eq!(
+ prepared.1.command_args,
+ vec![prepared.1.target_path.to_string_lossy().into_owned()]
+ );
+
+ assert!(
+ runtime
+ .finish_pack_day_print(prepared.0, Ok(()))
+ .expect("print success should apply")
+ );
+ }
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
+ fn runtime_finish_pack_day_print_records_failures_in_state() {
+ let (runtime, paths) = bootstrapped_runtime("pack_day_print_failure");
+ let (_, farm_id) = provision_ready_farmer_account(&runtime);
+
+ seed_order_workspace(&runtime, farm_id);
+ assert!(runtime.open_pack_day(None).expect("pack day should open"));
+ assert!(
+ runtime
+ .export_pack_day()
+ .expect("pack day export should succeed")
+ );
+
+ let (request, _) = runtime
+ .prepare_pack_day_print(PackDayPrintKind::PrintPackSheet)
+ .expect("print should prepare")
+ .expect("print should produce a plan");
+
+ let error = runtime
+ .finish_pack_day_print(request.clone(), Err(PackDayPrintError::UnsupportedPlatform))
+ .expect_err("print failure should surface");
+ assert!(matches!(
+ error,
+ DesktopAppRuntimeCommandError::PackDayPrint(PackDayPrintError::UnsupportedPlatform)
+ ));
+
+ let summary = runtime.summary();
+ assert_eq!(
+ summary.pack_day_projection.print.status,
+ PackDayPrintStatus::Failed
+ );
+ assert_eq!(summary.pack_day_projection.print.request, Some(request));
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
+ fn runtime_finish_pack_day_print_ignores_stale_background_completion() {
+ let (runtime, paths) = bootstrapped_runtime("pack_day_print_stale");
+ let (_, farm_id) = provision_ready_farmer_account(&runtime);
+
+ seed_order_workspace(&runtime, farm_id);
+ assert!(runtime.open_pack_day(None).expect("pack day should open"));
+ assert!(
+ runtime
+ .export_pack_day()
+ .expect("pack day export should succeed")
+ );
+
+ let (request, _) = runtime
+ .prepare_pack_day_print(PackDayPrintKind::PrintPickupRoster)
+ .expect("print should prepare")
+ .expect("print should produce a plan");
+
+ let _ = runtime
+ .lock_state_mut()
+ .state_store
+ .apply_in_memory(AppStateCommand::reset_pack_day_print());
+
+ assert!(
+ !runtime
+ .finish_pack_day_print(request, Ok(()))
+ .expect("stale completion should no-op")
+ );
+ assert_eq!(
+ runtime.summary().pack_day_projection.print.status,
+ PackDayPrintStatus::Idle
+ );
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
fn runtime_threads_canonical_seller_reminders_across_today_orders_and_pack_day() {
let runtime = memory_runtime();
let (_, farm_id) = provision_ready_farmer_account(&runtime);