Skip to main content

datasynth_generators/
user_generator.rs

1//! User pool generator with realistic names and department assignments.
2
3use datasynth_core::models::{
4    Department, OrganizationStructure, User, UserPersona, UserPool, WorkingHoursPattern,
5};
6use datasynth_core::templates::{MultiCultureNameGenerator, NameCulture, PersonName};
7use rand::prelude::*;
8use rand_chacha::ChaCha8Rng;
9
10/// Configuration for user generation.
11#[derive(Debug, Clone)]
12pub struct UserGeneratorConfig {
13    /// Name culture distribution
14    pub culture_distribution: Vec<(NameCulture, f64)>,
15    /// Email domain for user emails
16    pub email_domain: String,
17    /// Generate realistic names vs generic IDs
18    pub generate_realistic_names: bool,
19}
20
21impl Default for UserGeneratorConfig {
22    fn default() -> Self {
23        Self {
24            culture_distribution: vec![
25                (NameCulture::WesternUs, 0.40),
26                (NameCulture::Hispanic, 0.20),
27                (NameCulture::German, 0.10),
28                (NameCulture::French, 0.05),
29                (NameCulture::Chinese, 0.10),
30                (NameCulture::Japanese, 0.05),
31                (NameCulture::Indian, 0.10),
32            ],
33            email_domain: "company.com".to_string(),
34            generate_realistic_names: true,
35        }
36    }
37}
38
39/// Generator for user pools with realistic names.
40pub struct UserGenerator {
41    rng: ChaCha8Rng,
42    seed: u64,
43    name_generator: MultiCultureNameGenerator,
44    config: UserGeneratorConfig,
45    user_counter: usize,
46}
47
48impl UserGenerator {
49    /// Create a new user generator.
50    pub fn new(seed: u64) -> Self {
51        Self::with_config(seed, UserGeneratorConfig::default())
52    }
53
54    /// Create a new user generator with custom configuration.
55    pub fn with_config(seed: u64, config: UserGeneratorConfig) -> Self {
56        let mut name_gen =
57            MultiCultureNameGenerator::with_distribution(config.culture_distribution.clone());
58        name_gen.set_email_domain(&config.email_domain);
59
60        Self {
61            rng: ChaCha8Rng::seed_from_u64(seed),
62            seed,
63            name_generator: name_gen,
64            config,
65            user_counter: 0,
66        }
67    }
68
69    /// Generate a single user for a specific persona.
70    pub fn generate_user(&mut self, persona: UserPersona, department: Option<&Department>) -> User {
71        self.user_counter += 1;
72
73        let (user_id, display_name, email) = if self.config.generate_realistic_names {
74            let name = self.name_generator.generate_name(&mut self.rng);
75            let user_id = name.to_user_id(self.user_counter);
76            let email = self.name_generator.generate_email(&name);
77            (user_id, name.display_name, Some(email))
78        } else {
79            let user_id = User::generate_username(persona, self.user_counter);
80            let display_name = format!("{:?} {}", persona, self.user_counter);
81            (user_id, display_name, None)
82        };
83
84        let working_hours = if persona.is_human() {
85            self.select_working_hours_pattern()
86        } else {
87            WorkingHoursPattern::batch_processing()
88        };
89
90        let mut user = User::new(user_id, display_name, persona);
91        user.email = email;
92        user.working_hours = working_hours;
93
94        if let Some(dept) = department {
95            user.department = Some(dept.name.clone());
96            user.cost_centers = vec![dept.cost_center.clone()];
97        }
98
99        user
100    }
101
102    /// Generate a user with a specific name (for deterministic generation).
103    pub fn generate_user_with_name(
104        &mut self,
105        name: PersonName,
106        persona: UserPersona,
107        department: Option<&Department>,
108    ) -> User {
109        self.user_counter += 1;
110
111        let user_id = name.to_user_id(self.user_counter);
112        let email = self.name_generator.generate_email(&name);
113
114        let working_hours = if persona.is_human() {
115            self.select_working_hours_pattern()
116        } else {
117            WorkingHoursPattern::batch_processing()
118        };
119
120        let mut user = User::new(user_id, name.display_name, persona);
121        user.email = Some(email);
122        user.working_hours = working_hours;
123
124        if let Some(dept) = department {
125            user.department = Some(dept.name.clone());
126            user.cost_centers = vec![dept.cost_center.clone()];
127        }
128
129        user
130    }
131
132    /// Select a working hours pattern based on random distribution.
133    fn select_working_hours_pattern(&mut self) -> WorkingHoursPattern {
134        let roll: f64 = self.rng.gen();
135        if roll < 0.5 {
136            WorkingHoursPattern::us_standard()
137        } else if roll < 0.75 {
138            WorkingHoursPattern::european()
139        } else {
140            WorkingHoursPattern::asian()
141        }
142    }
143
144    /// Generate a user pool from an organization structure.
145    pub fn generate_from_organization(
146        &mut self,
147        org: &OrganizationStructure,
148        company_codes: &[String],
149    ) -> UserPool {
150        let mut pool = UserPool::new();
151
152        for dept in &org.departments {
153            self.generate_department_users(&mut pool, dept, company_codes);
154        }
155
156        pool
157    }
158
159    /// Generate users for a specific department.
160    fn generate_department_users(
161        &mut self,
162        pool: &mut UserPool,
163        dept: &Department,
164        company_codes: &[String],
165    ) {
166        let headcount = &dept.standard_headcount;
167
168        // Generate junior accountants
169        for _ in 0..headcount.junior_accountant {
170            let mut user = self.generate_user(UserPersona::JuniorAccountant, Some(dept));
171            user.company_codes = company_codes.to_vec();
172            pool.add_user(user);
173        }
174
175        // Generate senior accountants
176        for _ in 0..headcount.senior_accountant {
177            let mut user = self.generate_user(UserPersona::SeniorAccountant, Some(dept));
178            user.company_codes = company_codes.to_vec();
179            pool.add_user(user);
180        }
181
182        // Generate controllers
183        for _ in 0..headcount.controller {
184            let mut user = self.generate_user(UserPersona::Controller, Some(dept));
185            user.company_codes = company_codes.to_vec();
186            pool.add_user(user);
187        }
188
189        // Generate managers
190        for _ in 0..headcount.manager {
191            let mut user = self.generate_user(UserPersona::Manager, Some(dept));
192            user.company_codes = company_codes.to_vec();
193            pool.add_user(user);
194        }
195
196        // Generate executives
197        for _ in 0..headcount.executive {
198            let mut user = self.generate_user(UserPersona::Executive, Some(dept));
199            user.company_codes = company_codes.to_vec();
200            pool.add_user(user);
201        }
202
203        // Generate automated systems
204        for _ in 0..headcount.automated_system {
205            let mut user = self.generate_user(UserPersona::AutomatedSystem, Some(dept));
206            user.company_codes = company_codes.to_vec();
207            pool.add_user(user);
208        }
209    }
210
211    /// Generate a simple user pool with specified counts per persona.
212    pub fn generate_simple_pool(
213        &mut self,
214        junior_count: usize,
215        senior_count: usize,
216        controller_count: usize,
217        manager_count: usize,
218        automated_count: usize,
219        company_codes: &[String],
220    ) -> UserPool {
221        let mut pool = UserPool::new();
222
223        for _ in 0..junior_count {
224            let mut user = self.generate_user(UserPersona::JuniorAccountant, None);
225            user.company_codes = company_codes.to_vec();
226            pool.add_user(user);
227        }
228
229        for _ in 0..senior_count {
230            let mut user = self.generate_user(UserPersona::SeniorAccountant, None);
231            user.company_codes = company_codes.to_vec();
232            pool.add_user(user);
233        }
234
235        for _ in 0..controller_count {
236            let mut user = self.generate_user(UserPersona::Controller, None);
237            user.company_codes = company_codes.to_vec();
238            pool.add_user(user);
239        }
240
241        for _ in 0..manager_count {
242            let mut user = self.generate_user(UserPersona::Manager, None);
243            user.company_codes = company_codes.to_vec();
244            pool.add_user(user);
245        }
246
247        for _ in 0..automated_count {
248            let mut user = self.generate_user(UserPersona::AutomatedSystem, None);
249            user.company_codes = company_codes.to_vec();
250            pool.add_user(user);
251        }
252
253        pool
254    }
255
256    /// Generate a standard user pool (equivalent to UserPool::generate_standard but with realistic names).
257    pub fn generate_standard(&mut self, company_codes: &[String]) -> UserPool {
258        self.generate_simple_pool(10, 5, 2, 3, 20, company_codes)
259    }
260
261    /// Reset the generator.
262    pub fn reset(&mut self) {
263        self.rng = ChaCha8Rng::seed_from_u64(self.seed);
264        self.user_counter = 0;
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_user_generation() {
274        let mut gen = UserGenerator::new(42);
275
276        let user = gen.generate_user(UserPersona::SeniorAccountant, None);
277
278        assert!(!user.user_id.is_empty());
279        assert!(!user.display_name.is_empty());
280        assert!(user.email.is_some());
281        assert_eq!(user.persona, UserPersona::SeniorAccountant);
282    }
283
284    #[test]
285    fn test_generate_standard_pool() {
286        let mut gen = UserGenerator::new(42);
287        let pool = gen.generate_standard(&["1000".to_string()]);
288
289        assert_eq!(pool.users.len(), 40); // 10 + 5 + 2 + 3 + 20
290
291        // Check we have users of each type
292        assert!(!pool
293            .get_users_by_persona(UserPersona::JuniorAccountant)
294            .is_empty());
295        assert!(!pool
296            .get_users_by_persona(UserPersona::AutomatedSystem)
297            .is_empty());
298    }
299
300    #[test]
301    fn test_generate_from_organization() {
302        let mut gen = UserGenerator::new(42);
303        let org = OrganizationStructure::standard("1000");
304        let pool = gen.generate_from_organization(&org, &["1000".to_string()]);
305
306        // Should have users from all departments
307        assert!(pool.users.len() > 20);
308
309        // Users should have departments assigned
310        let has_dept_users = pool.users.iter().any(|u| u.department.is_some());
311        assert!(has_dept_users);
312    }
313
314    #[test]
315    fn test_deterministic_generation() {
316        let mut gen1 = UserGenerator::new(42);
317        let mut gen2 = UserGenerator::new(42);
318
319        let user1 = gen1.generate_user(UserPersona::SeniorAccountant, None);
320        let user2 = gen2.generate_user(UserPersona::SeniorAccountant, None);
321
322        assert_eq!(user1.user_id, user2.user_id);
323        assert_eq!(user1.display_name, user2.display_name);
324    }
325
326    #[test]
327    fn test_generic_names() {
328        let config = UserGeneratorConfig {
329            generate_realistic_names: false,
330            ..Default::default()
331        };
332        let mut gen = UserGenerator::with_config(42, config);
333
334        let user = gen.generate_user(UserPersona::JuniorAccountant, None);
335
336        // Should use the generic format
337        assert!(user.user_id.starts_with("JACC"));
338    }
339}