1use crate::distributions::drift::{DriftAdjustments, DriftConfig, DriftController};
8use crate::models::{
9 organizational_event::{OrganizationalEvent, OrganizationalEventType},
10 process_evolution::{ProcessEvolutionEvent, ProcessEvolutionType},
11 technology_transition::{TechnologyTransitionEvent, TechnologyTransitionType},
12};
13use chrono::NaiveDate;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
19#[serde(rename_all = "snake_case")]
20pub enum EffectBlendingMode {
21 #[default]
23 Multiplicative,
24 Additive,
26 Maximum,
28 Minimum,
30}
31
32#[derive(Debug, Clone, Default)]
34pub struct TimelineEffects {
35 pub drift: DriftAdjustments,
37 pub volume_multiplier: f64,
39 pub amount_multiplier: f64,
41 pub error_rate_delta: f64,
43 pub processing_time_multiplier: f64,
45 pub entity_changes: EntityChanges,
47 pub account_remapping: HashMap<String, String>,
49 pub control_changes: ControlChanges,
51 pub special_entries: Vec<SpecialEntryRequest>,
53 pub active_org_events: Vec<String>,
55 pub active_process_events: Vec<String>,
57 pub active_tech_events: Vec<String>,
59 pub in_parallel_posting: bool,
61 pub migration_phase: Option<String>,
63}
64
65impl TimelineEffects {
66 pub fn neutral() -> Self {
68 Self {
69 drift: DriftAdjustments::none(),
70 volume_multiplier: 1.0,
71 amount_multiplier: 1.0,
72 error_rate_delta: 0.0,
73 processing_time_multiplier: 1.0,
74 entity_changes: EntityChanges::default(),
75 account_remapping: HashMap::new(),
76 control_changes: ControlChanges::default(),
77 special_entries: Vec::new(),
78 active_org_events: Vec::new(),
79 active_process_events: Vec::new(),
80 active_tech_events: Vec::new(),
81 in_parallel_posting: false,
82 migration_phase: None,
83 }
84 }
85
86 pub fn combined_volume_multiplier(&self) -> f64 {
88 self.volume_multiplier * self.drift.combined_volume_multiplier()
89 }
90
91 pub fn combined_amount_multiplier(&self) -> f64 {
93 self.amount_multiplier * self.drift.combined_amount_multiplier()
94 }
95
96 pub fn total_error_rate(&self, base_error_rate: f64) -> f64 {
98 (base_error_rate + self.error_rate_delta).clamp(0.0, 1.0)
99 }
100
101 pub fn has_active_events(&self) -> bool {
103 !self.active_org_events.is_empty()
104 || !self.active_process_events.is_empty()
105 || !self.active_tech_events.is_empty()
106 }
107}
108
109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111pub struct EntityChanges {
112 pub entities_added: Vec<String>,
114 pub entities_removed: Vec<String>,
116 pub cost_center_remapping: HashMap<String, String>,
118 pub department_remapping: HashMap<String, String>,
120}
121
122impl EntityChanges {
123 pub fn has_changes(&self) -> bool {
125 !self.entities_added.is_empty()
126 || !self.entities_removed.is_empty()
127 || !self.cost_center_remapping.is_empty()
128 || !self.department_remapping.is_empty()
129 }
130}
131
132#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134pub struct ControlChanges {
135 pub controls_added: Vec<String>,
137 pub controls_modified: Vec<String>,
139 pub controls_removed: Vec<String>,
141 pub threshold_changes: HashMap<String, (f64, f64)>,
143}
144
145impl ControlChanges {
146 pub fn has_changes(&self) -> bool {
148 !self.controls_added.is_empty()
149 || !self.controls_modified.is_empty()
150 || !self.controls_removed.is_empty()
151 || !self.threshold_changes.is_empty()
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SpecialEntryRequest {
158 pub entry_type: String,
160 pub description: String,
162 pub debit_account: String,
164 pub credit_account: String,
166 pub amount: Option<rust_decimal::Decimal>,
168 pub related_event_id: String,
170}
171
172#[derive(Debug, Clone, Default)]
174pub struct ActiveEventsSummary {
175 pub org_events: Vec<ActiveEventInfo>,
177 pub process_events: Vec<ActiveEventInfo>,
179 pub tech_events: Vec<ActiveEventInfo>,
181}
182
183#[derive(Debug, Clone)]
185pub struct ActiveEventInfo {
186 pub event_id: String,
188 pub event_type: String,
190 pub progress: f64,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct EventTimelineConfig {
197 #[serde(default)]
199 pub org_events: Vec<OrganizationalEvent>,
200 #[serde(default)]
202 pub process_events: Vec<ProcessEvolutionEvent>,
203 #[serde(default)]
205 pub tech_events: Vec<TechnologyTransitionEvent>,
206 #[serde(default)]
208 pub effect_blending: EffectBlendingMode,
209 #[serde(default)]
211 pub drift_config: DriftConfig,
212}
213
214impl Default for EventTimelineConfig {
215 fn default() -> Self {
216 Self {
217 org_events: Vec::new(),
218 process_events: Vec::new(),
219 tech_events: Vec::new(),
220 effect_blending: EffectBlendingMode::Multiplicative,
221 drift_config: DriftConfig::default(),
222 }
223 }
224}
225
226pub struct EventTimeline {
228 org_events: Vec<OrganizationalEvent>,
230 process_events: Vec<ProcessEvolutionEvent>,
232 tech_events: Vec<TechnologyTransitionEvent>,
234 drift_controller: DriftController,
236 effect_blending: EffectBlendingMode,
238 start_date: NaiveDate,
240}
241
242impl EventTimeline {
243 pub fn new(
245 config: EventTimelineConfig,
246 seed: u64,
247 total_periods: u32,
248 start_date: NaiveDate,
249 ) -> Self {
250 Self {
251 org_events: config.org_events,
252 process_events: config.process_events,
253 tech_events: config.tech_events,
254 drift_controller: DriftController::new(config.drift_config, seed, total_periods),
255 effect_blending: config.effect_blending,
256 start_date,
257 }
258 }
259
260 pub fn compute_effects_for_date(&self, date: NaiveDate) -> TimelineEffects {
262 let period = self.date_to_period(date);
263 let mut effects = TimelineEffects::neutral();
264
265 effects.drift = self.drift_controller.compute_adjustments(period);
267
268 effects.volume_multiplier = 1.0;
270 effects.amount_multiplier = 1.0;
271 effects.processing_time_multiplier = 1.0;
272
273 for event in &self.org_events {
275 if event.is_active_at(date) {
276 self.apply_org_event_effects(&mut effects, event, date);
277 }
278 }
279
280 for event in &self.process_events {
282 if event.is_active_at(date) {
283 self.apply_process_event_effects(&mut effects, event, date);
284 }
285 }
286
287 for event in &self.tech_events {
289 if event.is_active_at(date) {
290 self.apply_tech_event_effects(&mut effects, event, date);
291 }
292 }
293
294 effects
295 }
296
297 pub fn compute_effects_for_period(&self, period: u32) -> TimelineEffects {
299 let date = self.period_to_date(period);
300 self.compute_effects_for_date(date)
301 }
302
303 pub fn active_events_at(&self, period: u32) -> ActiveEventsSummary {
305 let date = self.period_to_date(period);
306 let mut summary = ActiveEventsSummary::default();
307
308 for event in &self.org_events {
309 if event.is_active_at(date) {
310 summary.org_events.push(ActiveEventInfo {
311 event_id: event.event_id.clone(),
312 event_type: event.event_type.type_name().to_string(),
313 progress: event.progress_at(date),
314 });
315 }
316 }
317
318 for event in &self.process_events {
319 if event.is_active_at(date) {
320 summary.process_events.push(ActiveEventInfo {
321 event_id: event.event_id.clone(),
322 event_type: event.event_type.type_name().to_string(),
323 progress: event.progress_at(date),
324 });
325 }
326 }
327
328 for event in &self.tech_events {
329 if event.is_active_at(date) {
330 summary.tech_events.push(ActiveEventInfo {
331 event_id: event.event_id.clone(),
332 event_type: event.event_type.type_name().to_string(),
333 progress: event.progress_at(date),
334 });
335 }
336 }
337
338 summary
339 }
340
341 pub fn in_parallel_run(&self, date: NaiveDate) -> Option<&TechnologyTransitionEvent> {
343 for event in &self.tech_events {
344 if let TechnologyTransitionType::ErpMigration(config) = &event.event_type {
345 if let Some(parallel_start) = config.phases.parallel_run_start {
346 if date >= parallel_start && date < config.phases.cutover_date {
347 return Some(event);
348 }
349 }
350 }
351 }
352 None
353 }
354
355 pub fn drift_controller(&self) -> &DriftController {
357 &self.drift_controller
358 }
359
360 fn date_to_period(&self, date: NaiveDate) -> u32 {
362 let days = (date - self.start_date).num_days();
363 (days / 30).max(0) as u32
364 }
365
366 fn period_to_date(&self, period: u32) -> NaiveDate {
368 self.start_date + chrono::Duration::days(period as i64 * 30)
369 }
370
371 fn apply_org_event_effects(
373 &self,
374 effects: &mut TimelineEffects,
375 event: &OrganizationalEvent,
376 date: NaiveDate,
377 ) {
378 let progress = event.progress_at(date);
379 effects.active_org_events.push(event.event_id.clone());
380
381 match &event.event_type {
382 OrganizationalEventType::Acquisition(config) => {
383 let vol_mult = 1.0 + (config.volume_multiplier - 1.0) * progress;
385 self.blend_multiplier(&mut effects.volume_multiplier, vol_mult);
386
387 let error_rate = config.integration_phases.error_rate_at(date);
389 effects.error_rate_delta += error_rate;
390
391 if progress >= 0.0 {
393 effects
394 .entity_changes
395 .entities_added
396 .push(config.acquired_entity_code.clone());
397 }
398
399 if config.parallel_posting_days > 0 {
401 let parallel_end = config.acquisition_date
402 + chrono::Duration::days(config.parallel_posting_days as i64);
403 if date >= config.acquisition_date && date <= parallel_end {
404 effects.in_parallel_posting = true;
405 }
406 }
407
408 if let Some(ppa) = &config.purchase_price_allocation {
410 if progress < 0.1 {
411 effects.special_entries.push(SpecialEntryRequest {
413 entry_type: "goodwill".to_string(),
414 description: format!(
415 "Goodwill from acquisition of {}",
416 config.acquired_entity_code
417 ),
418 debit_account: "1800".to_string(), credit_account: "2100".to_string(), amount: Some(ppa.goodwill),
421 related_event_id: event.event_id.clone(),
422 });
423 }
424 }
425 }
426 OrganizationalEventType::Divestiture(config) => {
427 let vol_mult = 1.0 - (1.0 - config.volume_reduction) * progress;
429 self.blend_multiplier(&mut effects.volume_multiplier, vol_mult);
430
431 if config.remove_entity && progress >= 1.0 {
433 effects
434 .entity_changes
435 .entities_removed
436 .push(config.divested_entity_code.clone());
437 }
438
439 if progress < 1.0 {
441 effects.error_rate_delta += 0.02;
442 }
443 }
444 OrganizationalEventType::Reorganization(config) => {
445 for (old, new) in &config.cost_center_remapping {
447 effects
448 .entity_changes
449 .cost_center_remapping
450 .insert(old.clone(), new.clone());
451 }
452 for (old, new) in &config.department_remapping {
453 effects
454 .entity_changes
455 .department_remapping
456 .insert(old.clone(), new.clone());
457 }
458
459 effects.error_rate_delta += config.transition_error_rate * (1.0 - progress);
461 }
462 OrganizationalEventType::LeadershipChange(config) => {
463 effects.error_rate_delta += config.policy_change_error_rate * (1.0 - progress);
465 }
466 OrganizationalEventType::WorkforceReduction(config) => {
467 effects.error_rate_delta += config.error_rate_increase * (1.0 - progress * 0.5);
469
470 let time_mult = 1.0 + (config.processing_time_increase - 1.0) * (1.0 - progress);
472 self.blend_multiplier(&mut effects.processing_time_multiplier, time_mult);
473
474 let vol_mult = 1.0 - (config.reduction_percent * 0.3) * progress;
476 self.blend_multiplier(&mut effects.volume_multiplier, vol_mult);
477
478 if let Some(severance) = config.severance_costs {
480 if progress < 0.1 {
481 effects.special_entries.push(SpecialEntryRequest {
482 entry_type: "severance".to_string(),
483 description: "Workforce reduction severance costs".to_string(),
484 debit_account: "6500".to_string(), credit_account: "2200".to_string(), amount: Some(severance),
487 related_event_id: event.event_id.clone(),
488 });
489 }
490 }
491 }
492 OrganizationalEventType::Merger(config) => {
493 let vol_mult = 1.0 + (config.volume_multiplier - 1.0) * progress;
495 self.blend_multiplier(&mut effects.volume_multiplier, vol_mult);
496
497 let error_rate = config.integration_phases.error_rate_at(date);
499 effects.error_rate_delta += error_rate;
500
501 effects
503 .entity_changes
504 .entities_added
505 .push(config.merged_entity_code.clone());
506 }
507 }
508 }
509
510 fn apply_process_event_effects(
512 &self,
513 effects: &mut TimelineEffects,
514 event: &ProcessEvolutionEvent,
515 date: NaiveDate,
516 ) {
517 let progress = event.progress_at(date);
518 effects.active_process_events.push(event.event_id.clone());
519
520 match &event.event_type {
521 ProcessEvolutionType::ApprovalWorkflowChange(config) => {
522 let time_mult = 1.0 + (config.time_delta - 1.0) * progress;
524 self.blend_multiplier(&mut effects.processing_time_multiplier, time_mult);
525
526 effects.error_rate_delta += config.error_rate_impact * (1.0 - progress);
528 }
529 ProcessEvolutionType::ProcessAutomation(config) => {
530 let time_mult = 1.0
532 - (1.0 - config.processing_time_reduction)
533 * config.automation_rate_at_progress(progress);
534 self.blend_multiplier(&mut effects.processing_time_multiplier, time_mult);
535
536 let error_rate = config.error_rate_at_progress(progress);
538 effects.error_rate_delta += error_rate - config.error_rate_before;
539 }
540 ProcessEvolutionType::PolicyChange(config) => {
541 effects.error_rate_delta += config.transition_error_rate * (1.0 - progress);
543
544 for control_id in &config.affected_controls {
546 effects
547 .control_changes
548 .controls_modified
549 .push(control_id.clone());
550 }
551 }
552 ProcessEvolutionType::ControlEnhancement(config) => {
553 effects.error_rate_delta -= config.error_reduction * progress;
555
556 let time_mult = 1.0 + (config.processing_time_impact - 1.0) * progress;
558 self.blend_multiplier(&mut effects.processing_time_multiplier, time_mult);
559
560 effects
562 .control_changes
563 .controls_modified
564 .push(config.control_id.clone());
565 }
566 }
567 }
568
569 fn apply_tech_event_effects(
571 &self,
572 effects: &mut TimelineEffects,
573 event: &TechnologyTransitionEvent,
574 date: NaiveDate,
575 ) {
576 effects.active_tech_events.push(event.event_id.clone());
577
578 match &event.event_type {
579 TechnologyTransitionType::ErpMigration(config) => {
580 let phase = config.phases.phase_at(date);
581 effects.migration_phase = Some(format!("{:?}", phase));
582
583 effects.error_rate_delta +=
585 config.migration_issues.combined_error_rate() * phase.error_rate_multiplier();
586
587 self.blend_multiplier(
589 &mut effects.processing_time_multiplier,
590 phase.processing_time_multiplier(),
591 );
592
593 if matches!(
595 phase,
596 crate::models::technology_transition::MigrationPhase::ParallelRun
597 ) {
598 effects.in_parallel_posting = true;
599 }
600 }
601 TechnologyTransitionType::ModuleImplementation(config) => {
602 let progress = event.progress_at(date);
603
604 effects.error_rate_delta += config.implementation_error_rate * (1.0 - progress);
606
607 let time_mult = 1.0 + 0.2 * (1.0 - progress);
609 self.blend_multiplier(&mut effects.processing_time_multiplier, time_mult);
610 }
611 TechnologyTransitionType::IntegrationUpgrade(config) => {
612 let progress = event.progress_at(date);
613
614 effects.error_rate_delta += config.transition_error_rate * (1.0 - progress);
616 }
617 }
618 }
619
620 fn blend_multiplier(&self, current: &mut f64, new: f64) {
622 *current = match self.effect_blending {
623 EffectBlendingMode::Multiplicative => *current * new,
624 EffectBlendingMode::Additive => *current + new - 1.0,
625 EffectBlendingMode::Maximum => current.max(new),
626 EffectBlendingMode::Minimum => current.min(new),
627 };
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634 use crate::models::organizational_event::AcquisitionConfig;
635
636 #[test]
637 fn test_empty_timeline() {
638 let config = EventTimelineConfig::default();
639 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
640 let timeline = EventTimeline::new(config, 42, 12, start);
641
642 let effects = timeline.compute_effects_for_period(6);
643 assert!((effects.volume_multiplier - 1.0).abs() < 0.001);
644 assert!((effects.amount_multiplier - 1.0).abs() < 0.001);
645 assert!(!effects.has_active_events());
646 }
647
648 #[test]
649 fn test_acquisition_effects() {
650 let acq_date = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
651 let acq_config = AcquisitionConfig {
652 acquired_entity_code: "ACME".to_string(),
653 acquisition_date: acq_date,
654 volume_multiplier: 1.35,
655 ..Default::default()
656 };
657
658 let event =
659 OrganizationalEvent::new("ACQ-001", OrganizationalEventType::Acquisition(acq_config));
660
661 let config = EventTimelineConfig {
662 org_events: vec![event],
663 ..Default::default()
664 };
665
666 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
667 let timeline = EventTimeline::new(config, 42, 12, start);
668
669 let before =
671 timeline.compute_effects_for_date(NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
672 assert!((before.volume_multiplier - 1.0).abs() < 0.001);
673
674 let during =
676 timeline.compute_effects_for_date(NaiveDate::from_ymd_opt(2024, 4, 1).unwrap());
677 assert!(during.volume_multiplier > 1.0);
678 assert!(during.has_active_events());
679 }
680
681 #[test]
682 fn test_timeline_effects_neutral() {
683 let effects = TimelineEffects::neutral();
684 assert!((effects.volume_multiplier - 1.0).abs() < 0.001);
685 assert!((effects.amount_multiplier - 1.0).abs() < 0.001);
686 assert!((effects.error_rate_delta).abs() < 0.001);
687 }
688
689 #[test]
690 fn test_active_events_summary() {
691 let config = EventTimelineConfig::default();
692 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
693 let timeline = EventTimeline::new(config, 42, 12, start);
694
695 let summary = timeline.active_events_at(6);
696 assert!(summary.org_events.is_empty());
697 assert!(summary.process_events.is_empty());
698 assert!(summary.tech_events.is_empty());
699 }
700}