commerce-theory 0.1.2

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

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

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SubscriptionPlan {
    pub(crate) price: Money,
    pub(crate) period_days: Days,
}

impl SubscriptionPlan {
    pub fn try_new(price: Money, period_days: Days) -> DomainResult<Self> {
        if period_days <= Duration::ZERO {
            return Err(ValidationError::Invariant(
                "subscription period must be positive",
            ));
        }
        Ok(Self { price, period_days })
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RecurringSubscription {
    pub(crate) customer: CustomerId,
    pub(crate) plan: SubscriptionPlan,
    pub(crate) status: SubscriptionLifecycleStatus,
    pub(crate) current_billing_date: Timestamp,
    pub(crate) next_billing_date: Timestamp,
}

impl RecurringSubscription {
    pub fn try_new(
        customer: CustomerId,
        plan: SubscriptionPlan,
        status: SubscriptionLifecycleStatus,
        current_billing_date: Timestamp,
        next_billing_date: Timestamp,
    ) -> DomainResult<Self> {
        if current_billing_date >= next_billing_date {
            return Err(ValidationError::Invariant(
                "next billing date must be after current date",
            ));
        }
        Ok(Self {
            customer,
            plan,
            status,
            current_billing_date,
            next_billing_date,
        })
    }
}

domain_struct! {
    pub struct GiftCard {
        balance: Money,
        expires_at: Timestamp,
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GiftCardRedemption {
    pub(crate) card: GiftCard,
    pub(crate) amount: Money,
}

impl GiftCardRedemption {
    pub const fn try_new(card: GiftCard, amount: Money) -> DomainResult<Self> {
        if amount > card.balance {
            return Err(ValidationError::Invariant(
                "gift-card redemption exceeds balance",
            ));
        }
        Ok(Self { card, amount })
    }
}

#[must_use]
pub const fn gift_card_balance_after_redeem(redemption: &GiftCardRedemption) -> Money {
    nat_sub(redemption.card.balance, redemption.amount)
}

#[must_use]
pub fn gift_card_valid_at(now: Timestamp, card: &GiftCard) -> bool {
    now <= card.expires_at
}

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

impl Chargeback {
    pub const fn try_new(payment_amount: Money, chargeback_amount: Money) -> DomainResult<Self> {
        if chargeback_amount > payment_amount {
            return Err(ValidationError::Invariant(
                "chargeback exceeds payment amount",
            ));
        }
        Ok(Self {
            payment_amount,
            chargeback_amount,
        })
    }
}

domain_struct! {
    pub struct CashflowEvent {
        inflow: Money,
        outflow: Money,
    }
}

pub fn cashflow_inflows_total(events: &[CashflowEvent]) -> DomainResult<Money> {
    checked_sum(
        events.iter().map(|event| event.inflow),
        "cashflow_inflows_total",
    )
}

pub fn cashflow_outflows_total(events: &[CashflowEvent]) -> DomainResult<Money> {
    checked_sum(
        events.iter().map(|event| event.outflow),
        "cashflow_outflows_total",
    )
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CashflowPlan {
    pub(crate) starting_cash: Money,
    pub(crate) required_reserve: Money,
    pub(crate) expected_inflows: Money,
    pub(crate) expected_outflows: Money,
}

impl CashflowPlan {
    pub fn try_new(
        starting_cash: Money,
        required_reserve: Money,
        expected_inflows: Money,
        expected_outflows: Money,
    ) -> DomainResult<Self> {
        if checked_add(required_reserve, expected_outflows, "cashflow reserve")?
            > checked_add(starting_cash, expected_inflows, "cashflow available")?
        {
            return Err(ValidationError::Invariant("cashflow reserve is unsafe"));
        }
        Ok(Self {
            starting_cash,
            required_reserve,
            expected_inflows,
            expected_outflows,
        })
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EventBackedCashflowPlan {
    pub(crate) starting_cash: Money,
    pub(crate) required_reserve: Money,
    pub(crate) events: Vec<CashflowEvent>,
}

impl EventBackedCashflowPlan {
    pub fn try_new(
        starting_cash: Money,
        required_reserve: Money,
        events: Vec<CashflowEvent>,
    ) -> DomainResult<Self> {
        if checked_add(
            required_reserve,
            cashflow_outflows_total(&events)?,
            "event-backed outflows",
        )? > checked_add(
            starting_cash,
            cashflow_inflows_total(&events)?,
            "event-backed inflows",
        )? {
            return Err(ValidationError::Invariant(
                "event-backed cashflow reserve is unsafe",
            ));
        }
        Ok(Self {
            starting_cash,
            required_reserve,
            events,
        })
    }
}

pub(crate) const fn _event_anchor(_: Option<DomainEvent>) {}

impl_getters!(SubscriptionPlan {
    price: Money,
    period_days: Days,
});

impl_getters!(RecurringSubscription {
    customer: CustomerId,
    plan: SubscriptionPlan,
    status: SubscriptionLifecycleStatus,
    current_billing_date: Timestamp,
    next_billing_date: Timestamp,
});

impl_getters!(GiftCardRedemption {
    card: GiftCard,
    amount: Money,
});

impl_getters!(Chargeback {
    payment_amount: Money,
    chargeback_amount: Money,
});

impl_getters!(CashflowPlan {
    starting_cash: Money,
    required_reserve: Money,
    expected_inflows: Money,
    expected_outflows: Money,
});

impl_getters!(EventBackedCashflowPlan {
    starting_cash: Money,
    required_reserve: Money,
    events: Vec<CashflowEvent>,
});