1use crate::domains::routing::TransportMode;
2use crate::error::EveryMapResult;
3use crate::types::Coordinate;
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
13pub struct TourOptions {
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub transport_mode: Option<TransportMode>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub provider_extra: Option<serde_json::Value>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TourStop {
25 pub coordinate: Coordinate,
27 pub arrival_time: Option<String>,
29 pub departure_time: Option<String>,
31 pub duration: Option<f64>,
33 pub distance_from_previous: Option<f64>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct TourResponse {
40 pub stops: Vec<TourStop>,
42 pub total_distance: Option<f64>,
44 pub total_duration: Option<f64>,
46 pub unassigned_count: Option<u32>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub raw: Option<serde_json::Value>,
51}
52
53#[async_trait]
54pub trait TourPlanner: Send + Sync {
55 async fn optimize_tour(
56 &self,
57 stops: &[Coordinate],
58 options: &TourOptions,
59 ) -> EveryMapResult<TourResponse>;
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn test_tour_options_default() {
68 let options = TourOptions::default();
69 assert!(options.provider_extra.is_none());
70 assert!(options.transport_mode.is_none());
71 }
72
73 #[test]
74 fn test_tour_options_with_provider_extra() {
75 let options = TourOptions {
76 transport_mode: None,
77 provider_extra: Some(serde_json::json!({"fleet": {"types": []}})),
78 };
79 assert!(options.provider_extra.is_some());
80 }
81
82 #[test]
83 fn test_tour_stop_construction() {
84 let coordinate = Coordinate::new(52.5, 13.4).unwrap();
85 let stop = TourStop {
86 coordinate,
87 arrival_time: Some("2024-01-01T08:30:00".to_string()),
88 departure_time: Some("2024-01-01T09:00:00".to_string()),
89 duration: Some(1800.0),
90 distance_from_previous: Some(5000.0),
91 };
92 assert_eq!(stop.arrival_time, Some("2024-01-01T08:30:00".to_string()));
93 assert_eq!(stop.duration, Some(1800.0));
94 }
95
96 #[test]
97 fn test_tour_response_construction() {
98 let response = TourResponse {
99 stops: vec![],
100 total_distance: Some(15000.0),
101 total_duration: Some(3600.0),
102 unassigned_count: Some(0),
103 raw: None,
104 };
105 assert_eq!(response.total_distance, Some(15000.0));
106 assert!(response.stops.is_empty());
107 }
108
109 #[test]
112 fn test_tour_response_serde_roundtrip() {
113 let response = TourResponse {
114 stops: vec![TourStop {
115 coordinate: Coordinate::new(52.5, 13.4).unwrap(),
116 arrival_time: Some("2024-01-01T08:30:00".to_string()),
117 departure_time: Some("2024-01-01T09:00:00".to_string()),
118 duration: Some(1800.0),
119 distance_from_previous: Some(5000.0),
120 }],
121 total_distance: Some(15000.0),
122 total_duration: Some(3600.0),
123 unassigned_count: Some(0),
124 raw: Some(serde_json::json!({"optimization": "tsp"})),
125 };
126 let json = serde_json::to_string(&response).unwrap();
127 let back: TourResponse = serde_json::from_str(&json).unwrap();
128 assert_eq!(back.stops.len(), 1);
129 assert_eq!(back.total_distance, Some(15000.0));
130 assert_eq!(back.unassigned_count, Some(0));
131 assert!(back.raw.is_some());
132 }
133
134 #[test]
135 fn test_tour_response_empty_serde_roundtrip() {
136 let response = TourResponse {
137 stops: vec![],
138 total_distance: None,
139 total_duration: None,
140 unassigned_count: None,
141 raw: None,
142 };
143 let json = serde_json::to_string(&response).unwrap();
144 let back: TourResponse = serde_json::from_str(&json).unwrap();
145 assert!(back.stops.is_empty());
146 assert!(back.total_distance.is_none());
147 }
148
149 #[test]
152 fn test_tour_stop_serde_roundtrip() {
153 let stop = TourStop {
154 coordinate: Coordinate::new(48.8566, 2.3522).unwrap(),
155 arrival_time: Some("2024-06-01T10:00:00".to_string()),
156 departure_time: Some("2024-06-01T10:30:00".to_string()),
157 duration: Some(1800.0),
158 distance_from_previous: Some(2500.0),
159 };
160 let json = serde_json::to_string(&stop).unwrap();
161 let back: TourStop = serde_json::from_str(&json).unwrap();
162 assert_eq!(back.coordinate, stop.coordinate);
163 assert_eq!(back.arrival_time, stop.arrival_time);
164 assert_eq!(back.duration, Some(1800.0));
165 }
166
167 #[test]
170 fn test_tour_options_serde_roundtrip() {
171 let options = TourOptions {
172 transport_mode: None,
173 provider_extra: Some(serde_json::json!({
174 "fleet": {"types": [{"id": "truck"}]},
175 "plan": {"jobs": []}
176 })),
177 };
178 let json = serde_json::to_string(&options).unwrap();
179 let back: TourOptions = serde_json::from_str(&json).unwrap();
180 assert!(back.provider_extra.is_some());
181 }
182
183 #[test]
186 fn test_tour_stop_zero_distance() {
187 let stop = TourStop {
188 coordinate: Coordinate::ORIGIN,
189 arrival_time: None,
190 departure_time: None,
191 duration: Some(0.0),
192 distance_from_previous: Some(0.0),
193 };
194 assert_eq!(stop.distance_from_previous, Some(0.0));
195 }
196
197 #[test]
198 fn test_tour_response_multiple_stops() {
199 let stops: Vec<TourStop> = (0..5)
200 .map(|i| TourStop {
201 coordinate: Coordinate::new(52.0 + i as f64 * 0.1, 13.0 + i as f64 * 0.1).unwrap(),
202 arrival_time: None,
203 departure_time: None,
204 duration: None,
205 distance_from_previous: Some(i as f64 * 1000.0),
206 })
207 .collect();
208 let response = TourResponse {
209 stops,
210 total_distance: Some(10000.0),
211 total_duration: Some(1800.0),
212 unassigned_count: None,
213 raw: None,
214 };
215 assert_eq!(response.stops.len(), 5);
216 }
217}