use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{OrderId, ProductId, ShipmentId};
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 ShippingCarrier {
#[default]
Other,
Ups,
#[strum(serialize = "fedex", serialize = "fed_ex")]
FedEx,
Usps,
Dhl,
#[strum(serialize = "ontrac", serialize = "on_trac")]
OnTrac,
#[strum(serialize = "lasership", serialize = "laser_ship")]
LaserShip,
}
impl ShippingCarrier {
pub const fn tracking_url_base(&self) -> Option<&'static str> {
match self {
Self::Ups => Some("https://www.ups.com/track?tracknum="),
Self::FedEx => Some("https://www.fedex.com/apps/fedextrack/?tracknumbers="),
Self::Usps => Some("https://tools.usps.com/go/TrackConfirmAction?tLabels="),
Self::Dhl => Some(
"https://www.dhl.com/us-en/home/tracking/tracking-express.html?submit=1&tracking-id=",
),
Self::OnTrac => Some("https://www.ontrac.com/tracking/?number="),
Self::LaserShip => Some("https://www.lasership.com/track/"),
Self::Other => None,
}
}
pub fn tracking_url(&self, tracking_number: &str) -> Option<String> {
self.tracking_url_base().map(|base| format!("{}{}", base, tracking_number))
}
}
#[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 ShippingMethod {
#[default]
Standard,
Express,
Overnight,
#[strum(serialize = "two_day", serialize = "twoday")]
TwoDay,
Ground,
International,
#[strum(serialize = "same_day", serialize = "sameday")]
SameDay,
Freight,
}
#[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 ShipmentStatus {
#[default]
Pending,
Processing,
#[strum(serialize = "ready_to_ship", serialize = "readytoship")]
ReadyToShip,
Shipped,
#[strum(serialize = "in_transit", serialize = "intransit")]
InTransit,
#[strum(serialize = "out_for_delivery", serialize = "outfordelivery")]
OutForDelivery,
Delivered,
Failed,
Returned,
#[strum(serialize = "cancelled", serialize = "canceled")]
Cancelled,
#[strum(serialize = "on_hold", serialize = "onhold")]
OnHold,
}
impl ShipmentStatus {
pub const fn can_transition_to(&self, target: Self) -> bool {
use ShipmentStatus::*;
matches!(
(self, target),
(Pending, Processing) |
(Pending, Cancelled) |
(Processing, ReadyToShip) |
(Processing, Cancelled) |
(ReadyToShip, Shipped) |
(ReadyToShip, Cancelled) |
(Shipped, InTransit) |
(InTransit, OutForDelivery) |
(InTransit, Failed) |
(OutForDelivery, Delivered) |
(OutForDelivery, Failed) |
(Failed, InTransit) |
(Failed, Returned) |
(Delivered, Returned) |
(Pending, OnHold) |
(Processing, OnHold) |
(OnHold, Processing) |
(OnHold, Cancelled)
)
}
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Delivered | Self::Cancelled | Self::Returned)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Shipment {
pub id: ShipmentId,
pub shipment_number: String,
pub order_id: OrderId,
pub status: ShipmentStatus,
pub carrier: ShippingCarrier,
pub shipping_method: ShippingMethod,
pub tracking_number: Option<String>,
pub tracking_url: Option<String>,
pub recipient_name: String,
pub recipient_email: Option<String>,
pub recipient_phone: Option<String>,
pub shipping_address: String,
pub weight_kg: Option<Decimal>,
pub dimensions: Option<String>,
pub shipping_cost: Option<Decimal>,
pub insurance_amount: Option<Decimal>,
pub signature_required: bool,
pub shipped_at: Option<DateTime<Utc>>,
pub estimated_delivery: Option<DateTime<Utc>>,
pub delivered_at: Option<DateTime<Utc>>,
pub notes: Option<String>,
pub items: Vec<ShipmentItem>,
pub events: Vec<ShipmentEvent>,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Shipment {
pub fn generate_shipment_number() -> String {
let now = chrono::Utc::now();
format!("SHP-{}", now.format("%Y%m%d%H%M%S"))
}
pub fn transit_days(&self) -> Option<f64> {
match (self.shipped_at, self.delivered_at) {
(Some(shipped), Some(delivered)) => {
let duration = delivered - shipped;
Some(duration.num_hours() as f64 / 24.0)
}
_ => None,
}
}
pub fn is_late(&self) -> bool {
match (self.estimated_delivery, self.delivered_at) {
(Some(estimated), Some(delivered)) => delivered > estimated,
(Some(estimated), None) if !self.status.is_terminal() => Utc::now() > estimated,
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShipmentItem {
pub id: Uuid,
pub shipment_id: ShipmentId,
pub order_item_id: Option<Uuid>,
pub product_id: Option<ProductId>,
pub sku: String,
pub name: String,
pub quantity: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShipmentEvent {
pub id: Uuid,
pub shipment_id: ShipmentId,
pub event_type: String,
pub location: Option<String>,
pub description: Option<String>,
pub event_time: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default)]
pub struct CreateShipment {
pub order_id: OrderId,
pub carrier: Option<ShippingCarrier>,
pub shipping_method: Option<ShippingMethod>,
pub tracking_number: Option<String>,
pub recipient_name: String,
pub recipient_email: Option<String>,
pub recipient_phone: Option<String>,
pub shipping_address: String,
pub weight_kg: Option<Decimal>,
pub dimensions: Option<String>,
pub shipping_cost: Option<Decimal>,
pub insurance_amount: Option<Decimal>,
pub signature_required: Option<bool>,
pub estimated_delivery: Option<DateTime<Utc>>,
pub notes: Option<String>,
pub items: Option<Vec<CreateShipmentItem>>,
}
#[derive(Debug, Clone, Default)]
pub struct CreateShipmentItem {
pub order_item_id: Option<Uuid>,
pub product_id: Option<ProductId>,
pub sku: String,
pub name: String,
pub quantity: i32,
}
#[derive(Debug, Clone, Default)]
pub struct UpdateShipment {
pub status: Option<ShipmentStatus>,
pub carrier: Option<ShippingCarrier>,
pub tracking_number: Option<String>,
pub recipient_name: Option<String>,
pub recipient_email: Option<String>,
pub recipient_phone: Option<String>,
pub shipping_address: Option<String>,
pub weight_kg: Option<Decimal>,
pub dimensions: Option<String>,
pub shipping_cost: Option<Decimal>,
pub estimated_delivery: Option<DateTime<Utc>>,
pub notes: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AddShipmentEvent {
pub event_type: String,
pub location: Option<String>,
pub description: Option<String>,
pub event_time: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default)]
pub struct ShipmentFilter {
pub order_id: Option<OrderId>,
pub status: Option<ShipmentStatus>,
pub carrier: Option<ShippingCarrier>,
pub tracking_number: Option<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_shipment_status_from_str() {
assert_eq!(ShipmentStatus::from_str("pending").unwrap(), ShipmentStatus::Pending);
assert_eq!(ShipmentStatus::from_str("ready_to_ship").unwrap(), ShipmentStatus::ReadyToShip);
assert_eq!(
ShipmentStatus::from_str("outfordelivery").unwrap(),
ShipmentStatus::OutForDelivery
);
assert_eq!(ShipmentStatus::from_str("canceled").unwrap(), ShipmentStatus::Cancelled);
}
#[test]
fn test_shipping_carrier_from_str() {
assert_eq!(ShippingCarrier::from_str("ups").unwrap(), ShippingCarrier::Ups);
assert_eq!(ShippingCarrier::from_str("fed_ex").unwrap(), ShippingCarrier::FedEx);
assert_eq!(ShippingCarrier::from_str("on_trac").unwrap(), ShippingCarrier::OnTrac);
assert_eq!(ShippingCarrier::from_str("other").unwrap(), ShippingCarrier::Other);
}
#[test]
fn test_shipping_method_from_str() {
assert_eq!(ShippingMethod::from_str("standard").unwrap(), ShippingMethod::Standard);
assert_eq!(ShippingMethod::from_str("two_day").unwrap(), ShippingMethod::TwoDay);
assert_eq!(ShippingMethod::from_str("sameday").unwrap(), ShippingMethod::SameDay);
}
}