1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Company {
12 pub id: String,
14 pub name: String,
16 #[serde(default)]
18 pub description: String,
19 pub created_at: DateTime<Utc>,
21}
22
23impl Company {
24 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
26 Self {
27 id: id.into(),
28 name: name.into(),
29 description: String::new(),
30 created_at: Utc::now(),
31 }
32 }
33}
34
35#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum OrgRole {
44 Ceo,
46 Manager,
48 Worker,
50 Custom(String),
52}
53
54impl std::fmt::Display for OrgRole {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 OrgRole::Ceo => write!(f, "ceo"),
58 OrgRole::Manager => write!(f, "manager"),
59 OrgRole::Worker => write!(f, "worker"),
60 OrgRole::Custom(name) => write!(f, "custom({name})"),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct OrgNode {
68 pub agent_id: String,
70 pub role: OrgRole,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub reports_to: Option<String>,
75 #[serde(default)]
77 pub budget_cents: u64,
78 #[serde(default)]
80 pub goals: Vec<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct OrgChart {
86 pub company_id: String,
88 pub nodes: Vec<OrgNode>,
90}
91
92impl OrgChart {
93 pub fn new(company_id: impl Into<String>) -> Self {
95 Self {
96 company_id: company_id.into(),
97 nodes: Vec::new(),
98 }
99 }
100
101 pub fn direct_reports(&self, agent_id: &str) -> Vec<&OrgNode> {
103 self.nodes
104 .iter()
105 .filter(|n| n.reports_to.as_deref() == Some(agent_id))
106 .collect()
107 }
108
109 pub fn find_agent(&self, agent_id: &str) -> Option<&OrgNode> {
111 self.nodes.iter().find(|n| n.agent_id == agent_id)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn company_new() {
121 let c = Company::new("acme-1", "Acme Corp");
122 assert_eq!(c.id, "acme-1");
123 assert_eq!(c.name, "Acme Corp");
124 assert!(c.description.is_empty());
125 }
126
127 #[test]
128 fn company_serde_roundtrip() {
129 let c = Company {
130 id: "co-1".into(),
131 name: "Test Inc".into(),
132 description: "A test company".into(),
133 created_at: Utc::now(),
134 };
135 let json = serde_json::to_string(&c).unwrap();
136 let restored: Company = serde_json::from_str(&json).unwrap();
137 assert_eq!(restored.id, "co-1");
138 assert_eq!(restored.name, "Test Inc");
139 assert_eq!(restored.description, "A test company");
140 }
141
142 #[test]
143 fn org_role_serde() {
144 let roles = [
145 (OrgRole::Ceo, "\"ceo\""),
146 (OrgRole::Manager, "\"manager\""),
147 (OrgRole::Worker, "\"worker\""),
148 ];
149 for (role, expected) in &roles {
150 let json = serde_json::to_string(role).unwrap();
151 assert_eq!(&json, expected);
152 let restored: OrgRole = serde_json::from_str(&json).unwrap();
153 assert_eq!(&restored, role);
154 }
155 }
156
157 #[test]
158 fn org_role_custom_serde() {
159 let role = OrgRole::Custom("intern".into());
160 let json = serde_json::to_string(&role).unwrap();
161 let restored: OrgRole = serde_json::from_str(&json).unwrap();
162 assert_eq!(restored, OrgRole::Custom("intern".into()));
163 }
164
165 #[test]
166 fn org_role_display() {
167 assert_eq!(OrgRole::Ceo.to_string(), "ceo");
168 assert_eq!(OrgRole::Manager.to_string(), "manager");
169 assert_eq!(OrgRole::Worker.to_string(), "worker");
170 assert_eq!(OrgRole::Custom("vp".into()).to_string(), "custom(vp)");
171 }
172
173 #[test]
174 fn org_node_serde_roundtrip() {
175 let node = OrgNode {
176 agent_id: "agent-a".into(),
177 role: OrgRole::Manager,
178 reports_to: Some("agent-ceo".into()),
179 budget_cents: 500_000,
180 goals: vec!["ship v2".into(), "reduce churn".into()],
181 };
182 let json = serde_json::to_string(&node).unwrap();
183 let restored: OrgNode = serde_json::from_str(&json).unwrap();
184 assert_eq!(restored.agent_id, "agent-a");
185 assert_eq!(restored.role, OrgRole::Manager);
186 assert_eq!(restored.reports_to.as_deref(), Some("agent-ceo"));
187 assert_eq!(restored.budget_cents, 500_000);
188 assert_eq!(restored.goals.len(), 2);
189 }
190
191 #[test]
192 fn org_node_omits_none_reports_to() {
193 let node = OrgNode {
194 agent_id: "ceo".into(),
195 role: OrgRole::Ceo,
196 reports_to: None,
197 budget_cents: 0,
198 goals: vec![],
199 };
200 let json = serde_json::to_string(&node).unwrap();
201 assert!(!json.contains("reports_to"));
202 }
203
204 #[test]
205 fn org_chart_new_is_empty() {
206 let chart = OrgChart::new("co-1");
207 assert_eq!(chart.company_id, "co-1");
208 assert!(chart.nodes.is_empty());
209 }
210
211 #[test]
212 fn org_chart_direct_reports() {
213 let chart = OrgChart {
214 company_id: "co-1".into(),
215 nodes: vec![
216 OrgNode {
217 agent_id: "ceo".into(),
218 role: OrgRole::Ceo,
219 reports_to: None,
220 budget_cents: 1_000_000,
221 goals: vec!["grow".into()],
222 },
223 OrgNode {
224 agent_id: "mgr-1".into(),
225 role: OrgRole::Manager,
226 reports_to: Some("ceo".into()),
227 budget_cents: 300_000,
228 goals: vec![],
229 },
230 OrgNode {
231 agent_id: "mgr-2".into(),
232 role: OrgRole::Manager,
233 reports_to: Some("ceo".into()),
234 budget_cents: 200_000,
235 goals: vec![],
236 },
237 OrgNode {
238 agent_id: "wk-1".into(),
239 role: OrgRole::Worker,
240 reports_to: Some("mgr-1".into()),
241 budget_cents: 0,
242 goals: vec![],
243 },
244 ],
245 };
246 let ceo_reports = chart.direct_reports("ceo");
247 assert_eq!(ceo_reports.len(), 2);
248 assert!(ceo_reports.iter().any(|n| n.agent_id == "mgr-1"));
249 assert!(ceo_reports.iter().any(|n| n.agent_id == "mgr-2"));
250
251 let mgr1_reports = chart.direct_reports("mgr-1");
252 assert_eq!(mgr1_reports.len(), 1);
253 assert_eq!(mgr1_reports[0].agent_id, "wk-1");
254
255 let wk_reports = chart.direct_reports("wk-1");
256 assert!(wk_reports.is_empty());
257 }
258
259 #[test]
260 fn org_chart_find_agent() {
261 let chart = OrgChart {
262 company_id: "co-1".into(),
263 nodes: vec![OrgNode {
264 agent_id: "a1".into(),
265 role: OrgRole::Worker,
266 reports_to: None,
267 budget_cents: 0,
268 goals: vec![],
269 }],
270 };
271 assert!(chart.find_agent("a1").is_some());
272 assert!(chart.find_agent("nonexistent").is_none());
273 }
274
275 #[test]
276 fn org_chart_serde_roundtrip() {
277 let chart = OrgChart {
278 company_id: "co-1".into(),
279 nodes: vec![
280 OrgNode {
281 agent_id: "ceo".into(),
282 role: OrgRole::Ceo,
283 reports_to: None,
284 budget_cents: 1_000_000,
285 goals: vec!["profit".into()],
286 },
287 OrgNode {
288 agent_id: "w1".into(),
289 role: OrgRole::Worker,
290 reports_to: Some("ceo".into()),
291 budget_cents: 0,
292 goals: vec![],
293 },
294 ],
295 };
296 let json = serde_json::to_string(&chart).unwrap();
297 let restored: OrgChart = serde_json::from_str(&json).unwrap();
298 assert_eq!(restored.company_id, "co-1");
299 assert_eq!(restored.nodes.len(), 2);
300 }
301}