Skip to main content

max / makenotwork

8.9 KB · 223 lines History Blame Raw
1 //! SyncKit developer billing: pricing formula and constants.
2 //!
3 //! Two modes:
4 //!
5 //! bulk, price = storage_gb_cap × $0.03
6 //! per_key, price = key_cap × gb_per_key × $0.03
7 //!
8 //! Both are pure GB-based pricing. Egress and ingress are absorbed by the
9 //! storage rate's margin against Hetzner Object Storage (~$0.0065/GB), where
10 //! SyncKit blobs are hosted in the `synckit` bucket.
11 //!
12 //! Invoices are floored at a Stripe-fee-cover threshold so we never lose money
13 //! on a transaction. See `BASE_FLOOR_CENTS` for the math.
14
15 /// Storage rate in cents per GB per month. Runs ~4.6× the Hetzner Object
16 /// Storage cost behind it (€5.99/TB/month, ~$0.0065/GB at 1.09 USD/EUR, with
17 /// 1 TB of egress included per TB stored). That spread absorbs any
18 /// ingress/egress variance, so we don't need a separate egress price. The rate
19 /// was calibrated against Cloudflare R2 and documented as ~2× cost until
20 /// 2026-07-27; the blobs were never on R2, so the basis was wrong, not the
21 /// price. See `mnw-biz-synckit-pricing` in the wiki.
22 pub const STORAGE_RATE_CENTS_PER_GB: i64 = 3;
23
24 /// Upper bound on a developer-billing storage cap, in GB. Applies to the bulk
25 /// `storage_gb_cap` and to the `key_cap × gb_per_key` product in per_key mode.
26 /// 10 TiB, matches the end-user `MAX_CAP_BYTES` and the picker slider's `max`.
27 /// Without it, `validate_knobs` accepted any value `> 0`, so a developer could
28 /// provision a Stripe subscription priced in the billions per month.
29 pub const MAX_STORAGE_GB: i64 = 10 * 1024;
30
31 /// Stripe-fee-cover floor in cents. Stripe charges 2.9% + $0.30 per
32 /// successful charge. We pick the smallest invoice `F` (cents) such that the
33 /// remainder after Stripe fees is non-negative:
34 ///
35 /// F × (1 − 0.029) − 30 ≥ 0 ⇒ F ≥ 30 / 0.971 ⇒ F ≥ 30.9¢
36 ///
37 /// Round up to 31¢. At the floor, MNW nets ~$0, covered, not profitable.
38 pub const BASE_FLOOR_CENTS: i64 = 31;
39
40 /// Warning thresholds (percent of storage cap). Matches CHECK constraint on
41 /// `sync_app_usage_current.last_warning_pct`. Only storage is enforced, so
42 /// these thresholds apply to storage usage only.
43 pub const WARNING_THRESHOLDS_PCT: &[i16] = &[75, 90, 100];
44
45 /// Compute the monthly Stripe invoice amount in cents for a given knob set.
46 ///
47 /// In bulk mode: `storage_gb_cap` is set, others are `None`.
48 /// In per_key mode: `key_cap` and `gb_per_key` are set, `storage_gb_cap` is `None`.
49 ///
50 /// Floors at `BASE_FLOOR_CENTS` so we never invoice below the Stripe-fee
51 /// break-even amount.
52 pub fn monthly_price_cents(
53 enforcement_mode: crate::db::SyncEnforcementMode,
54 storage_gb_cap: Option<u32>,
55 key_cap: Option<u32>,
56 gb_per_key: Option<u32>,
57 ) -> i64 {
58 use crate::db::SyncEnforcementMode::{Bulk, PerKey};
59 // Pure integer-cents arithmetic. The rate is a whole number of cents and
60 // the caps are whole GB, so there is no fractional money to round; the old
61 // `(gb as f64 * 3.0).ceil()` was an unnecessary trip through f64. Saturating
62 // multiplies keep absurd admin-set caps from overflowing i64 instead of
63 // wrapping to a negative invoice. The match is exhaustive over the sealed
64 // enum, there is no unknown-mode arm that could silently price at the floor
65 // (Pay-S2); an invalid mode can't reach here because the column is
66 // CHECK-constrained and the type is parsed at the API boundary.
67 let gb: i64 = match enforcement_mode {
68 Bulk => i64::from(storage_gb_cap.unwrap_or(0)),
69 PerKey => {
70 let k = i64::from(key_cap.unwrap_or(0));
71 let g = i64::from(gb_per_key.unwrap_or(0));
72 k.saturating_mul(g)
73 }
74 };
75 let raw = gb.saturating_mul(STORAGE_RATE_CENTS_PER_GB);
76 raw.max(BASE_FLOOR_CENTS)
77 }
78
79 /// Storage cap in bytes for the given GB cap.
80 pub fn storage_cap_bytes(storage_gb: u32) -> i64 {
81 i64::from(storage_gb) * 1024 * 1024 * 1024
82 }
83
84 #[cfg(test)]
85 mod tests {
86 use super::*;
87 use crate::db::SyncEnforcementMode::{Bulk, PerKey};
88
89 // `validate_knobs`' bound tests in `routes/synckit/billing.rs` are written
90 // against the constant itself, so they hold whatever it says. Pin the value
91 // once, against the end-user cap it is documented to match: the two ceilings
92 // drifting apart is the failure that would otherwise be invisible.
93 #[test]
94 fn developer_storage_ceiling_matches_the_end_user_one() {
95 assert_eq!(MAX_STORAGE_GB, 10 * 1024, "10 TiB, in GiB");
96 assert_eq!(
97 MAX_STORAGE_GB * 1024 * 1024 * 1024,
98 crate::payments::synckit_app_pricing::MAX_CAP_BYTES,
99 );
100 }
101
102 #[test]
103 fn bulk_mode_pricing() {
104 // 100 GB bulk → 100 × 3 = 300 cents.
105 assert_eq!(monthly_price_cents(Bulk, Some(100), None, None), 300);
106 // 1000 GB → $30.
107 assert_eq!(monthly_price_cents(Bulk, Some(1000), None, None), 3000);
108 }
109
110 #[test]
111 fn per_key_mode_pricing() {
112 // 50 keys × 2 GB = 100 GB equivalent → 300 cents. Matches 100 GB bulk.
113 assert_eq!(monthly_price_cents(PerKey, None, Some(50), Some(2)), 300);
114 // 1000 keys × 1 GB → $30.
115 assert_eq!(monthly_price_cents(PerKey, None, Some(1000), Some(1)), 3000);
116 }
117
118 #[test]
119 fn floor_kicks_in_for_small_accounts() {
120 // 1 GB bulk → 3¢ raw, floored to 31¢.
121 assert_eq!(monthly_price_cents(Bulk, Some(1), None, None), 31);
122 // 10 GB → 30¢, also floored to 31¢ (one cent short).
123 assert_eq!(monthly_price_cents(Bulk, Some(10), None, None), 31);
124 // 11 GB → 33¢, above floor.
125 assert_eq!(monthly_price_cents(Bulk, Some(11), None, None), 33);
126 // 1 key × 1 GB → 3¢ raw, floored.
127 assert_eq!(monthly_price_cents(PerKey, None, Some(1), Some(1)), 31);
128 }
129
130 #[test]
131 fn heavy_workload_pricing() {
132 // 10 TB bulk → 10240 × 3 = 30720¢ = $307.20.
133 assert_eq!(monthly_price_cents(Bulk, Some(10_240), None, None), 30_720);
134 // 10k keys × 1 GB → same.
135 assert_eq!(
136 monthly_price_cents(PerKey, None, Some(10_000), Some(1)),
137 30_000
138 );
139 }
140
141 #[test]
142 fn missing_knobs_drop_to_floor() {
143 // Mode is set but no knobs provided, should hit the floor.
144 assert_eq!(
145 monthly_price_cents(Bulk, None, None, None),
146 BASE_FLOOR_CENTS
147 );
148 assert_eq!(
149 monthly_price_cents(PerKey, None, None, None),
150 BASE_FLOOR_CENTS
151 );
152 }
153
154 // (The former `unknown_mode_drops_to_floor` test is gone: `enforcement_mode`
155 // is now a sealed enum, so an unrecognized mode is unrepresentable, the
156 // Pay-S2 fail-open it guarded against can no longer be written.)
157
158 #[test]
159 fn floor_amount_covers_stripe_fee() {
160 // 31¢ × 0.971 = 30.10¢, minus 30¢ fixed fee = 0.10¢ net. Verifies the
161 // documented math: the floor covers Stripe's fee with ~0 margin.
162 let net = (BASE_FLOOR_CENTS as f64) * 0.971 - 30.0;
163 assert!(
164 net >= 0.0,
165 "floor must net ≥ 0 after Stripe fees, got {net}"
166 );
167 assert!(net < 1.0, "floor should be tight, not overshoot, got {net}");
168 }
169
170 #[test]
171 fn storage_cap_in_bytes() {
172 assert_eq!(storage_cap_bytes(10), 10 * 1024 * 1024 * 1024);
173 }
174
175 // ── Edge cases (test-fuzz) ──
176
177 #[test]
178 fn pricing_at_u32_max_does_not_panic() {
179 // u32::MAX GB × 3¢ ≈ 1.3e10 cents, fits in i64. The cast must not panic.
180 let p = monthly_price_cents(Bulk, Some(u32::MAX), None, None);
181 assert!(p > 0, "huge price should be positive, got {p}");
182 }
183
184 #[test]
185 fn per_key_pricing_at_u32_max_saturates_cleanly() {
186 // u32::MAX × u32::MAX overflows f64 precision but Rust's f64-as-i64 cast
187 // saturates at i64::MAX rather than UB. Must not panic.
188 let p = monthly_price_cents(PerKey, None, Some(u32::MAX), Some(u32::MAX));
189 assert!(p > 0, "saturated price should still be positive, got {p}");
190 }
191
192 #[test]
193 fn storage_cap_at_u32_max_fits_in_i64() {
194 // u32::MAX × 2^30 = ~4.6e18, well under i64::MAX (~9.2e18).
195 let bytes = storage_cap_bytes(u32::MAX);
196 assert!(bytes > 0, "u32::MAX GB should produce a positive i64");
197 assert_eq!(bytes, (u32::MAX as i64) * 1024 * 1024 * 1024);
198 }
199
200 #[test]
201 fn bulk_with_zero_gb_drops_to_floor() {
202 // Defensive: validate_knobs rejects gb=0 at the route layer, but the
203 // pure function should still produce the floor rather than 0.
204 assert_eq!(
205 monthly_price_cents(Bulk, Some(0), None, None),
206 BASE_FLOOR_CENTS
207 );
208 }
209
210 #[test]
211 fn per_key_one_dimension_zero_drops_to_floor() {
212 // If only one of key_cap/gb_per_key is 0, the product is 0 → floor.
213 assert_eq!(
214 monthly_price_cents(PerKey, None, Some(0), Some(10)),
215 BASE_FLOOR_CENTS
216 );
217 assert_eq!(
218 monthly_price_cents(PerKey, None, Some(10), Some(0)),
219 BASE_FLOOR_CENTS
220 );
221 }
222 }
223