use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{CurrencyCode, ProductId, PurchaseOrderId};
use strum::{Display, EnumString};
use uuid::Uuid;
#[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 PurchaseOrderStatus {
#[default]
Draft,
PendingApproval,
Approved,
Sent,
Acknowledged,
PartiallyReceived,
Received,
Completed,
Cancelled,
OnHold,
}
impl std::str::FromStr for PurchaseOrderStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"draft" => Ok(Self::Draft),
"pending_approval" => Ok(Self::PendingApproval),
"approved" => Ok(Self::Approved),
"sent" => Ok(Self::Sent),
"acknowledged" => Ok(Self::Acknowledged),
"partially_received" => Ok(Self::PartiallyReceived),
"received" => Ok(Self::Received),
"completed" => Ok(Self::Completed),
"cancelled" | "canceled" => Ok(Self::Cancelled),
"on_hold" => Ok(Self::OnHold),
_ => Err(format!("Unknown purchase order 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 PaymentTerms {
#[default]
DueOnReceipt,
#[strum(serialize = "net_15", serialize = "net15")]
Net15,
#[strum(serialize = "net_30", serialize = "net30")]
Net30,
#[strum(serialize = "net_45", serialize = "net45")]
Net45,
#[strum(serialize = "net_60", serialize = "net60")]
Net60,
#[strum(serialize = "net_90", serialize = "net90")]
Net90,
#[strum(serialize = "2_10_net_30", serialize = "2/10_net_30")]
TwoTenNet30,
Prepaid,
#[strum(serialize = "cash_on_delivery", serialize = "cod")]
CashOnDelivery,
#[strum(serialize = "letter_of_credit", serialize = "lc")]
LetterOfCredit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Supplier {
pub id: Uuid,
pub supplier_code: String,
pub name: String,
pub contact_name: Option<String>,
pub email: Option<String>,
pub phone: Option<String>,
pub website: Option<String>,
pub address: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub postal_code: Option<String>,
pub country: Option<String>,
pub tax_id: Option<String>,
pub payment_terms: PaymentTerms,
pub currency: CurrencyCode,
pub lead_time_days: Option<i32>,
pub minimum_order: Option<Decimal>,
pub is_active: bool,
pub notes: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateSupplier {
pub name: String,
pub supplier_code: Option<String>,
pub contact_name: Option<String>,
pub email: Option<String>,
pub phone: Option<String>,
pub website: Option<String>,
pub address: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub postal_code: Option<String>,
pub country: Option<String>,
pub tax_id: Option<String>,
pub payment_terms: Option<PaymentTerms>,
pub currency: Option<CurrencyCode>,
pub lead_time_days: Option<i32>,
pub minimum_order: Option<Decimal>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateSupplier {
pub name: Option<String>,
pub contact_name: Option<String>,
pub email: Option<String>,
pub phone: Option<String>,
pub website: Option<String>,
pub address: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub postal_code: Option<String>,
pub country: Option<String>,
pub tax_id: Option<String>,
pub payment_terms: Option<PaymentTerms>,
pub currency: Option<CurrencyCode>,
pub lead_time_days: Option<i32>,
pub minimum_order: Option<Decimal>,
pub is_active: Option<bool>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SupplierFilter {
pub name: Option<String>,
pub country: Option<String>,
pub active_only: Option<bool>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurchaseOrder {
pub id: PurchaseOrderId,
pub po_number: String,
pub supplier_id: Uuid,
pub status: PurchaseOrderStatus,
pub order_date: DateTime<Utc>,
pub expected_date: Option<DateTime<Utc>>,
pub delivered_date: Option<DateTime<Utc>>,
pub ship_to_address: Option<String>,
pub ship_to_city: Option<String>,
pub ship_to_state: Option<String>,
pub ship_to_postal_code: Option<String>,
pub ship_to_country: Option<String>,
pub payment_terms: PaymentTerms,
pub currency: CurrencyCode,
pub subtotal: Decimal,
pub tax_amount: Decimal,
pub shipping_cost: Decimal,
pub discount_amount: Decimal,
pub total: Decimal,
pub amount_paid: Decimal,
pub supplier_reference: Option<String>,
pub notes: Option<String>,
pub supplier_notes: Option<String>,
pub approved_by: Option<String>,
pub approved_at: Option<DateTime<Utc>>,
pub items: Vec<PurchaseOrderItem>,
pub sent_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurchaseOrderItem {
pub id: Uuid,
pub purchase_order_id: PurchaseOrderId,
pub product_id: Option<ProductId>,
pub sku: String,
pub name: String,
pub supplier_sku: Option<String>,
pub quantity_ordered: Decimal,
pub quantity_received: Decimal,
pub unit_of_measure: Option<String>,
pub unit_cost: Decimal,
pub line_total: Decimal,
pub tax_amount: Decimal,
pub discount_amount: Decimal,
pub expected_date: Option<DateTime<Utc>>,
pub notes: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreatePurchaseOrder {
pub supplier_id: Uuid,
pub order_date: Option<DateTime<Utc>>,
pub expected_date: Option<DateTime<Utc>>,
pub ship_to_address: Option<String>,
pub ship_to_city: Option<String>,
pub ship_to_state: Option<String>,
pub ship_to_postal_code: Option<String>,
pub ship_to_country: Option<String>,
pub payment_terms: Option<PaymentTerms>,
pub currency: Option<CurrencyCode>,
pub tax_amount: Option<Decimal>,
pub shipping_cost: Option<Decimal>,
pub discount_amount: Option<Decimal>,
pub notes: Option<String>,
pub supplier_notes: Option<String>,
pub items: Vec<CreatePurchaseOrderItem>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreatePurchaseOrderItem {
pub product_id: Option<ProductId>,
pub sku: String,
pub name: String,
pub supplier_sku: Option<String>,
pub quantity: Decimal,
pub unit_of_measure: Option<String>,
pub unit_cost: Decimal,
pub tax_amount: Option<Decimal>,
pub discount_amount: Option<Decimal>,
pub expected_date: Option<DateTime<Utc>>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdatePurchaseOrder {
pub expected_date: Option<DateTime<Utc>>,
pub ship_to_address: Option<String>,
pub ship_to_city: Option<String>,
pub ship_to_state: Option<String>,
pub ship_to_postal_code: Option<String>,
pub ship_to_country: Option<String>,
pub payment_terms: Option<PaymentTerms>,
pub tax_amount: Option<Decimal>,
pub shipping_cost: Option<Decimal>,
pub discount_amount: Option<Decimal>,
pub notes: Option<String>,
pub supplier_notes: Option<String>,
pub supplier_reference: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReceivePurchaseOrderItems {
pub items: Vec<ReceivePurchaseOrderItem>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReceivePurchaseOrderItem {
pub item_id: Uuid,
pub quantity_received: Decimal,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PurchaseOrderFilter {
pub supplier_id: Option<Uuid>,
pub status: Option<PurchaseOrderStatus>,
pub from_date: Option<DateTime<Utc>>,
pub to_date: Option<DateTime<Utc>>,
pub min_total: Option<Decimal>,
pub max_total: Option<Decimal>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
pub fn generate_supplier_code() -> String {
let now = chrono::Utc::now();
let short_id = &uuid::Uuid::new_v4().simple().to_string()[..8];
format!("SUP-{}-{short_id}", now.format("%Y%m%d%H%M%S"))
}
pub fn generate_po_number() -> String {
let now = chrono::Utc::now();
let short_id = &uuid::Uuid::new_v4().to_string()[..8];
format!("PO-{}-{}", now.format("%Y%m%d%H%M%S"), short_id)
}
#[cfg(test)]
mod tests {
use super::generate_supplier_code;
#[test]
fn generated_supplier_codes_include_entropy_suffix() {
let first = generate_supplier_code();
let second = generate_supplier_code();
assert!(first.starts_with("SUP-"));
assert!(first.len() > "SUP-20260101120000".len());
assert_ne!(first, second);
}
}