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(
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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum MigrationPhase {
184 PreMigration,
186 Preparation,
188 DataMigration,
190 ParallelRun,
192 Stabilization,
194 Hypercare,
196 Complete,
198}
199
200impl MigrationPhase {
201 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 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, Self::Stabilization => 1.3,
222 Self::Hypercare => 1.1,
223 Self::Complete => 1.0,
224 }
225 }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
230#[serde(rename_all = "snake_case")]
231pub enum DataMigrationStrategy {
232 #[default]
234 BigBang,
235 Phased,
237 Parallel,
239 Hybrid,
241}
242
243impl DataMigrationStrategy {
244 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#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct MigrationIssueConfig {
258 #[serde(default = "default_duplicate_rate")]
260 pub duplicate_rate: f64,
261 #[serde(default = "default_missing_rate")]
263 pub missing_rate: f64,
264 #[serde(default = "default_format_mismatch_rate")]
266 pub format_mismatch_rate: f64,
267 #[serde(default = "default_mapping_error_rate")]
269 pub mapping_error_rate: f64,
270 #[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 pub fn combined_error_rate(&self) -> f64 {
310 (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) }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct ModuleImplementationConfig {
323 pub module_name: String,
325 #[serde(default)]
327 pub target_system: Option<String>,
328 pub go_live_date: NaiveDate,
330 #[serde(default = "default_module_rollout")]
332 pub rollout_months: u32,
333 #[serde(default = "default_implementation_error_rate")]
335 pub implementation_error_rate: f64,
336 #[serde(default = "default_training_rate")]
338 pub training_completion_rate: f64,
339 #[serde(default)]
341 pub affected_processes: Vec<String>,
342 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct ConfigurationChange {
377 pub item: String,
379 #[serde(default)]
381 pub old_value: Option<String>,
382 pub new_value: String,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct IntegrationUpgradeConfig {
389 pub integration_name: String,
391 #[serde(default)]
393 pub source_system: Option<String>,
394 #[serde(default)]
396 pub target_system: Option<String>,
397 pub upgrade_date: NaiveDate,
399 #[serde(default = "default_integration_transition")]
401 pub transition_months: u32,
402 #[serde(default = "default_integration_error_rate")]
404 pub transition_error_rate: f64,
405 #[serde(default)]
407 pub format_changes: Vec<FormatChange>,
408 #[serde(default)]
410 pub new_fields: Vec<String>,
411 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct FormatChange {
443 pub field: String,
445 pub old_format: String,
447 pub new_format: String,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct TechnologyTransitionEvent {
454 pub event_id: String,
456 pub event_type: TechnologyTransitionType,
458 pub effective_date: NaiveDate,
460 #[serde(default)]
462 pub description: Option<String>,
463 #[serde(default)]
465 pub tags: Vec<String>,
466}
467
468impl TechnologyTransitionEvent {
469 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 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 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 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 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 assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()));
607
608 assert!(event.is_active_at(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap()));
610
611 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}