commerce-theory 0.1.2

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

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Marketplace {
    AmazonLike,
    RozetkaLike,
    EtsyLike,
    EbayLike,
    Custom,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SalesChannel {
    OwnWebsite,
    MarketplaceChannel(Marketplace),
    B2BPortal,
    DropshipFeed,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ListingStatus {
    Draft,
    Active,
    Paused,
    Archived,
}

domain_struct! {
    pub struct MarketplaceListing {
        sku: Sku,
        marketplace: Marketplace,
        external_id: Nat,
        price: Money,
        currency: Currency,
        published_stock: Quantity,
        status: ListingStatus,
    }
}

#[must_use]
pub fn listing_active(listing: &MarketplaceListing) -> bool {
    listing.status == ListingStatus::Active
}

#[must_use]
pub const fn listing_in_stock(listing: &MarketplaceListing) -> bool {
    listing.published_stock > 0
}

#[must_use]
pub fn listing_can_be_advertised(listing: &MarketplaceListing) -> bool {
    listing_active(listing) && listing_in_stock(listing)
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SyncedMarketplaceListing {
    pub(crate) listing: MarketplaceListing,
    pub(crate) stock: StockState,
}

impl SyncedMarketplaceListing {
    pub fn try_new(listing: MarketplaceListing, stock: StockState) -> DomainResult<Self> {
        if listing.sku != stock.sku() {
            return Err(ValidationError::Invariant(
                "listing SKU must match stock SKU",
            ));
        }
        if listing.published_stock > available_stock(&stock) {
            return Err(ValidationError::Invariant(
                "published stock exceeds available stock",
            ));
        }
        Ok(Self { listing, stock })
    }

    #[must_use]
    pub const fn listing(&self) -> &MarketplaceListing {
        &self.listing
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ChannelPricePolicy {
    pub(crate) min_price: Money,
    pub(crate) max_price: Money,
}

impl ChannelPricePolicy {
    pub const fn try_new(min_price: Money, max_price: Money) -> DomainResult<Self> {
        if min_price > max_price {
            return Err(ValidationError::Invariant("minimum price exceeds maximum"));
        }
        Ok(Self {
            min_price,
            max_price,
        })
    }
}

#[must_use]
pub const fn valid_channel_price(policy: &ChannelPricePolicy, price: Money) -> bool {
    policy.min_price <= price && price <= policy.max_price
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SafeProductFeedLine {
    pub(crate) sku: Sku,
    pub(crate) channel: SalesChannel,
    pub(crate) price: Money,
    pub(crate) currency: Currency,
    pub(crate) stock: Quantity,
    pub(crate) stock_state: StockState,
    pub(crate) price_policy: ChannelPricePolicy,
}

impl SafeProductFeedLine {
    pub fn try_new(
        sku: Sku,
        channel: SalesChannel,
        price: Money,
        currency: Currency,
        stock: Quantity,
        stock_state: StockState,
        price_policy: ChannelPricePolicy,
    ) -> DomainResult<Self> {
        if sku != stock_state.sku() {
            return Err(ValidationError::Invariant("feed SKU must match stock SKU"));
        }
        if !valid_channel_price(&price_policy, price) {
            return Err(ValidationError::Invariant("feed price outside policy"));
        }
        if stock > available_stock(&stock_state) {
            return Err(ValidationError::Invariant(
                "feed stock exceeds availability",
            ));
        }
        Ok(Self {
            sku,
            channel,
            price,
            currency,
            stock,
            stock_state,
            price_policy,
        })
    }
}

pub fn marketplace_fee_rounded(
    mode: RoundingMode,
    gross: Money,
    fee_rate: BasisPoints,
) -> DomainResult<Money> {
    round_bps_amount(mode, gross, fee_rate)
}

pub fn marketplace_payout_rounded(
    mode: RoundingMode,
    gross: Money,
    payout_rate: BasisPoints,
) -> DomainResult<Money> {
    round_bps_amount(mode, gross, payout_rate)
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MarketplaceFeeLedger {
    pub(crate) gross: Money,
    pub(crate) fee_rate: BasisPoints,
    pub(crate) fee_rounding_mode: RoundingMode,
    pub(crate) fee: Money,
    pub(crate) payout: Money,
}

impl MarketplaceFeeLedger {
    pub fn try_new(
        gross: Money,
        fee_rate: BasisPoints,
        fee_rounding_mode: RoundingMode,
        fee: Money,
        payout: Money,
    ) -> DomainResult<Self> {
        if fee != marketplace_fee_rounded(fee_rounding_mode, gross, fee_rate)? {
            return Err(ValidationError::Invariant("marketplace fee is incorrect"));
        }
        if fee > gross {
            return Err(ValidationError::Invariant("marketplace fee exceeds gross"));
        }
        if payout != nat_sub(gross, fee) {
            return Err(ValidationError::Invariant(
                "marketplace payout is incorrect",
            ));
        }
        Ok(Self {
            gross,
            fee_rate,
            fee_rounding_mode,
            fee,
            payout,
        })
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MarketplacePayoutCalculation {
    pub(crate) gross: Money,
    pub(crate) payout_rate: BasisPoints,
    pub(crate) payout_rounding_mode: RoundingMode,
    pub(crate) payout: Money,
}

impl MarketplacePayoutCalculation {
    pub fn try_new(
        gross: Money,
        payout_rate: BasisPoints,
        payout_rounding_mode: RoundingMode,
        payout: Money,
    ) -> DomainResult<Self> {
        if payout != marketplace_payout_rounded(payout_rounding_mode, gross, payout_rate)? {
            return Err(ValidationError::Invariant(
                "marketplace payout is incorrect",
            ));
        }
        Ok(Self {
            gross,
            payout_rate,
            payout_rounding_mode,
            payout,
        })
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MarketplaceOrder {
    pub(crate) marketplace: Marketplace,
    pub(crate) external_order_id: MarketplaceOrderId,
    pub(crate) internal_order: Order,
    pub(crate) gross_from_marketplace: Money,
    pub(crate) fee_ledger: MarketplaceFeeLedger,
}

impl MarketplaceOrder {
    pub fn try_new(
        marketplace: Marketplace,
        external_order_id: MarketplaceOrderId,
        internal_order: Order,
        gross_from_marketplace: Money,
        fee_ledger: MarketplaceFeeLedger,
    ) -> DomainResult<Self> {
        if gross_from_marketplace != internal_order.total() {
            return Err(ValidationError::Invariant(
                "marketplace gross must match internal order total",
            ));
        }
        if fee_ledger.gross != gross_from_marketplace {
            return Err(ValidationError::Invariant(
                "fee ledger gross must match marketplace gross",
            ));
        }
        Ok(Self {
            marketplace,
            external_order_id,
            internal_order,
            gross_from_marketplace,
            fee_ledger,
        })
    }
}

impl_getters!(ChannelPricePolicy {
    min_price: Money,
    max_price: Money,
});

impl_getters!(SyncedMarketplaceListing { stock: StockState });

impl_getters!(SafeProductFeedLine {
    sku: Sku,
    channel: SalesChannel,
    price: Money,
    currency: Currency,
    stock: Quantity,
    stock_state: StockState,
    price_policy: ChannelPricePolicy,
});

impl_getters!(MarketplaceFeeLedger {
    gross: Money,
    fee_rate: BasisPoints,
    fee_rounding_mode: RoundingMode,
    fee: Money,
    payout: Money,
});

impl_getters!(MarketplacePayoutCalculation {
    gross: Money,
    payout_rate: BasisPoints,
    payout_rounding_mode: RoundingMode,
    payout: Money,
});

impl_getters!(MarketplaceOrder {
    marketplace: Marketplace,
    external_order_id: MarketplaceOrderId,
    internal_order: Order,
    gross_from_marketplace: Money,
    fee_ledger: MarketplaceFeeLedger,
});