use anyhow::{Result, bail};
use serde_json::{Map, Value, json};
pub(crate) fn application_fee(percent: Option<f64>, amount: Option<u64>) -> Result<Option<Value>> {
let mut fee = Map::new();
if let Some(percent) = percent {
if !percent.is_finite() || !(0.0..100.0).contains(&percent) {
bail!(
"the fee percent must be from 0 to under 100 (got {})",
percent
);
}
fee.insert("percent".to_string(), json!(percent));
}
if let Some(amount) = amount {
fee.insert("amount".to_string(), json!(amount));
}
Ok(if fee.is_empty() {
None
} else {
Some(Value::Object(fee))
})
}
pub(crate) fn voice_flow(pair: &str) -> Result<(String, String)> {
let Some((name, id)) = pair.split_once('=') else {
bail!(
"--voice-flow '{}' must be name=contact-flow-id, e.g. appointment_reminder=0f1e2d3c-…",
pair
);
};
let (name, id) = (name.trim(), id.trim());
let valid_name = name.chars().next().is_some_and(|c| c.is_ascii_lowercase())
&& name.len() <= 64
&& name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-');
if !valid_name {
bail!(
"voice flow name '{}' must be lowercase letters, digits, _ or - (starting with a letter)",
name
);
}
if id.is_empty() {
bail!("--voice-flow '{}' has no contact flow id after '='", pair);
}
Ok((name.to_string(), id.to_string()))
}
fn num(value: &Value, key: &str) -> u64 {
value.get(key).and_then(Value::as_u64).unwrap_or(0)
}
fn str_of<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
value.get(key).and_then(Value::as_str)
}
fn fee_text(fee: Option<&Value>) -> String {
let Some(fee) = fee else {
return "no application fee".to_string();
};
let percent = fee.get("percent").and_then(Value::as_f64);
let amount = fee.get("amount").and_then(Value::as_u64);
match (percent, amount) {
(Some(p), Some(a)) => format!("fee {}% / {} minor units", p, a),
(Some(p), None) => format!("fee {}%", p),
(None, Some(a)) => format!("fee {} minor units", a),
(None, None) => "no application fee".to_string(),
}
}
pub(crate) fn describe(feature: &str, settings: &Value) -> String {
let overridden = settings.get("override").is_some_and(|o| !o.is_null());
let suffix = if overridden {
" (instance override)"
} else {
""
};
let line = match feature {
"sms" => {
if settings.get("enabled").and_then(Value::as_bool) == Some(false) {
"off for this product".to_string()
} else {
format!(
"from {}, {} segments/month, {} texts/min, promotional {}",
str_of(settings, "originationIdentity").unwrap_or("(no pool configured)"),
num(settings, "monthlySegmentCap"),
num(settings, "messagesPerMinute"),
if settings.get("allowPromotional").and_then(Value::as_bool) == Some(true) {
"allowed"
} else {
"not allowed"
}
)
}
}
"email" => format!(
"{} recipients/day, {} messages/min",
num(settings, "dailyQuota"),
num(settings, "perMinute")
),
"payments" => format!(
"{} (from {}), {} Stripe calls/min",
fee_text(settings.get("applicationFee")),
str_of(settings, "applicationFeeSource").unwrap_or("none"),
num(settings, "requestsPerMinute")
),
"voice" => {
let flows: Vec<&str> = settings
.get("flows")
.and_then(Value::as_array)
.map(|f| f.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
if settings.get("enabled").and_then(Value::as_bool) == Some(false) {
"off (the product has no flows)".to_string()
} else {
format!(
"flows {}, {} concurrent calls, {} minutes/month",
flows.join(", "),
num(settings, "maxConcurrentCalls"),
num(settings, "monthlyMinutes")
)
}
}
"whatsapp" => {
if settings.get("linked").and_then(Value::as_bool) != Some(true) {
"no number linked".to_string()
} else {
format!(
"{} ({} number){}, {} sends/min",
str_of(settings, "originationPhoneNumberId").unwrap_or("-"),
str_of(settings, "source").unwrap_or("-"),
if settings.get("enabled").and_then(Value::as_bool) == Some(true) {
""
} else {
" — turned off"
},
num(settings, "requestsPerMinute")
)
}
}
_ => settings.to_string(),
};
format!("{}{}", line, suffix)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fee_is_a_percent_under_100_and_or_an_amount() {
assert_eq!(application_fee(None, None).unwrap(), None);
assert_eq!(
application_fee(Some(2.5), Some(30)).unwrap(),
Some(json!({ "percent": 2.5, "amount": 30 }))
);
assert_eq!(
application_fee(Some(0.0), None).unwrap(),
Some(json!({ "percent": 0.0 }))
);
assert!(application_fee(Some(100.0), None).is_err());
assert!(application_fee(Some(-1.0), None).is_err());
}
#[test]
fn a_voice_flow_is_a_catalog_name_and_an_id() {
assert_eq!(
voice_flow(" reminder = flow-1 ").unwrap(),
("reminder".to_string(), "flow-1".to_string())
);
assert!(voice_flow("reminder").is_err());
assert!(voice_flow("Reminder=flow-1").is_err());
assert!(voice_flow("reminder=").is_err());
}
#[test]
fn describes_what_is_in_force_and_whether_it_is_overridden() {
assert_eq!(
describe(
"email",
&json!({ "dailyQuota": 500, "perMinute": 10, "override": { "dailyQuota": 500 } })
),
"500 recipients/day, 10 messages/min (instance override)"
);
assert_eq!(
describe(
"payments",
&json!({
"applicationFee": { "percent": 3 },
"applicationFeeSource": "env",
"requestsPerMinute": 300
})
),
"fee 3% (from env), 300 Stripe calls/min"
);
assert_eq!(
describe(
"whatsapp",
&json!({ "linked": false, "requestsPerMinute": 30 })
),
"no number linked"
);
assert_eq!(
describe("voice", &json!({ "enabled": false, "flows": [] })),
"off (the product has no flows)"
);
}
}