commerce-theory 0.1.0

Runtime Rust mirror of the CommerceTheory Lean package
Documentation
use crate::foundation::*;
use crate::marketing::*;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TradeMode {
    Retail,
    Wholesale,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CustomerKind {
    Guest,
    Registered,
    WholesaleAccount,
}

domain_struct! {
    pub struct Customer {
        id: CustomerId,
        kind: CustomerKind,
        wholesale_approved: bool,
    }
}

pub fn customer_can_buy_wholesale(customer: &Customer) -> bool {
    customer.kind == CustomerKind::WholesaleAccount && customer.wholesale_approved
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PaymentTerms {
    Prepaid,
    NetDays(Nat),
}

pub fn payment_terms_allowed(mode: TradeMode, terms: PaymentTerms) -> bool {
    !matches!((mode, terms), (TradeMode::Retail, PaymentTerms::NetDays(_)))
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TradePriceBookEntry {
    pub(crate) sku: Sku,
    pub(crate) currency: Currency,
    pub(crate) unit_cost: Money,
    pub(crate) retail_unit_price: Money,
    pub(crate) wholesale_unit_price: Money,
    pub(crate) retail_margin: Money,
    pub(crate) wholesale_margin: Money,
    pub(crate) wholesale_min_qty: Quantity,
}

impl TradePriceBookEntry {
    #[allow(clippy::too_many_arguments)]
    pub fn try_new(
        sku: Sku,
        currency: Currency,
        unit_cost: Money,
        retail_unit_price: Money,
        wholesale_unit_price: Money,
        retail_margin: Money,
        wholesale_margin: Money,
        wholesale_min_qty: Quantity,
    ) -> DomainResult<Self> {
        if checked_add(unit_cost, retail_margin, "retail margin")? > retail_unit_price {
            return Err(ValidationError::Invariant("retail margin is unsafe"));
        }
        if checked_add(unit_cost, wholesale_margin, "wholesale margin")? > wholesale_unit_price {
            return Err(ValidationError::Invariant("wholesale margin is unsafe"));
        }
        if wholesale_unit_price > retail_unit_price {
            return Err(ValidationError::Invariant(
                "wholesale price exceeds retail price",
            ));
        }
        if wholesale_min_qty == 0 {
            return Err(ValidationError::Invariant(
                "wholesale minimum quantity must be positive",
            ));
        }
        Ok(Self {
            sku,
            currency,
            unit_cost,
            retail_unit_price,
            wholesale_unit_price,
            retail_margin,
            wholesale_margin,
            wholesale_min_qty,
        })
    }
}

pub fn unit_price_for_trade_mode(mode: TradeMode, entry: &TradePriceBookEntry) -> Money {
    match mode {
        TradeMode::Retail => entry.retail_unit_price,
        TradeMode::Wholesale => entry.wholesale_unit_price,
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RetailLine {
    pub(crate) entry: TradePriceBookEntry,
    pub(crate) quantity: Quantity,
    pub(crate) discount: Money,
}

impl RetailLine {
    pub fn try_new(
        entry: TradePriceBookEntry,
        quantity: Quantity,
        discount: Money,
    ) -> DomainResult<Self> {
        if discount > checked_mul(entry.retail_unit_price, quantity, "retail line gross")? {
            return Err(ValidationError::Invariant("retail discount exceeds gross"));
        }
        Ok(Self {
            entry,
            quantity,
            discount,
        })
    }
}

pub fn retail_line_gross_total(line: &RetailLine) -> DomainResult<Money> {
    checked_mul(
        line.entry.retail_unit_price,
        line.quantity,
        "retail_line_gross_total",
    )
}

pub fn retail_line_net_total(line: &RetailLine) -> DomainResult<Money> {
    Ok(nat_sub(retail_line_gross_total(line)?, line.discount))
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WholesaleLine {
    pub(crate) entry: TradePriceBookEntry,
    pub(crate) quantity: Quantity,
    pub(crate) discount: Money,
}

impl WholesaleLine {
    pub fn try_new(
        entry: TradePriceBookEntry,
        quantity: Quantity,
        discount: Money,
    ) -> DomainResult<Self> {
        if quantity < entry.wholesale_min_qty {
            return Err(ValidationError::Invariant(
                "wholesale quantity below minimum",
            ));
        }
        if discount > checked_mul(entry.wholesale_unit_price, quantity, "wholesale line gross")? {
            return Err(ValidationError::Invariant(
                "wholesale discount exceeds gross",
            ));
        }
        Ok(Self {
            entry,
            quantity,
            discount,
        })
    }
}

pub fn wholesale_line_gross_total(line: &WholesaleLine) -> DomainResult<Money> {
    checked_mul(
        line.entry.wholesale_unit_price,
        line.quantity,
        "wholesale_line_gross_total",
    )
}

pub fn wholesale_line_retail_equivalent_total(line: &WholesaleLine) -> DomainResult<Money> {
    checked_mul(
        line.entry.retail_unit_price,
        line.quantity,
        "wholesale_line_retail_equivalent_total",
    )
}

pub fn wholesale_line_net_total(line: &WholesaleLine) -> DomainResult<Money> {
    Ok(nat_sub(wholesale_line_gross_total(line)?, line.discount))
}

pub fn wholesale_order_net_total(lines: &[WholesaleLine]) -> DomainResult<Money> {
    checked_sum(
        lines
            .iter()
            .map(wholesale_line_net_total)
            .collect::<DomainResult<Vec<_>>>()?,
        "wholesale_order_net_total",
    )
}

pub fn wholesale_retail_equivalent_total(lines: &[WholesaleLine]) -> DomainResult<Money> {
    checked_sum(
        lines
            .iter()
            .map(wholesale_line_retail_equivalent_total)
            .collect::<DomainResult<Vec<_>>>()?,
        "wholesale_retail_equivalent_total",
    )
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WholesaleCreditAccount {
    pub(crate) customer: Customer,
    pub(crate) credit_limit: Money,
    pub(crate) outstanding: Money,
}

impl WholesaleCreditAccount {
    pub fn try_new(
        customer: Customer,
        credit_limit: Money,
        outstanding: Money,
    ) -> DomainResult<Self> {
        if !customer_can_buy_wholesale(&customer) {
            return Err(ValidationError::Invariant("customer cannot buy wholesale"));
        }
        if outstanding > credit_limit {
            return Err(ValidationError::Invariant(
                "outstanding exceeds credit limit",
            ));
        }
        Ok(Self {
            customer,
            credit_limit,
            outstanding,
        })
    }
}

pub fn can_place_wholesale_credit_order(
    account: &WholesaleCreditAccount,
    order_total: Money,
) -> bool {
    account
        .outstanding
        .checked_add(order_total)
        .is_some_and(|total| total <= account.credit_limit)
}

pub(crate) fn _marketing_anchor(_: Option<ConsentStatus>) {}

impl_getters!(TradePriceBookEntry {
    sku: Sku,
    currency: Currency,
    unit_cost: Money,
    retail_unit_price: Money,
    wholesale_unit_price: Money,
    retail_margin: Money,
    wholesale_margin: Money,
    wholesale_min_qty: Quantity,
});

impl_getters!(RetailLine {
    entry: TradePriceBookEntry,
    quantity: Quantity,
    discount: Money,
});

impl_getters!(WholesaleLine {
    entry: TradePriceBookEntry,
    quantity: Quantity,
    discount: Money,
});

impl_getters!(WholesaleCreditAccount {
    customer: Customer,
    credit_limit: Money,
    outstanding: Money,
});