1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
//! Print station domain models
//!
//! A print station is a paired agent (e.g. a warehouse PC driving label
//! printers) that long-polls for print jobs. Pairing issues a bearer token —
//! returned exactly once — of which only a hash is stored. Jobs are enqueued by
//! the platform and picked up by the station's agent.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use stateset_primitives::{PrintJobId, PrintStationId};
use strum::{Display, EnumString};
/// A paired print station. The pairing token's hash is never exposed.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrintStation {
/// Unique station ID.
pub id: PrintStationId,
/// Human-readable station name.
pub name: String,
/// Printers available at this station.
pub printers: Vec<String>,
/// Whether the station's token has been revoked.
pub revoked: bool,
/// When the station was last seen (long-poll / job pickup).
pub last_seen_at: Option<DateTime<Utc>>,
/// When the station was paired.
pub created_at: DateTime<Utc>,
/// When the station was last updated.
pub updated_at: DateTime<Utc>,
}
/// Result of pairing a station: the station plus its one-time bearer token.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairStationResult {
/// The newly-paired station.
pub station: PrintStation,
/// The plaintext bearer token — shown only once; only a hash is persisted.
pub token: String,
}
/// Kind of print payload.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum PrintPayloadKind {
/// Raw ZPL command stream.
#[default]
Zpl,
/// Base64-encoded PDF bytes.
Pdf,
}
/// Lifecycle status of a print job.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum PrintJobStatus {
/// Waiting to be picked up by the station agent.
#[default]
Queued,
/// Picked up by the agent, printing in progress.
PickedUp,
/// Printed successfully.
Printed,
/// Printing failed.
Failed,
}
impl PrintJobStatus {
/// Whether the job is in a terminal state.
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Printed | Self::Failed)
}
}
/// A print job queued to a station.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrintJob {
/// Unique job ID.
pub id: PrintJobId,
/// Station the job is queued to.
pub station_id: PrintStationId,
/// Target printer name (optional; defaults to the station default).
pub printer_name: Option<String>,
/// Payload kind.
pub payload_kind: PrintPayloadKind,
/// Payload (raw ZPL text or base64-encoded PDF bytes).
pub payload: String,
/// Lifecycle status.
pub status: PrintJobStatus,
/// When the job was enqueued.
pub created_at: DateTime<Utc>,
/// When the job was picked up by the agent.
pub picked_up_at: Option<DateTime<Utc>>,
}
/// Input for pairing a print station.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatePrintStation {
/// Station name.
pub name: String,
/// Available printers.
#[serde(default)]
pub printers: Vec<String>,
}
/// Input for enqueuing a print job.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnqueuePrintJob {
/// Target printer (optional).
pub printer_name: Option<String>,
/// Payload kind.
pub payload_kind: PrintPayloadKind,
/// Payload contents.
pub payload: String,
}
/// Filter for listing print jobs.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PrintJobFilter {
/// Filter by status.
pub status: Option<PrintJobStatus>,
/// Maximum results.
pub limit: Option<u32>,
/// Offset for pagination.
pub offset: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn job_terminal_states() {
assert!(PrintJobStatus::Printed.is_terminal());
assert!(PrintJobStatus::Failed.is_terminal());
assert!(!PrintJobStatus::Queued.is_terminal());
assert!(!PrintJobStatus::PickedUp.is_terminal());
}
#[test]
fn payload_kind_roundtrip() {
for k in [PrintPayloadKind::Zpl, PrintPayloadKind::Pdf] {
assert_eq!(k.to_string().parse::<PrintPayloadKind>().unwrap(), k);
}
assert_eq!(PrintPayloadKind::default(), PrintPayloadKind::Zpl);
}
#[test]
fn job_status_roundtrip() {
for s in [
PrintJobStatus::Queued,
PrintJobStatus::PickedUp,
PrintJobStatus::Printed,
PrintJobStatus::Failed,
] {
assert_eq!(s.to_string().parse::<PrintJobStatus>().unwrap(), s);
}
}
}