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