Skip to main content

max / makenotwork

3.7 KB · 112 lines History Blame Raw
1 //! Stripe billing helpers: fee estimation and timestamp conversion.
2
3 /// Convert a Unix timestamp from Stripe into a UTC datetime, falling back to now.
4 pub fn stripe_timestamp(ts: i64) -> chrono::DateTime<chrono::Utc> {
5 chrono::DateTime::from_timestamp(ts, 0).unwrap_or_else(chrono::Utc::now)
6 }
7
8 /// Estimate Stripe's processing fee and the net amount the creator receives.
9 ///
10 /// Returns `(fee_cents, creator_receives_cents)`. Uses the standard
11 /// Stripe rate from [`constants`](crate::constants).
12 ///
13 /// # US pricing only
14 ///
15 /// The rate here (2.9% + 30c) is Stripe's US card pricing. Stripe charges
16 /// differently by country and currency — 1.5% + 20p in the UK, 1.5% + EUR 0.25
17 /// in much of Europe — and MNW does not model that. So this estimate is only
18 /// meaningful for a creator settling in USD, and callers must not show it to
19 /// anyone else: see [`stripe_fee_estimate_applies`]. Quoting a US fee to a
20 /// British creator would be a specific wrong number, which is worse than
21 /// saying nothing.
22 pub fn estimate_stripe_fee(price_cents: i32) -> (i32, i32) {
23 if price_cents <= 0 {
24 return (0, 0);
25 }
26 let fee = (price_cents as f64 * crate::constants::STRIPE_FEE_PERCENTAGE
27 + crate::constants::STRIPE_FEE_FIXED_CENTS) as i32;
28 let creator_receives = (price_cents - fee).max(0);
29 (fee.min(price_cents), creator_receives)
30 }
31
32 /// Whether the [`estimate_stripe_fee`] figure can honestly be shown.
33 ///
34 /// Only for USD, because that is the only pricing this codebase models. For
35 /// every other currency the surface should say that Stripe's processing fee
36 /// applies without putting a number on it.
37 pub fn stripe_fee_estimate_applies(currency: crate::currency::SettlementCurrency) -> bool {
38 currency == crate::currency::SettlementCurrency::Usd
39 }
40
41 #[cfg(test)]
42 mod tests {
43 use super::*;
44
45 // ── estimate_stripe_fee ──
46
47 #[test]
48 fn stripe_fee_standard_price() {
49 let (fee, receives) = estimate_stripe_fee(1000);
50 assert_eq!(fee, 59);
51 assert_eq!(receives, 941);
52 }
53
54 #[test]
55 fn stripe_fee_small_price() {
56 let (fee, receives) = estimate_stripe_fee(100);
57 assert_eq!(fee, 32);
58 assert_eq!(receives, 68);
59 }
60
61 #[test]
62 fn stripe_fee_zero_is_free() {
63 let (fee, receives) = estimate_stripe_fee(0);
64 assert_eq!(fee, 0);
65 assert_eq!(receives, 0);
66 }
67
68 #[test]
69 fn stripe_fee_negative_price() {
70 let (fee, receives) = estimate_stripe_fee(-100);
71 assert_eq!(fee, 0);
72 assert_eq!(receives, 0);
73 }
74
75 #[test]
76 fn stripe_fee_plus_receives_equals_price() {
77 for price in [50, 100, 250, 500, 999, 1000, 2500, 5000, 10000, 50000] {
78 let (fee, receives) = estimate_stripe_fee(price);
79 assert_eq!(
80 fee + receives,
81 price,
82 "fee + receives should equal price for {price} cents"
83 );
84 }
85 }
86
87 // ── stripe_timestamp ──
88
89 #[test]
90 fn stripe_timestamp_zero() {
91 assert_eq!(stripe_timestamp(0).timestamp(), 0);
92 }
93
94 #[test]
95 fn stripe_timestamp_valid() {
96 assert_eq!(stripe_timestamp(1_714_400_000).timestamp(), 1_714_400_000);
97 }
98
99 // ── Property-based tests ──
100
101 proptest::proptest! {
102 #[test]
103 fn prop_stripe_fee_invariant(price in 1..=1_000_000i32) {
104 let (fee, receives) = estimate_stripe_fee(price);
105 proptest::prop_assert_eq!(fee + receives, price,
106 "fee ({}) + receives ({}) must equal price ({})", fee, receives, price);
107 proptest::prop_assert!(fee > 0, "Fee should be positive for price {}", price);
108 proptest::prop_assert!(receives >= 0, "Receives should be non-negative");
109 }
110 }
111 }
112