use std::fmt;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum UserPersona {
JuniorAccountant,
SeniorAccountant,
Controller,
Manager,
Executive,
#[default]
AutomatedSystem,
ExternalAuditor,
FraudActor,
}
impl fmt::Display for UserPersona {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::JuniorAccountant => write!(f, "junior_accountant"),
Self::SeniorAccountant => write!(f, "senior_accountant"),
Self::Controller => write!(f, "controller"),
Self::Manager => write!(f, "manager"),
Self::Executive => write!(f, "executive"),
Self::AutomatedSystem => write!(f, "automated_system"),
Self::ExternalAuditor => write!(f, "external_auditor"),
Self::FraudActor => write!(f, "fraud_actor"),
}
}
}
impl UserPersona {
pub fn is_human(&self) -> bool {
!matches!(self, Self::AutomatedSystem)
}
pub fn has_approval_authority(&self) -> bool {
matches!(self, Self::Controller | Self::Manager | Self::Executive)
}
pub fn error_rate(&self) -> f64 {
match self {
Self::JuniorAccountant => 0.02,
Self::SeniorAccountant => 0.005,
Self::Controller => 0.002,
Self::Manager => 0.003,
Self::Executive => 0.001,
Self::AutomatedSystem => 0.0001,
Self::ExternalAuditor => 0.0,
Self::FraudActor => 0.01,
}
}
pub fn typical_daily_volume(&self) -> (u32, u32) {
match self {
Self::JuniorAccountant => (20, 100),
Self::SeniorAccountant => (10, 50),
Self::Controller => (5, 20),
Self::Manager => (1, 10),
Self::Executive => (0, 5),
Self::AutomatedSystem => (100, 10000),
Self::ExternalAuditor => (0, 0),
Self::FraudActor => (1, 5),
}
}
pub fn approval_threshold(&self) -> Option<f64> {
match self {
Self::JuniorAccountant => Some(1000.0),
Self::SeniorAccountant => Some(10000.0),
Self::Controller => Some(100000.0),
Self::Manager => Some(500000.0),
Self::Executive => None, Self::AutomatedSystem => Some(1000000.0),
Self::ExternalAuditor => Some(0.0), Self::FraudActor => Some(10000.0),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkingHoursPattern {
pub start_hour: u8,
pub end_hour: u8,
pub peak_hours: Vec<u8>,
pub weekend_probability: f64,
pub after_hours_probability: f64,
}
impl Default for WorkingHoursPattern {
fn default() -> Self {
Self {
start_hour: 8,
end_hour: 18,
peak_hours: vec![10, 11, 14, 15],
weekend_probability: 0.05,
after_hours_probability: 0.10,
}
}
}
impl WorkingHoursPattern {
pub fn european() -> Self {
Self {
start_hour: 9,
end_hour: 17,
peak_hours: vec![10, 11, 14, 15],
weekend_probability: 0.02,
after_hours_probability: 0.05,
}
}
pub fn us_standard() -> Self {
Self {
start_hour: 8,
end_hour: 17,
peak_hours: vec![9, 10, 14, 15],
weekend_probability: 0.05,
after_hours_probability: 0.10,
}
}
pub fn asian() -> Self {
Self {
start_hour: 9,
end_hour: 18,
peak_hours: vec![10, 11, 15, 16],
weekend_probability: 0.10,
after_hours_probability: 0.15,
}
}
pub fn batch_processing() -> Self {
Self {
start_hour: 0,
end_hour: 24,
peak_hours: vec![2, 3, 4, 22, 23], weekend_probability: 1.0,
after_hours_probability: 1.0,
}
}
pub fn is_working_hour(&self, hour: u8) -> bool {
hour >= self.start_hour && hour < self.end_hour
}
pub fn is_peak_hour(&self, hour: u8) -> bool {
self.peak_hours.contains(&hour)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub user_id: String,
pub display_name: String,
pub email: Option<String>,
pub persona: UserPersona,
pub department: Option<String>,
pub working_hours: WorkingHoursPattern,
pub company_codes: Vec<String>,
pub cost_centers: Vec<String>,
pub is_active: bool,
pub start_date: Option<chrono::NaiveDate>,
pub end_date: Option<chrono::NaiveDate>,
}
impl User {
pub fn new(user_id: String, display_name: String, persona: UserPersona) -> Self {
let working_hours = if persona.is_human() {
WorkingHoursPattern::default()
} else {
WorkingHoursPattern::batch_processing()
};
Self {
user_id,
display_name,
email: None,
persona,
department: None,
working_hours,
company_codes: Vec::new(),
cost_centers: Vec::new(),
is_active: true,
start_date: None,
end_date: None,
}
}
pub fn system(user_id: &str) -> Self {
Self::new(
user_id.to_string(),
format!("System User {user_id}"),
UserPersona::AutomatedSystem,
)
}
pub fn can_post_to_company(&self, company_code: &str) -> bool {
self.company_codes.is_empty() || self.company_codes.iter().any(|c| c == company_code)
}
pub fn generate_username(persona: UserPersona, index: usize) -> String {
match persona {
UserPersona::JuniorAccountant => format!("JACC{index:04}"),
UserPersona::SeniorAccountant => format!("SACC{index:04}"),
UserPersona::Controller => format!("CTRL{index:04}"),
UserPersona::Manager => format!("MGR{index:04}"),
UserPersona::Executive => format!("EXEC{index:04}"),
UserPersona::AutomatedSystem => format!("BATCH{index:04}"),
UserPersona::ExternalAuditor => format!("AUDIT{index:04}"),
UserPersona::FraudActor => format!("USER{index:04}"), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPool {
pub users: Vec<User>,
#[serde(skip)]
persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
}
impl UserPool {
pub fn new() -> Self {
Self {
users: Vec::new(),
persona_index: std::collections::HashMap::new(),
}
}
pub fn add_user(&mut self, user: User) {
let idx = self.users.len();
let persona = user.persona;
self.users.push(user);
self.persona_index.entry(persona).or_default().push(idx);
}
pub fn get_users_by_persona(&self, persona: UserPersona) -> Vec<&User> {
self.persona_index
.get(&persona)
.map(|indices| indices.iter().map(|&i| &self.users[i]).collect())
.unwrap_or_default()
}
pub fn get_random_user(&self, persona: UserPersona, rng: &mut impl rand::Rng) -> Option<&User> {
use rand::seq::IndexedRandom;
self.get_users_by_persona(persona).choose(rng).copied()
}
pub fn rebuild_index(&mut self) {
self.persona_index.clear();
for (idx, user) in self.users.iter().enumerate() {
self.persona_index
.entry(user.persona)
.or_default()
.push(idx);
}
}
pub fn generate_standard(company_codes: &[String]) -> Self {
let mut pool = Self::new();
for i in 0..10 {
let mut user = User::new(
User::generate_username(UserPersona::JuniorAccountant, i),
format!("Junior Accountant {}", i + 1),
UserPersona::JuniorAccountant,
);
user.company_codes = company_codes.to_vec();
pool.add_user(user);
}
for i in 0..5 {
let mut user = User::new(
User::generate_username(UserPersona::SeniorAccountant, i),
format!("Senior Accountant {}", i + 1),
UserPersona::SeniorAccountant,
);
user.company_codes = company_codes.to_vec();
pool.add_user(user);
}
for i in 0..2 {
let mut user = User::new(
User::generate_username(UserPersona::Controller, i),
format!("Controller {}", i + 1),
UserPersona::Controller,
);
user.company_codes = company_codes.to_vec();
pool.add_user(user);
}
for i in 0..3 {
let mut user = User::new(
User::generate_username(UserPersona::Manager, i),
format!("Finance Manager {}", i + 1),
UserPersona::Manager,
);
user.company_codes = company_codes.to_vec();
pool.add_user(user);
}
for i in 0..20 {
let mut user = User::new(
User::generate_username(UserPersona::AutomatedSystem, i),
format!("Batch Job {}", i + 1),
UserPersona::AutomatedSystem,
);
user.company_codes = company_codes.to_vec();
pool.add_user(user);
}
pool
}
}
impl Default for UserPool {
fn default() -> Self {
Self::new()
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum JobLevel {
#[default]
Staff,
Senior,
Lead,
Supervisor,
Manager,
Director,
VicePresident,
Executive,
}
impl JobLevel {
pub fn management_level(&self) -> u8 {
match self {
Self::Staff => 0,
Self::Senior => 0,
Self::Lead => 1,
Self::Supervisor => 2,
Self::Manager => 3,
Self::Director => 4,
Self::VicePresident => 5,
Self::Executive => 6,
}
}
pub fn is_manager(&self) -> bool {
self.management_level() >= 2
}
pub fn typical_direct_reports(&self) -> (u16, u16) {
match self {
Self::Staff | Self::Senior => (0, 0),
Self::Lead => (0, 3),
Self::Supervisor => (3, 10),
Self::Manager => (5, 15),
Self::Director => (3, 8),
Self::VicePresident => (3, 6),
Self::Executive => (5, 12),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EmployeeStatus {
#[default]
Active,
OnLeave,
Suspended,
NoticePeriod,
Terminated,
Retired,
Contractor,
}
impl EmployeeStatus {
pub fn can_transact(&self) -> bool {
matches!(self, Self::Active | Self::Contractor)
}
pub fn is_active(&self) -> bool {
!matches!(self, Self::Terminated | Self::Retired)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SystemRole {
Viewer,
Creator,
Approver,
PaymentReleaser,
BankProcessor,
JournalPoster,
PeriodClose,
Admin,
ApAccountant,
ArAccountant,
Buyer,
Executive,
FinancialAnalyst,
GeneralAccountant,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionCodeAuth {
pub tcode: String,
pub activity: ActivityType,
pub active: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ActivityType {
#[default]
Display,
Create,
Change,
Delete,
Execute,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Employee {
pub employee_id: String,
pub user_id: String,
pub display_name: String,
pub first_name: String,
pub last_name: String,
pub email: String,
pub persona: UserPersona,
pub job_level: JobLevel,
pub job_title: String,
pub department_id: Option<String>,
pub cost_center: Option<String>,
pub manager_id: Option<String>,
pub direct_reports: Vec<String>,
pub status: EmployeeStatus,
pub company_code: String,
pub working_hours: WorkingHoursPattern,
pub authorized_company_codes: Vec<String>,
pub authorized_cost_centers: Vec<String>,
pub approval_limit: Decimal,
pub can_approve_pr: bool,
pub can_approve_po: bool,
pub can_approve_invoice: bool,
pub can_approve_je: bool,
pub can_release_payment: bool,
pub system_roles: Vec<SystemRole>,
pub transaction_codes: Vec<TransactionCodeAuth>,
pub hire_date: Option<chrono::NaiveDate>,
pub termination_date: Option<chrono::NaiveDate>,
pub location: Option<String>,
pub is_shared_services: bool,
pub phone: Option<String>,
#[serde(with = "crate::serde_decimal")]
pub base_salary: rust_decimal::Decimal,
}
impl Employee {
pub fn new(
employee_id: impl Into<String>,
user_id: impl Into<String>,
first_name: impl Into<String>,
last_name: impl Into<String>,
company_code: impl Into<String>,
) -> Self {
let first = first_name.into();
let last = last_name.into();
let uid = user_id.into();
let display_name = format!("{first} {last}");
let email = format!(
"{}.{}@company.com",
first.to_lowercase(),
last.to_lowercase()
);
Self {
employee_id: employee_id.into(),
user_id: uid,
display_name,
first_name: first,
last_name: last,
email,
persona: UserPersona::JuniorAccountant,
job_level: JobLevel::Staff,
job_title: "Staff Accountant".to_string(),
department_id: None,
cost_center: None,
manager_id: None,
direct_reports: Vec::new(),
status: EmployeeStatus::Active,
company_code: company_code.into(),
working_hours: WorkingHoursPattern::default(),
authorized_company_codes: Vec::new(),
authorized_cost_centers: Vec::new(),
approval_limit: Decimal::ZERO,
can_approve_pr: false,
can_approve_po: false,
can_approve_invoice: false,
can_approve_je: false,
can_release_payment: false,
system_roles: Vec::new(),
transaction_codes: Vec::new(),
hire_date: None,
termination_date: None,
location: None,
is_shared_services: false,
phone: None,
base_salary: Decimal::ZERO,
}
}
pub fn with_persona(mut self, persona: UserPersona) -> Self {
self.persona = persona;
match persona {
UserPersona::JuniorAccountant => {
self.job_level = JobLevel::Staff;
self.job_title = "Junior Accountant".to_string();
self.approval_limit = Decimal::from(1000);
}
UserPersona::SeniorAccountant => {
self.job_level = JobLevel::Senior;
self.job_title = "Senior Accountant".to_string();
self.approval_limit = Decimal::from(10000);
self.can_approve_je = true;
}
UserPersona::Controller => {
self.job_level = JobLevel::Manager;
self.job_title = "Controller".to_string();
self.approval_limit = Decimal::from(100000);
self.can_approve_pr = true;
self.can_approve_po = true;
self.can_approve_invoice = true;
self.can_approve_je = true;
}
UserPersona::Manager => {
self.job_level = JobLevel::Director;
self.job_title = "Finance Director".to_string();
self.approval_limit = Decimal::from(500000);
self.can_approve_pr = true;
self.can_approve_po = true;
self.can_approve_invoice = true;
self.can_approve_je = true;
self.can_release_payment = true;
}
UserPersona::Executive => {
self.job_level = JobLevel::Executive;
self.job_title = "CFO".to_string();
self.approval_limit = Decimal::from(999_999_999); self.can_approve_pr = true;
self.can_approve_po = true;
self.can_approve_invoice = true;
self.can_approve_je = true;
self.can_release_payment = true;
}
UserPersona::AutomatedSystem => {
self.job_level = JobLevel::Staff;
self.job_title = "Batch Process".to_string();
self.working_hours = WorkingHoursPattern::batch_processing();
}
UserPersona::ExternalAuditor => {
self.job_level = JobLevel::Senior;
self.job_title = "External Auditor".to_string();
self.approval_limit = Decimal::ZERO;
}
UserPersona::FraudActor => {
self.job_level = JobLevel::Staff;
self.job_title = "Staff Accountant".to_string();
self.approval_limit = Decimal::from(10000);
}
}
self
}
pub fn with_job_level(mut self, level: JobLevel) -> Self {
self.job_level = level;
self
}
pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
self.job_title = title.into();
self
}
pub fn with_manager(mut self, manager_id: impl Into<String>) -> Self {
self.manager_id = Some(manager_id.into());
self
}
pub fn with_department(mut self, department_id: impl Into<String>) -> Self {
self.department_id = Some(department_id.into());
self
}
pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
self.cost_center = Some(cost_center.into());
self
}
pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
self.approval_limit = limit;
self
}
pub fn with_authorized_company(mut self, company_code: impl Into<String>) -> Self {
self.authorized_company_codes.push(company_code.into());
self
}
pub fn with_role(mut self, role: SystemRole) -> Self {
self.system_roles.push(role);
self
}
pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
self.hire_date = Some(date);
self
}
pub fn add_direct_report(&mut self, employee_id: String) {
if !self.direct_reports.contains(&employee_id) {
self.direct_reports.push(employee_id);
}
}
pub fn can_approve_amount(&self, amount: Decimal) -> bool {
if self.status != EmployeeStatus::Active {
return false;
}
amount <= self.approval_limit
}
pub fn can_approve_in_company(&self, company_code: &str) -> bool {
if self.status != EmployeeStatus::Active {
return false;
}
self.authorized_company_codes.is_empty()
|| self
.authorized_company_codes
.iter()
.any(|c| c == company_code)
}
pub fn has_role(&self, role: &SystemRole) -> bool {
self.system_roles.contains(role)
}
pub fn hierarchy_depth(&self) -> u8 {
match self.job_level {
JobLevel::Executive => 0,
JobLevel::VicePresident => 1,
JobLevel::Director => 2,
JobLevel::Manager => 3,
JobLevel::Supervisor => 4,
JobLevel::Lead => 5,
JobLevel::Senior => 6,
JobLevel::Staff => 7,
}
}
pub fn to_user(&self) -> User {
let mut user = User::new(
self.user_id.clone(),
self.display_name.clone(),
self.persona,
);
user.email = Some(self.email.clone());
user.department = self.department_id.clone();
user.working_hours = self.working_hours.clone();
user.company_codes = self.authorized_company_codes.clone();
user.cost_centers = self.authorized_cost_centers.clone();
user.is_active = self.status.can_transact();
user.start_date = self.hire_date;
user.end_date = self.termination_date;
user
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EmployeePool {
pub employees: Vec<Employee>,
#[serde(skip)]
id_index: std::collections::HashMap<String, usize>,
#[serde(skip)]
manager_index: std::collections::HashMap<String, Vec<usize>>,
#[serde(skip)]
persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
#[serde(skip)]
department_index: std::collections::HashMap<String, Vec<usize>>,
}
impl EmployeePool {
pub fn new() -> Self {
Self::default()
}
pub fn add_employee(&mut self, employee: Employee) {
let idx = self.employees.len();
self.id_index.insert(employee.employee_id.clone(), idx);
if let Some(ref mgr_id) = employee.manager_id {
self.manager_index
.entry(mgr_id.clone())
.or_default()
.push(idx);
}
self.persona_index
.entry(employee.persona)
.or_default()
.push(idx);
if let Some(ref dept_id) = employee.department_id {
self.department_index
.entry(dept_id.clone())
.or_default()
.push(idx);
}
self.employees.push(employee);
}
pub fn get_by_id(&self, employee_id: &str) -> Option<&Employee> {
self.id_index
.get(employee_id)
.map(|&idx| &self.employees[idx])
}
pub fn get_by_id_mut(&mut self, employee_id: &str) -> Option<&mut Employee> {
self.id_index
.get(employee_id)
.copied()
.map(|idx| &mut self.employees[idx])
}
pub fn get_direct_reports(&self, manager_id: &str) -> Vec<&Employee> {
self.manager_index
.get(manager_id)
.map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
.unwrap_or_default()
}
pub fn get_by_persona(&self, persona: UserPersona) -> Vec<&Employee> {
self.persona_index
.get(&persona)
.map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
.unwrap_or_default()
}
pub fn get_by_department(&self, department_id: &str) -> Vec<&Employee> {
self.department_index
.get(department_id)
.map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
.unwrap_or_default()
}
pub fn get_random_approver(&self, rng: &mut impl rand::Rng) -> Option<&Employee> {
use rand::seq::IndexedRandom;
let approvers: Vec<_> = self
.employees
.iter()
.filter(|e| e.persona.has_approval_authority() && e.status == EmployeeStatus::Active)
.collect();
approvers.choose(rng).copied()
}
pub fn get_approver_for_amount(
&self,
amount: Decimal,
rng: &mut impl rand::Rng,
) -> Option<&Employee> {
use rand::seq::IndexedRandom;
let approvers: Vec<_> = self
.employees
.iter()
.filter(|e| e.can_approve_amount(amount))
.collect();
approvers.choose(rng).copied()
}
pub fn get_managers(&self) -> Vec<&Employee> {
self.employees
.iter()
.filter(|e| !e.direct_reports.is_empty() || e.job_level.is_manager())
.collect()
}
pub fn get_reporting_chain(&self, employee_id: &str) -> Vec<&Employee> {
let mut chain = Vec::new();
let mut current_id = employee_id.to_string();
while let Some(emp) = self.get_by_id(¤t_id) {
chain.push(emp);
if let Some(ref mgr_id) = emp.manager_id {
current_id = mgr_id.clone();
} else {
break;
}
}
chain
}
pub fn rebuild_indices(&mut self) {
self.id_index.clear();
self.manager_index.clear();
self.persona_index.clear();
self.department_index.clear();
for (idx, employee) in self.employees.iter().enumerate() {
self.id_index.insert(employee.employee_id.clone(), idx);
if let Some(ref mgr_id) = employee.manager_id {
self.manager_index
.entry(mgr_id.clone())
.or_default()
.push(idx);
}
self.persona_index
.entry(employee.persona)
.or_default()
.push(idx);
if let Some(ref dept_id) = employee.department_id {
self.department_index
.entry(dept_id.clone())
.or_default()
.push(idx);
}
}
}
pub fn len(&self) -> usize {
self.employees.len()
}
pub fn is_empty(&self) -> bool {
self.employees.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EmployeeEventType {
#[default]
Hired,
Promoted,
SalaryAdjustment,
Transfer,
Terminated,
}
impl std::fmt::Display for EmployeeEventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hired => write!(f, "hired"),
Self::Promoted => write!(f, "promoted"),
Self::SalaryAdjustment => write!(f, "salary_adjustment"),
Self::Transfer => write!(f, "transfer"),
Self::Terminated => write!(f, "terminated"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmployeeChangeEvent {
pub employee_id: String,
pub event_date: chrono::NaiveDate,
pub event_type: EmployeeEventType,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub effective_date: chrono::NaiveDate,
}
impl EmployeeChangeEvent {
pub fn hired(employee_id: impl Into<String>, hire_date: chrono::NaiveDate) -> Self {
Self {
employee_id: employee_id.into(),
event_date: hire_date,
event_type: EmployeeEventType::Hired,
old_value: None,
new_value: Some("active".to_string()),
effective_date: hire_date,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_persona_properties() {
assert!(UserPersona::JuniorAccountant.is_human());
assert!(!UserPersona::AutomatedSystem.is_human());
assert!(UserPersona::Controller.has_approval_authority());
assert!(!UserPersona::JuniorAccountant.has_approval_authority());
}
#[test]
fn test_persona_display_snake_case() {
assert_eq!(
UserPersona::JuniorAccountant.to_string(),
"junior_accountant"
);
assert_eq!(
UserPersona::SeniorAccountant.to_string(),
"senior_accountant"
);
assert_eq!(UserPersona::Controller.to_string(), "controller");
assert_eq!(UserPersona::Manager.to_string(), "manager");
assert_eq!(UserPersona::Executive.to_string(), "executive");
assert_eq!(UserPersona::AutomatedSystem.to_string(), "automated_system");
assert_eq!(UserPersona::ExternalAuditor.to_string(), "external_auditor");
assert_eq!(UserPersona::FraudActor.to_string(), "fraud_actor");
for persona in [
UserPersona::JuniorAccountant,
UserPersona::SeniorAccountant,
UserPersona::Controller,
UserPersona::Manager,
UserPersona::Executive,
UserPersona::AutomatedSystem,
UserPersona::ExternalAuditor,
UserPersona::FraudActor,
] {
let s = persona.to_string();
assert!(
!s.contains(char::is_uppercase),
"Display output '{}' should be all lowercase snake_case",
s
);
}
}
#[test]
fn test_user_pool() {
let pool = UserPool::generate_standard(&["1000".to_string()]);
assert!(!pool.users.is_empty());
assert!(!pool
.get_users_by_persona(UserPersona::JuniorAccountant)
.is_empty());
}
#[test]
fn test_job_level_hierarchy() {
assert!(JobLevel::Executive.management_level() > JobLevel::Manager.management_level());
assert!(JobLevel::Manager.is_manager());
assert!(!JobLevel::Staff.is_manager());
}
#[test]
fn test_employee_creation() {
let employee = Employee::new("E-001", "jsmith", "John", "Smith", "1000")
.with_persona(UserPersona::Controller);
assert_eq!(employee.employee_id, "E-001");
assert_eq!(employee.display_name, "John Smith");
assert!(employee.can_approve_je);
assert_eq!(employee.job_level, JobLevel::Manager);
}
#[test]
fn test_employee_approval_limits() {
let employee = Employee::new("E-001", "test", "Test", "User", "1000")
.with_approval_limit(Decimal::from(10000));
assert!(employee.can_approve_amount(Decimal::from(5000)));
assert!(!employee.can_approve_amount(Decimal::from(15000)));
}
#[test]
fn test_employee_pool_hierarchy() {
let mut pool = EmployeePool::new();
let cfo = Employee::new("E-001", "cfo", "Jane", "CEO", "1000")
.with_persona(UserPersona::Executive);
let controller = Employee::new("E-002", "ctrl", "Bob", "Controller", "1000")
.with_persona(UserPersona::Controller)
.with_manager("E-001");
let accountant = Employee::new("E-003", "acc", "Alice", "Accountant", "1000")
.with_persona(UserPersona::JuniorAccountant)
.with_manager("E-002");
pool.add_employee(cfo);
pool.add_employee(controller);
pool.add_employee(accountant);
let direct_reports = pool.get_direct_reports("E-001");
assert_eq!(direct_reports.len(), 1);
assert_eq!(direct_reports[0].employee_id, "E-002");
let chain = pool.get_reporting_chain("E-003");
assert_eq!(chain.len(), 3);
assert_eq!(chain[0].employee_id, "E-003");
assert_eq!(chain[1].employee_id, "E-002");
assert_eq!(chain[2].employee_id, "E-001");
}
#[test]
fn test_employee_to_user() {
let employee = Employee::new("E-001", "jdoe", "John", "Doe", "1000")
.with_persona(UserPersona::SeniorAccountant);
let user = employee.to_user();
assert_eq!(user.user_id, "jdoe");
assert_eq!(user.persona, UserPersona::SeniorAccountant);
assert!(user.is_active);
}
#[test]
fn test_employee_status() {
assert!(EmployeeStatus::Active.can_transact());
assert!(EmployeeStatus::Contractor.can_transact());
assert!(!EmployeeStatus::Terminated.can_transact());
assert!(!EmployeeStatus::OnLeave.can_transact());
}
}