Skip to main content

datasynth_core/distributions/
event_timeline.rs

1//! Event timeline orchestrator for pattern drift simulation.
2//!
3//! Provides a unified controller that manages organizational events,
4//! process evolution, and technology transitions, computing their
5//! combined effects on data generation.
6
7use 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/// Effect blending mode for combining multiple event effects.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
19#[serde(rename_all = "snake_case")]
20pub enum EffectBlendingMode {
21    /// Multiply effects together.
22    #[default]
23    Multiplicative,
24    /// Add effects together.
25    Additive,
26    /// Take the maximum effect.
27    Maximum,
28    /// Take the minimum effect.
29    Minimum,
30}
31
32/// Timeline effects computed for a specific date.
33#[derive(Debug, Clone, Default)]
34pub struct TimelineEffects {
35    /// Drift adjustments from base drift controller.
36    pub drift: DriftAdjustments,
37    /// Volume multiplier from all events.
38    pub volume_multiplier: f64,
39    /// Amount multiplier from all events.
40    pub amount_multiplier: f64,
41    /// Error rate delta (additive).
42    pub error_rate_delta: f64,
43    /// Processing time multiplier.
44    pub processing_time_multiplier: f64,
45    /// Entity changes (additions, removals).
46    pub entity_changes: EntityChanges,
47    /// Account remapping (old account -> new account).
48    pub account_remapping: HashMap<String, String>,
49    /// Control changes active.
50    pub control_changes: ControlChanges,
51    /// Special entries to generate (e.g., goodwill, fair value adjustments).
52    pub special_entries: Vec<SpecialEntryRequest>,
53    /// Active organizational events.
54    pub active_org_events: Vec<String>,
55    /// Active process events.
56    pub active_process_events: Vec<String>,
57    /// Active technology events.
58    pub active_tech_events: Vec<String>,
59    /// Whether in parallel posting mode (dual system entry).
60    pub in_parallel_posting: bool,
61    /// Current ERP migration phase if applicable.
62    pub migration_phase: Option<String>,
63}
64
65impl TimelineEffects {
66    /// Create a new effects instance with neutral values.
67    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    /// Get the combined volume multiplier including drift.
87    pub fn combined_volume_multiplier(&self) -> f64 {
88        self.volume_multiplier * self.drift.combined_volume_multiplier()
89    }
90
91    /// Get the combined amount multiplier including drift.
92    pub fn combined_amount_multiplier(&self) -> f64 {
93        self.amount_multiplier * self.drift.combined_amount_multiplier()
94    }
95
96    /// Get the total error rate (base + delta).
97    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    /// Check if any events are active.
102    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/// Entity changes from organizational events.
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111pub struct EntityChanges {
112    /// Entities added (e.g., from acquisition).
113    pub entities_added: Vec<String>,
114    /// Entities removed (e.g., from divestiture).
115    pub entities_removed: Vec<String>,
116    /// Cost center remapping.
117    pub cost_center_remapping: HashMap<String, String>,
118    /// Department remapping.
119    pub department_remapping: HashMap<String, String>,
120}
121
122impl EntityChanges {
123    /// Check if there are any changes.
124    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/// Control changes from process and organizational events.
133#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134pub struct ControlChanges {
135    /// New controls added.
136    pub controls_added: Vec<String>,
137    /// Controls modified.
138    pub controls_modified: Vec<String>,
139    /// Controls removed or deprecated.
140    pub controls_removed: Vec<String>,
141    /// Threshold changes (control_id -> (old, new)).
142    pub threshold_changes: HashMap<String, (f64, f64)>,
143}
144
145impl ControlChanges {
146    /// Check if there are any changes.
147    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/// Request for a special journal entry.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SpecialEntryRequest {
158    /// Entry type (e.g., "goodwill", "fair_value_adjustment", "severance").
159    pub entry_type: String,
160    /// Description.
161    pub description: String,
162    /// Debit account.
163    pub debit_account: String,
164    /// Credit account.
165    pub credit_account: String,
166    /// Amount (if known).
167    pub amount: Option<rust_decimal::Decimal>,
168    /// Related event ID.
169    pub related_event_id: String,
170}
171
172/// Summary of active events at a point in time.
173#[derive(Debug, Clone, Default)]
174pub struct ActiveEventsSummary {
175    /// Active organizational events.
176    pub org_events: Vec<ActiveEventInfo>,
177    /// Active process events.
178    pub process_events: Vec<ActiveEventInfo>,
179    /// Active technology events.
180    pub tech_events: Vec<ActiveEventInfo>,
181}
182
183/// Information about an active event.
184#[derive(Debug, Clone)]
185pub struct ActiveEventInfo {
186    /// Event ID.
187    pub event_id: String,
188    /// Event type name.
189    pub event_type: String,
190    /// Progress through the event (0.0 to 1.0).
191    pub progress: f64,
192}
193
194/// Configuration for the event timeline.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct EventTimelineConfig {
197    /// Organizational events.
198    #[serde(default)]
199    pub org_events: Vec<OrganizationalEvent>,
200    /// Process evolution events.
201    #[serde(default)]
202    pub process_events: Vec<ProcessEvolutionEvent>,
203    /// Technology transition events.
204    #[serde(default)]
205    pub tech_events: Vec<TechnologyTransitionEvent>,
206    /// Effect blending mode.
207    #[serde(default)]
208    pub effect_blending: EffectBlendingMode,
209    /// Base drift configuration.
210    #[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
226/// Event timeline controller that orchestrates all events.
227pub struct EventTimeline {
228    /// Organizational events.
229    org_events: Vec<OrganizationalEvent>,
230    /// Process evolution events.
231    process_events: Vec<ProcessEvolutionEvent>,
232    /// Technology transition events.
233    tech_events: Vec<TechnologyTransitionEvent>,
234    /// Drift controller for base drift patterns.
235    drift_controller: DriftController,
236    /// Effect blending mode.
237    effect_blending: EffectBlendingMode,
238    /// Start date of the simulation.
239    start_date: NaiveDate,
240}
241
242impl EventTimeline {
243    /// Create a new event timeline.
244    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    /// Compute effects for a specific date.
261    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        // Get base drift adjustments
266        effects.drift = self.drift_controller.compute_adjustments(period);
267
268        // Initialize multipliers
269        effects.volume_multiplier = 1.0;
270        effects.amount_multiplier = 1.0;
271        effects.processing_time_multiplier = 1.0;
272
273        // Process organizational events
274        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        // Process evolution events
281        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        // Process technology events
288        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    /// Compute effects for a specific period.
298    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    /// Get active events at a specific period.
304    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    /// Check if in parallel run mode at a given date.
342    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    /// Get the drift controller.
356    pub fn drift_controller(&self) -> &DriftController {
357        &self.drift_controller
358    }
359
360    /// Convert a date to a period number.
361    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    /// Convert a period number to a date.
367    fn period_to_date(&self, period: u32) -> NaiveDate {
368        self.start_date + chrono::Duration::days(period as i64 * 30)
369    }
370
371    /// Apply effects from an organizational event.
372    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                // Volume and amount multipliers scale with progress
384                let vol_mult = 1.0 + (config.volume_multiplier - 1.0) * progress;
385                self.blend_multiplier(&mut effects.volume_multiplier, vol_mult);
386
387                // Error rate during integration
388                let error_rate = config.integration_phases.error_rate_at(date);
389                effects.error_rate_delta += error_rate;
390
391                // Add acquired entity
392                if progress >= 0.0 {
393                    effects
394                        .entity_changes
395                        .entities_added
396                        .push(config.acquired_entity_code.clone());
397                }
398
399                // Check for parallel posting
400                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                // Special entries for purchase price allocation
409                if let Some(ppa) = &config.purchase_price_allocation {
410                    if progress < 0.1 {
411                        // Only at the start
412                        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(), // Goodwill account
419                            credit_account: "2100".to_string(), // Consider payable
420                            amount: Some(ppa.goodwill),
421                            related_event_id: event.event_id.clone(),
422                        });
423                    }
424                }
425            }
426            OrganizationalEventType::Divestiture(config) => {
427                // Volume reduction scales with progress
428                let vol_mult = 1.0 - (1.0 - config.volume_reduction) * progress;
429                self.blend_multiplier(&mut effects.volume_multiplier, vol_mult);
430
431                // Add divested entity to removals
432                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                // Small error rate during transition
440                if progress < 1.0 {
441                    effects.error_rate_delta += 0.02;
442                }
443            }
444            OrganizationalEventType::Reorganization(config) => {
445                // Apply remappings
446                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                // Transition error rate
460                effects.error_rate_delta += config.transition_error_rate * (1.0 - progress);
461            }
462            OrganizationalEventType::LeadershipChange(config) => {
463                // Policy change error rate
464                effects.error_rate_delta += config.policy_change_error_rate * (1.0 - progress);
465            }
466            OrganizationalEventType::WorkforceReduction(config) => {
467                // Error rate increase
468                effects.error_rate_delta += config.error_rate_increase * (1.0 - progress * 0.5);
469
470                // Processing time increase
471                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                // Volume slightly reduced
475                let vol_mult = 1.0 - (config.reduction_percent * 0.3) * progress;
476                self.blend_multiplier(&mut effects.volume_multiplier, vol_mult);
477
478                // Severance entry at start
479                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(), // Severance expense
485                            credit_account: "2200".to_string(), // Accrued liabilities
486                            amount: Some(severance),
487                            related_event_id: event.event_id.clone(),
488                        });
489                    }
490                }
491            }
492            OrganizationalEventType::Merger(config) => {
493                // Volume multiplier
494                let vol_mult = 1.0 + (config.volume_multiplier - 1.0) * progress;
495                self.blend_multiplier(&mut effects.volume_multiplier, vol_mult);
496
497                // Integration error rate
498                let error_rate = config.integration_phases.error_rate_at(date);
499                effects.error_rate_delta += error_rate;
500
501                // Add merged entity
502                effects
503                    .entity_changes
504                    .entities_added
505                    .push(config.merged_entity_code.clone());
506            }
507        }
508    }
509
510    /// Apply effects from a process evolution event.
511    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                // Processing time change
523                let time_mult = 1.0 + (config.time_delta - 1.0) * progress;
524                self.blend_multiplier(&mut effects.processing_time_multiplier, time_mult);
525
526                // Transition error rate
527                effects.error_rate_delta += config.error_rate_impact * (1.0 - progress);
528            }
529            ProcessEvolutionType::ProcessAutomation(config) => {
530                // Processing time reduction
531                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                // Error rate change
537                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                // Transition error rate
542                effects.error_rate_delta += config.transition_error_rate * (1.0 - progress);
543
544                // Track control changes
545                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                // Error reduction (negative delta)
554                effects.error_rate_delta -= config.error_reduction * progress;
555
556                // Processing time impact
557                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                // Track control changes
561                effects
562                    .control_changes
563                    .controls_modified
564                    .push(config.control_id.clone());
565            }
566        }
567    }
568
569    /// Apply effects from a technology transition event.
570    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                // Error rate multiplier based on phase
584                effects.error_rate_delta +=
585                    config.migration_issues.combined_error_rate() * phase.error_rate_multiplier();
586
587                // Processing time multiplier
588                self.blend_multiplier(
589                    &mut effects.processing_time_multiplier,
590                    phase.processing_time_multiplier(),
591                );
592
593                // Parallel posting during parallel run
594                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                // Error rate during implementation
605                effects.error_rate_delta += config.implementation_error_rate * (1.0 - progress);
606
607                // Processing time slightly higher initially
608                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                // Transition error rate
615                effects.error_rate_delta += config.transition_error_rate * (1.0 - progress);
616            }
617        }
618    }
619
620    /// Blend a multiplier according to the blending mode.
621    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        // Before acquisition
670        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        // During acquisition
675        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}