use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InventoryItem {
pub id: i64,
pub sku: String,
pub name: String,
pub description: Option<String>,
pub unit_of_measure: String,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InventoryBalance {
pub id: i64,
pub item_id: i64,
pub location_id: i32,
pub quantity_on_hand: Decimal,
pub quantity_allocated: Decimal,
pub quantity_available: Decimal,
pub reorder_point: Option<Decimal>,
pub safety_stock: Option<Decimal>,
pub version: i32,
pub last_counted_at: Option<DateTime<Utc>>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InventoryTransaction {
pub id: i64,
pub item_id: i64,
pub location_id: i32,
pub transaction_type: TransactionType,
pub quantity: Decimal,
pub reference_type: Option<String>,
pub reference_id: Option<String>,
pub reason: Option<String>,
pub created_by: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InventoryReservation {
pub id: Uuid,
pub item_id: i64,
pub location_id: i32,
pub quantity: Decimal,
pub status: ReservationStatus,
pub reference_type: String,
pub reference_id: String,
pub expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum TransactionType {
Receipt,
Shipment,
Adjustment,
Transfer,
Return,
Allocation,
Deallocation,
CycleCount,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum ReservationStatus {
Pending,
Confirmed,
Allocated,
#[strum(serialize = "cancelled", serialize = "canceled")]
Cancelled,
Released,
Expired,
}
impl Default for ReservationStatus {
fn default() -> Self {
Self::Pending
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdjustInventory {
pub sku: String,
pub location_id: Option<i32>,
pub quantity: Decimal,
pub reason: String,
pub reference_type: Option<String>,
pub reference_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReserveInventory {
pub sku: String,
pub location_id: Option<i32>,
pub quantity: Decimal,
pub reference_type: String,
pub reference_id: String,
pub expires_in_seconds: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CreateInventoryItem {
pub sku: String,
pub name: String,
pub description: Option<String>,
pub unit_of_measure: Option<String>,
pub initial_quantity: Option<Decimal>,
pub location_id: Option<i32>,
pub reorder_point: Option<Decimal>,
pub safety_stock: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StockLevel {
pub sku: String,
pub name: String,
pub total_on_hand: Decimal,
pub total_allocated: Decimal,
pub total_available: Decimal,
pub locations: Vec<LocationStock>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationStock {
pub location_id: i32,
pub location_name: Option<String>,
pub on_hand: Decimal,
pub allocated: Decimal,
pub available: Decimal,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InventoryFilter {
pub sku: Option<String>,
pub location_id: Option<i32>,
pub below_reorder_point: Option<bool>,
pub is_active: Option<bool>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
impl InventoryBalance {
#[must_use]
pub fn needs_reorder(&self) -> bool {
if let Some(reorder_point) = self.reorder_point {
self.quantity_available < reorder_point
} else {
false
}
}
#[must_use]
pub fn calculate_available(&self) -> Decimal {
self.quantity_on_hand - self.quantity_allocated
}
#[must_use]
pub fn can_allocate(&self, quantity: Decimal) -> bool {
self.quantity_available >= quantity
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn transaction_type_from_str() {
assert_eq!(TransactionType::from_str("receipt").unwrap(), TransactionType::Receipt);
assert!(TransactionType::from_str("unknown").is_err());
}
#[test]
fn reservation_status_from_str() {
assert_eq!(ReservationStatus::from_str("allocated").unwrap(), ReservationStatus::Allocated);
assert!(ReservationStatus::from_str("unknown").is_err());
}
}