1use chrono::NaiveDate;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum TechnologyTransitionType {
15 ErpMigration(ErpMigrationConfig),
17 ModuleImplementation(ModuleImplementationConfig),
19 IntegrationUpgrade(IntegrationUpgradeConfig),
21}
22
23impl TechnologyTransitionType {
24 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ErpMigrationConfig {
55 pub source_system: String,
57 pub target_system: String,
59 #[serde(default)]
61 pub phases: MigrationPhases,
62 #[serde(default)]
64 pub migration_issues: MigrationIssueConfig,
65 #[serde(default)]
67 pub data_migration_strategy: DataMigrationStrategy,
68 #[serde(default)]
70 pub migrated_entities: Vec<String>,
71 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct MigrationPhases {
93 #[serde(default)]
95 pub preparation_start: Option<NaiveDate>,
96 #[serde(default)]
98 pub data_migration_start: Option<NaiveDate>,
99 #[serde(default)]
101 pub parallel_run_start: Option<NaiveDate>,
102 pub cutover_date: NaiveDate,
104 pub stabilization_end: NaiveDate,
106 #[serde(default)]
108 pub hypercare_end: Option<NaiveDate>,
109}
110
111impl Default for MigrationPhases {
112 fn default() -> Self {
113 Self {
114 preparation_start: Some(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
115 data_migration_start: Some(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap()),
116 parallel_run_start: Some(NaiveDate::from_ymd_opt(2024, 8, 1).unwrap()),
117 cutover_date: NaiveDate::from_ymd_opt(2024, 9, 1).unwrap(),
118 stabilization_end: NaiveDate::from_ymd_opt(2024, 11, 30).unwrap(),
119 hypercare_end: Some(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()),
120 }
121 }
122}
123
124impl MigrationPhases {
125 pub fn total_duration_months(&self) -> u32 {
127 let start = self.preparation_start.unwrap_or(self.cutover_date);
128 let end = self.hypercare_end.unwrap_or(self.stabilization_end);
129 let days = (end - start).num_days();
130 (days / 30) as u32
131 }
132
133 pub fn phase_at(&self, date: NaiveDate) -> MigrationPhase {
135 if let Some(prep) = self.preparation_start {
136 if date < prep {
137 return MigrationPhase::PreMigration;
138 }
139 }
140
141 if let Some(data_mig) = self.data_migration_start {
142 if date < data_mig {
143 return MigrationPhase::Preparation;
144 }
145 }
146
147 if let Some(parallel) = self.parallel_run_start {
148 if date < parallel {
149 return MigrationPhase::DataMigration;
150 }
151 if date < self.cutover_date {
152 return MigrationPhase::ParallelRun;
153 }
154 }
155
156 if date < self.cutover_date {
157 return MigrationPhase::DataMigration;
158 }
159
160 if date < self.stabilization_end {
161 return MigrationPhase::Stabilization;
162 }
163
164 if let Some(hypercare) = self.hypercare_end {
165 if date < hypercare {
166 return MigrationPhase::Hypercare;
167 }
168 }
169
170 MigrationPhase::Complete
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "snake_case")]
177pub enum MigrationPhase {
178 PreMigration,
180 Preparation,
182 DataMigration,
184 ParallelRun,
186 Stabilization,
188 Hypercare,
190 Complete,
192}
193
194impl MigrationPhase {
195 pub fn error_rate_multiplier(&self) -> f64 {
197 match self {
198 Self::PreMigration => 1.0,
199 Self::Preparation => 1.0,
200 Self::DataMigration => 1.5,
201 Self::ParallelRun => 2.0,
202 Self::Stabilization => 1.8,
203 Self::Hypercare => 1.3,
204 Self::Complete => 1.0,
205 }
206 }
207
208 pub fn processing_time_multiplier(&self) -> f64 {
210 match self {
211 Self::PreMigration => 1.0,
212 Self::Preparation => 1.0,
213 Self::DataMigration => 1.2,
214 Self::ParallelRun => 1.5, Self::Stabilization => 1.3,
216 Self::Hypercare => 1.1,
217 Self::Complete => 1.0,
218 }
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
224#[serde(rename_all = "snake_case")]
225pub enum DataMigrationStrategy {
226 #[default]
228 BigBang,
229 Phased,
231 Parallel,
233 Hybrid,
235}
236
237impl DataMigrationStrategy {
238 pub fn risk_level(&self) -> &'static str {
240 match self {
241 Self::BigBang => "high",
242 Self::Phased => "medium",
243 Self::Parallel => "low",
244 Self::Hybrid => "medium",
245 }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct MigrationIssueConfig {
252 #[serde(default = "default_duplicate_rate")]
254 pub duplicate_rate: f64,
255 #[serde(default = "default_missing_rate")]
257 pub missing_rate: f64,
258 #[serde(default = "default_format_mismatch_rate")]
260 pub format_mismatch_rate: f64,
261 #[serde(default = "default_mapping_error_rate")]
263 pub mapping_error_rate: f64,
264 #[serde(default = "default_cutoff_issue_rate")]
266 pub cutoff_issue_rate: f64,
267}
268
269fn default_duplicate_rate() -> f64 {
270 0.02
271}
272
273fn default_missing_rate() -> f64 {
274 0.01
275}
276
277fn default_format_mismatch_rate() -> f64 {
278 0.03
279}
280
281fn default_mapping_error_rate() -> f64 {
282 0.02
283}
284
285fn default_cutoff_issue_rate() -> f64 {
286 0.01
287}
288
289impl Default for MigrationIssueConfig {
290 fn default() -> Self {
291 Self {
292 duplicate_rate: 0.02,
293 missing_rate: 0.01,
294 format_mismatch_rate: 0.03,
295 mapping_error_rate: 0.02,
296 cutoff_issue_rate: 0.01,
297 }
298 }
299}
300
301impl MigrationIssueConfig {
302 pub fn combined_error_rate(&self) -> f64 {
304 (self.duplicate_rate
306 + self.missing_rate
307 + self.format_mismatch_rate
308 + self.mapping_error_rate
309 + self.cutoff_issue_rate)
310 .min(0.20) }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ModuleImplementationConfig {
317 pub module_name: String,
319 #[serde(default)]
321 pub target_system: Option<String>,
322 pub go_live_date: NaiveDate,
324 #[serde(default = "default_module_rollout")]
326 pub rollout_months: u32,
327 #[serde(default = "default_implementation_error_rate")]
329 pub implementation_error_rate: f64,
330 #[serde(default = "default_training_rate")]
332 pub training_completion_rate: f64,
333 #[serde(default)]
335 pub affected_processes: Vec<String>,
336 #[serde(default)]
338 pub configuration_changes: Vec<ConfigurationChange>,
339}
340
341fn default_module_rollout() -> u32 {
342 4
343}
344
345fn default_implementation_error_rate() -> f64 {
346 0.04
347}
348
349fn default_training_rate() -> f64 {
350 0.85
351}
352
353impl Default for ModuleImplementationConfig {
354 fn default() -> Self {
355 Self {
356 module_name: String::new(),
357 target_system: None,
358 go_live_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
359 rollout_months: 4,
360 implementation_error_rate: 0.04,
361 training_completion_rate: 0.85,
362 affected_processes: Vec::new(),
363 configuration_changes: Vec::new(),
364 }
365 }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct ConfigurationChange {
371 pub item: String,
373 #[serde(default)]
375 pub old_value: Option<String>,
376 pub new_value: String,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct IntegrationUpgradeConfig {
383 pub integration_name: String,
385 #[serde(default)]
387 pub source_system: Option<String>,
388 #[serde(default)]
390 pub target_system: Option<String>,
391 pub upgrade_date: NaiveDate,
393 #[serde(default = "default_integration_transition")]
395 pub transition_months: u32,
396 #[serde(default = "default_integration_error_rate")]
398 pub transition_error_rate: f64,
399 #[serde(default)]
401 pub format_changes: Vec<FormatChange>,
402 #[serde(default)]
404 pub new_fields: Vec<String>,
405 #[serde(default)]
407 pub deprecated_fields: Vec<String>,
408}
409
410fn default_integration_transition() -> u32 {
411 2
412}
413
414fn default_integration_error_rate() -> f64 {
415 0.03
416}
417
418impl Default for IntegrationUpgradeConfig {
419 fn default() -> Self {
420 Self {
421 integration_name: String::new(),
422 source_system: None,
423 target_system: None,
424 upgrade_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
425 transition_months: 2,
426 transition_error_rate: 0.03,
427 format_changes: Vec::new(),
428 new_fields: Vec::new(),
429 deprecated_fields: Vec::new(),
430 }
431 }
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct FormatChange {
437 pub field: String,
439 pub old_format: String,
441 pub new_format: String,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct TechnologyTransitionEvent {
448 pub event_id: String,
450 pub event_type: TechnologyTransitionType,
452 pub effective_date: NaiveDate,
454 #[serde(default)]
456 pub description: Option<String>,
457 #[serde(default)]
459 pub tags: Vec<String>,
460}
461
462impl TechnologyTransitionEvent {
463 pub fn new(event_id: impl Into<String>, event_type: TechnologyTransitionType) -> Self {
465 let effective_date = match &event_type {
466 TechnologyTransitionType::ErpMigration(c) => c.phases.cutover_date,
467 TechnologyTransitionType::ModuleImplementation(c) => c.go_live_date,
468 TechnologyTransitionType::IntegrationUpgrade(c) => c.upgrade_date,
469 };
470
471 Self {
472 event_id: event_id.into(),
473 event_type,
474 effective_date,
475 description: None,
476 tags: Vec::new(),
477 }
478 }
479
480 pub fn is_active_at(&self, date: NaiveDate) -> bool {
482 match &self.event_type {
483 TechnologyTransitionType::ErpMigration(c) => {
484 let start = c.phases.preparation_start.unwrap_or(c.phases.cutover_date);
485 let end = c.phases.hypercare_end.unwrap_or(c.phases.stabilization_end);
486 date >= start && date <= end
487 }
488 TechnologyTransitionType::ModuleImplementation(c) => {
489 let end = c.go_live_date + chrono::Duration::days(c.rollout_months as i64 * 30);
490 date >= c.go_live_date && date <= end
491 }
492 TechnologyTransitionType::IntegrationUpgrade(c) => {
493 let end = c.upgrade_date + chrono::Duration::days(c.transition_months as i64 * 30);
494 date >= c.upgrade_date && date <= end
495 }
496 }
497 }
498
499 pub fn progress_at(&self, date: NaiveDate) -> f64 {
501 let (start, total_days) = match &self.event_type {
502 TechnologyTransitionType::ErpMigration(c) => {
503 let start = c.phases.preparation_start.unwrap_or(c.phases.cutover_date);
504 let end = c.phases.hypercare_end.unwrap_or(c.phases.stabilization_end);
505 (start, (end - start).num_days() as f64)
506 }
507 TechnologyTransitionType::ModuleImplementation(c) => {
508 (c.go_live_date, c.rollout_months as f64 * 30.0)
509 }
510 TechnologyTransitionType::IntegrationUpgrade(c) => {
511 (c.upgrade_date, c.transition_months as f64 * 30.0)
512 }
513 };
514
515 if date < start {
516 return 0.0;
517 }
518 if total_days <= 0.0 {
519 return 1.0;
520 }
521
522 let days_elapsed = (date - start).num_days() as f64;
523 (days_elapsed / total_days).min(1.0)
524 }
525
526 pub fn migration_phase_at(&self, date: NaiveDate) -> Option<MigrationPhase> {
528 match &self.event_type {
529 TechnologyTransitionType::ErpMigration(c) => Some(c.phases.phase_at(date)),
530 _ => None,
531 }
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn test_migration_phases() {
541 let phases = MigrationPhases {
542 preparation_start: Some(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
543 data_migration_start: Some(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap()),
544 parallel_run_start: Some(NaiveDate::from_ymd_opt(2024, 8, 1).unwrap()),
545 cutover_date: NaiveDate::from_ymd_opt(2024, 9, 1).unwrap(),
546 stabilization_end: NaiveDate::from_ymd_opt(2024, 11, 30).unwrap(),
547 hypercare_end: Some(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()),
548 };
549
550 assert_eq!(
551 phases.phase_at(NaiveDate::from_ymd_opt(2023, 12, 1).unwrap()),
552 MigrationPhase::PreMigration
553 );
554 assert_eq!(
555 phases.phase_at(NaiveDate::from_ymd_opt(2024, 3, 1).unwrap()),
556 MigrationPhase::Preparation
557 );
558 assert_eq!(
559 phases.phase_at(NaiveDate::from_ymd_opt(2024, 7, 1).unwrap()),
560 MigrationPhase::DataMigration
561 );
562 assert_eq!(
563 phases.phase_at(NaiveDate::from_ymd_opt(2024, 8, 15).unwrap()),
564 MigrationPhase::ParallelRun
565 );
566 assert_eq!(
567 phases.phase_at(NaiveDate::from_ymd_opt(2024, 10, 1).unwrap()),
568 MigrationPhase::Stabilization
569 );
570 assert_eq!(
571 phases.phase_at(NaiveDate::from_ymd_opt(2024, 12, 15).unwrap()),
572 MigrationPhase::Hypercare
573 );
574 assert_eq!(
575 phases.phase_at(NaiveDate::from_ymd_opt(2025, 2, 1).unwrap()),
576 MigrationPhase::Complete
577 );
578 }
579
580 #[test]
581 fn test_migration_issue_combined_rate() {
582 let issues = MigrationIssueConfig::default();
583 let combined = issues.combined_error_rate();
584
585 assert!(combined > 0.0);
587 assert!(combined <= 0.20);
588 }
589
590 #[test]
591 fn test_technology_transition_event() {
592 let config = ErpMigrationConfig::default();
593 let event = TechnologyTransitionEvent::new(
594 "ERP-001",
595 TechnologyTransitionType::ErpMigration(config.clone()),
596 );
597
598 assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()));
600
601 assert!(event.is_active_at(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap()));
603
604 assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2025, 6, 1).unwrap()));
606 }
607
608 #[test]
609 fn test_data_migration_strategy_risk() {
610 assert_eq!(DataMigrationStrategy::BigBang.risk_level(), "high");
611 assert_eq!(DataMigrationStrategy::Parallel.risk_level(), "low");
612 }
613}