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#[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 pub fn new(
160 change_id: String,
161 title: String,
162 description: String,
163 requester_id: Uuid,
164 change_type: ChangeType,
165 priority: ChangePriority,
166 urgency: ChangeUrgency,
167 affected_systems: Vec<String>,
168 testing_required: bool,
169 approvers: Vec<String>,
170 ) -> Self {
171 let now = Utc::now();
172 let mut approval_status = HashMap::new();
173 for approver in &approvers {
174 approval_status.insert(approver.clone(), ApprovalStatus::Pending);
175 }
176
177 Self {
178 change_id,
179 title,
180 description,
181 requester_id,
182 request_date: now,
183 change_type,
184 priority,
185 urgency,
186 affected_systems,
187 impact_scope: None,
188 risk_level: None,
189 rollback_plan: None,
190 testing_required,
191 test_plan: None,
192 test_environment: None,
193 status: ChangeStatus::PendingApproval,
194 approvers,
195 approval_status,
196 implementation_plan: None,
197 scheduled_time: None,
198 implementation_started: None,
199 implementation_completed: None,
200 test_results: None,
201 post_implementation_review: None,
202 history: vec![ChangeHistoryEntry {
203 timestamp: now,
204 action: "created".to_string(),
205 user_id: requester_id,
206 details: "Change request created".to_string(),
207 }],
208 }
209 }
210
211 pub fn is_fully_approved(&self) -> bool {
213 self.approval_status.values().all(|status| *status == ApprovalStatus::Approved)
214 }
215
216 pub fn is_rejected(&self) -> bool {
218 self.approval_status.values().any(|status| *status == ApprovalStatus::Rejected)
219 }
220
221 pub fn add_history(&mut self, action: String, user_id: Uuid, details: String) {
223 self.history.push(ChangeHistoryEntry {
224 timestamp: Utc::now(),
225 action,
226 user_id,
227 details,
228 });
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235pub struct ChangeManagementConfig {
236 pub enabled: bool,
238 pub approval_workflow: ApprovalWorkflowConfig,
240 pub testing: TestingConfig,
242 pub notifications: NotificationConfig,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249pub struct ApprovalWorkflowConfig {
250 pub emergency: ApprovalLevelConfig,
252 pub high: ApprovalLevelConfig,
254 pub medium: ApprovalLevelConfig,
256 pub low: ApprovalLevelConfig,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
263pub struct ApprovalLevelConfig {
264 pub approvers: Vec<String>,
266 pub approval_timeout_hours: u64,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
273pub struct TestingConfig {
274 pub required_for: Vec<ChangeType>,
276 pub test_environments: Vec<String>,
278 pub test_coverage_required: u8,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285pub struct NotificationConfig {
286 pub enabled: bool,
288 pub channels: Vec<String>,
290 pub recipients: Vec<String>,
292}
293
294impl Default for ChangeManagementConfig {
295 fn default() -> Self {
296 Self {
297 enabled: true,
298 approval_workflow: ApprovalWorkflowConfig {
299 emergency: ApprovalLevelConfig {
300 approvers: vec![
301 "security-team-lead".to_string(),
302 "engineering-manager".to_string(),
303 ],
304 approval_timeout_hours: 1,
305 },
306 high: ApprovalLevelConfig {
307 approvers: vec![
308 "security-team".to_string(),
309 "engineering-manager".to_string(),
310 "change-manager".to_string(),
311 ],
312 approval_timeout_hours: 24,
313 },
314 medium: ApprovalLevelConfig {
315 approvers: vec![
316 "engineering-manager".to_string(),
317 "change-manager".to_string(),
318 ],
319 approval_timeout_hours: 72,
320 },
321 low: ApprovalLevelConfig {
322 approvers: vec!["change-manager".to_string()],
323 approval_timeout_hours: 168, },
325 },
326 testing: TestingConfig {
327 required_for: vec![ChangeType::Security, ChangeType::Infrastructure],
328 test_environments: vec!["staging".to_string(), "production-like".to_string()],
329 test_coverage_required: 80,
330 },
331 notifications: NotificationConfig {
332 enabled: true,
333 channels: vec!["email".to_string(), "slack".to_string()],
334 recipients: vec![
335 "change-manager".to_string(),
336 "security-team".to_string(),
337 "engineering-team".to_string(),
338 ],
339 },
340 }
341 }
342}
343
344pub struct ChangeManagementEngine {
346 config: ChangeManagementConfig,
347 changes: std::sync::Arc<tokio::sync::RwLock<HashMap<String, ChangeRequest>>>,
349 change_id_counter: std::sync::Arc<tokio::sync::RwLock<u64>>,
351}
352
353impl ChangeManagementEngine {
354 pub fn new(config: ChangeManagementConfig) -> Self {
356 Self {
357 config,
358 changes: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
359 change_id_counter: std::sync::Arc::new(tokio::sync::RwLock::new(0)),
360 }
361 }
362
363 async fn generate_change_id(&self) -> String {
365 let now = Utc::now();
366 let year = now.format("%Y").to_string();
367 let mut counter = self.change_id_counter.write().await;
368 *counter += 1;
369 format!("CHG-{}-{:03}", year, *counter)
370 }
371
372 fn get_approvers_for_priority(&self, priority: ChangePriority) -> Vec<String> {
374 match priority {
375 ChangePriority::Critical => self.config.approval_workflow.emergency.approvers.clone(),
376 ChangePriority::High => self.config.approval_workflow.high.approvers.clone(),
377 ChangePriority::Medium => self.config.approval_workflow.medium.approvers.clone(),
378 ChangePriority::Low => self.config.approval_workflow.low.approvers.clone(),
379 }
380 }
381
382 pub async fn create_change_request(
384 &self,
385 title: String,
386 description: String,
387 requester_id: Uuid,
388 change_type: ChangeType,
389 priority: ChangePriority,
390 urgency: ChangeUrgency,
391 affected_systems: Vec<String>,
392 testing_required: bool,
393 test_plan: Option<String>,
394 test_environment: Option<String>,
395 rollback_plan: Option<String>,
396 impact_scope: Option<String>,
397 risk_level: Option<String>,
398 ) -> Result<ChangeRequest, Error> {
399 let change_id = self.generate_change_id().await;
400 let approvers = self.get_approvers_for_priority(priority);
401
402 let mut change = ChangeRequest::new(
403 change_id,
404 title,
405 description,
406 requester_id,
407 change_type,
408 priority,
409 urgency,
410 affected_systems,
411 testing_required,
412 approvers,
413 );
414
415 change.test_plan = test_plan;
416 change.test_environment = test_environment;
417 change.rollback_plan = rollback_plan;
418 change.impact_scope = impact_scope;
419 change.risk_level = risk_level;
420
421 let change_id = change.change_id.clone();
422 let mut changes = self.changes.write().await;
423 changes.insert(change_id, change.clone());
424
425 Ok(change)
426 }
427
428 pub async fn approve_change(
430 &self,
431 change_id: &str,
432 approver: &str,
433 approver_id: Uuid,
434 comments: Option<String>,
435 conditions: Option<Vec<String>>,
436 ) -> Result<(), Error> {
437 let mut changes = self.changes.write().await;
438 let change = changes
439 .get_mut(change_id)
440 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
441
442 if change.status != ChangeStatus::PendingApproval {
443 return Err(Error::Generic("Change request is not pending approval".to_string()));
444 }
445
446 if !change.approvers.contains(&approver.to_string()) {
447 return Err(Error::Generic("User is not an approver for this change".to_string()));
448 }
449
450 change.approval_status.insert(approver.to_string(), ApprovalStatus::Approved);
451
452 let details = format!(
453 "Change approved by {}{}{}",
454 approver,
455 comments.map(|c| format!(" - {}", c)).unwrap_or_default(),
456 conditions
457 .map(|conds| format!(" - Conditions: {}", conds.join(", ")))
458 .unwrap_or_default()
459 );
460 change.add_history("approved".to_string(), approver_id, details);
461
462 if change.is_fully_approved() {
464 change.status = ChangeStatus::Approved;
465 change.add_history(
466 "all_approvals_complete".to_string(),
467 approver_id,
468 "All approvals received, change ready for implementation".to_string(),
469 );
470 }
471
472 Ok(())
473 }
474
475 pub async fn reject_change(
477 &self,
478 change_id: &str,
479 approver: &str,
480 approver_id: Uuid,
481 reason: String,
482 ) -> Result<(), Error> {
483 let mut changes = self.changes.write().await;
484 let change = changes
485 .get_mut(change_id)
486 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
487
488 if change.status != ChangeStatus::PendingApproval {
489 return Err(Error::Generic("Change request is not pending approval".to_string()));
490 }
491
492 change.approval_status.insert(approver.to_string(), ApprovalStatus::Rejected);
493 change.status = ChangeStatus::Rejected;
494 change.add_history(
495 "rejected".to_string(),
496 approver_id,
497 format!("Change rejected: {}", reason),
498 );
499
500 Ok(())
501 }
502
503 pub async fn start_implementation(
505 &self,
506 change_id: &str,
507 implementer_id: Uuid,
508 implementation_plan: String,
509 scheduled_time: Option<DateTime<Utc>>,
510 ) -> Result<(), Error> {
511 let mut changes = self.changes.write().await;
512 let change = changes
513 .get_mut(change_id)
514 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
515
516 if change.status != ChangeStatus::Approved {
517 return Err(Error::Generic(
518 "Change request must be approved before implementation".to_string(),
519 ));
520 }
521
522 change.status = ChangeStatus::Implementing;
523 change.implementation_plan = Some(implementation_plan);
524 change.scheduled_time = scheduled_time;
525 change.implementation_started = Some(Utc::now());
526
527 change.add_history(
528 "implementation_started".to_string(),
529 implementer_id,
530 "Change implementation started".to_string(),
531 );
532
533 Ok(())
534 }
535
536 pub async fn complete_change(
538 &self,
539 change_id: &str,
540 implementer_id: Uuid,
541 test_results: Option<String>,
542 post_implementation_review: Option<String>,
543 ) -> Result<(), Error> {
544 let mut changes = self.changes.write().await;
545 let change = changes
546 .get_mut(change_id)
547 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
548
549 if change.status != ChangeStatus::Implementing {
550 return Err(Error::Generic(
551 "Change request must be in implementing status".to_string(),
552 ));
553 }
554
555 change.status = ChangeStatus::Completed;
556 change.implementation_completed = Some(Utc::now());
557 change.test_results = test_results;
558 change.post_implementation_review = post_implementation_review;
559
560 change.add_history(
561 "completed".to_string(),
562 implementer_id,
563 "Change implementation completed".to_string(),
564 );
565
566 Ok(())
567 }
568
569 pub async fn get_change(&self, change_id: &str) -> Result<Option<ChangeRequest>, Error> {
571 let changes = self.changes.read().await;
572 Ok(changes.get(change_id).cloned())
573 }
574
575 pub async fn get_all_changes(&self) -> Result<Vec<ChangeRequest>, Error> {
577 let changes = self.changes.read().await;
578 Ok(changes.values().cloned().collect())
579 }
580
581 pub async fn get_changes_by_status(
583 &self,
584 status: ChangeStatus,
585 ) -> Result<Vec<ChangeRequest>, Error> {
586 let changes = self.changes.read().await;
587 Ok(changes.values().filter(|c| c.status == status).cloned().collect())
588 }
589
590 pub async fn get_changes_by_requester(
592 &self,
593 requester_id: Uuid,
594 ) -> Result<Vec<ChangeRequest>, Error> {
595 let changes = self.changes.read().await;
596 Ok(changes.values().filter(|c| c.requester_id == requester_id).cloned().collect())
597 }
598
599 pub async fn cancel_change(
601 &self,
602 change_id: &str,
603 user_id: Uuid,
604 reason: String,
605 ) -> Result<(), Error> {
606 let mut changes = self.changes.write().await;
607 let change = changes
608 .get_mut(change_id)
609 .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
610
611 change.status = ChangeStatus::Cancelled;
612 change.add_history(
613 "cancelled".to_string(),
614 user_id,
615 format!("Change cancelled: {}", reason),
616 );
617
618 Ok(())
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[tokio::test]
627 async fn test_change_request_creation() {
628 let config = ChangeManagementConfig::default();
629 let engine = ChangeManagementEngine::new(config);
630
631 let change = engine
632 .create_change_request(
633 "Test Change".to_string(),
634 "Test description".to_string(),
635 Uuid::new_v4(),
636 ChangeType::Security,
637 ChangePriority::High,
638 ChangeUrgency::High,
639 vec!["system1".to_string()],
640 true,
641 Some("Test plan".to_string()),
642 Some("staging".to_string()),
643 None,
644 None,
645 None,
646 )
647 .await
648 .unwrap();
649
650 assert_eq!(change.status, ChangeStatus::PendingApproval);
651 assert!(!change.approvers.is_empty());
652 }
653}