Skip to main content

max / makenotwork

4.5 KB · 115 lines History Blame Raw
1 //! Validators for subscription tiers and payment-related fields.
2
3 use super::limits;
4 use crate::error::AppError;
5
6 /// Validate a Stripe Connect account ID.
7 ///
8 /// Stripe Connect account IDs are `acct_` followed by an alphanumeric token.
9 /// We only ever store values Stripe minted for us (via `create_connect_account`
10 /// or the account webhook), so this is a shape check that fences off obviously
11 /// wrong values entering the [`crate::db::validated_types::StripeAccountId`]
12 /// newtype at an input boundary, not a claim of Stripe-side existence.
13 pub fn validate_stripe_account_id(id: &str) -> Result<(), AppError> {
14 if !id.starts_with("acct_")
15 || id.len() < 6
16 || id.len() > 255
17 || !id["acct_".len()..]
18 .chars()
19 .all(|c| c.is_ascii_alphanumeric())
20 {
21 return Err(AppError::validation(
22 "Invalid Stripe account ID".to_string(),
23 ));
24 }
25 Ok(())
26 }
27
28 /// Validate a subscription tier name
29 pub fn validate_tier_name(name: &str) -> Result<(), AppError> {
30 if name.is_empty() {
31 return Err(AppError::validation("Tier name is required".to_string()));
32 }
33 if name.chars().count() > limits::TIER_NAME_MAX {
34 return Err(AppError::validation(format!(
35 "Tier name must be {} characters or less",
36 limits::TIER_NAME_MAX
37 )));
38 }
39 // Single-line: a tier name can reach an email subject (Postmark rejects
40 // CRLF, but reject at source), so no line breaks or control chars.
41 super::reject_control_chars("Tier name", name)?;
42 Ok(())
43 }
44
45 /// Validate a subscription tier description
46 pub fn validate_tier_description(description: &str) -> Result<(), AppError> {
47 if description.chars().count() > limits::TIER_DESCRIPTION_MAX {
48 return Err(AppError::validation(format!(
49 "Tier description must be {} characters or less",
50 limits::TIER_DESCRIPTION_MAX
51 )));
52 }
53 super::reject_control_chars_multiline("Tier description", description)?;
54 Ok(())
55 }
56
57 /// Validate a subscription tier price in cents (must be at least $1.00)
58 pub fn validate_tier_price(
59 price_cents: i32,
60 currency: crate::currency::SettlementCurrency,
61 ) -> Result<(), AppError> {
62 if price_cents < crate::constants::MIN_SUBSCRIPTION_PRICE_CENTS {
63 return Err(AppError::validation(format!(
64 "Subscription price must be at least {}",
65 crate::formatting::format_revenue(
66 crate::constants::MIN_SUBSCRIPTION_PRICE_CENTS as i64,
67 currency
68 )
69 )));
70 }
71 // Currency-relative: the ceiling is a round 10,000 in the creator's own
72 // currency, not a USD equivalence (which would need an FX table).
73 let ceiling = currency.max_price_cents();
74 if price_cents > ceiling {
75 return Err(AppError::validation(format!(
76 "Subscription price cannot exceed {}",
77 crate::formatting::format_revenue(ceiling as i64, currency)
78 )));
79 }
80 Ok(())
81 }
82
83 #[cfg(test)]
84 mod tests {
85 use super::*;
86
87 #[test]
88 fn test_validate_tier_name() {
89 assert!(validate_tier_name("Basic Tier").is_ok());
90 assert!(validate_tier_name("X").is_ok());
91 assert!(validate_tier_name("").is_err()); // empty
92 assert!(validate_tier_name(&"a".repeat(100)).is_ok()); // at limit
93 assert!(validate_tier_name(&"a".repeat(101)).is_err()); // over limit
94 }
95
96 #[test]
97 fn test_validate_tier_description() {
98 assert!(validate_tier_description("Access to all content").is_ok());
99 assert!(validate_tier_description("").is_ok()); // empty is valid
100 assert!(validate_tier_description(&"a".repeat(2000)).is_ok()); // at limit
101 assert!(validate_tier_description(&"a".repeat(2001)).is_err()); // over limit
102 }
103
104 #[test]
105 fn test_validate_tier_price() {
106 assert!(validate_tier_price(100, crate::currency::SettlementCurrency::Usd).is_ok()); // $1.00 minimum
107 assert!(validate_tier_price(999, crate::currency::SettlementCurrency::Usd).is_ok());
108 assert!(validate_tier_price(1_000_000, crate::currency::SettlementCurrency::Usd).is_ok()); // $10,000
109 assert!(validate_tier_price(99, crate::currency::SettlementCurrency::Usd).is_err()); // below minimum
110 assert!(validate_tier_price(0, crate::currency::SettlementCurrency::Usd).is_err()); // zero
111 assert!(validate_tier_price(-1, crate::currency::SettlementCurrency::Usd).is_err()); // negative
112 assert!(validate_tier_price(1_000_001, crate::currency::SettlementCurrency::Usd).is_err()); // over cap
113 }
114 }
115