use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use strum::{Display, EnumString};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Backorder {
pub id: Uuid,
pub backorder_number: String,
pub order_id: Uuid,
pub order_line_id: Option<Uuid>,
pub customer_id: Uuid,
pub sku: String,
pub quantity_ordered: Decimal,
pub quantity_fulfilled: Decimal,
pub quantity_remaining: Decimal,
pub status: BackorderStatus,
pub priority: BackorderPriority,
pub expected_date: Option<DateTime<Utc>>,
pub promised_date: Option<DateTime<Utc>>,
pub source_location_id: Option<i32>,
pub notes: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackorderFulfillment {
pub id: Uuid,
pub backorder_id: Uuid,
pub quantity: Decimal,
pub source_type: FulfillmentSourceType,
pub source_id: Option<Uuid>,
pub notes: Option<String>,
pub fulfilled_at: DateTime<Utc>,
pub fulfilled_by: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackorderAllocation {
pub id: Uuid,
pub backorder_id: Uuid,
pub sku: String,
pub quantity: Decimal,
pub location_id: Option<i32>,
pub lot_id: Option<Uuid>,
pub status: AllocationStatus,
pub allocated_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum BackorderStatus {
#[default]
Pending,
PartiallyFulfilled,
Allocated,
ReadyToShip,
Fulfilled,
Cancelled,
}
impl FromStr for BackorderStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"pending" => Ok(Self::Pending),
"partially_fulfilled" | "partiallyfulfilled" => Ok(Self::PartiallyFulfilled),
"allocated" => Ok(Self::Allocated),
"ready_to_ship" | "readytoship" => Ok(Self::ReadyToShip),
"fulfilled" => Ok(Self::Fulfilled),
"cancelled" | "canceled" => Ok(Self::Cancelled),
_ => Err(format!("Unknown backorder status: {s}")),
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum BackorderPriority {
Low,
#[default]
Normal,
High,
Critical,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum FulfillmentSourceType {
#[default]
Inventory,
#[strum(serialize = "purchase_order", serialize = "purchaseorder", serialize = "po")]
PurchaseOrder,
Transfer,
Production,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum AllocationStatus {
#[default]
Reserved,
Confirmed,
Released,
Expired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateBackorder {
pub order_id: Uuid,
pub order_line_id: Option<Uuid>,
pub customer_id: Uuid,
pub sku: String,
pub quantity: Decimal,
pub priority: Option<BackorderPriority>,
pub expected_date: Option<DateTime<Utc>>,
pub promised_date: Option<DateTime<Utc>>,
pub source_location_id: Option<i32>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateBackorder {
pub priority: Option<BackorderPriority>,
pub expected_date: Option<DateTime<Utc>>,
pub promised_date: Option<DateTime<Utc>>,
pub source_location_id: Option<i32>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FulfillBackorder {
pub backorder_id: Uuid,
pub quantity: Decimal,
pub source_type: FulfillmentSourceType,
pub source_id: Option<Uuid>,
pub notes: Option<String>,
pub fulfilled_by: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllocateBackorder {
pub backorder_id: Uuid,
pub quantity: Decimal,
pub location_id: Option<i32>,
pub lot_id: Option<Uuid>,
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BackorderFilter {
pub order_id: Option<Uuid>,
pub customer_id: Option<Uuid>,
pub sku: Option<String>,
pub status: Option<BackorderStatus>,
pub priority: Option<BackorderPriority>,
pub expected_before: Option<DateTime<Utc>>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkuBackorderSummary {
pub sku: String,
pub total_quantity: Decimal,
pub backorder_count: i32,
pub oldest_date: Option<DateTime<Utc>>,
pub earliest_expected: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackorderSummary {
pub total_backorders: i32,
pub total_quantity: Decimal,
pub pending_count: i32,
pub allocated_count: i32,
pub critical_count: i32,
pub overdue_count: i32,
}
#[must_use]
pub fn generate_backorder_number() -> String {
let timestamp = chrono::Utc::now().format("%Y%m%d").to_string();
let suffix = Uuid::new_v4().simple().to_string();
let random = suffix[..12].to_uppercase();
format!("BO-{timestamp}-{random}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backorder_status_from_str() {
assert_eq!(BackorderStatus::from_str("pending").unwrap(), BackorderStatus::Pending);
assert_eq!(
BackorderStatus::from_str("partiallyfulfilled").unwrap(),
BackorderStatus::PartiallyFulfilled
);
assert!(BackorderStatus::from_str("nope").is_err());
}
#[test]
fn test_backorder_priority_from_str() {
assert_eq!(BackorderPriority::from_str("low").unwrap(), BackorderPriority::Low);
assert_eq!(BackorderPriority::from_str("critical").unwrap(), BackorderPriority::Critical);
assert!(BackorderPriority::from_str("nope").is_err());
}
#[test]
fn test_fulfillment_source_type_from_str() {
assert_eq!(
FulfillmentSourceType::from_str("purchaseorder").unwrap(),
FulfillmentSourceType::PurchaseOrder
);
assert_eq!(
FulfillmentSourceType::from_str("transfer").unwrap(),
FulfillmentSourceType::Transfer
);
assert!(FulfillmentSourceType::from_str("nope").is_err());
}
#[test]
fn test_allocation_status_from_str() {
assert_eq!(AllocationStatus::from_str("confirmed").unwrap(), AllocationStatus::Confirmed);
assert!(AllocationStatus::from_str("nope").is_err());
}
#[test]
fn backorder_numbers_are_unique_under_tight_loops() {
let mut seen = std::collections::HashSet::new();
for _ in 0..10_000 {
let number = generate_backorder_number();
assert!(number.starts_with("BO-"));
assert!(seen.insert(number));
}
}
}