1use chrono::NaiveDate;
9use datasynth_core::utils::seeded_rng;
10use rand::Rng;
11use rand_chacha::ChaCha8Rng;
12use std::collections::HashMap;
13
14use datasynth_core::models::organizational_event::{
15 AcquisitionConfig, DateRange, DivestitureConfig, IntegrationPhaseConfig,
16 LeadershipChangeConfig, MergerConfig, OrganizationalEvent, OrganizationalEventType, PolicyArea,
17 PolicyChangeDetail, ReorganizationConfig, ReportingChange, WorkforceReductionConfig,
18};
19
20#[derive(Debug, Clone)]
25pub struct OrgEventGeneratorConfig {
26 pub type_weights: [f64; 6],
28 pub events_per_year: f64,
30}
31
32impl Default for OrgEventGeneratorConfig {
33 fn default() -> Self {
34 Self {
35 type_weights: [0.15, 0.10, 0.25, 0.20, 0.15, 0.15],
36 events_per_year: 3.0,
37 }
38 }
39}
40
41pub struct OrganizationalEventGenerator {
47 rng: ChaCha8Rng,
48 config: OrgEventGeneratorConfig,
49 event_counter: usize,
50}
51
52const SEED_DISCRIMINATOR: u64 = 0xAE_0B;
55
56impl OrganizationalEventGenerator {
57 pub fn new(seed: u64) -> Self {
59 Self {
60 rng: seeded_rng(seed, SEED_DISCRIMINATOR),
61 config: OrgEventGeneratorConfig::default(),
62 event_counter: 0,
63 }
64 }
65
66 pub fn with_config(seed: u64, config: OrgEventGeneratorConfig) -> Self {
68 Self {
69 rng: seeded_rng(seed, SEED_DISCRIMINATOR),
70 config,
71 event_counter: 0,
72 }
73 }
74
75 pub fn generate_events(
80 &mut self,
81 start_date: NaiveDate,
82 end_date: NaiveDate,
83 company_codes: &[String],
84 ) -> Vec<OrganizationalEvent> {
85 let total_days = (end_date - start_date).num_days().max(1) as f64;
86 let total_years = total_days / 365.25;
87 let expected_count = (self.config.events_per_year * total_years).round() as usize;
88 let count = expected_count.max(1);
89
90 let mut events = Vec::with_capacity(count);
91
92 for _ in 0..count {
93 self.event_counter += 1;
94 let days_offset = self.rng.random_range(0..total_days as i64);
95 let effective_date = start_date + chrono::Duration::days(days_offset);
96
97 let company_code = if company_codes.is_empty() {
98 "C001".to_string()
99 } else {
100 let idx = self.rng.random_range(0..company_codes.len());
101 company_codes[idx].clone()
102 };
103
104 let event = self.build_event(effective_date, &company_code);
105 events.push(event);
106 }
107
108 events.sort_by_key(|e| e.effective_date);
109 events
110 }
111
112 fn pick_event_type_index(&mut self) -> usize {
114 let weights = &self.config.type_weights;
115 let total: f64 = weights.iter().sum();
116 let mut r: f64 = self.rng.random_range(0.0..total);
117
118 for (i, &w) in weights.iter().enumerate() {
119 r -= w;
120 if r <= 0.0 {
121 return i;
122 }
123 }
124 0
125 }
126
127 fn build_event(
130 &mut self,
131 effective_date: NaiveDate,
132 company_code: &str,
133 ) -> OrganizationalEvent {
134 let event_id = format!("ORG-EVT-{:06}", self.event_counter);
135 let type_idx = self.pick_event_type_index();
136
137 let event_type = match type_idx {
138 0 => self.build_acquisition(effective_date, company_code),
139 1 => self.build_divestiture(effective_date, company_code),
140 2 => self.build_reorganization(effective_date),
141 3 => self.build_leadership_change(effective_date),
142 4 => self.build_workforce_reduction(effective_date),
143 _ => self.build_merger(effective_date, company_code),
144 };
145
146 let description = match &event_type {
147 OrganizationalEventType::Acquisition(c) => Some(format!(
148 "Acquisition of {} by {}",
149 c.acquired_entity_code, company_code
150 )),
151 OrganizationalEventType::Divestiture(c) => Some(format!(
152 "Divestiture of {} from {}",
153 c.divested_entity_code, company_code
154 )),
155 OrganizationalEventType::Reorganization(_) => {
156 Some(format!("Organizational restructuring at {}", company_code))
157 }
158 OrganizationalEventType::LeadershipChange(c) => {
159 Some(format!("{} transition at {}", c.role, company_code))
160 }
161 OrganizationalEventType::WorkforceReduction(c) => Some(format!(
162 "Workforce reduction ({:.0}%) at {}",
163 c.reduction_percent * 100.0,
164 company_code
165 )),
166 OrganizationalEventType::Merger(c) => Some(format!(
167 "Merger with {} for {}",
168 c.merged_entity_code, company_code
169 )),
170 };
171
172 let tags = vec![
173 format!("company:{}", company_code),
174 format!("type:{}", event_type.type_name()),
175 ];
176
177 OrganizationalEvent {
178 event_id,
179 event_type,
180 effective_date,
181 description,
182 tags,
183 }
184 }
185
186 fn build_acquisition(
191 &mut self,
192 effective_date: NaiveDate,
193 company_code: &str,
194 ) -> OrganizationalEventType {
195 let seq = self.event_counter;
196 let entity_code = format!("ACQ-{}-{:04}", company_code, seq);
197 let volume_mult = self.rng.random_range(1.10..1.60);
198 let parallel_days = self.rng.random_range(15..60_u32);
199
200 let cutover = effective_date + chrono::Duration::days(parallel_days as i64);
201 let stabilization_end = cutover + chrono::Duration::days(self.rng.random_range(60..120));
202
203 OrganizationalEventType::Acquisition(AcquisitionConfig {
204 acquired_entity_code: entity_code.clone(),
205 acquired_entity_name: Some(format!("Acquired Entity {}", seq)),
206 acquisition_date: effective_date,
207 volume_multiplier: volume_mult,
208 integration_error_rate: self.rng.random_range(0.02..0.08),
209 parallel_posting_days: parallel_days,
210 coding_error_rate: self.rng.random_range(0.01..0.05),
211 integration_phases: IntegrationPhaseConfig {
212 parallel_run: Some(DateRange {
213 start: effective_date,
214 end: cutover - chrono::Duration::days(1),
215 }),
216 cutover_date: cutover,
217 stabilization_end,
218 parallel_run_error_rate: self.rng.random_range(0.05..0.12),
219 stabilization_error_rate: self.rng.random_range(0.01..0.05),
220 },
221 purchase_price_allocation: None,
222 })
223 }
224
225 fn build_divestiture(
226 &mut self,
227 effective_date: NaiveDate,
228 company_code: &str,
229 ) -> OrganizationalEventType {
230 let seq = self.event_counter;
231 let entity_code = format!("DIV-{}-{:04}", company_code, seq);
232 let transition = self.rng.random_range(2..6_u32);
233
234 OrganizationalEventType::Divestiture(DivestitureConfig {
235 divested_entity_code: entity_code,
236 divested_entity_name: Some(format!("Divested Unit {}", seq)),
237 divestiture_date: effective_date,
238 volume_reduction: self.rng.random_range(0.50..0.85),
239 transition_months: transition,
240 remove_entity: true,
241 account_closures: Vec::new(),
242 disposal_gain_loss: None,
243 })
244 }
245
246 fn build_reorganization(&mut self, effective_date: NaiveDate) -> OrganizationalEventType {
247 let transition = self.rng.random_range(2..6_u32);
248 let remap_count = self.rng.random_range(1..4_usize);
249
250 let mut cost_center_remapping = HashMap::new();
251 for i in 0..remap_count {
252 cost_center_remapping.insert(
253 format!("CC-{:03}", 100 + i * 10),
254 format!("CC-{:03}", 500 + i * 10),
255 );
256 }
257
258 let reporting_changes = if self.rng.random_bool(0.5) {
259 vec![ReportingChange {
260 entity: "Engineering".to_string(),
261 from_reports_to: "VP Engineering".to_string(),
262 to_reports_to: "CTO".to_string(),
263 }]
264 } else {
265 Vec::new()
266 };
267
268 OrganizationalEventType::Reorganization(ReorganizationConfig {
269 description: Some("Organizational restructuring".to_string()),
270 effective_date,
271 cost_center_remapping,
272 department_remapping: HashMap::new(),
273 reporting_changes,
274 transition_months: transition,
275 transition_error_rate: self.rng.random_range(0.02..0.06),
276 })
277 }
278
279 fn build_leadership_change(&mut self, effective_date: NaiveDate) -> OrganizationalEventType {
280 let roles = ["CFO", "CEO", "Controller", "COO", "CTO", "VP Finance"];
281 let role_idx = self.rng.random_range(0..roles.len());
282 let role = roles[role_idx].to_string();
283
284 let policy_changes = if self.rng.random_bool(0.6) {
285 vec![PolicyChangeDetail {
286 policy_area: PolicyArea::ApprovalThreshold,
287 description: "Updated approval thresholds".to_string(),
288 old_value: None,
289 new_value: None,
290 }]
291 } else {
292 Vec::new()
293 };
294
295 OrganizationalEventType::LeadershipChange(LeadershipChangeConfig {
296 role,
297 change_date: effective_date,
298 policy_changes,
299 vendor_review_triggered: self.rng.random_bool(0.3),
300 policy_transition_months: self.rng.random_range(3..9_u32),
301 policy_change_error_rate: self.rng.random_range(0.01..0.04),
302 })
303 }
304
305 fn build_workforce_reduction(&mut self, effective_date: NaiveDate) -> OrganizationalEventType {
306 let departments = ["Finance", "Operations", "Sales", "Engineering", "HR"];
307 let affected_count = self.rng.random_range(1..=3_usize);
308 let mut affected = Vec::with_capacity(affected_count);
309 for i in 0..affected_count {
310 let idx = (self.rng.random_range(0..departments.len()) + i) % departments.len();
311 let dept = departments[idx].to_string();
312 if !affected.contains(&dept) {
313 affected.push(dept);
314 }
315 }
316
317 OrganizationalEventType::WorkforceReduction(WorkforceReductionConfig {
318 reduction_date: effective_date,
319 reduction_percent: self.rng.random_range(0.05..0.20),
320 affected_departments: affected,
321 error_rate_increase: self.rng.random_range(0.02..0.08),
322 processing_time_increase: self.rng.random_range(1.1..1.5),
323 transition_months: self.rng.random_range(3..9_u32),
324 severance_costs: None,
325 })
326 }
327
328 fn build_merger(
329 &mut self,
330 effective_date: NaiveDate,
331 company_code: &str,
332 ) -> OrganizationalEventType {
333 let seq = self.event_counter;
334 let entity_code = format!("MRG-{}-{:04}", company_code, seq);
335 let volume_mult = self.rng.random_range(1.50..2.20);
336
337 let cutover = effective_date + chrono::Duration::days(self.rng.random_range(30..90));
338 let stabilization_end = cutover + chrono::Duration::days(self.rng.random_range(90..180));
339
340 OrganizationalEventType::Merger(MergerConfig {
341 merged_entity_code: entity_code,
342 merged_entity_name: Some(format!("Merged Entity {}", seq)),
343 merger_date: effective_date,
344 volume_multiplier: volume_mult,
345 integration_error_rate: self.rng.random_range(0.03..0.08),
346 integration_phases: IntegrationPhaseConfig {
347 parallel_run: Some(DateRange {
348 start: effective_date,
349 end: cutover - chrono::Duration::days(1),
350 }),
351 cutover_date: cutover,
352 stabilization_end,
353 parallel_run_error_rate: self.rng.random_range(0.05..0.12),
354 stabilization_error_rate: self.rng.random_range(0.02..0.05),
355 },
356 fair_value_adjustments: Vec::new(),
357 goodwill: None,
358 })
359 }
360}
361
362#[cfg(test)]
363#[allow(clippy::unwrap_used)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_deterministic_generation() {
369 let mut gen1 = OrganizationalEventGenerator::new(42);
370 let mut gen2 = OrganizationalEventGenerator::new(42);
371 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
372 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
373 let companies = vec!["C001".to_string(), "C002".to_string()];
374
375 let events1 = gen1.generate_events(start, end, &companies);
376 let events2 = gen2.generate_events(start, end, &companies);
377
378 assert_eq!(events1.len(), events2.len());
379 for (e1, e2) in events1.iter().zip(events2.iter()) {
380 assert_eq!(e1.event_id, e2.event_id);
381 assert_eq!(e1.effective_date, e2.effective_date);
382 assert_eq!(e1.event_type.type_name(), e2.event_type.type_name());
383 }
384 }
385
386 #[test]
387 fn test_events_sorted_by_date() {
388 let mut gen = OrganizationalEventGenerator::new(42);
389 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
390 let end = NaiveDate::from_ymd_opt(2025, 12, 31).unwrap();
391 let companies = vec!["C001".to_string()];
392
393 let events = gen.generate_events(start, end, &companies);
394 for w in events.windows(2) {
395 assert!(w[0].effective_date <= w[1].effective_date);
396 }
397 }
398
399 #[test]
400 fn test_all_event_types_generated() {
401 let config = OrgEventGeneratorConfig {
402 type_weights: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
403 events_per_year: 100.0,
404 };
405 let mut gen = OrganizationalEventGenerator::with_config(42, config);
406 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
407 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
408 let companies = vec!["C001".to_string()];
409
410 let events = gen.generate_events(start, end, &companies);
411
412 let has_acquisition = events
413 .iter()
414 .any(|e| matches!(e.event_type, OrganizationalEventType::Acquisition(_)));
415 let has_divestiture = events
416 .iter()
417 .any(|e| matches!(e.event_type, OrganizationalEventType::Divestiture(_)));
418 let has_reorg = events
419 .iter()
420 .any(|e| matches!(e.event_type, OrganizationalEventType::Reorganization(_)));
421 let has_leadership = events
422 .iter()
423 .any(|e| matches!(e.event_type, OrganizationalEventType::LeadershipChange(_)));
424 let has_workforce = events
425 .iter()
426 .any(|e| matches!(e.event_type, OrganizationalEventType::WorkforceReduction(_)));
427 let has_merger = events
428 .iter()
429 .any(|e| matches!(e.event_type, OrganizationalEventType::Merger(_)));
430
431 assert!(has_acquisition, "should generate acquisitions");
432 assert!(has_divestiture, "should generate divestitures");
433 assert!(has_reorg, "should generate reorganizations");
434 assert!(has_leadership, "should generate leadership changes");
435 assert!(has_workforce, "should generate workforce reductions");
436 assert!(has_merger, "should generate mergers");
437 }
438
439 #[test]
440 fn test_events_within_date_range() {
441 let mut gen = OrganizationalEventGenerator::new(42);
442 let start = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
443 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
444 let companies = vec!["C001".to_string()];
445
446 let events = gen.generate_events(start, end, &companies);
447 for e in &events {
448 assert!(e.effective_date >= start, "event date before start");
449 assert!(e.effective_date <= end, "event date after end");
450 }
451 }
452
453 #[test]
454 fn test_empty_company_codes() {
455 let mut gen = OrganizationalEventGenerator::new(42);
456 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
457 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
458
459 let events = gen.generate_events(start, end, &[]);
460 assert!(!events.is_empty(), "should still generate events");
461 for e in &events {
463 assert!(
464 e.tags.iter().any(|t| t == "company:C001"),
465 "should use C001 fallback"
466 );
467 }
468 }
469
470 #[test]
471 fn test_event_has_tags_and_description() {
472 let mut gen = OrganizationalEventGenerator::new(99);
473 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
474 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
475 let companies = vec!["ACME".to_string()];
476
477 let events = gen.generate_events(start, end, &companies);
478 for e in &events {
479 assert!(e.description.is_some(), "event should have a description");
480 assert!(!e.tags.is_empty(), "event should have tags");
481 assert!(
482 e.tags.iter().any(|t| t.starts_with("company:")),
483 "should have company tag"
484 );
485 assert!(
486 e.tags.iter().any(|t| t.starts_with("type:")),
487 "should have type tag"
488 );
489 }
490 }
491
492 #[test]
493 fn test_acquisition_config_populated() {
494 let config = OrgEventGeneratorConfig {
495 type_weights: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
496 events_per_year: 5.0,
497 };
498 let mut gen = OrganizationalEventGenerator::with_config(42, config);
499 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
500 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
501 let companies = vec!["C001".to_string()];
502
503 let events = gen.generate_events(start, end, &companies);
504 for e in &events {
505 if let OrganizationalEventType::Acquisition(ref acq) = e.event_type {
506 assert!(!acq.acquired_entity_code.is_empty());
507 assert!(acq.volume_multiplier >= 1.10);
508 assert!(acq.integration_error_rate > 0.0);
509 assert!(acq.integration_phases.parallel_run.is_some());
510 } else {
511 panic!("expected only acquisitions");
512 }
513 }
514 }
515
516 #[test]
517 fn test_event_is_active_at_effective_date() {
518 let mut gen = OrganizationalEventGenerator::new(42);
519 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
520 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
521 let companies = vec!["C001".to_string()];
522
523 let events = gen.generate_events(start, end, &companies);
524 for e in &events {
525 assert!(
526 e.is_active_at(e.effective_date),
527 "event should be active at its effective date"
528 );
529 }
530 }
531}