datasynth_graph/builders/
approval_graph.rs1use chrono::NaiveDate;
8use rust_decimal::Decimal;
9use std::collections::HashMap;
10
11use datasynth_core::models::{ApprovalRecord, User};
12
13use crate::models::{ApprovalEdge, EdgeType, Graph, GraphEdge, GraphType, NodeId, UserNode};
14
15#[derive(Debug, Clone)]
17pub struct ApprovalGraphConfig {
18 pub include_hierarchy: bool,
20 pub track_sod_violations: bool,
22 pub min_approval_count: usize,
24 pub aggregate_approvals: bool,
26}
27
28impl Default for ApprovalGraphConfig {
29 fn default() -> Self {
30 Self {
31 include_hierarchy: true,
32 track_sod_violations: true,
33 min_approval_count: 1,
34 aggregate_approvals: false,
35 }
36 }
37}
38
39pub struct ApprovalGraphBuilder {
41 config: ApprovalGraphConfig,
42 graph: Graph,
43 user_nodes: HashMap<String, NodeId>,
45 approval_aggregation: HashMap<(NodeId, NodeId), ApprovalAggregation>,
47}
48
49impl ApprovalGraphBuilder {
50 pub fn new(config: ApprovalGraphConfig) -> Self {
52 Self {
53 config,
54 graph: Graph::new("approval_network", GraphType::Approval),
55 user_nodes: HashMap::new(),
56 approval_aggregation: HashMap::new(),
57 }
58 }
59
60 pub fn add_users(&mut self, users: &[User]) {
62 for user in users {
63 self.get_or_create_user_node(user);
64 }
65
66 }
69
70 pub fn add_approval(&mut self, approval: &ApprovalRecord) {
72 let approver_id = self.ensure_user_node(&approval.approver_id, &approval.approver_name);
73 let requester_id = self.ensure_user_node(
74 &approval.requester_id,
75 approval.requester_name.as_deref().unwrap_or("Unknown"),
76 );
77
78 if self.config.aggregate_approvals {
79 self.aggregate_approval(approver_id, requester_id, approval);
80 } else {
81 let mut edge = ApprovalEdge::new(
82 0,
83 approver_id,
84 requester_id,
85 approval.document_number.clone(),
86 approval.approval_date,
87 approval.amount,
88 &approval.action,
89 );
90
91 if let Some(limit) = approval.approval_limit {
93 edge.within_limit = approval.amount <= limit;
94 if !edge.within_limit && self.config.track_sod_violations {
95 edge.edge = edge.edge.as_anomaly("ApprovalLimitExceeded");
96 }
97 }
98
99 edge.compute_features();
100 self.graph.add_edge(edge.edge);
101 }
102 }
103
104 pub fn add_approvals(&mut self, approvals: &[ApprovalRecord]) {
106 for approval in approvals {
107 self.add_approval(approval);
108 }
109 }
110
111 pub fn mark_self_approval(&mut self, user_id: &str, _document_number: &str, date: NaiveDate) {
113 if let Some(&node_id) = self.user_nodes.get(user_id) {
114 let edge = GraphEdge::new(0, node_id, node_id, EdgeType::Approval)
116 .with_timestamp(date)
117 .as_anomaly("SelfApproval");
118 self.graph.add_edge(edge);
119 }
120 }
121
122 fn get_or_create_user_node(&mut self, user: &User) -> NodeId {
124 if let Some(&id) = self.user_nodes.get(&user.user_id) {
125 return id;
126 }
127
128 let mut user_node = UserNode::new(0, user.user_id.clone(), user.display_name.clone());
129 user_node.department = user.department.clone();
130 user_node.is_active = user.is_active;
131 user_node.compute_features();
132
133 let id = self.graph.add_node(user_node.node);
134 self.user_nodes.insert(user.user_id.clone(), id);
135 id
136 }
137
138 fn ensure_user_node(&mut self, user_id: &str, user_name: &str) -> NodeId {
140 if let Some(&id) = self.user_nodes.get(user_id) {
141 return id;
142 }
143
144 let mut user_node = UserNode::new(0, user_id.to_string(), user_name.to_string());
145 user_node.compute_features();
146
147 let id = self.graph.add_node(user_node.node);
148 self.user_nodes.insert(user_id.to_string(), id);
149 id
150 }
151
152 fn aggregate_approval(
154 &mut self,
155 approver: NodeId,
156 requester: NodeId,
157 approval: &ApprovalRecord,
158 ) {
159 let key = (approver, requester);
160 let amount: f64 = approval.amount.try_into().unwrap_or(0.0);
161
162 let agg = self
163 .approval_aggregation
164 .entry(key)
165 .or_insert(ApprovalAggregation {
166 approver,
167 requester,
168 total_amount: 0.0,
169 count: 0,
170 approve_count: 0,
171 reject_count: 0,
172 first_date: approval.approval_date,
173 last_date: approval.approval_date,
174 });
175
176 agg.total_amount += amount;
177 agg.count += 1;
178
179 match approval.action.as_str() {
180 "Approve" | "Approved" => agg.approve_count += 1,
181 "Reject" | "Rejected" => agg.reject_count += 1,
182 _ => {}
183 }
184
185 if approval.approval_date < agg.first_date {
186 agg.first_date = approval.approval_date;
187 }
188 if approval.approval_date > agg.last_date {
189 agg.last_date = approval.approval_date;
190 }
191 }
192
193 pub fn build(mut self) -> Graph {
195 if self.config.aggregate_approvals {
197 for ((approver, requester), agg) in self.approval_aggregation {
198 if agg.count < self.config.min_approval_count {
199 continue;
200 }
201
202 let mut edge = GraphEdge::new(0, approver, requester, EdgeType::Approval)
203 .with_weight(agg.total_amount)
204 .with_timestamp(agg.last_date);
205
206 edge.features.push((agg.total_amount + 1.0).ln());
208 edge.features.push(agg.count as f64);
209 edge.features
210 .push(agg.approve_count as f64 / agg.count as f64);
211 edge.features
212 .push((agg.last_date - agg.first_date).num_days() as f64);
213
214 self.graph.add_edge(edge);
215 }
216 }
217
218 self.graph.compute_statistics();
219 self.graph
220 }
221}
222
223#[allow(dead_code)]
225struct ApprovalAggregation {
226 approver: NodeId,
227 requester: NodeId,
228 total_amount: f64,
229 count: usize,
230 approve_count: usize,
231 reject_count: usize,
232 first_date: NaiveDate,
233 last_date: NaiveDate,
234}
235
236#[derive(Debug, Clone)]
238pub struct SimpleApproval {
239 pub approver_id: String,
240 pub approver_name: String,
241 pub requester_id: String,
242 pub requester_name: String,
243 pub document_number: String,
244 pub approval_date: NaiveDate,
245 pub amount: Decimal,
246 pub action: String,
247}
248
249impl SimpleApproval {
250 pub fn to_approval_record(&self) -> ApprovalRecord {
252 ApprovalRecord {
253 approval_id: format!("APR-{}", self.document_number),
254 document_number: self.document_number.clone(),
255 document_type: "JE".to_string(),
256 company_code: "1000".to_string(),
257 requester_id: self.requester_id.clone(),
258 requester_name: Some(self.requester_name.clone()),
259 approver_id: self.approver_id.clone(),
260 approver_name: self.approver_name.clone(),
261 approval_date: self.approval_date,
262 action: self.action.clone(),
263 amount: self.amount,
264 approval_limit: None,
265 comments: None,
266 delegation_from: None,
267 is_auto_approved: false,
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use rust_decimal_macros::dec;
276
277 #[test]
278 fn test_approval_graph() {
279 let mut builder = ApprovalGraphBuilder::new(ApprovalGraphConfig::default());
280
281 let approval = ApprovalRecord {
282 approval_id: "APR001".to_string(),
283 document_number: "JE001".to_string(),
284 document_type: "JE".to_string(),
285 company_code: "1000".to_string(),
286 requester_id: "USER001".to_string(),
287 requester_name: Some("John Doe".to_string()),
288 approver_id: "USER002".to_string(),
289 approver_name: "Jane Smith".to_string(),
290 approval_date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
291 action: "Approve".to_string(),
292 amount: dec!(10000),
293 approval_limit: Some(dec!(50000)),
294 comments: None,
295 delegation_from: None,
296 is_auto_approved: false,
297 };
298
299 builder.add_approval(&approval);
300
301 let graph = builder.build();
302
303 assert_eq!(graph.node_count(), 2);
304 assert_eq!(graph.edge_count(), 1);
305 }
306}