Skip to main content

datasynth_core/models/
technology_transition.rs

1//! Technology transition models for pattern drift simulation.
2//!
3//! Provides comprehensive technology change modeling including:
4//! - ERP migrations with cutover phases
5//! - Module implementations
6//! - Integration upgrades
7
8use chrono::NaiveDate;
9use serde::{Deserialize, Serialize};
10
11/// Technology transition event type with associated configuration.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum TechnologyTransitionType {
15    /// Full ERP system migration.
16    ErpMigration(ErpMigrationConfig),
17    /// New module implementation.
18    ModuleImplementation(ModuleImplementationConfig),
19    /// Integration or interface upgrade.
20    IntegrationUpgrade(IntegrationUpgradeConfig),
21}
22
23impl TechnologyTransitionType {
24    /// Get the event type name.
25    pub fn type_name(&self) -> &'static str {
26        match self {
27            Self::ErpMigration(_) => "erp_migration",
28            Self::ModuleImplementation(_) => "module_implementation",
29            Self::IntegrationUpgrade(_) => "integration_upgrade",
30        }
31    }
32
33    /// Get the error rate impact for this transition.
34    pub fn error_rate_impact(&self) -> f64 {
35        match self {
36            Self::ErpMigration(c) => c.migration_issues.combined_error_rate(),
37            Self::ModuleImplementation(c) => c.implementation_error_rate,
38            Self::IntegrationUpgrade(c) => c.transition_error_rate,
39        }
40    }
41
42    /// Get the transition duration in months.
43    pub fn transition_months(&self) -> u32 {
44        match self {
45            Self::ErpMigration(c) => c.phases.total_duration_months(),
46            Self::ModuleImplementation(c) => c.rollout_months,
47            Self::IntegrationUpgrade(c) => c.transition_months,
48        }
49    }
50}
51
52/// Configuration for ERP migration.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ErpMigrationConfig {
55    /// Source system identifier.
56    pub source_system: String,
57    /// Target system identifier.
58    pub target_system: String,
59    /// Migration phases configuration.
60    #[serde(default)]
61    pub phases: MigrationPhases,
62    /// Migration issue configuration.
63    #[serde(default)]
64    pub migration_issues: MigrationIssueConfig,
65    /// Data migration strategy.
66    #[serde(default)]
67    pub data_migration_strategy: DataMigrationStrategy,
68    /// Entities being migrated.
69    #[serde(default)]
70    pub migrated_entities: Vec<String>,
71    /// Legacy system decommission date.
72    #[serde(default)]
73    pub decommission_date: Option<NaiveDate>,
74}
75
76impl Default for ErpMigrationConfig {
77    fn default() -> Self {
78        Self {
79            source_system: "SAP_R3".to_string(),
80            target_system: "SAP_S4HANA".to_string(),
81            phases: MigrationPhases::default(),
82            migration_issues: MigrationIssueConfig::default(),
83            data_migration_strategy: DataMigrationStrategy::BigBang,
84            migrated_entities: Vec::new(),
85            decommission_date: None,
86        }
87    }
88}
89
90/// Migration phases configuration.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct MigrationPhases {
93    /// Preparation phase start.
94    #[serde(default)]
95    pub preparation_start: Option<NaiveDate>,
96    /// Data migration start.
97    #[serde(default)]
98    pub data_migration_start: Option<NaiveDate>,
99    /// Parallel run period start.
100    #[serde(default)]
101    pub parallel_run_start: Option<NaiveDate>,
102    /// Cutover date (go-live).
103    pub cutover_date: NaiveDate,
104    /// Stabilization end date.
105    pub stabilization_end: NaiveDate,
106    /// Hypercare end date.
107    #[serde(default)]
108    pub hypercare_end: Option<NaiveDate>,
109}
110
111impl Default for MigrationPhases {
112    fn default() -> Self {
113        Self {
114            preparation_start: Some(
115                NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"),
116            ),
117            data_migration_start: Some(
118                NaiveDate::from_ymd_opt(2024, 6, 1).expect("valid default date"),
119            ),
120            parallel_run_start: Some(
121                NaiveDate::from_ymd_opt(2024, 8, 1).expect("valid default date"),
122            ),
123            cutover_date: NaiveDate::from_ymd_opt(2024, 9, 1).expect("valid default date"),
124            stabilization_end: NaiveDate::from_ymd_opt(2024, 11, 30).expect("valid default date"),
125            hypercare_end: Some(NaiveDate::from_ymd_opt(2024, 12, 31).expect("valid default date")),
126        }
127    }
128}
129
130impl MigrationPhases {
131    /// Calculate total migration duration in months.
132    pub fn total_duration_months(&self) -> u32 {
133        let start = self.preparation_start.unwrap_or(self.cutover_date);
134        let end = self.hypercare_end.unwrap_or(self.stabilization_end);
135        let days = (end - start).num_days();
136        (days / 30) as u32
137    }
138
139    /// Get the current migration phase for a given date.
140    pub fn phase_at(&self, date: NaiveDate) -> MigrationPhase {
141        if let Some(prep) = self.preparation_start {
142            if date < prep {
143                return MigrationPhase::PreMigration;
144            }
145        }
146
147        if let Some(data_mig) = self.data_migration_start {
148            if date < data_mig {
149                return MigrationPhase::Preparation;
150            }
151        }
152
153        if let Some(parallel) = self.parallel_run_start {
154            if date < parallel {
155                return MigrationPhase::DataMigration;
156            }
157            if date < self.cutover_date {
158                return MigrationPhase::ParallelRun;
159            }
160        }
161
162        if date < self.cutover_date {
163            return MigrationPhase::DataMigration;
164        }
165
166        if date < self.stabilization_end {
167            return MigrationPhase::Stabilization;
168        }
169
170        if let Some(hypercare) = self.hypercare_end {
171            if date < hypercare {
172                return MigrationPhase::Hypercare;
173            }
174        }
175
176        MigrationPhase::Complete
177    }
178}
179
180/// Migration phase stage.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum MigrationPhase {
184    /// Before migration starts.
185    PreMigration,
186    /// Preparation and planning.
187    Preparation,
188    /// Data migration in progress.
189    DataMigration,
190    /// Both systems running in parallel.
191    ParallelRun,
192    /// Post-cutover stabilization.
193    Stabilization,
194    /// Hypercare period.
195    Hypercare,
196    /// Migration complete.
197    Complete,
198}
199
200impl MigrationPhase {
201    /// Get the typical error rate multiplier for this phase.
202    pub fn error_rate_multiplier(&self) -> f64 {
203        match self {
204            Self::PreMigration => 1.0,
205            Self::Preparation => 1.0,
206            Self::DataMigration => 1.5,
207            Self::ParallelRun => 2.0,
208            Self::Stabilization => 1.8,
209            Self::Hypercare => 1.3,
210            Self::Complete => 1.0,
211        }
212    }
213
214    /// Get the typical processing time multiplier.
215    pub fn processing_time_multiplier(&self) -> f64 {
216        match self {
217            Self::PreMigration => 1.0,
218            Self::Preparation => 1.0,
219            Self::DataMigration => 1.2,
220            Self::ParallelRun => 1.5, // Dual entry
221            Self::Stabilization => 1.3,
222            Self::Hypercare => 1.1,
223            Self::Complete => 1.0,
224        }
225    }
226}
227
228/// Data migration strategy.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
230#[serde(rename_all = "snake_case")]
231pub enum DataMigrationStrategy {
232    /// All at once migration.
233    #[default]
234    BigBang,
235    /// Phased migration by module or entity.
236    Phased,
237    /// Parallel run with gradual cutover.
238    Parallel,
239    /// Hybrid approach.
240    Hybrid,
241}
242
243impl DataMigrationStrategy {
244    /// Get the risk level associated with this strategy.
245    pub fn risk_level(&self) -> &'static str {
246        match self {
247            Self::BigBang => "high",
248            Self::Phased => "medium",
249            Self::Parallel => "low",
250            Self::Hybrid => "medium",
251        }
252    }
253}
254
255/// Migration issue configuration.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct MigrationIssueConfig {
258    /// Rate of duplicate records during migration.
259    #[serde(default = "default_duplicate_rate")]
260    pub duplicate_rate: f64,
261    /// Rate of missing records.
262    #[serde(default = "default_missing_rate")]
263    pub missing_rate: f64,
264    /// Rate of format mismatch issues.
265    #[serde(default = "default_format_mismatch_rate")]
266    pub format_mismatch_rate: f64,
267    /// Rate of mapping errors.
268    #[serde(default = "default_mapping_error_rate")]
269    pub mapping_error_rate: f64,
270    /// Rate of timing/cutoff issues.
271    #[serde(default = "default_cutoff_issue_rate")]
272    pub cutoff_issue_rate: f64,
273}
274
275fn default_duplicate_rate() -> f64 {
276    0.02
277}
278
279fn default_missing_rate() -> f64 {
280    0.01
281}
282
283fn default_format_mismatch_rate() -> f64 {
284    0.03
285}
286
287fn default_mapping_error_rate() -> f64 {
288    0.02
289}
290
291fn default_cutoff_issue_rate() -> f64 {
292    0.01
293}
294
295impl Default for MigrationIssueConfig {
296    fn default() -> Self {
297        Self {
298            duplicate_rate: 0.02,
299            missing_rate: 0.01,
300            format_mismatch_rate: 0.03,
301            mapping_error_rate: 0.02,
302            cutoff_issue_rate: 0.01,
303        }
304    }
305}
306
307impl MigrationIssueConfig {
308    /// Calculate combined error rate.
309    pub fn combined_error_rate(&self) -> f64 {
310        // These aren't strictly additive, but this gives a reasonable approximation
311        (self.duplicate_rate
312            + self.missing_rate
313            + self.format_mismatch_rate
314            + self.mapping_error_rate
315            + self.cutoff_issue_rate)
316            .min(0.20) // Cap at 20%
317    }
318}
319
320/// Configuration for module implementation.
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct ModuleImplementationConfig {
323    /// Module being implemented.
324    pub module_name: String,
325    /// System the module is being added to.
326    #[serde(default)]
327    pub target_system: Option<String>,
328    /// Go-live date.
329    pub go_live_date: NaiveDate,
330    /// Number of months for rollout.
331    #[serde(default = "default_module_rollout")]
332    pub rollout_months: u32,
333    /// Implementation error rate.
334    #[serde(default = "default_implementation_error_rate")]
335    pub implementation_error_rate: f64,
336    /// Training completion rate (0.0 to 1.0).
337    #[serde(default = "default_training_rate")]
338    pub training_completion_rate: f64,
339    /// Affected business processes.
340    #[serde(default)]
341    pub affected_processes: Vec<String>,
342    /// Configuration changes.
343    #[serde(default)]
344    pub configuration_changes: Vec<ConfigurationChange>,
345}
346
347fn default_module_rollout() -> u32 {
348    4
349}
350
351fn default_implementation_error_rate() -> f64 {
352    0.04
353}
354
355fn default_training_rate() -> f64 {
356    0.85
357}
358
359impl Default for ModuleImplementationConfig {
360    fn default() -> Self {
361        Self {
362            module_name: String::new(),
363            target_system: None,
364            go_live_date: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"),
365            rollout_months: 4,
366            implementation_error_rate: 0.04,
367            training_completion_rate: 0.85,
368            affected_processes: Vec::new(),
369            configuration_changes: Vec::new(),
370        }
371    }
372}
373
374/// Configuration change for module implementation.
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct ConfigurationChange {
377    /// Configuration item.
378    pub item: String,
379    /// Old value.
380    #[serde(default)]
381    pub old_value: Option<String>,
382    /// New value.
383    pub new_value: String,
384}
385
386/// Configuration for integration upgrade.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct IntegrationUpgradeConfig {
389    /// Integration being upgraded.
390    pub integration_name: String,
391    /// Source system.
392    #[serde(default)]
393    pub source_system: Option<String>,
394    /// Target system.
395    #[serde(default)]
396    pub target_system: Option<String>,
397    /// Upgrade date.
398    pub upgrade_date: NaiveDate,
399    /// Number of months for transition.
400    #[serde(default = "default_integration_transition")]
401    pub transition_months: u32,
402    /// Error rate during transition.
403    #[serde(default = "default_integration_error_rate")]
404    pub transition_error_rate: f64,
405    /// Message format changes.
406    #[serde(default)]
407    pub format_changes: Vec<FormatChange>,
408    /// New fields added.
409    #[serde(default)]
410    pub new_fields: Vec<String>,
411    /// Fields deprecated.
412    #[serde(default)]
413    pub deprecated_fields: Vec<String>,
414}
415
416fn default_integration_transition() -> u32 {
417    2
418}
419
420fn default_integration_error_rate() -> f64 {
421    0.03
422}
423
424impl Default for IntegrationUpgradeConfig {
425    fn default() -> Self {
426        Self {
427            integration_name: String::new(),
428            source_system: None,
429            target_system: None,
430            upgrade_date: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"),
431            transition_months: 2,
432            transition_error_rate: 0.03,
433            format_changes: Vec::new(),
434            new_fields: Vec::new(),
435            deprecated_fields: Vec::new(),
436        }
437    }
438}
439
440/// Format change in integration upgrade.
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct FormatChange {
443    /// Field affected.
444    pub field: String,
445    /// Old format.
446    pub old_format: String,
447    /// New format.
448    pub new_format: String,
449}
450
451/// A scheduled technology transition event.
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct TechnologyTransitionEvent {
454    /// Unique event ID.
455    pub event_id: String,
456    /// Event type and configuration.
457    pub event_type: TechnologyTransitionType,
458    /// Effective date of the event.
459    pub effective_date: NaiveDate,
460    /// Human-readable description.
461    #[serde(default)]
462    pub description: Option<String>,
463    /// Tags for categorization.
464    #[serde(default)]
465    pub tags: Vec<String>,
466}
467
468impl TechnologyTransitionEvent {
469    /// Create a new technology transition event.
470    pub fn new(event_id: impl Into<String>, event_type: TechnologyTransitionType) -> Self {
471        let effective_date = match &event_type {
472            TechnologyTransitionType::ErpMigration(c) => c.phases.cutover_date,
473            TechnologyTransitionType::ModuleImplementation(c) => c.go_live_date,
474            TechnologyTransitionType::IntegrationUpgrade(c) => c.upgrade_date,
475        };
476
477        Self {
478            event_id: event_id.into(),
479            event_type,
480            effective_date,
481            description: None,
482            tags: Vec::new(),
483        }
484    }
485
486    /// Check if the event is active at a given date.
487    pub fn is_active_at(&self, date: NaiveDate) -> bool {
488        match &self.event_type {
489            TechnologyTransitionType::ErpMigration(c) => {
490                let start = c.phases.preparation_start.unwrap_or(c.phases.cutover_date);
491                let end = c.phases.hypercare_end.unwrap_or(c.phases.stabilization_end);
492                date >= start && date <= end
493            }
494            TechnologyTransitionType::ModuleImplementation(c) => {
495                let end = c.go_live_date + chrono::Duration::days(c.rollout_months as i64 * 30);
496                date >= c.go_live_date && date <= end
497            }
498            TechnologyTransitionType::IntegrationUpgrade(c) => {
499                let end = c.upgrade_date + chrono::Duration::days(c.transition_months as i64 * 30);
500                date >= c.upgrade_date && date <= end
501            }
502        }
503    }
504
505    /// Get the progress through the event (0.0 to 1.0).
506    pub fn progress_at(&self, date: NaiveDate) -> f64 {
507        let (start, total_days) = match &self.event_type {
508            TechnologyTransitionType::ErpMigration(c) => {
509                let start = c.phases.preparation_start.unwrap_or(c.phases.cutover_date);
510                let end = c.phases.hypercare_end.unwrap_or(c.phases.stabilization_end);
511                (start, (end - start).num_days() as f64)
512            }
513            TechnologyTransitionType::ModuleImplementation(c) => {
514                (c.go_live_date, c.rollout_months as f64 * 30.0)
515            }
516            TechnologyTransitionType::IntegrationUpgrade(c) => {
517                (c.upgrade_date, c.transition_months as f64 * 30.0)
518            }
519        };
520
521        if date < start {
522            return 0.0;
523        }
524        if total_days <= 0.0 {
525            return 1.0;
526        }
527
528        let days_elapsed = (date - start).num_days() as f64;
529        (days_elapsed / total_days).min(1.0)
530    }
531
532    /// Get the current migration phase (for ERP migrations).
533    pub fn migration_phase_at(&self, date: NaiveDate) -> Option<MigrationPhase> {
534        match &self.event_type {
535            TechnologyTransitionType::ErpMigration(c) => Some(c.phases.phase_at(date)),
536            _ => None,
537        }
538    }
539}
540
541#[cfg(test)]
542#[allow(clippy::unwrap_used)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn test_migration_phases() {
548        let phases = MigrationPhases {
549            preparation_start: Some(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
550            data_migration_start: Some(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap()),
551            parallel_run_start: Some(NaiveDate::from_ymd_opt(2024, 8, 1).unwrap()),
552            cutover_date: NaiveDate::from_ymd_opt(2024, 9, 1).unwrap(),
553            stabilization_end: NaiveDate::from_ymd_opt(2024, 11, 30).unwrap(),
554            hypercare_end: Some(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()),
555        };
556
557        assert_eq!(
558            phases.phase_at(NaiveDate::from_ymd_opt(2023, 12, 1).unwrap()),
559            MigrationPhase::PreMigration
560        );
561        assert_eq!(
562            phases.phase_at(NaiveDate::from_ymd_opt(2024, 3, 1).unwrap()),
563            MigrationPhase::Preparation
564        );
565        assert_eq!(
566            phases.phase_at(NaiveDate::from_ymd_opt(2024, 7, 1).unwrap()),
567            MigrationPhase::DataMigration
568        );
569        assert_eq!(
570            phases.phase_at(NaiveDate::from_ymd_opt(2024, 8, 15).unwrap()),
571            MigrationPhase::ParallelRun
572        );
573        assert_eq!(
574            phases.phase_at(NaiveDate::from_ymd_opt(2024, 10, 1).unwrap()),
575            MigrationPhase::Stabilization
576        );
577        assert_eq!(
578            phases.phase_at(NaiveDate::from_ymd_opt(2024, 12, 15).unwrap()),
579            MigrationPhase::Hypercare
580        );
581        assert_eq!(
582            phases.phase_at(NaiveDate::from_ymd_opt(2025, 2, 1).unwrap()),
583            MigrationPhase::Complete
584        );
585    }
586
587    #[test]
588    fn test_migration_issue_combined_rate() {
589        let issues = MigrationIssueConfig::default();
590        let combined = issues.combined_error_rate();
591
592        // Should be reasonable combined rate
593        assert!(combined > 0.0);
594        assert!(combined <= 0.20);
595    }
596
597    #[test]
598    fn test_technology_transition_event() {
599        let config = ErpMigrationConfig::default();
600        let event = TechnologyTransitionEvent::new(
601            "ERP-001",
602            TechnologyTransitionType::ErpMigration(config.clone()),
603        );
604
605        // Before start
606        assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()));
607
608        // During migration
609        assert!(event.is_active_at(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap()));
610
611        // After completion
612        assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2025, 6, 1).unwrap()));
613    }
614
615    #[test]
616    fn test_data_migration_strategy_risk() {
617        assert_eq!(DataMigrationStrategy::BigBang.risk_level(), "high");
618        assert_eq!(DataMigrationStrategy::Parallel.risk_level(), "low");
619    }
620}