1use crate::Error;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[serde(rename_all = "snake_case")]
16pub enum ChangeType {
17 Security,
19 Feature,
21 Bugfix,
23 Infrastructure,
25 Configuration,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
31#[serde(rename_all = "lowercase")]
32pub enum ChangePriority {
33 Critical,
35 High,
37 Medium,
39 Low,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(rename_all = "lowercase")]
46pub enum ChangeUrgency {
47 Emergency,
49 High,
51 Medium,
53 Low,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum ChangeStatus {
61 PendingApproval,
63 Approved,
65 Rejected,
67 Implementing,
69 Completed,
71 Cancelled,
73 RolledBack,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ChangeRequest {
80 pub change_id: String,
82 pub title: String,
84 pub description: String,
86 pub requester_id: Uuid,
88 pub request_date: DateTime<Utc>,
90 pub change_type: ChangeType,
92 pub priority: ChangePriority,
94 pub urgency: ChangeUrgency,
96 pub affected_systems: Vec<String>,
98 pub impact_scope: Option<String>,
100 pub risk_level: Option<String>,
102 pub rollback_plan: Option<String>,
104 pub testing_required: bool,
106 pub test_plan: Option<String>,
108 pub test_environment: Option<String>,
110 pub status: ChangeStatus,
112 pub approvers: Vec<String>,
114 pub approval_status: HashMap<String, ApprovalStatus>,
116 pub implementation_plan: Option<String>,
118 pub scheduled_time: Option<DateTime<Utc>>,
120 pub implementation_started: Option<DateTime<Utc>>,
122 pub implementation_completed: Option<DateTime<Utc>>,
124 pub test_results: Option<String>,
126 pub post_implementation_review: Option<String>,
128 pub history: Vec<ChangeHistoryEntry>,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "lowercase")]
135pub enum ApprovalStatus {
136 Pending,
138 Approved,
140 Rejected,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ChangeHistoryEntry {
147 pub timestamp: DateTime<Utc>,
149 pub action: String,
151 pub user_id: Uuid,
153 pub details: String,
155}
156
157impl ChangeRequest {
158 #[allow(clippy::too_many_arguments)]
160 pub fn new(
161 change_id: String,
162 title: String,
163 description: String,
164 requester_id: Uuid,
165 change_type: ChangeType,
166 priority: ChangePriority,
167 urgency: ChangeUrgency,
168 affected_systems: Vec<String>,
169 testing_required: bool,
170 approvers: Vec<String>,
171 ) -> Self {
172 let now = Utc::now();
173 let mut approval_status = HashMap::new();
174 for approver in &approvers {
175 approval_status.insert(approver.clone(), ApprovalStatus::Pending);
176 }
177
178 Self {
179 change_id,
180 title,
181 description,
182 requester_id,
183 request_date: now,
184 change_type,
185 priority,
186 urgency,
187 affected_systems,
188 impact_scope: None,
189 risk_level: None,
190 rollback_plan: None,
191 testing_required,
192 test_plan: None,
193 test_environment: None,
194 status: ChangeStatus::PendingApproval,
195 approvers,
196 approval_status,
197 implementation_plan: None,
198 scheduled_time: None,
199 implementation_started: None,
200 implementation_completed: None,
201 test_results: None,
202 post_implementation_review: None,
203 history: vec![ChangeHistoryEntry {
204 timestamp: now,
205 action: "created".to_string(),
206 user_id: requester_id,
207 details: "Change request created".to_string(),
208 }],
209 }
210 }
211
212 pub fn is_fully_approved(&self) -> bool {
214 self.approval_status.values().all(|status| *status == ApprovalStatus::Approved)
215 }
216
217 pub fn is_rejected(&self) -> bool {
219 self.approval_status.values().any(|status| *status == ApprovalStatus::Rejected)
220 }
221
222 pub fn add_history(&mut self, action: String, user_id: Uuid, details: String) {
224 self.history.push(ChangeHistoryEntry {
225 timestamp: Utc::now(),
226 action,
227 user_id,
228 details,
229 });
230 }
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
236pub struct ChangeManagementConfig {
237 pub enabled: bool,
239 pub approval_workflow: ApprovalWorkflowConfig,
241 pub testing: TestingConfig,
243 pub notifications: NotificationConfig,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
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)]
263#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
264pub struct ApprovalLevelConfig {
265 pub approvers: Vec<String>,
267 pub approval_timeout_hours: u64,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
274pub struct TestingConfig {
275 pub required_for: Vec<ChangeType>,
277 pub test_environments: Vec<String>,
279 pub test_coverage_required: u8,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
286pub struct NotificationConfig {
287 pub enabled: bool,
289 pub channels: Vec<String>,
291 pub recipients: Vec<String>,
293}
294
295impl Default for ChangeManagementConfig {
296 fn default() -> Self {
297 Self {
298 enabled: true,
299 approval_workflow: ApprovalWorkflowConfig {
300 emergency: ApprovalLevelConfig {
301 approvers: vec![
302 "security-team-lead".to_string(),
303 "engineering-manager".to_string(),
304 ],
305 approval_timeout_hours: 1,
306 },
307 high: ApprovalLevelConfig {
308 approvers: vec![
309 "security-team".to_string(),
310 "engineering-manager".to_string(),
311 "change-manager".to_string(),
312 ],
313 approval_timeout_hours: 24,
314 },
315 medium: ApprovalLevelConfig {
316 approvers: vec![
317 "engineering-manager".to_string(),
318 "change-manager".to_string(),
319 ],
320 approval_timeout_hours: 72,
321 },
322 low: ApprovalLevelConfig {
323 approvers: vec!["change-manager".to_string()],
324 approval_timeout_hours: 168, },
326 },
327 testing: TestingConfig {
328 required_for: vec![ChangeType::Security, ChangeType::Infrastructure],
329 test_environments: vec!["staging".to_string(), "production-like".to_string()],
330 test_coverage_required: 80,
331 },
332 notifications: NotificationConfig {
333 enabled: true,
334 channels: vec!["email".to_string(), "slack".to_string()],
335 recipients: vec![
336 "change-manager".to_string(),
337 "security-team".to_string(),
338 "engineering-team".to_string(),
339 ],
340 },
341 }
342 }
343}
344
345pub struct ChangeManagementEngine {
347 config: ChangeManagementConfig,
348 changes: std::sync::Arc<tokio::sync::RwLock<HashMap<String, ChangeRequest>>>,
350 change_id_counter: std::sync::Arc<tokio::sync::RwLock<u64>>,
352}
353
354impl ChangeManagementEngine {
355 pub fn new(config: ChangeManagementConfig) -> Self {
357 Self {
358 config,
359 changes: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
360 change_id_counter: std::sync::Arc::new(tokio::sync::RwLock::new(0)),
361 }
362 }
363
364 async fn generate_change_id(&self) -> String {
366 let now = Utc::now();
367 let year = now.format("%Y").to_string();
368 let mut counter = self.change_id_counter.write().await;
369 *counter += 1;
370 format!("CHG-{}-{:03}", year, *counter)
371 }
372
373 fn get_approvers_for_priority(&self, priority: ChangePriority) -> Vec<String> {
375 match priority {
376 ChangePriority::Critical => self.config.approval_workflow.emergency.approvers.clone(),
377 ChangePriority::High => self.config.approval_workflow.high.approvers.clone(),
378 ChangePriority::Medium => self.config.approval_workflow.medium.approvers.clone(),
379 ChangePriority::Low => self.config.approval_workflow.low.approvers.clone(),
380 }
381 }
382
383 #[allow(clippy::too_many_arguments)]
385 pub async fn create_change_request(
386 &self,
387 title: String,
388 description: String,
389 requester_id: Uuid,
390 change_type: ChangeType,
391 priority: ChangePriority,
392 urgency: ChangeUrgency,
393 affected_systems: Vec<String>,
394 testing_required: bool,
395 test_plan: Option<String>,
396 test_environment: Option<String>,
397 rollback_plan: Option<String>,
398 impact_scope: Option<String>,
399 risk_level: Option<String>,
400 ) -> Result<ChangeRequest, Error> {
401 let change_id = self.generate_change_id().await;
402 let approvers = self.get_approvers_for_priority(priority);
403
404 let mut change = ChangeRequest::new(
405 change_id,
406 title,
407 description,
408 requester_id,
409 change_type,
410 priority,
411 urgency,
412 affected_systems,
413 testing_required,
414 approvers,
415 );
416
417 change.test_plan = test_plan;
418 change.test_environment = test_environment;
419 change.rollback_plan = rollback_plan;
420 change.impact_scope = impact_scope;
421 change.risk_level = risk_level;
422
423 let change_id = change.change_id.clone();
424 let mut changes = self.changes.write().await;
425 changes.insert(change_id, change.clone());
426
427 Ok(change)
428 }
429
430 pub async fn approve_change(
432 &self,
433 change_id: &str,
434 approver: &str,
435 approver_id: Uuid,
436 comments: Option<String>,
437 conditions: Option<Vec<String>>,
438 ) -> Result<(), Error> {
439 let mut changes = self.changes.write().await;
440 let change = changes
441 .get_mut(change_id)
442 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
443
444 if change.status != ChangeStatus::PendingApproval {
445 return Err(Error::Generic("Change request is not pending approval".to_string()));
446 }
447
448 if !change.approvers.contains(&approver.to_string()) {
449 return Err(Error::Generic("User is not an approver for this change".to_string()));
450 }
451
452 change.approval_status.insert(approver.to_string(), ApprovalStatus::Approved);
453
454 let details = format!(
455 "Change approved by {}{}{}",
456 approver,
457 comments.map(|c| format!(" - {}", c)).unwrap_or_default(),
458 conditions
459 .map(|conds| format!(" - Conditions: {}", conds.join(", ")))
460 .unwrap_or_default()
461 );
462 change.add_history("approved".to_string(), approver_id, details);
463
464 if change.is_fully_approved() {
466 change.status = ChangeStatus::Approved;
467 change.add_history(
468 "all_approvals_complete".to_string(),
469 approver_id,
470 "All approvals received, change ready for implementation".to_string(),
471 );
472 }
473
474 Ok(())
475 }
476
477 pub async fn reject_change(
479 &self,
480 change_id: &str,
481 approver: &str,
482 approver_id: Uuid,
483 reason: String,
484 ) -> Result<(), Error> {
485 let mut changes = self.changes.write().await;
486 let change = changes
487 .get_mut(change_id)
488 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
489
490 if change.status != ChangeStatus::PendingApproval {
491 return Err(Error::Generic("Change request is not pending approval".to_string()));
492 }
493
494 change.approval_status.insert(approver.to_string(), ApprovalStatus::Rejected);
495 change.status = ChangeStatus::Rejected;
496 change.add_history(
497 "rejected".to_string(),
498 approver_id,
499 format!("Change rejected: {}", reason),
500 );
501
502 Ok(())
503 }
504
505 pub async fn start_implementation(
507 &self,
508 change_id: &str,
509 implementer_id: Uuid,
510 implementation_plan: String,
511 scheduled_time: Option<DateTime<Utc>>,
512 ) -> Result<(), Error> {
513 let mut changes = self.changes.write().await;
514 let change = changes
515 .get_mut(change_id)
516 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
517
518 if change.status != ChangeStatus::Approved {
519 return Err(Error::Generic(
520 "Change request must be approved before implementation".to_string(),
521 ));
522 }
523
524 change.status = ChangeStatus::Implementing;
525 change.implementation_plan = Some(implementation_plan);
526 change.scheduled_time = scheduled_time;
527 change.implementation_started = Some(Utc::now());
528
529 change.add_history(
530 "implementation_started".to_string(),
531 implementer_id,
532 "Change implementation started".to_string(),
533 );
534
535 Ok(())
536 }
537
538 pub async fn complete_change(
540 &self,
541 change_id: &str,
542 implementer_id: Uuid,
543 test_results: Option<String>,
544 post_implementation_review: Option<String>,
545 ) -> Result<(), Error> {
546 let mut changes = self.changes.write().await;
547 let change = changes
548 .get_mut(change_id)
549 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
550
551 if change.status != ChangeStatus::Implementing {
552 return Err(Error::Generic(
553 "Change request must be in implementing status".to_string(),
554 ));
555 }
556
557 change.status = ChangeStatus::Completed;
558 change.implementation_completed = Some(Utc::now());
559 change.test_results = test_results;
560 change.post_implementation_review = post_implementation_review;
561
562 change.add_history(
563 "completed".to_string(),
564 implementer_id,
565 "Change implementation completed".to_string(),
566 );
567
568 Ok(())
569 }
570
571 pub async fn get_change(&self, change_id: &str) -> Result<Option<ChangeRequest>, Error> {
573 let changes = self.changes.read().await;
574 Ok(changes.get(change_id).cloned())
575 }
576
577 pub async fn get_all_changes(&self) -> Result<Vec<ChangeRequest>, Error> {
579 let changes = self.changes.read().await;
580 Ok(changes.values().cloned().collect())
581 }
582
583 pub async fn get_changes_by_status(
585 &self,
586 status: ChangeStatus,
587 ) -> Result<Vec<ChangeRequest>, Error> {
588 let changes = self.changes.read().await;
589 Ok(changes.values().filter(|c| c.status == status).cloned().collect())
590 }
591
592 pub async fn get_changes_by_requester(
594 &self,
595 requester_id: Uuid,
596 ) -> Result<Vec<ChangeRequest>, Error> {
597 let changes = self.changes.read().await;
598 Ok(changes.values().filter(|c| c.requester_id == requester_id).cloned().collect())
599 }
600
601 pub async fn cancel_change(
603 &self,
604 change_id: &str,
605 user_id: Uuid,
606 reason: String,
607 ) -> Result<(), Error> {
608 let mut changes = self.changes.write().await;
609 let change = changes
610 .get_mut(change_id)
611 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
612
613 change.status = ChangeStatus::Cancelled;
614 change.add_history(
615 "cancelled".to_string(),
616 user_id,
617 format!("Change cancelled: {}", reason),
618 );
619
620 Ok(())
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[tokio::test]
629 async fn test_change_request_creation() {
630 let config = ChangeManagementConfig::default();
631 let engine = ChangeManagementEngine::new(config);
632
633 let change = engine
634 .create_change_request(
635 "Test Change".to_string(),
636 "Test description".to_string(),
637 Uuid::new_v4(),
638 ChangeType::Security,
639 ChangePriority::High,
640 ChangeUrgency::High,
641 vec!["system1".to_string()],
642 true,
643 Some("Test plan".to_string()),
644 Some("staging".to_string()),
645 None,
646 None,
647 None,
648 )
649 .await
650 .unwrap();
651
652 assert_eq!(change.status, ChangeStatus::PendingApproval);
653 assert!(!change.approvers.is_empty());
654 }
655}