1use chrono::NaiveDate;
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(tag = "type", rename_all = "snake_case")]
19pub enum OrganizationalEventType {
20 Acquisition(AcquisitionConfig),
22 Divestiture(DivestitureConfig),
24 Reorganization(ReorganizationConfig),
26 LeadershipChange(LeadershipChangeConfig),
28 WorkforceReduction(WorkforceReductionConfig),
30 Merger(MergerConfig),
32}
33
34impl OrganizationalEventType {
35 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 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), Self::Merger(c) => c.volume_multiplier,
56 }
57 }
58
59 pub fn error_rate_impact(&self) -> f64 {
61 match self {
62 Self::Acquisition(c) => c.integration_error_rate,
63 Self::Divestiture(_) => 0.02, 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AcquisitionConfig {
87 pub acquired_entity_code: String,
89 #[serde(default)]
91 pub acquired_entity_name: Option<String>,
92 pub acquisition_date: NaiveDate,
94 #[serde(default = "default_acquisition_volume_mult")]
96 pub volume_multiplier: f64,
97 #[serde(default = "default_integration_error_rate")]
99 pub integration_error_rate: f64,
100 #[serde(default = "default_parallel_posting_days")]
102 pub parallel_posting_days: u32,
103 #[serde(default = "default_coding_error_rate")]
105 pub coding_error_rate: f64,
106 #[serde(default)]
108 pub integration_phases: IntegrationPhaseConfig,
109 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct IntegrationPhaseConfig {
149 #[serde(default)]
151 pub parallel_run: Option<DateRange>,
152 pub cutover_date: NaiveDate,
154 pub stabilization_end: NaiveDate,
156 #[serde(default = "default_parallel_error_rate")]
158 pub parallel_run_error_rate: f64,
159 #[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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum IntegrationPhase {
231 PreIntegration,
233 ParallelRun,
235 Stabilization,
237 Complete,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct DateRange {
244 pub start: NaiveDate,
246 pub end: NaiveDate,
248}
249
250impl DateRange {
251 pub fn contains(&self, date: NaiveDate) -> bool {
253 date >= self.start && date <= self.end
254 }
255
256 pub fn duration_days(&self) -> i64 {
258 (self.end - self.start).num_days()
259 }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct PurchasePriceAllocation {
265 #[serde(with = "rust_decimal::serde::str")]
267 pub purchase_price: Decimal,
268 #[serde(with = "rust_decimal::serde::str")]
270 pub net_identifiable_assets: Decimal,
271 #[serde(with = "rust_decimal::serde::str")]
273 pub goodwill: Decimal,
274 #[serde(default, with = "rust_decimal::serde::str_option")]
276 pub bargain_purchase_gain: Option<Decimal>,
277 #[serde(default)]
279 pub intangible_assets: Vec<IntangibleAsset>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct IntangibleAsset {
285 pub asset_type: String,
287 #[serde(with = "rust_decimal::serde::str")]
289 pub fair_value: Decimal,
290 pub useful_life_years: Option<u8>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct DivestitureConfig {
297 pub divested_entity_code: String,
299 #[serde(default)]
301 pub divested_entity_name: Option<String>,
302 pub divestiture_date: NaiveDate,
304 #[serde(default = "default_volume_reduction")]
306 pub volume_reduction: f64,
307 #[serde(default = "default_transition_months")]
309 pub transition_months: u32,
310 #[serde(default = "default_true")]
312 pub remove_entity: bool,
313 #[serde(default)]
315 pub account_closures: Vec<String>,
316 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct ReorganizationConfig {
351 #[serde(default)]
353 pub description: Option<String>,
354 pub effective_date: NaiveDate,
356 #[serde(default)]
358 pub cost_center_remapping: HashMap<String, String>,
359 #[serde(default)]
361 pub department_remapping: HashMap<String, String>,
362 #[serde(default)]
364 pub reporting_changes: Vec<ReportingChange>,
365 #[serde(default = "default_transition_months")]
367 pub transition_months: u32,
368 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct ReportingChange {
394 pub entity: String,
396 pub from_reports_to: String,
398 pub to_reports_to: String,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct LeadershipChangeConfig {
405 pub role: String,
407 pub change_date: NaiveDate,
409 #[serde(default)]
411 pub policy_changes: Vec<PolicyChangeDetail>,
412 #[serde(default)]
414 pub vendor_review_triggered: bool,
415 #[serde(default = "default_policy_transition_months")]
417 pub policy_transition_months: u32,
418 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct PolicyChangeDetail {
447 pub policy_area: PolicyArea,
449 pub description: String,
451 #[serde(default, with = "rust_decimal::serde::str_option")]
453 pub old_value: Option<Decimal>,
454 #[serde(default, with = "rust_decimal::serde::str_option")]
456 pub new_value: Option<Decimal>,
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
461#[serde(rename_all = "snake_case")]
462pub enum PolicyArea {
463 ApprovalThreshold,
465 VendorManagement,
467 ExpensePolicy,
469 RevenueRecognition,
471 InternalControls,
473 RiskManagement,
475 Other,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct WorkforceReductionConfig {
482 pub reduction_date: NaiveDate,
484 #[serde(default = "default_reduction_percent")]
486 pub reduction_percent: f64,
487 #[serde(default)]
489 pub affected_departments: Vec<String>,
490 #[serde(default = "default_error_increase")]
492 pub error_rate_increase: f64,
493 #[serde(default = "default_processing_increase")]
495 pub processing_time_increase: f64,
496 #[serde(default = "default_workforce_transition")]
498 pub transition_months: u32,
499 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct MergerConfig {
537 pub merged_entity_code: String,
539 #[serde(default)]
541 pub merged_entity_name: Option<String>,
542 pub merger_date: NaiveDate,
544 #[serde(default = "default_merger_volume_mult")]
546 pub volume_multiplier: f64,
547 #[serde(default = "default_integration_error_rate")]
549 pub integration_error_rate: f64,
550 #[serde(default)]
552 pub integration_phases: IntegrationPhaseConfig,
553 #[serde(default)]
555 pub fair_value_adjustments: Vec<FairValueAdjustment>,
556 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct FairValueAdjustment {
583 pub account: String,
585 #[serde(with = "rust_decimal::serde::str")]
587 pub adjustment_amount: Decimal,
588 pub reason: String,
590}
591
592#[derive(Debug, Clone, Serialize, Deserialize)]
594pub struct OrganizationalEvent {
595 pub event_id: String,
597 pub event_type: OrganizationalEventType,
599 pub effective_date: NaiveDate,
601 #[serde(default)]
603 pub description: Option<String>,
604 #[serde(default)]
606 pub tags: Vec<String>,
607}
608
609impl OrganizationalEvent {
610 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 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 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(), 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 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 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}