1use crate::Error;
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ChangeType {
16 Security,
18 Feature,
20 Bugfix,
22 Infrastructure,
24 Configuration,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
30#[serde(rename_all = "lowercase")]
31pub enum ChangePriority {
32 Critical,
34 High,
36 Medium,
38 Low,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[serde(rename_all = "lowercase")]
45pub enum ChangeUrgency {
46 Emergency,
48 High,
50 Medium,
52 Low,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum ChangeStatus {
60 PendingApproval,
62 Approved,
64 Rejected,
66 Implementing,
68 Completed,
70 Cancelled,
72 RolledBack,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ChangeRequest {
79 pub change_id: String,
81 pub title: String,
83 pub description: String,
85 pub requester_id: Uuid,
87 pub request_date: DateTime<Utc>,
89 pub change_type: ChangeType,
91 pub priority: ChangePriority,
93 pub urgency: ChangeUrgency,
95 pub affected_systems: Vec<String>,
97 pub impact_scope: Option<String>,
99 pub risk_level: Option<String>,
101 pub rollback_plan: Option<String>,
103 pub testing_required: bool,
105 pub test_plan: Option<String>,
107 pub test_environment: Option<String>,
109 pub status: ChangeStatus,
111 pub approvers: Vec<String>,
113 pub approval_status: HashMap<String, ApprovalStatus>,
115 pub implementation_plan: Option<String>,
117 pub scheduled_time: Option<DateTime<Utc>>,
119 pub implementation_started: Option<DateTime<Utc>>,
121 pub implementation_completed: Option<DateTime<Utc>>,
123 pub test_results: Option<String>,
125 pub post_implementation_review: Option<String>,
127 pub history: Vec<ChangeHistoryEntry>,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "lowercase")]
134pub enum ApprovalStatus {
135 Pending,
137 Approved,
139 Rejected,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ChangeHistoryEntry {
146 pub timestamp: DateTime<Utc>,
148 pub action: String,
150 pub user_id: Uuid,
152 pub details: String,
154}
155
156impl ChangeRequest {
157 pub fn new(
159 change_id: String,
160 title: String,
161 description: String,
162 requester_id: Uuid,
163 change_type: ChangeType,
164 priority: ChangePriority,
165 urgency: ChangeUrgency,
166 affected_systems: Vec<String>,
167 testing_required: bool,
168 approvers: Vec<String>,
169 ) -> Self {
170 let now = Utc::now();
171 let mut approval_status = HashMap::new();
172 for approver in &approvers {
173 approval_status.insert(approver.clone(), ApprovalStatus::Pending);
174 }
175
176 Self {
177 change_id,
178 title,
179 description,
180 requester_id,
181 request_date: now,
182 change_type,
183 priority,
184 urgency,
185 affected_systems,
186 impact_scope: None,
187 risk_level: None,
188 rollback_plan: None,
189 testing_required,
190 test_plan: None,
191 test_environment: None,
192 status: ChangeStatus::PendingApproval,
193 approvers,
194 approval_status,
195 implementation_plan: None,
196 scheduled_time: None,
197 implementation_started: None,
198 implementation_completed: None,
199 test_results: None,
200 post_implementation_review: None,
201 history: vec![ChangeHistoryEntry {
202 timestamp: now,
203 action: "created".to_string(),
204 user_id: requester_id,
205 details: "Change request created".to_string(),
206 }],
207 }
208 }
209
210 pub fn is_fully_approved(&self) -> bool {
212 self.approval_status
213 .values()
214 .all(|status| *status == ApprovalStatus::Approved)
215 }
216
217 pub fn is_rejected(&self) -> bool {
219 self.approval_status
220 .values()
221 .any(|status| *status == ApprovalStatus::Rejected)
222 }
223
224 pub fn add_history(&mut self, action: String, user_id: Uuid, details: String) {
226 self.history.push(ChangeHistoryEntry {
227 timestamp: Utc::now(),
228 action,
229 user_id,
230 details,
231 });
232 }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct ChangeManagementConfig {
238 pub enabled: bool,
240 pub approval_workflow: ApprovalWorkflowConfig,
242 pub testing: TestingConfig,
244 pub notifications: NotificationConfig,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ApprovalWorkflowConfig {
251 pub emergency: ApprovalLevelConfig,
253 pub high: ApprovalLevelConfig,
255 pub medium: ApprovalLevelConfig,
257 pub low: ApprovalLevelConfig,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct ApprovalLevelConfig {
264 pub approvers: Vec<String>,
266 pub approval_timeout_hours: u64,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct TestingConfig {
273 pub required_for: Vec<ChangeType>,
275 pub test_environments: Vec<String>,
277 pub test_coverage_required: u8,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct NotificationConfig {
284 pub enabled: bool,
286 pub channels: Vec<String>,
288 pub recipients: Vec<String>,
290}
291
292impl Default for ChangeManagementConfig {
293 fn default() -> Self {
294 Self {
295 enabled: true,
296 approval_workflow: ApprovalWorkflowConfig {
297 emergency: ApprovalLevelConfig {
298 approvers: vec!["security-team-lead".to_string(), "engineering-manager".to_string()],
299 approval_timeout_hours: 1,
300 },
301 high: ApprovalLevelConfig {
302 approvers: vec!["security-team".to_string(), "engineering-manager".to_string(), "change-manager".to_string()],
303 approval_timeout_hours: 24,
304 },
305 medium: ApprovalLevelConfig {
306 approvers: vec!["engineering-manager".to_string(), "change-manager".to_string()],
307 approval_timeout_hours: 72,
308 },
309 low: ApprovalLevelConfig {
310 approvers: vec!["change-manager".to_string()],
311 approval_timeout_hours: 168, },
313 },
314 testing: TestingConfig {
315 required_for: vec![ChangeType::Security, ChangeType::Infrastructure],
316 test_environments: vec!["staging".to_string(), "production-like".to_string()],
317 test_coverage_required: 80,
318 },
319 notifications: NotificationConfig {
320 enabled: true,
321 channels: vec!["email".to_string(), "slack".to_string()],
322 recipients: vec!["change-manager".to_string(), "security-team".to_string(), "engineering-team".to_string()],
323 },
324 }
325 }
326}
327
328pub struct ChangeManagementEngine {
330 config: ChangeManagementConfig,
331 changes: std::sync::Arc<tokio::sync::RwLock<HashMap<String, ChangeRequest>>>,
333 change_id_counter: std::sync::Arc<tokio::sync::RwLock<u64>>,
335}
336
337impl ChangeManagementEngine {
338 pub fn new(config: ChangeManagementConfig) -> Self {
340 Self {
341 config,
342 changes: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
343 change_id_counter: std::sync::Arc::new(tokio::sync::RwLock::new(0)),
344 }
345 }
346
347 async fn generate_change_id(&self) -> String {
349 let now = Utc::now();
350 let year = now.format("%Y").to_string();
351 let mut counter = self.change_id_counter.write().await;
352 *counter += 1;
353 format!("CHG-{}-{:03}", year, *counter)
354 }
355
356 fn get_approvers_for_priority(&self, priority: ChangePriority) -> Vec<String> {
358 match priority {
359 ChangePriority::Critical => self.config.approval_workflow.emergency.approvers.clone(),
360 ChangePriority::High => self.config.approval_workflow.high.approvers.clone(),
361 ChangePriority::Medium => self.config.approval_workflow.medium.approvers.clone(),
362 ChangePriority::Low => self.config.approval_workflow.low.approvers.clone(),
363 }
364 }
365
366 pub async fn create_change_request(
368 &self,
369 title: String,
370 description: String,
371 requester_id: Uuid,
372 change_type: ChangeType,
373 priority: ChangePriority,
374 urgency: ChangeUrgency,
375 affected_systems: Vec<String>,
376 testing_required: bool,
377 test_plan: Option<String>,
378 test_environment: Option<String>,
379 rollback_plan: Option<String>,
380 impact_scope: Option<String>,
381 risk_level: Option<String>,
382 ) -> Result<ChangeRequest, Error> {
383 let change_id = self.generate_change_id().await;
384 let approvers = self.get_approvers_for_priority(priority);
385
386 let mut change = ChangeRequest::new(
387 change_id,
388 title,
389 description,
390 requester_id,
391 change_type,
392 priority,
393 urgency,
394 affected_systems,
395 testing_required,
396 approvers,
397 );
398
399 change.test_plan = test_plan;
400 change.test_environment = test_environment;
401 change.rollback_plan = rollback_plan;
402 change.impact_scope = impact_scope;
403 change.risk_level = risk_level;
404
405 let change_id = change.change_id.clone();
406 let mut changes = self.changes.write().await;
407 changes.insert(change_id, change.clone());
408
409 Ok(change)
410 }
411
412 pub async fn approve_change(
414 &self,
415 change_id: &str,
416 approver: &str,
417 approver_id: Uuid,
418 comments: Option<String>,
419 conditions: Option<Vec<String>>,
420 ) -> Result<(), Error> {
421 let mut changes = self.changes.write().await;
422 let change = changes
423 .get_mut(change_id)
424 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
425
426 if change.status != ChangeStatus::PendingApproval {
427 return Err(Error::Generic("Change request is not pending approval".to_string()));
428 }
429
430 if !change.approvers.contains(&approver.to_string()) {
431 return Err(Error::Generic("User is not an approver for this change".to_string()));
432 }
433
434 change.approval_status.insert(approver.to_string(), ApprovalStatus::Approved);
435
436 let details = format!(
437 "Change approved by {}{}{}",
438 approver,
439 comments.map(|c| format!(" - {}", c)).unwrap_or_default(),
440 conditions
441 .map(|conds| format!(" - Conditions: {}", conds.join(", ")))
442 .unwrap_or_default()
443 );
444 change.add_history("approved".to_string(), approver_id, details);
445
446 if change.is_fully_approved() {
448 change.status = ChangeStatus::Approved;
449 change.add_history(
450 "all_approvals_complete".to_string(),
451 approver_id,
452 "All approvals received, change ready for implementation".to_string(),
453 );
454 }
455
456 Ok(())
457 }
458
459 pub async fn reject_change(
461 &self,
462 change_id: &str,
463 approver: &str,
464 approver_id: Uuid,
465 reason: String,
466 ) -> Result<(), Error> {
467 let mut changes = self.changes.write().await;
468 let change = changes
469 .get_mut(change_id)
470 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
471
472 if change.status != ChangeStatus::PendingApproval {
473 return Err(Error::Generic("Change request is not pending approval".to_string()));
474 }
475
476 change.approval_status.insert(approver.to_string(), ApprovalStatus::Rejected);
477 change.status = ChangeStatus::Rejected;
478 change.add_history("rejected".to_string(), approver_id, format!("Change rejected: {}", reason));
479
480 Ok(())
481 }
482
483 pub async fn start_implementation(
485 &self,
486 change_id: &str,
487 implementer_id: Uuid,
488 implementation_plan: String,
489 scheduled_time: Option<DateTime<Utc>>,
490 ) -> Result<(), Error> {
491 let mut changes = self.changes.write().await;
492 let change = changes
493 .get_mut(change_id)
494 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
495
496 if change.status != ChangeStatus::Approved {
497 return Err(Error::Generic("Change request must be approved before implementation".to_string()));
498 }
499
500 change.status = ChangeStatus::Implementing;
501 change.implementation_plan = Some(implementation_plan);
502 change.scheduled_time = scheduled_time;
503 change.implementation_started = Some(Utc::now());
504
505 change.add_history(
506 "implementation_started".to_string(),
507 implementer_id,
508 "Change implementation started".to_string(),
509 );
510
511 Ok(())
512 }
513
514 pub async fn complete_change(
516 &self,
517 change_id: &str,
518 implementer_id: Uuid,
519 test_results: Option<String>,
520 post_implementation_review: Option<String>,
521 ) -> Result<(), Error> {
522 let mut changes = self.changes.write().await;
523 let change = changes
524 .get_mut(change_id)
525 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
526
527 if change.status != ChangeStatus::Implementing {
528 return Err(Error::Generic("Change request must be in implementing status".to_string()));
529 }
530
531 change.status = ChangeStatus::Completed;
532 change.implementation_completed = Some(Utc::now());
533 change.test_results = test_results;
534 change.post_implementation_review = post_implementation_review;
535
536 change.add_history(
537 "completed".to_string(),
538 implementer_id,
539 "Change implementation completed".to_string(),
540 );
541
542 Ok(())
543 }
544
545 pub async fn get_change(&self, change_id: &str) -> Result<Option<ChangeRequest>, Error> {
547 let changes = self.changes.read().await;
548 Ok(changes.get(change_id).cloned())
549 }
550
551 pub async fn get_all_changes(&self) -> Result<Vec<ChangeRequest>, Error> {
553 let changes = self.changes.read().await;
554 Ok(changes.values().cloned().collect())
555 }
556
557 pub async fn get_changes_by_status(&self, status: ChangeStatus) -> Result<Vec<ChangeRequest>, Error> {
559 let changes = self.changes.read().await;
560 Ok(changes
561 .values()
562 .filter(|c| c.status == status)
563 .cloned()
564 .collect())
565 }
566
567 pub async fn get_changes_by_requester(&self, requester_id: Uuid) -> Result<Vec<ChangeRequest>, Error> {
569 let changes = self.changes.read().await;
570 Ok(changes
571 .values()
572 .filter(|c| c.requester_id == requester_id)
573 .cloned()
574 .collect())
575 }
576
577 pub async fn cancel_change(&self, change_id: &str, user_id: Uuid, reason: String) -> Result<(), Error> {
579 let mut changes = self.changes.write().await;
580 let change = changes
581 .get_mut(change_id)
582 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
583
584 change.status = ChangeStatus::Cancelled;
585 change.add_history("cancelled".to_string(), user_id, format!("Change cancelled: {}", reason));
586
587 Ok(())
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 #[tokio::test]
596 async fn test_change_request_creation() {
597 let config = ChangeManagementConfig::default();
598 let engine = ChangeManagementEngine::new(config);
599
600 let change = engine
601 .create_change_request(
602 "Test Change".to_string(),
603 "Test description".to_string(),
604 Uuid::new_v4(),
605 ChangeType::Security,
606 ChangePriority::High,
607 ChangeUrgency::High,
608 vec!["system1".to_string()],
609 true,
610 Some("Test plan".to_string()),
611 Some("staging".to_string()),
612 None,
613 None,
614 None,
615 )
616 .await
617 .unwrap();
618
619 assert_eq!(change.status, ChangeStatus::PendingApproval);
620 assert!(!change.approvers.is_empty());
621 }
622}