Skip to main content

commerce_theory/
post_purchase.rs

1use crate::event_sourcing::*;
2use crate::foundation::*;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub enum SubscriptionLifecycleStatus {
7    Active,
8    Paused,
9    PastDue,
10    Cancelled,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct SubscriptionPlan {
16    pub(crate) price: Money,
17    pub(crate) period_days: Days,
18}
19
20impl SubscriptionPlan {
21    pub fn try_new(price: Money, period_days: Days) -> DomainResult<Self> {
22        if period_days <= Duration::ZERO {
23            return Err(ValidationError::Invariant(
24                "subscription period must be positive",
25            ));
26        }
27        Ok(Self { price, period_days })
28    }
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct RecurringSubscription {
34    pub(crate) customer: CustomerId,
35    pub(crate) plan: SubscriptionPlan,
36    pub(crate) status: SubscriptionLifecycleStatus,
37    pub(crate) current_billing_date: Timestamp,
38    pub(crate) next_billing_date: Timestamp,
39}
40
41impl RecurringSubscription {
42    pub fn try_new(
43        customer: CustomerId,
44        plan: SubscriptionPlan,
45        status: SubscriptionLifecycleStatus,
46        current_billing_date: Timestamp,
47        next_billing_date: Timestamp,
48    ) -> DomainResult<Self> {
49        if current_billing_date >= next_billing_date {
50            return Err(ValidationError::Invariant(
51                "next billing date must be after current date",
52            ));
53        }
54        Ok(Self {
55            customer,
56            plan,
57            status,
58            current_billing_date,
59            next_billing_date,
60        })
61    }
62}
63
64domain_struct! {
65    pub struct GiftCard {
66        balance: Money,
67        expires_at: Timestamp,
68    }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub struct GiftCardRedemption {
74    pub(crate) card: GiftCard,
75    pub(crate) amount: Money,
76}
77
78impl GiftCardRedemption {
79    pub const fn try_new(card: GiftCard, amount: Money) -> DomainResult<Self> {
80        if amount > card.balance {
81            return Err(ValidationError::Invariant(
82                "gift-card redemption exceeds balance",
83            ));
84        }
85        Ok(Self { card, amount })
86    }
87}
88
89#[must_use]
90pub const fn gift_card_balance_after_redeem(redemption: &GiftCardRedemption) -> Money {
91    nat_sub(redemption.card.balance, redemption.amount)
92}
93
94#[must_use]
95pub fn gift_card_valid_at(now: Timestamp, card: &GiftCard) -> bool {
96    now <= card.expires_at
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct Chargeback {
102    pub(crate) payment_amount: Money,
103    pub(crate) chargeback_amount: Money,
104}
105
106impl Chargeback {
107    pub const fn try_new(payment_amount: Money, chargeback_amount: Money) -> DomainResult<Self> {
108        if chargeback_amount > payment_amount {
109            return Err(ValidationError::Invariant(
110                "chargeback exceeds payment amount",
111            ));
112        }
113        Ok(Self {
114            payment_amount,
115            chargeback_amount,
116        })
117    }
118}
119
120domain_struct! {
121    pub struct CashflowEvent {
122        inflow: Money,
123        outflow: Money,
124    }
125}
126
127pub fn cashflow_inflows_total(events: &[CashflowEvent]) -> DomainResult<Money> {
128    checked_sum(
129        events.iter().map(|event| event.inflow),
130        "cashflow_inflows_total",
131    )
132}
133
134pub fn cashflow_outflows_total(events: &[CashflowEvent]) -> DomainResult<Money> {
135    checked_sum(
136        events.iter().map(|event| event.outflow),
137        "cashflow_outflows_total",
138    )
139}
140
141#[derive(Clone, Debug, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143pub struct CashflowPlan {
144    pub(crate) starting_cash: Money,
145    pub(crate) required_reserve: Money,
146    pub(crate) expected_inflows: Money,
147    pub(crate) expected_outflows: Money,
148}
149
150impl CashflowPlan {
151    pub fn try_new(
152        starting_cash: Money,
153        required_reserve: Money,
154        expected_inflows: Money,
155        expected_outflows: Money,
156    ) -> DomainResult<Self> {
157        if checked_add(required_reserve, expected_outflows, "cashflow reserve")?
158            > checked_add(starting_cash, expected_inflows, "cashflow available")?
159        {
160            return Err(ValidationError::Invariant("cashflow reserve is unsafe"));
161        }
162        Ok(Self {
163            starting_cash,
164            required_reserve,
165            expected_inflows,
166            expected_outflows,
167        })
168    }
169}
170
171#[derive(Clone, Debug, PartialEq, Eq)]
172#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173pub struct EventBackedCashflowPlan {
174    pub(crate) starting_cash: Money,
175    pub(crate) required_reserve: Money,
176    pub(crate) events: Vec<CashflowEvent>,
177}
178
179impl EventBackedCashflowPlan {
180    pub fn try_new(
181        starting_cash: Money,
182        required_reserve: Money,
183        events: Vec<CashflowEvent>,
184    ) -> DomainResult<Self> {
185        if checked_add(
186            required_reserve,
187            cashflow_outflows_total(&events)?,
188            "event-backed outflows",
189        )? > checked_add(
190            starting_cash,
191            cashflow_inflows_total(&events)?,
192            "event-backed inflows",
193        )? {
194            return Err(ValidationError::Invariant(
195                "event-backed cashflow reserve is unsafe",
196            ));
197        }
198        Ok(Self {
199            starting_cash,
200            required_reserve,
201            events,
202        })
203    }
204}
205
206pub(crate) const fn _event_anchor(_: Option<DomainEvent>) {}
207
208impl_getters!(SubscriptionPlan {
209    price: Money,
210    period_days: Days,
211});
212
213impl_getters!(RecurringSubscription {
214    customer: CustomerId,
215    plan: SubscriptionPlan,
216    status: SubscriptionLifecycleStatus,
217    current_billing_date: Timestamp,
218    next_billing_date: Timestamp,
219});
220
221impl_getters!(GiftCardRedemption {
222    card: GiftCard,
223    amount: Money,
224});
225
226impl_getters!(Chargeback {
227    payment_amount: Money,
228    chargeback_amount: Money,
229});
230
231impl_getters!(CashflowPlan {
232    starting_cash: Money,
233    required_reserve: Money,
234    expected_inflows: Money,
235    expected_outflows: Money,
236});
237
238impl_getters!(EventBackedCashflowPlan {
239    starting_cash: Money,
240    required_reserve: Money,
241    events: Vec<CashflowEvent>,
242});