use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{ChannelId, ProductId, PurgatoryLineItemId, PurgatoryOrderId};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurgatoryLineItem {
pub id: PurgatoryLineItemId,
pub purgatory_order_id: PurgatoryOrderId,
pub external_sku: String,
pub product_id: Option<ProductId>,
pub quantity: Decimal,
pub ignore_item: bool,
pub non_physical: bool,
}
impl PurgatoryLineItem {
#[must_use]
pub const fn is_resolved(&self) -> bool {
self.product_id.is_some() || self.ignore_item || self.non_physical
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurgatoryOrder {
pub id: PurgatoryOrderId,
pub channel_id: Option<ChannelId>,
pub external_order_id: String,
pub external_status: Option<String>,
pub is_posted: bool,
pub hold_reason: Option<String>,
pub metadata: serde_json::Value,
pub items: Vec<PurgatoryLineItem>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl PurgatoryOrder {
#[must_use]
pub fn is_ready_to_post(&self) -> bool {
!self.items.is_empty() && self.items.iter().all(PurgatoryLineItem::is_resolved)
}
#[must_use]
pub fn unresolved_count(&self) -> usize {
self.items.iter().filter(|i| !i.is_resolved()).count()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IngestLineItem {
pub external_sku: String,
pub quantity: Decimal,
pub product_id: Option<ProductId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IngestOrder {
pub channel_id: Option<ChannelId>,
pub external_order_id: String,
pub external_status: Option<String>,
#[serde(default)]
pub metadata: serde_json::Value,
pub items: Vec<IngestLineItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MapPurgatoryLine {
pub product_id: Option<ProductId>,
pub ignore_item: Option<bool>,
pub non_physical: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PurgatoryFilter {
pub channel_id: Option<ChannelId>,
pub is_posted: Option<bool>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn line(product: Option<ProductId>, ignore: bool, non_physical: bool) -> PurgatoryLineItem {
PurgatoryLineItem {
id: PurgatoryLineItemId::new(),
purgatory_order_id: PurgatoryOrderId::new(),
external_sku: "EXT-1".into(),
product_id: product,
quantity: dec!(1),
ignore_item: ignore,
non_physical,
}
}
fn order(items: Vec<PurgatoryLineItem>) -> PurgatoryOrder {
PurgatoryOrder {
id: PurgatoryOrderId::new(),
channel_id: None,
external_order_id: "SHOP-1001".into(),
external_status: Some("paid".into()),
is_posted: false,
hold_reason: None,
metadata: serde_json::Value::Null,
items,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
#[test]
fn line_resolution_rules() {
assert!(line(Some(ProductId::new()), false, false).is_resolved());
assert!(line(None, true, false).is_resolved());
assert!(line(None, false, true).is_resolved());
assert!(!line(None, false, false).is_resolved());
}
#[test]
fn ready_to_post_requires_all_resolved() {
let ready =
order(vec![line(Some(ProductId::new()), false, false), line(None, true, false)]);
assert!(ready.is_ready_to_post());
assert_eq!(ready.unresolved_count(), 0);
let not_ready =
order(vec![line(Some(ProductId::new()), false, false), line(None, false, false)]);
assert!(!not_ready.is_ready_to_post());
assert_eq!(not_ready.unresolved_count(), 1);
}
#[test]
fn empty_order_not_ready() {
assert!(!order(vec![]).is_ready_to_post());
}
}