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});