use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use stateset_primitives::ProductionBatchId;
use strum::{Display, EnumString};
use uuid::Uuid;
#[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 ProductionBatchStatus {
#[default]
Planned,
InProgress,
Completed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductionBatch {
pub id: ProductionBatchId,
pub name: String,
pub status: ProductionBatchStatus,
pub vendor_id: Option<Uuid>,
pub work_order_ids: Vec<Uuid>,
pub notes: Option<String>,
pub scheduled_start: Option<DateTime<Utc>>,
pub scheduled_end: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl ProductionBatch {
#[must_use]
pub fn work_order_count(&self) -> usize {
self.work_order_ids.len()
}
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(self.status, ProductionBatchStatus::Completed | ProductionBatchStatus::Cancelled)
}
#[must_use]
pub fn derive_status(
&self,
total: usize,
completed: usize,
any_in_progress: bool,
) -> ProductionBatchStatus {
if self.status == ProductionBatchStatus::Cancelled {
return ProductionBatchStatus::Cancelled;
}
if total > 0 && completed >= total {
ProductionBatchStatus::Completed
} else if any_in_progress || completed > 0 {
ProductionBatchStatus::InProgress
} else {
ProductionBatchStatus::Planned
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateProductionBatch {
pub name: String,
pub vendor_id: Option<Uuid>,
#[serde(default)]
pub work_order_ids: Vec<Uuid>,
pub notes: Option<String>,
pub scheduled_start: Option<DateTime<Utc>>,
pub scheduled_end: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateProductionBatch {
pub name: Option<String>,
pub vendor_id: Option<Uuid>,
pub status: Option<ProductionBatchStatus>,
pub notes: Option<String>,
pub scheduled_start: Option<DateTime<Utc>>,
pub scheduled_end: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProductionBatchFilter {
pub status: Option<ProductionBatchStatus>,
pub vendor_id: Option<Uuid>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_batch(status: ProductionBatchStatus, work_orders: Vec<Uuid>) -> ProductionBatch {
ProductionBatch {
id: ProductionBatchId::new(),
name: "Batch 1".to_string(),
status,
vendor_id: None,
work_order_ids: work_orders,
notes: None,
scheduled_start: None,
scheduled_end: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
#[test]
fn work_order_count_reflects_links() {
let batch = make_batch(ProductionBatchStatus::Planned, vec![Uuid::nil(), Uuid::nil()]);
assert_eq!(batch.work_order_count(), 2);
}
#[test]
fn derive_completed_when_all_done() {
let batch = make_batch(ProductionBatchStatus::InProgress, vec![]);
assert_eq!(batch.derive_status(3, 3, false), ProductionBatchStatus::Completed);
}
#[test]
fn derive_in_progress_when_some_active() {
let batch = make_batch(ProductionBatchStatus::Planned, vec![]);
assert_eq!(batch.derive_status(3, 1, true), ProductionBatchStatus::InProgress);
}
#[test]
fn derive_planned_when_nothing_started() {
let batch = make_batch(ProductionBatchStatus::Planned, vec![]);
assert_eq!(batch.derive_status(3, 0, false), ProductionBatchStatus::Planned);
}
#[test]
fn derive_status_cancelled_sticky() {
let batch = make_batch(ProductionBatchStatus::Cancelled, vec![]);
assert_eq!(batch.derive_status(3, 3, false), ProductionBatchStatus::Cancelled);
}
#[test]
fn derive_status_empty_batch_is_planned() {
let batch = make_batch(ProductionBatchStatus::Planned, vec![]);
assert_eq!(batch.derive_status(0, 0, false), ProductionBatchStatus::Planned);
}
#[test]
fn terminal_states() {
assert!(make_batch(ProductionBatchStatus::Completed, vec![]).is_terminal());
assert!(make_batch(ProductionBatchStatus::Cancelled, vec![]).is_terminal());
assert!(!make_batch(ProductionBatchStatus::InProgress, vec![]).is_terminal());
}
#[test]
fn status_roundtrip() {
for s in [
ProductionBatchStatus::Planned,
ProductionBatchStatus::InProgress,
ProductionBatchStatus::Completed,
ProductionBatchStatus::Cancelled,
] {
let parsed: ProductionBatchStatus = s.to_string().parse().unwrap();
assert_eq!(parsed, s);
}
}
}