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).unwrap())
460 .and_utc();
461 } else if hour >= 18 {
462 result = (result.date_naive() + Duration::days(1))
464 .and_time(NaiveTime::from_hms_opt(9, rng.gen_range(0..59), 0).unwrap())
465 .and_utc();
466 }
467
468 let weekday = result.weekday();
470 if weekday == Weekday::Sat {
471 result += Duration::days(2);
472 } else if weekday == Weekday::Sun {
473 result += Duration::days(1);
474 }
475
476 result
477 }
478
479 pub fn determine_outcome(&self, rng: &mut impl Rng) -> ApprovalActionType {
481 let roll: f64 = rng.gen();
482
483 if roll < self.rejection_rate {
484 ApprovalActionType::Reject
485 } else if roll < self.rejection_rate + self.revision_rate {
486 ApprovalActionType::RequestRevision
487 } else {
488 ApprovalActionType::Approve
489 }
490 }
491}
492
493pub mod rejection_reasons {
495 pub const REJECTION_REASONS: &[&str] = &[
497 "Missing supporting documentation",
498 "Amount exceeds budget allocation",
499 "Incorrect account coding",
500 "Duplicate entry detected",
501 "Policy violation",
502 "Vendor not approved",
503 "Missing purchase order reference",
504 "Expense not business-related",
505 "Incorrect cost center",
506 "Authorization not obtained",
507 ];
508
509 pub const REVISION_REASONS: &[&str] = &[
511 "Please provide additional documentation",
512 "Clarify business purpose",
513 "Split between multiple cost centers",
514 "Update account coding",
515 "Add reference number",
516 "Correct posting date",
517 "Update description",
518 "Verify amount",
519 "Add tax information",
520 "Update vendor information",
521 ];
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct ApprovalRecord {
527 pub approval_id: String,
529 pub document_number: String,
531 pub document_type: String,
533 pub company_code: String,
535 pub requester_id: String,
537 pub requester_name: Option<String>,
539 pub approver_id: String,
541 pub approver_name: String,
543 pub approval_date: chrono::NaiveDate,
545 pub action: String,
547 pub amount: Decimal,
549 pub approval_limit: Option<Decimal>,
551 pub comments: Option<String>,
553 pub delegation_from: Option<String>,
555 pub is_auto_approved: bool,
557}
558
559impl ApprovalRecord {
560 #[allow(clippy::too_many_arguments)]
562 pub fn new(
563 document_number: String,
564 approver_id: String,
565 approver_name: String,
566 requester_id: String,
567 approval_date: chrono::NaiveDate,
568 amount: Decimal,
569 action: String,
570 company_code: String,
571 ) -> Self {
572 Self {
573 approval_id: uuid::Uuid::new_v4().to_string(),
574 document_number,
575 document_type: "JE".to_string(),
576 company_code,
577 requester_id,
578 requester_name: None,
579 approver_id,
580 approver_name,
581 approval_date,
582 action,
583 amount,
584 approval_limit: None,
585 comments: None,
586 delegation_from: None,
587 is_auto_approved: false,
588 }
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
597 fn test_approval_status() {
598 assert!(ApprovalStatus::Approved.is_terminal());
599 assert!(ApprovalStatus::Rejected.is_terminal());
600 assert!(!ApprovalStatus::Pending.is_terminal());
601 assert!(ApprovalStatus::Approved.is_approved());
602 assert!(ApprovalStatus::AutoApproved.is_approved());
603 }
604
605 #[test]
606 fn test_approval_chain_levels() {
607 let chain = ApprovalChain::standard();
608
609 assert_eq!(chain.required_level(Decimal::from(500)), 0);
611
612 assert_eq!(chain.required_level(Decimal::from(5000)), 1);
614
615 assert_eq!(chain.required_level(Decimal::from(50000)), 2);
617
618 assert_eq!(chain.required_level(Decimal::from(200000)), 3);
620
621 assert_eq!(chain.required_level(Decimal::from(1000000)), 4);
623 }
624
625 #[test]
626 fn test_workflow_lifecycle() {
627 let mut workflow = ApprovalWorkflow::new(
628 "JSMITH001".to_string(),
629 "John Smith".to_string(),
630 Decimal::from(5000),
631 );
632
633 workflow.required_levels = 1;
634 workflow.submit(Utc::now());
635
636 assert_eq!(workflow.status, ApprovalStatus::Pending);
637
638 workflow.approve(
639 "MBROWN001".to_string(),
640 "Mary Brown".to_string(),
641 UserPersona::SeniorAccountant,
642 Utc::now(),
643 None,
644 );
645
646 assert_eq!(workflow.status, ApprovalStatus::Approved);
647 assert!(workflow.is_complete());
648 }
649
650 #[test]
651 fn test_auto_approval() {
652 let workflow = ApprovalWorkflow::auto_approved(
653 "JSMITH001".to_string(),
654 "John Smith".to_string(),
655 Decimal::from(500),
656 Utc::now(),
657 );
658
659 assert_eq!(workflow.status, ApprovalStatus::AutoApproved);
660 assert!(workflow.is_complete());
661 assert!(workflow.approved_at.is_some());
662 }
663
664 #[test]
665 fn test_rejection() {
666 let mut workflow = ApprovalWorkflow::new(
667 "JSMITH001".to_string(),
668 "John Smith".to_string(),
669 Decimal::from(5000),
670 );
671
672 workflow.required_levels = 1;
673 workflow.submit(Utc::now());
674
675 workflow.reject(
676 "MBROWN001".to_string(),
677 "Mary Brown".to_string(),
678 UserPersona::SeniorAccountant,
679 Utc::now(),
680 "Missing documentation",
681 );
682
683 assert_eq!(workflow.status, ApprovalStatus::Rejected);
684 assert!(workflow.is_complete());
685 }
686
687 #[test]
688 fn test_required_personas() {
689 let chain = ApprovalChain::standard();
690
691 let personas = chain.required_personas(Decimal::from(50000));
692 assert!(personas.contains(&UserPersona::SeniorAccountant));
693 assert!(personas.contains(&UserPersona::Controller));
694 }
695}