1use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use stateset_primitives::{FulfillmentId, OrderId, OrderItemId, ShipmentId};
9use std::str::FromStr;
10use strum::{Display, EnumString};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct Wave {
20 pub id: FulfillmentId,
21 pub wave_number: String,
22 pub warehouse_id: i32,
23 pub status: WaveStatus,
24 pub order_count: i32,
25 pub pick_count: i32,
26 pub completed_pick_count: i32,
27 pub priority: i32,
28 pub started_at: Option<DateTime<Utc>>,
29 pub completed_at: Option<DateTime<Utc>>,
30 pub notes: Option<String>,
31 pub created_by: Option<String>,
32 pub created_at: DateTime<Utc>,
33 pub updated_at: DateTime<Utc>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct PickTask {
39 pub id: Uuid,
40 pub wave_id: Option<FulfillmentId>,
41 pub order_id: OrderId,
42 pub order_item_id: OrderItemId,
43 pub warehouse_id: i32,
44 pub status: PickStatus,
45 pub sku: String,
46 pub product_name: Option<String>,
47 pub source_location_id: i32,
48 pub source_location_code: Option<String>,
49 pub quantity_requested: Decimal,
50 pub quantity_picked: Decimal,
51 pub quantity_short: Decimal,
52 pub lot_id: Option<Uuid>,
53 pub serial_number: Option<String>,
54 pub assigned_to: Option<String>,
55 pub priority: i32,
56 pub pick_sequence: i32,
57 pub started_at: Option<DateTime<Utc>>,
58 pub completed_at: Option<DateTime<Utc>>,
59 pub notes: Option<String>,
60 pub created_at: DateTime<Utc>,
61 pub updated_at: DateTime<Utc>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct PackTask {
67 pub id: Uuid,
68 pub order_id: OrderId,
69 pub shipment_id: Option<ShipmentId>,
70 pub status: PackStatus,
71 pub carton_count: i32,
72 pub total_weight_kg: Option<Decimal>,
73 pub assigned_to: Option<String>,
74 pub packing_station: Option<String>,
75 pub started_at: Option<DateTime<Utc>>,
76 pub completed_at: Option<DateTime<Utc>>,
77 pub notes: Option<String>,
78 pub created_at: DateTime<Utc>,
79 pub updated_at: DateTime<Utc>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct Carton {
85 pub id: Uuid,
86 pub pack_task_id: Uuid,
87 pub carton_number: String,
88 pub package_type: PackageType,
89 pub weight_kg: Option<Decimal>,
90 pub length_cm: Option<Decimal>,
91 pub width_cm: Option<Decimal>,
92 pub height_cm: Option<Decimal>,
93 pub tracking_number: Option<String>,
94 pub label_printed: bool,
95 pub created_at: DateTime<Utc>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct CartonItem {
101 pub id: Uuid,
102 pub carton_id: Uuid,
103 pub sku: String,
104 pub quantity: Decimal,
105 pub lot_id: Option<Uuid>,
106 pub serial_number: Option<String>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct ShipTask {
112 pub id: Uuid,
113 pub order_id: OrderId,
114 pub shipment_id: ShipmentId,
115 pub pack_task_id: Uuid,
116 pub status: ShipStatus,
117 pub carrier: Option<String>,
118 pub service_level: Option<String>,
119 pub tracking_number: Option<String>,
120 pub label_url: Option<String>,
121 pub shipping_cost: Option<Decimal>,
122 pub assigned_to: Option<String>,
123 pub shipped_at: Option<DateTime<Utc>>,
124 pub notes: Option<String>,
125 pub created_at: DateTime<Utc>,
126 pub updated_at: DateTime<Utc>,
127}
128
129#[derive(
135 Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, Serialize, Deserialize, Default,
136)]
137#[strum(serialize_all = "snake_case")]
138#[serde(rename_all = "snake_case")]
139#[non_exhaustive]
140pub enum WaveStatus {
141 #[default]
143 Draft,
144 Released,
146 InProgress,
148 Completed,
150 Cancelled,
152}
153
154impl FromStr for WaveStatus {
155 type Err = String;
156 fn from_str(s: &str) -> Result<Self, Self::Err> {
157 match s.trim().to_ascii_lowercase().as_str() {
158 "draft" => Ok(Self::Draft),
159 "released" => Ok(Self::Released),
160 "in_progress" | "inprogress" => Ok(Self::InProgress),
161 "completed" => Ok(Self::Completed),
162 "cancelled" | "canceled" => Ok(Self::Cancelled),
163 _ => Err(format!("Unknown wave status: {s}")),
164 }
165 }
166}
167
168#[derive(
170 Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, Serialize, Deserialize, Default,
171)]
172#[strum(serialize_all = "snake_case")]
173#[serde(rename_all = "snake_case")]
174#[non_exhaustive]
175pub enum PickStatus {
176 #[default]
178 Pending,
179 Assigned,
181 InProgress,
183 Completed,
185 Short,
187 Cancelled,
189}
190
191impl FromStr for PickStatus {
192 type Err = String;
193 fn from_str(s: &str) -> Result<Self, Self::Err> {
194 match s.trim().to_ascii_lowercase().as_str() {
195 "pending" => Ok(Self::Pending),
196 "assigned" => Ok(Self::Assigned),
197 "in_progress" | "inprogress" => Ok(Self::InProgress),
198 "completed" => Ok(Self::Completed),
199 "short" => Ok(Self::Short),
200 "cancelled" | "canceled" => Ok(Self::Cancelled),
201 _ => Err(format!("Unknown pick status: {s}")),
202 }
203 }
204}
205
206#[derive(
208 Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, Serialize, Deserialize, Default,
209)]
210#[strum(serialize_all = "snake_case")]
211#[serde(rename_all = "snake_case")]
212#[non_exhaustive]
213pub enum PackStatus {
214 #[default]
216 Pending,
217 Assigned,
219 ReadyToPack,
221 InProgress,
223 Completed,
225 Cancelled,
227}
228
229impl FromStr for PackStatus {
230 type Err = String;
231 fn from_str(s: &str) -> Result<Self, Self::Err> {
232 match s.trim().to_ascii_lowercase().as_str() {
233 "pending" => Ok(Self::Pending),
234 "assigned" => Ok(Self::Assigned),
235 "ready_to_pack" | "readytopack" => Ok(Self::ReadyToPack),
236 "in_progress" | "inprogress" => Ok(Self::InProgress),
237 "completed" => Ok(Self::Completed),
238 "cancelled" | "canceled" => Ok(Self::Cancelled),
239 _ => Err(format!("Unknown pack status: {s}")),
240 }
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
246#[serde(rename_all = "snake_case")]
247#[non_exhaustive]
248pub enum ShipStatus {
249 #[default]
251 Pending,
252 ReadyToShip,
254 LabelPrinted,
256 Shipped,
258 Cancelled,
260}
261
262impl std::fmt::Display for ShipStatus {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 Self::Pending => write!(f, "pending"),
266 Self::ReadyToShip => write!(f, "ready_to_ship"),
267 Self::LabelPrinted => write!(f, "label_printed"),
268 Self::Shipped => write!(f, "shipped"),
269 Self::Cancelled => write!(f, "cancelled"),
270 }
271 }
272}
273
274impl FromStr for ShipStatus {
275 type Err = String;
276 fn from_str(s: &str) -> Result<Self, Self::Err> {
277 match s.trim().to_ascii_lowercase().as_str() {
278 "pending" => Ok(Self::Pending),
279 "ready_to_ship" | "readytoship" => Ok(Self::ReadyToShip),
280 "label_printed" | "labelprinted" => Ok(Self::LabelPrinted),
281 "shipped" => Ok(Self::Shipped),
282 "cancelled" | "canceled" => Ok(Self::Cancelled),
283 _ => Err(format!("Unknown ship status: {s}")),
284 }
285 }
286}
287
288#[derive(
290 Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize, Deserialize, Default,
291)]
292#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
293#[serde(rename_all = "snake_case")]
294#[non_exhaustive]
295pub enum PackageType {
296 #[default]
298 Box,
299 Envelope,
301 Tube,
303 Pallet,
305 Custom,
307}
308
309#[derive(
311 Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize, Deserialize, Default,
312)]
313#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
314#[serde(rename_all = "snake_case")]
315#[non_exhaustive]
316pub enum WaveType {
317 #[default]
319 Batch,
320 Priority,
322 Zone,
324 #[strum(serialize = "single", serialize = "single_order", serialize = "singleorder")]
326 Single,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize, Default)]
335pub struct CreateWave {
336 pub warehouse_id: i32,
337 pub order_ids: Vec<OrderId>,
338 pub priority: Option<i32>,
339 pub notes: Option<String>,
340 pub created_by: Option<String>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct CreatePickTask {
346 pub wave_id: Option<FulfillmentId>,
347 pub order_id: OrderId,
348 pub order_item_id: OrderItemId,
349 pub warehouse_id: i32,
350 pub sku: String,
351 pub product_name: Option<String>,
352 pub source_location_id: i32,
353 pub quantity_requested: Decimal,
354 pub lot_id: Option<Uuid>,
355 pub serial_number: Option<String>,
356 pub priority: Option<i32>,
357 pub notes: Option<String>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct CompletePick {
363 pub pick_id: Uuid,
364 pub quantity_picked: Decimal,
365 pub quantity_short: Option<Decimal>,
366 pub short_reason: Option<String>,
367 pub lot_id: Option<Uuid>,
368 pub serial_number: Option<String>,
369 pub completed_by: Option<String>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct CreatePackTask {
375 pub order_id: OrderId,
376 pub notes: Option<String>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize, Default)]
381pub struct AddCarton {
382 pub pack_task_id: Uuid,
383 pub package_type: PackageType,
384 pub weight_kg: Option<Decimal>,
385 pub length_cm: Option<Decimal>,
386 pub width_cm: Option<Decimal>,
387 pub height_cm: Option<Decimal>,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct AddCartonItem {
393 pub carton_id: Uuid,
394 pub sku: String,
395 pub quantity: Decimal,
396 pub lot_id: Option<Uuid>,
397 pub serial_number: Option<String>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct CreateShipTask {
403 pub order_id: OrderId,
404 pub shipment_id: ShipmentId,
405 pub pack_task_id: Uuid,
406 pub carrier: Option<String>,
407 pub service_level: Option<String>,
408 pub notes: Option<String>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct CompleteShip {
414 pub ship_task_id: Uuid,
415 pub tracking_number: String,
416 pub shipping_cost: Option<Decimal>,
417 pub shipped_by: Option<String>,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize, Default)]
426pub struct WaveFilter {
427 pub warehouse_id: Option<i32>,
428 pub status: Option<WaveStatus>,
429 pub from_date: Option<DateTime<Utc>>,
430 pub to_date: Option<DateTime<Utc>>,
431 pub limit: Option<u32>,
432 pub offset: Option<u32>,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, Default)]
437pub struct PickTaskFilter {
438 pub warehouse_id: Option<i32>,
439 pub wave_id: Option<FulfillmentId>,
440 pub order_id: Option<OrderId>,
441 pub status: Option<PickStatus>,
442 pub assigned_to: Option<String>,
443 pub limit: Option<u32>,
444 pub offset: Option<u32>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, Default)]
449pub struct PackTaskFilter {
450 pub order_id: Option<OrderId>,
451 pub status: Option<PackStatus>,
452 pub assigned_to: Option<String>,
453 pub limit: Option<u32>,
454 pub offset: Option<u32>,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize, Default)]
459pub struct ShipTaskFilter {
460 pub order_id: Option<OrderId>,
461 pub status: Option<ShipStatus>,
462 pub carrier: Option<String>,
463 pub limit: Option<u32>,
464 pub offset: Option<u32>,
465}
466
467#[must_use]
476pub fn generate_wave_number() -> String {
477 let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S").to_string();
478 let random = &uuid::Uuid::new_v4().to_string().replace('-', "")[..8].to_uppercase();
479 format!("WV-{timestamp}-{random}")
480}
481
482#[must_use]
486pub fn generate_carton_number() -> String {
487 let timestamp = chrono::Utc::now().format("%H%M%S").to_string();
488 let random = &uuid::Uuid::new_v4().to_string().replace('-', "")[..8].to_uppercase();
489 format!("CTN-{timestamp}-{random}")
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 fn test_wave_status_from_str() {
498 assert_eq!(WaveStatus::from_str("released").unwrap(), WaveStatus::Released);
499 assert_eq!(WaveStatus::from_str("InProgress").unwrap(), WaveStatus::InProgress);
500 assert!(WaveStatus::from_str("nope").is_err());
501 }
502
503 #[test]
504 fn test_pick_status_from_str() {
505 assert_eq!(PickStatus::from_str("assigned").unwrap(), PickStatus::Assigned);
506 assert_eq!(PickStatus::from_str("canceled").unwrap(), PickStatus::Cancelled);
507 assert!(PickStatus::from_str("nope").is_err());
508 }
509
510 #[test]
511 fn test_pack_status_from_str() {
512 assert_eq!(PackStatus::from_str("assigned").unwrap(), PackStatus::Assigned);
513 assert_eq!(PackStatus::from_str("readytopack").unwrap(), PackStatus::ReadyToPack);
514 assert!(PackStatus::from_str("nope").is_err());
515 }
516
517 #[test]
518 fn test_ship_status_from_str() {
519 assert_eq!(ShipStatus::from_str("labelprinted").unwrap(), ShipStatus::LabelPrinted);
520 assert_eq!(ShipStatus::from_str("ready_to_ship").unwrap(), ShipStatus::ReadyToShip);
521 assert!(ShipStatus::from_str("nope").is_err());
522 }
523
524 #[test]
525 fn test_package_type_from_str() {
526 assert_eq!(PackageType::from_str("box").unwrap(), PackageType::Box);
527 assert!(PackageType::from_str("nope").is_err());
528 }
529
530 #[test]
531 fn test_wave_type_from_str() {
532 assert_eq!(WaveType::from_str("single_order").unwrap(), WaveType::Single);
533 assert!(WaveType::from_str("nope").is_err());
534 }
535}