use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalResponse {
pub id: String,
pub agent_id: String,
pub action: String,
pub reason: String,
pub status: String,
pub created_at: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeoutColor {
Red,
Yellow,
Green,
}
pub fn compute_timeout_color(remaining_secs: i64) -> TimeoutColor {
if remaining_secs < 60 {
TimeoutColor::Red
} else if remaining_secs <= 180 {
TimeoutColor::Yellow
} else {
TimeoutColor::Green
}
}
pub fn format_countdown(remaining_secs: i64) -> String {
if remaining_secs <= 0 {
return "expired".to_string();
}
let mins = remaining_secs / 60;
let secs = remaining_secs % 60;
if mins > 0 {
format!("{mins}m {secs:02}s")
} else {
format!("{secs}s")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound = "T: Serialize + for<'a> Deserialize<'a>")]
pub struct PaginatedResponse<T> {
pub items: Vec<T>,
pub page: u64,
pub per_page: u64,
pub total: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn approval_response_deserializes_from_json() {
let json = r#"{
"id": "abc-123",
"agent_id": "support-agent",
"action": "process_refund",
"reason": "amount > $100",
"status": "pending",
"created_at": "2026-04-30T10:00:00Z"
}"#;
let resp: ApprovalResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.id, "abc-123");
assert_eq!(resp.agent_id, "support-agent");
assert_eq!(resp.action, "process_refund");
assert_eq!(resp.reason, "amount > $100");
assert_eq!(resp.status, "pending");
assert_eq!(resp.created_at, "2026-04-30T10:00:00Z");
}
#[test]
fn paginated_response_deserializes_from_json() {
let json = r#"{
"items": [{
"id": "abc-123",
"agent_id": "support-agent",
"action": "process_refund",
"reason": "amount > $100",
"status": "pending",
"created_at": "2026-04-30T10:00:00Z"
}],
"page": 1,
"per_page": 20,
"total": 1
}"#;
let resp: PaginatedResponse<ApprovalResponse> = serde_json::from_str(json).unwrap();
assert_eq!(resp.items.len(), 1);
assert_eq!(resp.page, 1);
assert_eq!(resp.per_page, 20);
assert_eq!(resp.total, 1);
assert_eq!(resp.items[0].id, "abc-123");
}
#[test]
fn compute_timeout_color_red_below_60() {
assert_eq!(compute_timeout_color(0), TimeoutColor::Red);
assert_eq!(compute_timeout_color(59), TimeoutColor::Red);
assert_eq!(compute_timeout_color(-10), TimeoutColor::Red);
}
#[test]
fn compute_timeout_color_yellow_60_to_180() {
assert_eq!(compute_timeout_color(60), TimeoutColor::Yellow);
assert_eq!(compute_timeout_color(120), TimeoutColor::Yellow);
assert_eq!(compute_timeout_color(180), TimeoutColor::Yellow);
}
#[test]
fn compute_timeout_color_green_above_180() {
assert_eq!(compute_timeout_color(181), TimeoutColor::Green);
assert_eq!(compute_timeout_color(300), TimeoutColor::Green);
}
#[test]
fn format_countdown_with_minutes_and_seconds() {
assert_eq!(format_countdown(150), "2m 30s");
}
#[test]
fn format_countdown_seconds_only() {
assert_eq!(format_countdown(45), "45s");
}
#[test]
fn format_countdown_expired() {
assert_eq!(format_countdown(0), "expired");
assert_eq!(format_countdown(-5), "expired");
}
}