1use crate::models::UserPersona;
7use chrono::{DateTime, Datelike, Duration, NaiveTime, Timelike, Utc, Weekday};
8use rand::Rng;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum ApprovalStatus {
16 #[default]
18 Draft,
19 Pending,
21 Approved,
23 Rejected,
25 AutoApproved,
27 RequiresRevision,
29}
30
31impl ApprovalStatus {
32 pub fn is_terminal(&self) -> bool {
34 matches!(self, Self::Approved | Self::Rejected | Self::AutoApproved)
35 }
36
37 pub fn is_approved(&self) -> bool {
39 matches!(self, Self::Approved | Self::AutoApproved)
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum ApprovalActionType {
47 Submit,
49 Approve,
51 Reject,
53 RequestRevision,
55 Resubmit,
57 AutoApprove,
59 Escalate,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ApprovalAction {
66 pub actor_id: String,
68
69 pub actor_name: String,
71
72 pub actor_role: UserPersona,
74
75 pub action: ApprovalActionType,
77
78 pub action_timestamp: DateTime<Utc>,
80
81 pub comments: Option<String>,
83
84 pub approval_level: u8,
86}
87
88impl ApprovalAction {
89 #[allow(clippy::too_many_arguments)]
91 pub fn new(
92 actor_id: String,
93 actor_name: String,
94 actor_role: UserPersona,
95 action: ApprovalActionType,
96 level: u8,
97 ) -> Self {
98 Self {
99 actor_id,
100 actor_name,
101 actor_role,
102 action,
103 action_timestamp: Utc::now(),
104 comments: None,
105 approval_level: level,
106 }
107 }
108
109 pub fn with_comment(mut self, comment: &str) -> Self {
111 self.comments = Some(comment.to_string());
112 self
113 }
114
115 pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
117 self.action_timestamp = timestamp;
118 self
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ApprovalWorkflow {
125 pub status: ApprovalStatus,
127
128 pub actions: Vec<ApprovalAction>,
130
131 pub required_levels: u8,
133
134 pub current_level: u8,
136
137 pub preparer_id: String,
139
140 pub preparer_name: String,
142
143 pub submitted_at: Option<DateTime<Utc>>,
145
146 pub approved_at: Option<DateTime<Utc>>,
148
149 pub amount: Decimal,
151}
152
153impl ApprovalWorkflow {
154 pub fn new(preparer_id: String, preparer_name: String, amount: Decimal) -> Self {
156 Self {
157 status: ApprovalStatus::Draft,
158 actions: Vec::new(),
159 required_levels: 0,
160 current_level: 0,
161 preparer_id,
162 preparer_name,
163 submitted_at: None,
164 approved_at: None,
165 amount,
166 }
167 }
168
169 pub fn auto_approved(
171 preparer_id: String,
172 preparer_name: String,
173 amount: Decimal,
174 timestamp: DateTime<Utc>,
175 ) -> Self {
176 let action = ApprovalAction {
177 actor_id: "SYSTEM".to_string(),
178 actor_name: "Automated System".to_string(),
179 actor_role: UserPersona::AutomatedSystem,
180 action: ApprovalActionType::AutoApprove,
181 action_timestamp: timestamp,
182 comments: Some("Amount below auto-approval threshold".to_string()),
183 approval_level: 0,
184 };
185
186 Self {
187 status: ApprovalStatus::AutoApproved,
188 actions: vec![action],
189 required_levels: 0,
190 current_level: 0,
191 preparer_id,
192 preparer_name,
193 submitted_at: Some(timestamp),
194 approved_at: Some(timestamp),
195 amount,
196 }
197 }
198
199 pub fn submit(&mut self, timestamp: DateTime<Utc>) {
201 self.status = ApprovalStatus::Pending;
202 self.submitted_at = Some(timestamp);
203
204 let action = ApprovalAction {
205 actor_id: self.preparer_id.clone(),
206 actor_name: self.preparer_name.clone(),
207 actor_role: UserPersona::JuniorAccountant, action: ApprovalActionType::Submit,
209 action_timestamp: timestamp,
210 comments: None,
211 approval_level: 0,
212 };
213 self.actions.push(action);
214 }
215
216 pub fn approve(
218 &mut self,
219 approver_id: String,
220 approver_name: String,
221 approver_role: UserPersona,
222 timestamp: DateTime<Utc>,
223 comment: Option<String>,
224 ) {
225 self.current_level += 1;
226
227 let mut action = ApprovalAction::new(
228 approver_id,
229 approver_name,
230 approver_role,
231 ApprovalActionType::Approve,
232 self.current_level,
233 )
234 .with_timestamp(timestamp);
235
236 if let Some(c) = comment {
237 action = action.with_comment(&c);
238 }
239
240 self.actions.push(action);
241
242 if self.current_level >= self.required_levels {
244 self.status = ApprovalStatus::Approved;
245 self.approved_at = Some(timestamp);
246 }
247 }
248
249 pub fn reject(
251 &mut self,
252 rejector_id: String,
253 rejector_name: String,
254 rejector_role: UserPersona,
255 timestamp: DateTime<Utc>,
256 reason: &str,
257 ) {
258 self.status = ApprovalStatus::Rejected;
259
260 let action = ApprovalAction::new(
261 rejector_id,
262 rejector_name,
263 rejector_role,
264 ApprovalActionType::Reject,
265 self.current_level + 1,
266 )
267 .with_timestamp(timestamp)
268 .with_comment(reason);
269
270 self.actions.push(action);
271 }
272
273 pub fn request_revision(
275 &mut self,
276 reviewer_id: String,
277 reviewer_name: String,
278 reviewer_role: UserPersona,
279 timestamp: DateTime<Utc>,
280 reason: &str,
281 ) {
282 self.status = ApprovalStatus::RequiresRevision;
283
284 let action = ApprovalAction::new(
285 reviewer_id,
286 reviewer_name,
287 reviewer_role,
288 ApprovalActionType::RequestRevision,
289 self.current_level + 1,
290 )
291 .with_timestamp(timestamp)
292 .with_comment(reason);
293
294 self.actions.push(action);
295 }
296
297 pub fn is_complete(&self) -> bool {
299 self.status.is_terminal()
300 }
301
302 pub fn final_approver(&self) -> Option<&ApprovalAction> {
304 self.actions
305 .iter()
306 .rev()
307 .find(|a| a.action == ApprovalActionType::Approve)
308 }
309}
310
311#[derive(Debug, Clone)]
313pub struct ApprovalChain {
314 pub thresholds: Vec<ApprovalThreshold>,
316 pub auto_approve_threshold: Decimal,
318}
319
320impl Default for ApprovalChain {
321 fn default() -> Self {
322 Self::standard()
323 }
324}
325
326impl ApprovalChain {
327 pub fn standard() -> Self {
329 Self {
330 auto_approve_threshold: Decimal::from(1000),
331 thresholds: vec![
332 ApprovalThreshold {
333 amount: Decimal::from(1000),
334 level: 1,
335 required_personas: vec![UserPersona::SeniorAccountant],
336 },
337 ApprovalThreshold {
338 amount: Decimal::from(10000),
339 level: 2,
340 required_personas: vec![UserPersona::SeniorAccountant, UserPersona::Controller],
341 },
342 ApprovalThreshold {
343 amount: Decimal::from(100000),
344 level: 3,
345 required_personas: vec![
346 UserPersona::SeniorAccountant,
347 UserPersona::Controller,
348 UserPersona::Manager,
349 ],
350 },
351 ApprovalThreshold {
352 amount: Decimal::from(500000),
353 level: 4,
354 required_personas: vec![
355 UserPersona::SeniorAccountant,
356 UserPersona::Controller,
357 UserPersona::Manager,
358 UserPersona::Executive,
359 ],
360 },
361 ],
362 }
363 }
364
365 pub fn required_level(&self, amount: Decimal) -> u8 {
367 let abs_amount = amount.abs();
368
369 if abs_amount < self.auto_approve_threshold {
370 return 0;
371 }
372
373 for threshold in self.thresholds.iter().rev() {
374 if abs_amount >= threshold.amount {
375 return threshold.level;
376 }
377 }
378
379 1 }
381
382 pub fn required_personas(&self, amount: Decimal) -> Vec<UserPersona> {
384 let level = self.required_level(amount);
385
386 if level == 0 {
387 return Vec::new();
388 }
389
390 self.thresholds
391 .iter()
392 .find(|t| t.level == level)
393 .map(|t| t.required_personas.clone())
394 .unwrap_or_default()
395 }
396
397 pub fn is_auto_approve(&self, amount: Decimal) -> bool {
399 amount.abs() < self.auto_approve_threshold
400 }
401}
402
403#[derive(Debug, Clone)]
405pub struct ApprovalThreshold {
406 pub amount: Decimal,
408 pub level: u8,
410 pub required_personas: Vec<UserPersona>,
412}
413
414#[derive(Debug, Clone)]
416pub struct ApprovalWorkflowGenerator {
417 pub chain: ApprovalChain,
419 pub rejection_rate: f64,
421 pub revision_rate: f64,
423 pub average_delay_hours: f64,
425}
426
427impl Default for ApprovalWorkflowGenerator {
428 fn default() -> Self {
429 Self {
430 chain: ApprovalChain::standard(),
431 rejection_rate: 0.02,
432 revision_rate: 0.05,
433 average_delay_hours: 4.0,
434 }
435 }
436}
437
438impl ApprovalWorkflowGenerator {
439 pub fn generate_approval_timestamp(
441 &self,
442 base_timestamp: DateTime<Utc>,
443 rng: &mut impl Rng,
444 ) -> DateTime<Utc> {
445 let delay_hours = self.average_delay_hours * (-rng.gen::<f64>().ln());
447 let delay_hours = delay_hours.min(48.0); let mut result = base_timestamp + Duration::hours(delay_hours as i64);
450
451 let time = result.time();
453 let hour = time.hour();
454
455 if hour < 9 {
456 result = result
458 .date_naive()
459 .and_time(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time components"))
460 .and_utc();
461 } else if hour >= 18 {
462 result = (result.date_naive() + Duration::days(1))
464 .and_time(
465 NaiveTime::from_hms_opt(9, rng.gen_range(0..59), 0)
466 .expect("valid time components"),
467 )
468 .and_utc();
469 }
470
471 let weekday = result.weekday();
473 if weekday == Weekday::Sat {
474 result += Duration::days(2);
475 } else if weekday == Weekday::Sun {
476 result += Duration::days(1);
477 }
478
479 result
480 }
481
482 pub fn determine_outcome(&self, rng: &mut impl Rng) -> ApprovalActionType {
484 let roll: f64 = rng.gen();
485
486 if roll < self.rejection_rate {
487 ApprovalActionType::Reject
488 } else if roll < self.rejection_rate + self.revision_rate {
489 ApprovalActionType::RequestRevision
490 } else {
491 ApprovalActionType::Approve
492 }
493 }
494}
495
496pub mod rejection_reasons {
498 pub const REJECTION_REASONS: &[&str] = &[
500 "Missing supporting documentation",
501 "Amount exceeds budget allocation",
502 "Incorrect account coding",
503 "Duplicate entry detected",
504 "Policy violation",
505 "Vendor not approved",
506 "Missing purchase order reference",
507 "Expense not business-related",
508 "Incorrect cost center",
509 "Authorization not obtained",
510 ];
511
512 pub const REVISION_REASONS: &[&str] = &[
514 "Please provide additional documentation",
515 "Clarify business purpose",
516 "Split between multiple cost centers",
517 "Update account coding",
518 "Add reference number",
519 "Correct posting date",
520 "Update description",
521 "Verify amount",
522 "Add tax information",
523 "Update vendor information",
524 ];
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct ApprovalRecord {
530 pub approval_id: String,
532 pub document_number: String,
534 pub document_type: String,
536 pub company_code: String,
538 pub requester_id: String,
540 pub requester_name: Option<String>,
542 pub approver_id: String,
544 pub approver_name: String,
546 pub approval_date: chrono::NaiveDate,
548 pub action: String,
550 pub amount: Decimal,
552 pub approval_limit: Option<Decimal>,
554 pub comments: Option<String>,
556 pub delegation_from: Option<String>,
558 pub is_auto_approved: bool,
560}
561
562impl ApprovalRecord {
563 #[allow(clippy::too_many_arguments)]
565 pub fn new(
566 document_number: String,
567 approver_id: String,
568 approver_name: String,
569 requester_id: String,
570 approval_date: chrono::NaiveDate,
571 amount: Decimal,
572 action: String,
573 company_code: String,
574 ) -> Self {
575 Self {
576 approval_id: uuid::Uuid::new_v4().to_string(),
577 document_number,
578 document_type: "JE".to_string(),
579 company_code,
580 requester_id,
581 requester_name: None,
582 approver_id,
583 approver_name,
584 approval_date,
585 action,
586 amount,
587 approval_limit: None,
588 comments: None,
589 delegation_from: None,
590 is_auto_approved: false,
591 }
592 }
593}
594
595#[cfg(test)]
596#[allow(clippy::unwrap_used)]
597mod tests {
598 use super::*;
599
600 #[test]
601 fn test_approval_status() {
602 assert!(ApprovalStatus::Approved.is_terminal());
603 assert!(ApprovalStatus::Rejected.is_terminal());
604 assert!(!ApprovalStatus::Pending.is_terminal());
605 assert!(ApprovalStatus::Approved.is_approved());
606 assert!(ApprovalStatus::AutoApproved.is_approved());
607 }
608
609 #[test]
610 fn test_approval_chain_levels() {
611 let chain = ApprovalChain::standard();
612
613 assert_eq!(chain.required_level(Decimal::from(500)), 0);
615
616 assert_eq!(chain.required_level(Decimal::from(5000)), 1);
618
619 assert_eq!(chain.required_level(Decimal::from(50000)), 2);
621
622 assert_eq!(chain.required_level(Decimal::from(200000)), 3);
624
625 assert_eq!(chain.required_level(Decimal::from(1000000)), 4);
627 }
628
629 #[test]
630 fn test_workflow_lifecycle() {
631 let mut workflow = ApprovalWorkflow::new(
632 "JSMITH001".to_string(),
633 "John Smith".to_string(),
634 Decimal::from(5000),
635 );
636
637 workflow.required_levels = 1;
638 workflow.submit(Utc::now());
639
640 assert_eq!(workflow.status, ApprovalStatus::Pending);
641
642 workflow.approve(
643 "MBROWN001".to_string(),
644 "Mary Brown".to_string(),
645 UserPersona::SeniorAccountant,
646 Utc::now(),
647 None,
648 );
649
650 assert_eq!(workflow.status, ApprovalStatus::Approved);
651 assert!(workflow.is_complete());
652 }
653
654 #[test]
655 fn test_auto_approval() {
656 let workflow = ApprovalWorkflow::auto_approved(
657 "JSMITH001".to_string(),
658 "John Smith".to_string(),
659 Decimal::from(500),
660 Utc::now(),
661 );
662
663 assert_eq!(workflow.status, ApprovalStatus::AutoApproved);
664 assert!(workflow.is_complete());
665 assert!(workflow.approved_at.is_some());
666 }
667
668 #[test]
669 fn test_rejection() {
670 let mut workflow = ApprovalWorkflow::new(
671 "JSMITH001".to_string(),
672 "John Smith".to_string(),
673 Decimal::from(5000),
674 );
675
676 workflow.required_levels = 1;
677 workflow.submit(Utc::now());
678
679 workflow.reject(
680 "MBROWN001".to_string(),
681 "Mary Brown".to_string(),
682 UserPersona::SeniorAccountant,
683 Utc::now(),
684 "Missing documentation",
685 );
686
687 assert_eq!(workflow.status, ApprovalStatus::Rejected);
688 assert!(workflow.is_complete());
689 }
690
691 #[test]
692 fn test_required_personas() {
693 let chain = ApprovalChain::standard();
694
695 let personas = chain.required_personas(Decimal::from(50000));
696 assert!(personas.contains(&UserPersona::SeniorAccountant));
697 assert!(personas.contains(&UserPersona::Controller));
698 }
699}