Skip to main content

datasynth_core/models/
organizational_event.rs

1//! Organizational event models for pattern drift simulation.
2//!
3//! Provides comprehensive organizational event modeling including:
4//! - Acquisitions with integration phases
5//! - Divestitures with entity removal
6//! - Reorganizations with cost center remapping
7//! - Leadership changes with policy shifts
8//! - Workforce reductions with error rate impacts
9//! - Mergers with goodwill and fair value adjustments
10
11use chrono::NaiveDate;
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// Organizational event type with associated configuration.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(tag = "type", rename_all = "snake_case")]
19pub enum OrganizationalEventType {
20    /// Company acquisition event.
21    Acquisition(AcquisitionConfig),
22    /// Company or business unit divestiture.
23    Divestiture(DivestitureConfig),
24    /// Internal reorganization event.
25    Reorganization(ReorganizationConfig),
26    /// Leadership or management change.
27    LeadershipChange(LeadershipChangeConfig),
28    /// Workforce reduction event.
29    WorkforceReduction(WorkforceReductionConfig),
30    /// Company merger event.
31    Merger(MergerConfig),
32}
33
34impl OrganizationalEventType {
35    /// Get the event type name.
36    pub fn type_name(&self) -> &'static str {
37        match self {
38            Self::Acquisition(_) => "acquisition",
39            Self::Divestiture(_) => "divestiture",
40            Self::Reorganization(_) => "reorganization",
41            Self::LeadershipChange(_) => "leadership_change",
42            Self::WorkforceReduction(_) => "workforce_reduction",
43            Self::Merger(_) => "merger",
44        }
45    }
46
47    /// Get the default volume multiplier for this event type.
48    pub fn default_volume_multiplier(&self) -> f64 {
49        match self {
50            Self::Acquisition(c) => c.volume_multiplier,
51            Self::Divestiture(c) => c.volume_reduction,
52            Self::Reorganization(_) => 1.0,
53            Self::LeadershipChange(_) => 1.0,
54            Self::WorkforceReduction(c) => 1.0 - (c.reduction_percent * 0.3), // 30% of reduction impacts volume
55            Self::Merger(c) => c.volume_multiplier,
56        }
57    }
58
59    /// Get the error rate impact for this event type.
60    pub fn error_rate_impact(&self) -> f64 {
61        match self {
62            Self::Acquisition(c) => c.integration_error_rate,
63            Self::Divestiture(_) => 0.02, // Some transition errors
64            Self::Reorganization(c) => c.transition_error_rate,
65            Self::LeadershipChange(c) => c.policy_change_error_rate,
66            Self::WorkforceReduction(c) => c.error_rate_increase,
67            Self::Merger(c) => c.integration_error_rate,
68        }
69    }
70
71    /// Get the transition duration in months.
72    pub fn transition_months(&self) -> u32 {
73        match self {
74            Self::Acquisition(c) => c.integration_phases.total_duration_months(),
75            Self::Divestiture(c) => c.transition_months,
76            Self::Reorganization(c) => c.transition_months,
77            Self::LeadershipChange(c) => c.policy_transition_months,
78            Self::WorkforceReduction(c) => c.transition_months,
79            Self::Merger(c) => c.integration_phases.total_duration_months(),
80        }
81    }
82}
83
84/// Configuration for an acquisition event.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AcquisitionConfig {
87    /// Code of the acquired entity.
88    pub acquired_entity_code: String,
89    /// Name of the acquired entity.
90    #[serde(default)]
91    pub acquired_entity_name: Option<String>,
92    /// Date of acquisition closing.
93    pub acquisition_date: NaiveDate,
94    /// Volume multiplier after acquisition (e.g., 1.35 = 35% increase).
95    #[serde(default = "default_acquisition_volume_mult")]
96    pub volume_multiplier: f64,
97    /// Error rate during integration period.
98    #[serde(default = "default_integration_error_rate")]
99    pub integration_error_rate: f64,
100    /// Days of parallel posting (dual systems).
101    #[serde(default = "default_parallel_posting_days")]
102    pub parallel_posting_days: u32,
103    /// Coding error rate during integration.
104    #[serde(default = "default_coding_error_rate")]
105    pub coding_error_rate: f64,
106    /// Integration phase configuration.
107    #[serde(default)]
108    pub integration_phases: IntegrationPhaseConfig,
109    /// Purchase price allocation details.
110    #[serde(default)]
111    pub purchase_price_allocation: Option<PurchasePriceAllocation>,
112}
113
114fn default_acquisition_volume_mult() -> f64 {
115    1.35
116}
117
118fn default_integration_error_rate() -> f64 {
119    0.05
120}
121
122fn default_parallel_posting_days() -> u32 {
123    30
124}
125
126fn default_coding_error_rate() -> f64 {
127    0.03
128}
129
130impl Default for AcquisitionConfig {
131    fn default() -> Self {
132        Self {
133            acquired_entity_code: String::new(),
134            acquired_entity_name: None,
135            acquisition_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
136            volume_multiplier: 1.35,
137            integration_error_rate: 0.05,
138            parallel_posting_days: 30,
139            coding_error_rate: 0.03,
140            integration_phases: IntegrationPhaseConfig::default(),
141            purchase_price_allocation: None,
142        }
143    }
144}
145
146/// Integration phase configuration for acquisitions and mergers.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct IntegrationPhaseConfig {
149    /// Parallel run period (both systems active).
150    #[serde(default)]
151    pub parallel_run: Option<DateRange>,
152    /// Cutover date (switch to single system).
153    pub cutover_date: NaiveDate,
154    /// End of stabilization period.
155    pub stabilization_end: NaiveDate,
156    /// Error rate during parallel run.
157    #[serde(default = "default_parallel_error_rate")]
158    pub parallel_run_error_rate: f64,
159    /// Error rate during stabilization.
160    #[serde(default = "default_stabilization_error_rate")]
161    pub stabilization_error_rate: f64,
162}
163
164fn default_parallel_error_rate() -> f64 {
165    0.08
166}
167
168fn default_stabilization_error_rate() -> f64 {
169    0.03
170}
171
172impl Default for IntegrationPhaseConfig {
173    fn default() -> Self {
174        let cutover = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
175        Self {
176            parallel_run: Some(DateRange {
177                start: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
178                end: NaiveDate::from_ymd_opt(2024, 2, 28).unwrap(),
179            }),
180            cutover_date: cutover,
181            stabilization_end: NaiveDate::from_ymd_opt(2024, 5, 31).unwrap(),
182            parallel_run_error_rate: 0.08,
183            stabilization_error_rate: 0.03,
184        }
185    }
186}
187
188impl IntegrationPhaseConfig {
189    /// Calculate total integration duration in months.
190    pub fn total_duration_months(&self) -> u32 {
191        let days = (self.stabilization_end - self.cutover_date).num_days();
192        let parallel_days = self
193            .parallel_run
194            .as_ref()
195            .map(|r| (r.end - r.start).num_days())
196            .unwrap_or(0);
197        ((days + parallel_days) / 30) as u32
198    }
199
200    /// Get the current integration phase for a given date.
201    pub fn phase_at(&self, date: NaiveDate) -> IntegrationPhase {
202        if let Some(ref parallel) = self.parallel_run {
203            if date >= parallel.start && date <= parallel.end {
204                return IntegrationPhase::ParallelRun;
205            }
206        }
207        if date >= self.cutover_date && date <= self.stabilization_end {
208            IntegrationPhase::Stabilization
209        } else if date > self.stabilization_end {
210            IntegrationPhase::Complete
211        } else {
212            IntegrationPhase::PreIntegration
213        }
214    }
215
216    /// Get the error rate for a given date.
217    pub fn error_rate_at(&self, date: NaiveDate) -> f64 {
218        match self.phase_at(date) {
219            IntegrationPhase::PreIntegration => 0.0,
220            IntegrationPhase::ParallelRun => self.parallel_run_error_rate,
221            IntegrationPhase::Stabilization => self.stabilization_error_rate,
222            IntegrationPhase::Complete => 0.0,
223        }
224    }
225}
226
227/// Integration phase stage.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum IntegrationPhase {
231    /// Before integration starts.
232    PreIntegration,
233    /// Both systems running in parallel.
234    ParallelRun,
235    /// Post-cutover stabilization.
236    Stabilization,
237    /// Integration complete.
238    Complete,
239}
240
241/// Date range helper struct.
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct DateRange {
244    /// Start date (inclusive).
245    pub start: NaiveDate,
246    /// End date (inclusive).
247    pub end: NaiveDate,
248}
249
250impl DateRange {
251    /// Check if a date is within this range.
252    pub fn contains(&self, date: NaiveDate) -> bool {
253        date >= self.start && date <= self.end
254    }
255
256    /// Get duration in days.
257    pub fn duration_days(&self) -> i64 {
258        (self.end - self.start).num_days()
259    }
260}
261
262/// Purchase price allocation details for acquisition accounting.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct PurchasePriceAllocation {
265    /// Total purchase price.
266    #[serde(with = "rust_decimal::serde::str")]
267    pub purchase_price: Decimal,
268    /// Fair value of net identifiable assets.
269    #[serde(with = "rust_decimal::serde::str")]
270    pub net_identifiable_assets: Decimal,
271    /// Goodwill recognized (purchase price - net identifiable assets).
272    #[serde(with = "rust_decimal::serde::str")]
273    pub goodwill: Decimal,
274    /// Bargain purchase gain if applicable.
275    #[serde(default, with = "rust_decimal::serde::str_option")]
276    pub bargain_purchase_gain: Option<Decimal>,
277    /// Intangible assets acquired.
278    #[serde(default)]
279    pub intangible_assets: Vec<IntangibleAsset>,
280}
281
282/// Intangible asset acquired in an acquisition.
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct IntangibleAsset {
285    /// Asset type (e.g., "customer_relationships", "brand", "technology").
286    pub asset_type: String,
287    /// Fair value.
288    #[serde(with = "rust_decimal::serde::str")]
289    pub fair_value: Decimal,
290    /// Useful life in years (None = indefinite).
291    pub useful_life_years: Option<u8>,
292}
293
294/// Configuration for a divestiture event.
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct DivestitureConfig {
297    /// Code of the divested entity.
298    pub divested_entity_code: String,
299    /// Name of the divested entity.
300    #[serde(default)]
301    pub divested_entity_name: Option<String>,
302    /// Date of divestiture closing.
303    pub divestiture_date: NaiveDate,
304    /// Revenue reduction factor (e.g., 0.70 = 30% reduction).
305    #[serde(default = "default_volume_reduction")]
306    pub volume_reduction: f64,
307    /// Number of months for transition.
308    #[serde(default = "default_transition_months")]
309    pub transition_months: u32,
310    /// Whether to remove entity from subsequent generation.
311    #[serde(default = "default_true")]
312    pub remove_entity: bool,
313    /// Accounts to be remapped or closed.
314    #[serde(default)]
315    pub account_closures: Vec<String>,
316    /// Gain/loss on disposal.
317    #[serde(default, with = "rust_decimal::serde::str_option")]
318    pub disposal_gain_loss: Option<Decimal>,
319}
320
321fn default_volume_reduction() -> f64 {
322    0.70
323}
324
325fn default_transition_months() -> u32 {
326    3
327}
328
329fn default_true() -> bool {
330    true
331}
332
333impl Default for DivestitureConfig {
334    fn default() -> Self {
335        Self {
336            divested_entity_code: String::new(),
337            divested_entity_name: None,
338            divestiture_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
339            volume_reduction: 0.70,
340            transition_months: 3,
341            remove_entity: true,
342            account_closures: Vec::new(),
343            disposal_gain_loss: None,
344        }
345    }
346}
347
348/// Configuration for a reorganization event.
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct ReorganizationConfig {
351    /// Description of the reorganization.
352    #[serde(default)]
353    pub description: Option<String>,
354    /// Effective date of reorganization.
355    pub effective_date: NaiveDate,
356    /// Cost center remapping (old -> new).
357    #[serde(default)]
358    pub cost_center_remapping: HashMap<String, String>,
359    /// Department remapping (old -> new).
360    #[serde(default)]
361    pub department_remapping: HashMap<String, String>,
362    /// Reporting line changes.
363    #[serde(default)]
364    pub reporting_changes: Vec<ReportingChange>,
365    /// Number of months for transition.
366    #[serde(default = "default_transition_months")]
367    pub transition_months: u32,
368    /// Error rate during transition.
369    #[serde(default = "default_reorg_error_rate")]
370    pub transition_error_rate: f64,
371}
372
373fn default_reorg_error_rate() -> f64 {
374    0.04
375}
376
377impl Default for ReorganizationConfig {
378    fn default() -> Self {
379        Self {
380            description: None,
381            effective_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
382            cost_center_remapping: HashMap::new(),
383            department_remapping: HashMap::new(),
384            reporting_changes: Vec::new(),
385            transition_months: 3,
386            transition_error_rate: 0.04,
387        }
388    }
389}
390
391/// A reporting line change in a reorganization.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct ReportingChange {
394    /// Entity or department being changed.
395    pub entity: String,
396    /// Previous reporting to.
397    pub from_reports_to: String,
398    /// New reporting to.
399    pub to_reports_to: String,
400}
401
402/// Configuration for a leadership change event.
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct LeadershipChangeConfig {
405    /// Role that changed (e.g., "CFO", "Controller").
406    pub role: String,
407    /// Date of change.
408    pub change_date: NaiveDate,
409    /// Policy changes associated with leadership change.
410    #[serde(default)]
411    pub policy_changes: Vec<PolicyChangeDetail>,
412    /// Vendor review triggered.
413    #[serde(default)]
414    pub vendor_review_triggered: bool,
415    /// Number of months for policy transition.
416    #[serde(default = "default_policy_transition_months")]
417    pub policy_transition_months: u32,
418    /// Error rate during policy transition.
419    #[serde(default = "default_policy_error_rate")]
420    pub policy_change_error_rate: f64,
421}
422
423fn default_policy_transition_months() -> u32 {
424    6
425}
426
427fn default_policy_error_rate() -> f64 {
428    0.02
429}
430
431impl Default for LeadershipChangeConfig {
432    fn default() -> Self {
433        Self {
434            role: "CFO".to_string(),
435            change_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
436            policy_changes: Vec::new(),
437            vendor_review_triggered: false,
438            policy_transition_months: 6,
439            policy_change_error_rate: 0.02,
440        }
441    }
442}
443
444/// A specific policy change detail.
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct PolicyChangeDetail {
447    /// Policy area affected.
448    pub policy_area: PolicyArea,
449    /// Description of change.
450    pub description: String,
451    /// Old threshold or value (if applicable).
452    #[serde(default, with = "rust_decimal::serde::str_option")]
453    pub old_value: Option<Decimal>,
454    /// New threshold or value (if applicable).
455    #[serde(default, with = "rust_decimal::serde::str_option")]
456    pub new_value: Option<Decimal>,
457}
458
459/// Policy area categories.
460#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
461#[serde(rename_all = "snake_case")]
462pub enum PolicyArea {
463    /// Approval thresholds.
464    ApprovalThreshold,
465    /// Vendor management.
466    VendorManagement,
467    /// Expense policies.
468    ExpensePolicy,
469    /// Revenue recognition.
470    RevenueRecognition,
471    /// Internal controls.
472    InternalControls,
473    /// Risk management.
474    RiskManagement,
475    /// Other policy area.
476    Other,
477}
478
479/// Configuration for a workforce reduction event.
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct WorkforceReductionConfig {
482    /// Date of reduction.
483    pub reduction_date: NaiveDate,
484    /// Percentage of workforce reduced (0.0 to 1.0).
485    #[serde(default = "default_reduction_percent")]
486    pub reduction_percent: f64,
487    /// Affected departments.
488    #[serde(default)]
489    pub affected_departments: Vec<String>,
490    /// Error rate increase due to understaffing.
491    #[serde(default = "default_error_increase")]
492    pub error_rate_increase: f64,
493    /// Processing time increase factor.
494    #[serde(default = "default_processing_increase")]
495    pub processing_time_increase: f64,
496    /// Number of months for transition.
497    #[serde(default = "default_workforce_transition")]
498    pub transition_months: u32,
499    /// Severance costs.
500    #[serde(default, with = "rust_decimal::serde::str_option")]
501    pub severance_costs: Option<Decimal>,
502}
503
504fn default_reduction_percent() -> f64 {
505    0.10
506}
507
508fn default_error_increase() -> f64 {
509    0.05
510}
511
512fn default_processing_increase() -> f64 {
513    1.3
514}
515
516fn default_workforce_transition() -> u32 {
517    6
518}
519
520impl Default for WorkforceReductionConfig {
521    fn default() -> Self {
522        Self {
523            reduction_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
524            reduction_percent: 0.10,
525            affected_departments: Vec::new(),
526            error_rate_increase: 0.05,
527            processing_time_increase: 1.3,
528            transition_months: 6,
529            severance_costs: None,
530        }
531    }
532}
533
534/// Configuration for a merger event.
535#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct MergerConfig {
537    /// Code of the merged entity.
538    pub merged_entity_code: String,
539    /// Name of the merged entity.
540    #[serde(default)]
541    pub merged_entity_name: Option<String>,
542    /// Merger closing date.
543    pub merger_date: NaiveDate,
544    /// Volume multiplier after merger.
545    #[serde(default = "default_merger_volume_mult")]
546    pub volume_multiplier: f64,
547    /// Integration error rate.
548    #[serde(default = "default_integration_error_rate")]
549    pub integration_error_rate: f64,
550    /// Integration phase configuration.
551    #[serde(default)]
552    pub integration_phases: IntegrationPhaseConfig,
553    /// Fair value adjustments required.
554    #[serde(default)]
555    pub fair_value_adjustments: Vec<FairValueAdjustment>,
556    /// Goodwill recognized.
557    #[serde(default, with = "rust_decimal::serde::str_option")]
558    pub goodwill: Option<Decimal>,
559}
560
561fn default_merger_volume_mult() -> f64 {
562    1.80
563}
564
565impl Default for MergerConfig {
566    fn default() -> Self {
567        Self {
568            merged_entity_code: String::new(),
569            merged_entity_name: None,
570            merger_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
571            volume_multiplier: 1.80,
572            integration_error_rate: 0.05,
573            integration_phases: IntegrationPhaseConfig::default(),
574            fair_value_adjustments: Vec::new(),
575            goodwill: None,
576        }
577    }
578}
579
580/// Fair value adjustment for merger accounting.
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct FairValueAdjustment {
583    /// Account affected.
584    pub account: String,
585    /// Adjustment amount.
586    #[serde(with = "rust_decimal::serde::str")]
587    pub adjustment_amount: Decimal,
588    /// Reason for adjustment.
589    pub reason: String,
590}
591
592/// A scheduled organizational event.
593#[derive(Debug, Clone, Serialize, Deserialize)]
594pub struct OrganizationalEvent {
595    /// Unique event ID.
596    pub event_id: String,
597    /// Event type and configuration.
598    pub event_type: OrganizationalEventType,
599    /// Effective date of the event.
600    pub effective_date: NaiveDate,
601    /// Human-readable description.
602    #[serde(default)]
603    pub description: Option<String>,
604    /// Tags for categorization.
605    #[serde(default)]
606    pub tags: Vec<String>,
607}
608
609impl OrganizationalEvent {
610    /// Create a new organizational event.
611    pub fn new(event_id: impl Into<String>, event_type: OrganizationalEventType) -> Self {
612        let effective_date = match &event_type {
613            OrganizationalEventType::Acquisition(c) => c.acquisition_date,
614            OrganizationalEventType::Divestiture(c) => c.divestiture_date,
615            OrganizationalEventType::Reorganization(c) => c.effective_date,
616            OrganizationalEventType::LeadershipChange(c) => c.change_date,
617            OrganizationalEventType::WorkforceReduction(c) => c.reduction_date,
618            OrganizationalEventType::Merger(c) => c.merger_date,
619        };
620
621        Self {
622            event_id: event_id.into(),
623            event_type,
624            effective_date,
625            description: None,
626            tags: Vec::new(),
627        }
628    }
629
630    /// Check if the event is active at a given date.
631    pub fn is_active_at(&self, date: NaiveDate) -> bool {
632        if date < self.effective_date {
633            return false;
634        }
635        let transition_months = self.event_type.transition_months();
636        let end_date = self.effective_date + chrono::Duration::days(transition_months as i64 * 30);
637        date <= end_date
638    }
639
640    /// Get the progress through the event (0.0 to 1.0).
641    pub fn progress_at(&self, date: NaiveDate) -> f64 {
642        if date < self.effective_date {
643            return 0.0;
644        }
645        let transition_months = self.event_type.transition_months();
646        if transition_months == 0 {
647            return 1.0;
648        }
649        let days_elapsed = (date - self.effective_date).num_days() as f64;
650        let total_days = transition_months as f64 * 30.0;
651        (days_elapsed / total_days).min(1.0)
652    }
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    fn test_acquisition_config_defaults() {
661        let config = AcquisitionConfig::default();
662        assert!((config.volume_multiplier - 1.35).abs() < 0.001);
663        assert!((config.integration_error_rate - 0.05).abs() < 0.001);
664        assert_eq!(config.parallel_posting_days, 30);
665    }
666
667    #[test]
668    fn test_integration_phase_detection() {
669        let parallel_start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
670        let parallel_end = NaiveDate::from_ymd_opt(2024, 2, 28).unwrap();
671        let cutover = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
672        let stab_end = NaiveDate::from_ymd_opt(2024, 5, 31).unwrap();
673
674        let config = IntegrationPhaseConfig {
675            parallel_run: Some(DateRange {
676                start: parallel_start,
677                end: parallel_end,
678            }),
679            cutover_date: cutover,
680            stabilization_end: stab_end,
681            parallel_run_error_rate: 0.08,
682            stabilization_error_rate: 0.03,
683        };
684
685        assert_eq!(
686            config.phase_at(NaiveDate::from_ymd_opt(2023, 12, 1).unwrap()),
687            IntegrationPhase::PreIntegration
688        );
689        assert_eq!(
690            config.phase_at(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap()),
691            IntegrationPhase::ParallelRun
692        );
693        assert_eq!(
694            config.phase_at(NaiveDate::from_ymd_opt(2024, 4, 1).unwrap()),
695            IntegrationPhase::Stabilization
696        );
697        assert_eq!(
698            config.phase_at(NaiveDate::from_ymd_opt(2024, 7, 1).unwrap()),
699            IntegrationPhase::Complete
700        );
701    }
702
703    #[test]
704    fn test_organizational_event_progress() {
705        let config = AcquisitionConfig {
706            acquisition_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
707            integration_phases: IntegrationPhaseConfig {
708                parallel_run: None,
709                cutover_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
710                stabilization_end: NaiveDate::from_ymd_opt(2024, 4, 1).unwrap(), // ~3 months
711                parallel_run_error_rate: 0.0,
712                stabilization_error_rate: 0.03,
713            },
714            ..Default::default()
715        };
716
717        let event =
718            OrganizationalEvent::new("ACQ-001", OrganizationalEventType::Acquisition(config));
719
720        // Before event
721        let before = NaiveDate::from_ymd_opt(2023, 12, 1).unwrap();
722        assert!(!event.is_active_at(before));
723        assert!((event.progress_at(before) - 0.0).abs() < 0.001);
724
725        // During event
726        let during = NaiveDate::from_ymd_opt(2024, 2, 15).unwrap();
727        assert!(event.is_active_at(during));
728        assert!(event.progress_at(during) > 0.0 && event.progress_at(during) < 1.0);
729    }
730
731    #[test]
732    fn test_date_range() {
733        let range = DateRange {
734            start: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
735            end: NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
736        };
737
738        assert!(range.contains(NaiveDate::from_ymd_opt(2024, 2, 15).unwrap()));
739        assert!(!range.contains(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap()));
740        assert!(!range.contains(NaiveDate::from_ymd_opt(2024, 4, 1).unwrap()));
741        assert_eq!(range.duration_days(), 90);
742    }
743}