1use serde::{Deserialize, Serialize};
7
8pub const DEFAULT_MAX_SPEED_KMH: f64 = 1000.0;
15
16pub const DEFAULT_MIN_DISTANCE_KM: f64 = 50.0;
19
20pub const DEFAULT_HISTORY_WINDOW_HOURS: f64 = 24.0;
22
23pub const DEFAULT_MAX_HISTORY_PER_USER: usize = 10;
25
26pub const EARTH_RADIUS_KM: f64 = 6371.0;
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct GeoLocation {
36 pub ip: String,
38 pub latitude: f64,
40 pub longitude: f64,
42 pub city: Option<String>,
44 pub country: String,
46 pub country_code: String,
48 pub accuracy_radius_km: u32,
50}
51
52impl GeoLocation {
53 pub fn new(
55 ip: impl Into<String>,
56 latitude: f64,
57 longitude: f64,
58 country: impl Into<String>,
59 country_code: impl Into<String>,
60 ) -> Self {
61 Self {
62 ip: ip.into(),
63 latitude,
64 longitude,
65 city: None,
66 country: country.into(),
67 country_code: country_code.into(),
68 accuracy_radius_km: 50, }
70 }
71
72 pub fn with_city(mut self, city: impl Into<String>) -> Self {
74 self.city = Some(city.into());
75 self
76 }
77
78 pub fn with_accuracy(mut self, accuracy_km: u32) -> Self {
80 self.accuracy_radius_km = accuracy_km;
81 self
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct LoginEvent {
94 pub user_id: String,
96 pub timestamp_ms: u64,
98 pub location: GeoLocation,
100 pub success: bool,
102 pub device_fingerprint: Option<String>,
104}
105
106impl LoginEvent {
107 pub fn new(user_id: impl Into<String>, timestamp_ms: u64, location: GeoLocation) -> Self {
109 Self {
110 user_id: user_id.into(),
111 timestamp_ms,
112 location,
113 success: true,
114 device_fingerprint: None,
115 }
116 }
117
118 pub fn with_success(mut self, success: bool) -> Self {
120 self.success = success;
121 self
122 }
123
124 pub fn with_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
126 self.device_fingerprint = Some(fingerprint.into());
127 self
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
139#[serde(rename_all = "lowercase")]
140pub enum Severity {
141 Low,
143 Medium,
145 High,
147 Critical,
149}
150
151impl Severity {
152 pub fn as_str(&self) -> &'static str {
154 match self {
155 Severity::Low => "low",
156 Severity::Medium => "medium",
157 Severity::High => "high",
158 Severity::Critical => "critical",
159 }
160 }
161}
162
163impl std::fmt::Display for Severity {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 write!(f, "{}", self.as_str())
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct TravelAlert {
179 pub user_id: String,
181 pub severity: Severity,
183 pub from_location: GeoLocation,
185 pub from_time: u64,
187 pub to_location: GeoLocation,
189 pub to_time: u64,
191 pub distance_km: f64,
193 pub time_diff_hours: f64,
195 pub required_speed_kmh: f64,
197 pub confidence: f64,
199}
200
201impl TravelAlert {
202 #[allow(clippy::too_many_arguments)]
204 pub fn new(
205 user_id: impl Into<String>,
206 severity: Severity,
207 from_location: GeoLocation,
208 from_time: u64,
209 to_location: GeoLocation,
210 to_time: u64,
211 distance_km: f64,
212 time_diff_hours: f64,
213 required_speed_kmh: f64,
214 confidence: f64,
215 ) -> Self {
216 Self {
217 user_id: user_id.into(),
218 severity,
219 from_location,
220 from_time,
221 to_location,
222 to_time,
223 distance_km,
224 time_diff_hours,
225 required_speed_kmh,
226 confidence,
227 }
228 }
229
230 pub fn is_high_severity(&self) -> bool {
232 matches!(self.severity, Severity::High | Severity::Critical)
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct TravelConfig {
243 pub max_speed_kmh: f64,
245 pub min_distance_km: f64,
247 pub history_window_ms: u64,
249 pub max_history_per_user: usize,
251}
252
253impl Default for TravelConfig {
254 fn default() -> Self {
255 Self {
256 max_speed_kmh: DEFAULT_MAX_SPEED_KMH,
257 min_distance_km: DEFAULT_MIN_DISTANCE_KM,
258 history_window_ms: (DEFAULT_HISTORY_WINDOW_HOURS * 3600.0 * 1000.0) as u64,
259 max_history_per_user: DEFAULT_MAX_HISTORY_PER_USER,
260 }
261 }
262}
263
264impl TravelConfig {
265 pub fn new(
267 max_speed_kmh: f64,
268 min_distance_km: f64,
269 history_window_hours: f64,
270 max_history_per_user: usize,
271 ) -> Self {
272 Self {
273 max_speed_kmh,
274 min_distance_km,
275 history_window_ms: (history_window_hours * 3600.0 * 1000.0) as u64,
276 max_history_per_user,
277 }
278 }
279
280 pub fn history_window_hours(&self) -> f64 {
282 self.history_window_ms as f64 / (3600.0 * 1000.0)
283 }
284}
285
286#[derive(Debug, Clone, Default, Serialize, Deserialize)]
292pub struct TravelStats {
293 pub tracked_users: u32,
295 pub total_logins: u64,
297 pub alerts_generated: u64,
299 pub whitelist_routes: u32,
301}
302
303#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_geo_location_builder() {
313 let loc = GeoLocation::new("1.2.3.4", 40.7128, -74.0060, "United States", "US")
314 .with_city("New York")
315 .with_accuracy(10);
316
317 assert_eq!(loc.ip, "1.2.3.4");
318 assert_eq!(loc.latitude, 40.7128);
319 assert_eq!(loc.longitude, -74.0060);
320 assert_eq!(loc.city, Some("New York".to_string()));
321 assert_eq!(loc.country_code, "US");
322 assert_eq!(loc.accuracy_radius_km, 10);
323 }
324
325 #[test]
326 fn test_login_event_builder() {
327 let loc = GeoLocation::new("1.2.3.4", 40.7128, -74.0060, "United States", "US");
328 let event = LoginEvent::new("user123", 1000000, loc)
329 .with_success(false)
330 .with_fingerprint("fp-abc123");
331
332 assert_eq!(event.user_id, "user123");
333 assert!(!event.success);
334 assert_eq!(event.device_fingerprint, Some("fp-abc123".to_string()));
335 }
336
337 #[test]
338 fn test_severity_ordering() {
339 assert!(Severity::Low < Severity::Medium);
340 assert!(Severity::Medium < Severity::High);
341 assert!(Severity::High < Severity::Critical);
342 }
343
344 #[test]
345 fn test_severity_display() {
346 assert_eq!(Severity::Low.to_string(), "low");
347 assert_eq!(Severity::Critical.to_string(), "critical");
348 }
349
350 #[test]
351 fn test_travel_config_default() {
352 let config = TravelConfig::default();
353 assert_eq!(config.max_speed_kmh, 1000.0);
354 assert_eq!(config.min_distance_km, 50.0);
355 assert_eq!(config.max_history_per_user, 10);
356 assert!((config.history_window_hours() - 24.0).abs() < 0.001);
357 }
358
359 #[test]
360 fn test_travel_alert_high_severity() {
361 let from = GeoLocation::new("1.1.1.1", 0.0, 0.0, "X", "XX");
362 let to = GeoLocation::new("2.2.2.2", 10.0, 10.0, "Y", "YY");
363
364 let alert = TravelAlert::new(
365 "user1",
366 Severity::High,
367 from.clone(),
368 1000,
369 to.clone(),
370 2000,
371 1000.0,
372 0.5,
373 5000.0,
374 0.9,
375 );
376
377 assert!(alert.is_high_severity());
378
379 let low_alert = TravelAlert::new(
380 "user2",
381 Severity::Low,
382 from,
383 1000,
384 to,
385 2000,
386 100.0,
387 1.0,
388 1100.0,
389 0.6,
390 );
391
392 assert!(!low_alert.is_high_severity());
393 }
394}