mockforge_core/incidents/
manager.rs

1//! Incident manager for creating and managing drift incidents
2//!
3//! This module provides high-level functionality for incident lifecycle management.
4
5use crate::incidents::store::IncidentStore;
6use crate::incidents::types::{
7    DriftIncident, IncidentQuery, IncidentSeverity, IncidentStatus, IncidentType,
8};
9use std::collections::HashMap;
10use std::sync::Arc;
11use uuid::Uuid;
12
13/// Manager for drift incidents
14#[derive(Debug, Clone)]
15pub struct IncidentManager {
16    store: Arc<IncidentStore>,
17    /// Webhook configurations for incident notifications
18    webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
19}
20
21impl IncidentManager {
22    /// Create a new incident manager
23    pub fn new(store: Arc<IncidentStore>) -> Self {
24        Self {
25            store,
26            webhook_configs: Vec::new(),
27        }
28    }
29
30    /// Create a new incident manager with webhook configurations
31    pub fn new_with_webhooks(
32        store: Arc<IncidentStore>,
33        webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
34    ) -> Self {
35        Self {
36            store,
37            webhook_configs,
38        }
39    }
40
41    /// Add webhook configuration
42    pub fn add_webhook(&mut self, config: crate::incidents::integrations::WebhookConfig) {
43        self.webhook_configs.push(config);
44    }
45
46    /// Create a new incident from drift result
47    pub async fn create_incident(
48        &self,
49        endpoint: String,
50        method: String,
51        incident_type: IncidentType,
52        severity: IncidentSeverity,
53        details: serde_json::Value,
54        budget_id: Option<String>,
55        workspace_id: Option<String>,
56    ) -> DriftIncident {
57        self.create_incident_with_samples(
58            endpoint,
59            method,
60            incident_type,
61            severity,
62            details,
63            budget_id,
64            workspace_id,
65            None, // sync_cycle_id
66            None, // contract_diff_id
67            None, // before_sample
68            None, // after_sample
69        )
70        .await
71    }
72
73    /// Create a new incident with before/after samples and traceability
74    pub async fn create_incident_with_samples(
75        &self,
76        endpoint: String,
77        method: String,
78        incident_type: IncidentType,
79        severity: IncidentSeverity,
80        details: serde_json::Value,
81        budget_id: Option<String>,
82        workspace_id: Option<String>,
83        sync_cycle_id: Option<String>,
84        contract_diff_id: Option<String>,
85        before_sample: Option<serde_json::Value>,
86        after_sample: Option<serde_json::Value>,
87    ) -> DriftIncident {
88        let id = Uuid::new_v4().to_string();
89        let mut incident =
90            DriftIncident::new(id, endpoint, method, incident_type, severity, details);
91        incident.budget_id = budget_id;
92        incident.workspace_id = workspace_id;
93        incident.sync_cycle_id = sync_cycle_id;
94        incident.contract_diff_id = contract_diff_id;
95        incident.before_sample = before_sample;
96        incident.after_sample = after_sample;
97
98        self.store.store(incident.clone()).await;
99
100        // Trigger webhook notifications for incident.created event
101        self.trigger_webhooks("incident.created", &incident).await;
102
103        incident
104    }
105
106    /// Get an incident by ID
107    pub async fn get_incident(&self, id: &str) -> Option<DriftIncident> {
108        self.store.get(id).await
109    }
110
111    /// Update an incident
112    pub async fn update_incident(&self, incident: DriftIncident) {
113        self.store.update(incident).await;
114    }
115
116    /// Acknowledge an incident
117    pub async fn acknowledge_incident(&self, id: &str) -> Option<DriftIncident> {
118        let mut incident = self.store.get(id).await?;
119        incident.acknowledge();
120        self.store.update(incident.clone()).await;
121        Some(incident)
122    }
123
124    /// Resolve an incident
125    pub async fn resolve_incident(&self, id: &str) -> Option<DriftIncident> {
126        let mut incident = self.store.get(id).await?;
127        incident.resolve();
128        self.store.update(incident.clone()).await;
129        Some(incident)
130    }
131
132    /// Close an incident
133    pub async fn close_incident(&self, id: &str) -> Option<DriftIncident> {
134        let mut incident = self.store.get(id).await?;
135        incident.close();
136        self.store.update(incident.clone()).await;
137        Some(incident)
138    }
139
140    /// Link an external ticket to an incident
141    pub async fn link_external_ticket(
142        &self,
143        id: &str,
144        ticket_id: String,
145        ticket_url: Option<String>,
146    ) -> Option<DriftIncident> {
147        let mut incident = self.store.get(id).await?;
148        incident.link_external_ticket(ticket_id, ticket_url);
149        self.store.update(incident.clone()).await;
150        Some(incident)
151    }
152
153    /// Query incidents
154    pub async fn query_incidents(&self, query: IncidentQuery) -> Vec<DriftIncident> {
155        self.store.query(query).await
156    }
157
158    /// Get all open incidents
159    pub async fn get_open_incidents(&self) -> Vec<DriftIncident> {
160        self.store.get_by_status(IncidentStatus::Open).await
161    }
162
163    /// Get incident statistics
164    pub async fn get_statistics(&self) -> HashMap<IncidentStatus, usize> {
165        self.store.count_by_status().await
166    }
167
168    /// Clean up old resolved incidents
169    pub async fn cleanup_old_incidents(&self, retention_days: u32) {
170        self.store.cleanup_old_resolved(retention_days).await;
171    }
172
173    /// Trigger webhook notifications for an event
174    async fn trigger_webhooks(&self, event_type: &str, incident: &DriftIncident) {
175        use crate::incidents::integrations::send_webhook;
176        use serde_json::json;
177
178        for webhook in &self.webhook_configs {
179            if !webhook.enabled {
180                continue;
181            }
182
183            // Check if webhook is subscribed to this event
184            if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
185                continue;
186            }
187
188            // Determine webhook format based on URL or headers
189            let payload = if webhook.url.contains("slack.com")
190                || webhook.url.contains("hooks.slack.com")
191            {
192                // Format as Slack message
193                use crate::incidents::slack_formatter::format_slack_webhook;
194                format_slack_webhook(incident)
195            } else if webhook.url.contains("jira") || webhook.headers.contains_key("X-Jira-Project")
196            {
197                // Format as Jira webhook
198                use crate::incidents::jira_formatter::format_jira_webhook;
199                format_jira_webhook(incident)
200            } else {
201                // Generic webhook format
202                json!({
203                    "event": event_type,
204                    "incident": {
205                        "id": incident.id,
206                        "endpoint": incident.endpoint,
207                        "method": incident.method,
208                        "type": format!("{:?}", incident.incident_type),
209                        "severity": format!("{:?}", incident.severity),
210                        "status": format!("{:?}", incident.status),
211                        "details": incident.details,
212                        "created_at": incident.created_at,
213                    }
214                })
215            };
216
217            // Send webhook asynchronously (fire and forget)
218            let webhook_clone = webhook.clone();
219            tokio::spawn(async move {
220                if let Err(e) = send_webhook(&webhook_clone, &payload).await {
221                    tracing::warn!("Failed to send webhook to {}: {}", webhook_clone.url, e);
222                }
223            });
224        }
225    }
226}