commerce-theory 0.1.0

Runtime Rust mirror of the CommerceTheory Lean package
Documentation
use crate::competitor_pricing::*;
use crate::dropship_profit::*;
use crate::forecasting::*;
use crate::foundation::*;

domain_struct! {
    pub struct DistributorProduct {
        distributor_id: SupplierId,
        sku: Sku,
        unit_cost: Money,
        supplier_shipping_per_unit: Money,
        available_qty: Quantity,
        min_order_qty: Quantity,
        currency: Currency,
        active: bool,
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DropshipOpportunityCandidate {
    pub(crate) sku: Sku,
    pub(crate) units: Quantity,
    pub(crate) target_price: Money,
    pub(crate) required_capital: Money,
    pub(crate) expected_profit: Money,
    pub(crate) min_profit: Money,
    pub(crate) competitor_price: Money,
    pub(crate) costs: DropshipProfitCosts,
}

impl DropshipOpportunityCandidate {
    pub fn try_new(
        sku: Sku,
        units: Quantity,
        target_price: Money,
        required_capital: Money,
        expected_profit: Money,
        min_profit: Money,
        competitor_price: Money,
        costs: DropshipProfitCosts,
    ) -> DomainResult<Self> {
        if required_capital == 0 {
            return Err(ValidationError::Invariant(
                "opportunity required capital must be positive",
            ));
        }
        if expected_profit < min_profit {
            return Err(ValidationError::Invariant(
                "expected profit below minimum profit",
            ));
        }
        if !price_profitable_for_min_profit(target_price, 0, &costs, min_profit)? {
            return Err(ValidationError::Invariant(
                "target price below profit floor",
            ));
        }
        if target_price > competitor_price {
            return Err(ValidationError::Invariant(
                "target price exceeds competitor price",
            ));
        }
        Ok(Self {
            sku,
            units,
            target_price,
            required_capital,
            expected_profit,
            min_profit,
            competitor_price,
            costs,
        })
    }

    pub fn expected_profit(&self) -> Money {
        self.expected_profit
    }

    pub fn min_profit(&self) -> Money {
        self.min_profit
    }
}

pub fn candidates_capital_total(
    candidates: &[DropshipOpportunityCandidate],
) -> DomainResult<Money> {
    checked_sum(
        candidates
            .iter()
            .map(|candidate| candidate.required_capital),
        "candidates_capital_total",
    )
}

pub fn candidates_profit_total(candidates: &[DropshipOpportunityCandidate]) -> DomainResult<Money> {
    checked_sum(
        candidates.iter().map(|candidate| candidate.expected_profit),
        "candidates_profit_total",
    )
}

pub fn candidates_min_profit_total(
    candidates: &[DropshipOpportunityCandidate],
) -> DomainResult<Money> {
    checked_sum(
        candidates.iter().map(|candidate| candidate.min_profit),
        "candidates_min_profit_total",
    )
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DropshipOpportunityPortfolio {
    pub(crate) selected: Vec<DropshipOpportunityCandidate>,
    pub(crate) investment_fund: Money,
}

impl DropshipOpportunityPortfolio {
    pub fn try_new(
        selected: Vec<DropshipOpportunityCandidate>,
        investment_fund: Money,
    ) -> DomainResult<Self> {
        if candidates_capital_total(&selected)? > investment_fund {
            return Err(ValidationError::Invariant(
                "opportunity portfolio exceeds investment fund",
            ));
        }
        Ok(Self {
            selected,
            investment_fund,
        })
    }
}

pub(crate) fn _forecasting_anchor(_: Option<Confidence>) {}

impl_getters!(DropshipOpportunityCandidate {
    sku: Sku,
    units: Quantity,
    target_price: Money,
    required_capital: Money,
    competitor_price: Money,
    costs: DropshipProfitCosts,
});

impl_getters!(DropshipOpportunityPortfolio {
    selected: Vec<DropshipOpportunityCandidate>,
    investment_fund: Money,
});