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 #[serde(with = "crate::serde_timestamp::utc")]
80 pub action_timestamp: DateTime<Utc>,
81
82 pub comments: Option<String>,
84
85 pub approval_level: u8,
87}
88
89impl ApprovalAction {
90 #[allow(clippy::too_many_arguments)]
92 pub fn new(
93 actor_id: String,
94 actor_name: String,
95 actor_role: UserPersona,
96 action: ApprovalActionType,
97 level: u8,
98 ) -> Self {
99 Self {
100 actor_id,
101 actor_name,
102 actor_role,
103 action,
104 action_timestamp: Utc::now(),
105 comments: None,
106 approval_level: level,
107 }
108 }
109
110 pub fn with_comment(mut self, comment: &str) -> Self {
112 self.comments = Some(comment.to_string());
113 self
114 }
115
116 pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
118 self.action_timestamp = timestamp;
119 self
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ApprovalWorkflow {
126 pub status: ApprovalStatus,
128
129 pub actions: Vec<ApprovalAction>,
131
132 pub required_levels: u8,
134
135 pub current_level: u8,
137
138 pub preparer_id: String,
140
141 pub preparer_name: String,
143
144 #[serde(default, with = "crate::serde_timestamp::utc::option")]
146 pub submitted_at: Option<DateTime<Utc>>,
147
148 #[serde(default, with = "crate::serde_timestamp::utc::option")]
150 pub approved_at: Option<DateTime<Utc>>,
151
152 pub amount: Decimal,
154}
155
156impl ApprovalWorkflow {
157 pub fn new(preparer_id: String, preparer_name: String, amount: Decimal) -> Self {
159 Self {
160 status: ApprovalStatus::Draft,
161 actions: Vec::new(),
162 required_levels: 0,
163 current_level: 0,
164 preparer_id,
165 preparer_name,
166 submitted_at: None,
167 approved_at: None,
168 amount,
169 }
170 }
171
172 pub fn auto_approved(
174 preparer_id: String,
175 preparer_name: String,
176 amount: Decimal,
177 timestamp: DateTime<Utc>,
178 ) -> Self {
179 let action = ApprovalAction {
180 actor_id: "SYSTEM".to_string(),
181 actor_name: "Automated System".to_string(),
182 actor_role: UserPersona::AutomatedSystem,
183 action: ApprovalActionType::AutoApprove,
184 action_timestamp: timestamp,
185 comments: Some("Amount below auto-approval threshold".to_string()),
186 approval_level: 0,
187 };
188
189 Self {
190 status: ApprovalStatus::AutoApproved,
191 actions: vec![action],
192 required_levels: 0,
193 current_level: 0,
194 preparer_id,
195 preparer_name,
196 submitted_at: Some(timestamp),
197 approved_at: Some(timestamp),
198 amount,
199 }
200 }
201
202 pub fn submit(&mut self, timestamp: DateTime<Utc>) {
204 self.status = ApprovalStatus::Pending;
205 self.submitted_at = Some(timestamp);
206
207 let action = ApprovalAction {
208 actor_id: self.preparer_id.clone(),
209 actor_name: self.preparer_name.clone(),
210 actor_role: UserPersona::JuniorAccountant, action: ApprovalActionType::Submit,
212 action_timestamp: timestamp,
213 comments: None,
214 approval_level: 0,
215 };
216 self.actions.push(action);
217 }
218
219 pub fn approve(
221 &mut self,
222 approver_id: String,
223 approver_name: String,
224 approver_role: UserPersona,
225 timestamp: DateTime<Utc>,
226 comment: Option<String>,
227 ) {
228 self.current_level += 1;
229
230 let mut action = ApprovalAction::new(
231 approver_id,
232 approver_name,
233 approver_role,
234 ApprovalActionType::Approve,
235 self.current_level,
236 )
237 .with_timestamp(timestamp);
238
239 if let Some(c) = comment {
240 action = action.with_comment(&c);
241 }
242
243 self.actions.push(action);
244
245 if self.current_level >= self.required_levels {
247 self.status = ApprovalStatus::Approved;
248 self.approved_at = Some(timestamp);
249 }
250 }
251
252 pub fn reject(
254 &mut self,
255 rejector_id: String,
256 rejector_name: String,
257 rejector_role: UserPersona,
258 timestamp: DateTime<Utc>,
259 reason: &str,
260 ) {
261 self.status = ApprovalStatus::Rejected;
262
263 let action = ApprovalAction::new(
264 rejector_id,
265 rejector_name,
266 rejector_role,
267 ApprovalActionType::Reject,
268 self.current_level + 1,
269 )
270 .with_timestamp(timestamp)
271 .with_comment(reason);
272
273 self.actions.push(action);
274 }
275
276 pub fn request_revision(
278 &mut self,
279 reviewer_id: String,
280 reviewer_name: String,
281 reviewer_role: UserPersona,
282 timestamp: DateTime<Utc>,
283 reason: &str,
284 ) {
285 self.status = ApprovalStatus::RequiresRevision;
286
287 let action = ApprovalAction::new(
288 reviewer_id,
289 reviewer_name,
290 reviewer_role,
291 ApprovalActionType::RequestRevision,
292 self.current_level + 1,
293 )
294 .with_timestamp(timestamp)
295 .with_comment(reason);
296
297 self.actions.push(action);
298 }
299
300 pub fn is_complete(&self) -> bool {
302 self.status.is_terminal()
303 }
304
305 pub fn final_approver(&self) -> Option<&ApprovalAction> {
307 self.actions
308 .iter()
309 .rev()
310 .find(|a| a.action == ApprovalActionType::Approve)
311 }
312}
313
314#[derive(Debug, Clone)]
316pub struct ApprovalChain {
317 pub thresholds: Vec<ApprovalThreshold>,
319 pub auto_approve_threshold: Decimal,
321}
322
323impl Default for ApprovalChain {
324 fn default() -> Self {
325 Self::standard()
326 }
327}
328
329impl ApprovalChain {
330 pub fn standard() -> Self {
332 Self {
333 auto_approve_threshold: Decimal::from(1000),
334 thresholds: vec![
335 ApprovalThreshold {
336 amount: Decimal::from(1000),
337 level: 1,
338 required_personas: vec![UserPersona::SeniorAccountant],
339 },
340 ApprovalThreshold {
341 amount: Decimal::from(10000),
342 level: 2,
343 required_personas: vec![UserPersona::SeniorAccountant, UserPersona::Controller],
344 },
345 ApprovalThreshold {
346 amount: Decimal::from(100000),
347 level: 3,
348 required_personas: vec![
349 UserPersona::SeniorAccountant,
350 UserPersona::Controller,
351 UserPersona::Manager,
352 ],
353 },
354 ApprovalThreshold {
355 amount: Decimal::from(500000),
356 level: 4,
357 required_personas: vec![
358 UserPersona::SeniorAccountant,
359 UserPersona::Controller,
360 UserPersona::Manager,
361 UserPersona::Executive,
362 ],
363 },
364 ],
365 }
366 }
367
368 pub fn required_level(&self, amount: Decimal) -> u8 {
370 let abs_amount = amount.abs();
371
372 if abs_amount < self.auto_approve_threshold {
373 return 0;
374 }
375
376 for threshold in self.thresholds.iter().rev() {
377 if abs_amount >= threshold.amount {
378 return threshold.level;
379 }
380 }
381
382 1 }
384
385 pub fn required_personas(&self, amount: Decimal) -> Vec<UserPersona> {
387 let level = self.required_level(amount);
388
389 if level == 0 {
390 return Vec::new();
391 }
392
393 self.thresholds
394 .iter()
395 .find(|t| t.level == level)
396 .map(|t| t.required_personas.clone())
397 .unwrap_or_default()
398 }
399
400 pub fn is_auto_approve(&self, amount: Decimal) -> bool {
402 amount.abs() < self.auto_approve_threshold
403 }
404}
405
406#[derive(Debug, Clone)]
408pub struct ApprovalThreshold {
409 pub amount: Decimal,
411 pub level: u8,
413 pub required_personas: Vec<UserPersona>,
415}
416
417#[derive(Debug, Clone)]
419pub struct ApprovalWorkflowGenerator {
420 pub chain: ApprovalChain,
422 pub rejection_rate: f64,
424 pub revision_rate: f64,
426 pub average_delay_hours: f64,
428}
429
430impl Default for ApprovalWorkflowGenerator {
431 fn default() -> Self {
432 Self {
433 chain: ApprovalChain::standard(),
434 rejection_rate: 0.02,
435 revision_rate: 0.05,
436 average_delay_hours: 4.0,
437 }
438 }
439}
440
441impl ApprovalWorkflowGenerator {
442 pub fn generate_approval_timestamp(
444 &self,
445 base_timestamp: DateTime<Utc>,
446 rng: &mut impl Rng,
447 ) -> DateTime<Utc> {
448 let delay_hours = self.average_delay_hours * (-rng.random::<f64>().ln());
450 let delay_hours = delay_hours.min(48.0); let mut result = base_timestamp + Duration::hours(delay_hours as i64);
453
454 let time = result.time();
456 let hour = time.hour();
457
458 if hour < 9 {
459 result = result
461 .date_naive()
462 .and_time(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time components"))
463 .and_utc();
464 } else if hour >= 18 {
465 result = (result.date_naive() + Duration::days(1))
467 .and_time(
468 NaiveTime::from_hms_opt(9, rng.random_range(0..59), 0)
469 .expect("valid time components"),
470 )
471 .and_utc();
472 }
473
474 let weekday = result.weekday();
476 if weekday == Weekday::Sat {
477 result += Duration::days(2);
478 } else if weekday == Weekday::Sun {
479 result += Duration::days(1);
480 }
481
482 result
483 }
484
485 pub fn determine_outcome(&self, rng: &mut impl Rng) -> ApprovalActionType {
487 let roll: f64 = rng.random();
488
489 if roll < self.rejection_rate {
490 ApprovalActionType::Reject
491 } else if roll < self.rejection_rate + self.revision_rate {
492 ApprovalActionType::RequestRevision
493 } else {
494 ApprovalActionType::Approve
495 }
496 }
497}
498
499pub mod rejection_reasons {
501 pub const REJECTION_REASONS: &[&str] = &[
503 "Missing supporting documentation",
504 "Amount exceeds budget allocation",
505 "Incorrect account coding",
506 "Duplicate entry detected",
507 "Policy violation",
508 "Vendor not approved",
509 "Missing purchase order reference",
510 "Expense not business-related",
511 "Incorrect cost center",
512 "Authorization not obtained",
513 ];
514
515 pub const REVISION_REASONS: &[&str] = &[
517 "Please provide additional documentation",
518 "Clarify business purpose",
519 "Split between multiple cost centers",
520 "Update account coding",
521 "Add reference number",
522 "Correct posting date",
523 "Update description",
524 "Verify amount",
525 "Add tax information",
526 "Update vendor information",
527 ];
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct ApprovalRecord {
533 pub approval_id: String,
535 pub document_number: String,
537 pub document_type: String,
539 pub company_code: String,
541 pub requester_id: String,
543 pub requester_name: Option<String>,
545 pub approver_id: String,
547 pub approver_name: String,
549 pub approval_date: chrono::NaiveDate,
551 pub action: String,
553 pub amount: Decimal,
555 pub approval_limit: Option<Decimal>,
557 pub comments: Option<String>,
559 pub delegation_from: Option<String>,
561 pub is_auto_approved: bool,
563}
564
565impl ApprovalRecord {
566 #[allow(clippy::too_many_arguments)]
568 pub fn new(
569 document_number: String,
570 approver_id: String,
571 approver_name: String,
572 requester_id: String,
573 approval_date: chrono::NaiveDate,
574 amount: Decimal,
575 action: String,
576 company_code: String,
577 ) -> Self {
578 Self {
579 approval_id: uuid::Uuid::new_v4().to_string(),
580 document_number,
581 document_type: "JE".to_string(),
582 company_code,
583 requester_id,
584 requester_name: None,
585 approver_id,
586 approver_name,
587 approval_date,
588 action,
589 amount,
590 approval_limit: None,
591 comments: None,
592 delegation_from: None,
593 is_auto_approved: false,
594 }
595 }
596}
597
598#[cfg(test)]
599#[allow(clippy::unwrap_used)]
600mod tests {
601 use super::*;
602
603 #[test]
604 fn test_approval_status() {
605 assert!(ApprovalStatus::Approved.is_terminal());
606 assert!(ApprovalStatus::Rejected.is_terminal());
607 assert!(!ApprovalStatus::Pending.is_terminal());
608 assert!(ApprovalStatus::Approved.is_approved());
609 assert!(ApprovalStatus::AutoApproved.is_approved());
610 }
611
612 #[test]
613 fn test_approval_chain_levels() {
614 let chain = ApprovalChain::standard();
615
616 assert_eq!(chain.required_level(Decimal::from(500)), 0);
618
619 assert_eq!(chain.required_level(Decimal::from(5000)), 1);
621
622 assert_eq!(chain.required_level(Decimal::from(50000)), 2);
624
625 assert_eq!(chain.required_level(Decimal::from(200000)), 3);
627
628 assert_eq!(chain.required_level(Decimal::from(1000000)), 4);
630 }
631
632 #[test]
633 fn test_workflow_lifecycle() {
634 let mut workflow = ApprovalWorkflow::new(
635 "JSMITH001".to_string(),
636 "John Smith".to_string(),
637 Decimal::from(5000),
638 );
639
640 workflow.required_levels = 1;
641 workflow.submit(Utc::now());
642
643 assert_eq!(workflow.status, ApprovalStatus::Pending);
644
645 workflow.approve(
646 "MBROWN001".to_string(),
647 "Mary Brown".to_string(),
648 UserPersona::SeniorAccountant,
649 Utc::now(),
650 None,
651 );
652
653 assert_eq!(workflow.status, ApprovalStatus::Approved);
654 assert!(workflow.is_complete());
655 }
656
657 #[test]
658 fn test_auto_approval() {
659 let workflow = ApprovalWorkflow::auto_approved(
660 "JSMITH001".to_string(),
661 "John Smith".to_string(),
662 Decimal::from(500),
663 Utc::now(),
664 );
665
666 assert_eq!(workflow.status, ApprovalStatus::AutoApproved);
667 assert!(workflow.is_complete());
668 assert!(workflow.approved_at.is_some());
669 }
670
671 #[test]
672 fn test_rejection() {
673 let mut workflow = ApprovalWorkflow::new(
674 "JSMITH001".to_string(),
675 "John Smith".to_string(),
676 Decimal::from(5000),
677 );
678
679 workflow.required_levels = 1;
680 workflow.submit(Utc::now());
681
682 workflow.reject(
683 "MBROWN001".to_string(),
684 "Mary Brown".to_string(),
685 UserPersona::SeniorAccountant,
686 Utc::now(),
687 "Missing documentation",
688 );
689
690 assert_eq!(workflow.status, ApprovalStatus::Rejected);
691 assert!(workflow.is_complete());
692 }
693
694 #[test]
695 fn test_required_personas() {
696 let chain = ApprovalChain::standard();
697
698 let personas = chain.required_personas(Decimal::from(50000));
699 assert!(personas.contains(&UserPersona::SeniorAccountant));
700 assert!(personas.contains(&UserPersona::Controller));
701 }
702}