turnframe_test/workflows/trip/
state.rs1use chrono::NaiveDate;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use turnframe_core::event::ExternalStatus;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
12#[serde(rename_all = "snake_case")]
13#[non_exhaustive]
14pub enum Payer {
15 Traveler,
17 Company,
19 Airline,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct NewExtra {
27 pub description: String,
29 pub quantity: u32,
31 pub unit_price_cents: i64,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct Extra {
38 pub extra_id: Uuid,
40 pub description: String,
42 pub quantity: u32,
44 pub unit_price_cents: i64,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub payer: Option<Payer>,
49}
50
51impl Extra {
52 #[must_use]
54 pub fn total_cents(&self) -> i64 {
55 i64::from(self.quantity).saturating_mul(self.unit_price_cents)
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62#[non_exhaustive]
63pub enum LegStatus {
64 #[default]
66 OnTime,
67 Delayed,
69 Cancelled,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct Leg {
76 pub number: u32,
78 pub flight: String,
80 pub from: String,
82 pub to: String,
84 pub departs: String,
86 pub status: LegStatus,
88 #[serde(default)]
90 pub protected: bool,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct Offer {
96 pub leg: u32,
98 pub flight: String,
100 pub departs: String,
102 pub fare_difference_cents: i64,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct TripTraveler {
109 pub traveler_id: Uuid,
111 pub display_name: String,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121#[non_exhaustive]
122pub enum TripStatus {
123 #[default]
125 Draft,
126 AwaitingRebookingConfirmation,
128 Rebooking,
130 Ticketed,
132 Refused,
134 Notified,
136 NotNotified,
138 Withdrawn,
140}
141
142impl TripStatus {
143 #[must_use]
145 pub fn is_editable(self) -> bool {
146 matches!(
147 self,
148 Self::Draft | Self::AwaitingRebookingConfirmation | Self::Refused
149 )
150 }
151
152 #[must_use]
155 pub fn is_sent(self) -> bool {
156 matches!(
157 self,
158 Self::Rebooking | Self::Ticketed | Self::Notified | Self::NotNotified
159 )
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
165#[serde(deny_unknown_fields)]
166pub struct TripState {
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub traveler: Option<TripTraveler>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub name: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub travel_date: Option<NaiveDate>,
176 #[serde(default)]
178 pub legs: Vec<Leg>,
179 #[serde(default)]
181 pub extras: Vec<Extra>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub offer: Option<Offer>,
185 pub status: TripStatus,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub external_status: Option<ExternalStatus>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub ticket_number: Option<String>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub refusal_code: Option<String>,
196}
197
198impl TripState {
199 #[must_use]
204 pub fn open_obligations(&self) -> Vec<TripObligation> {
205 if !self.status.is_editable() {
206 return Vec::new();
207 }
208 let mut obligations = Vec::new();
209 if self.traveler.is_none() {
210 obligations.push(TripObligation::SelectTraveler);
211 }
212 for extra in &self.extras {
213 if extra.payer.is_none() {
214 obligations.push(TripObligation::AssignPayer {
215 extra_id: extra.extra_id,
216 });
217 }
218 }
219 if self.name.is_none() {
220 obligations.push(TripObligation::SetName);
221 }
222 if self.travel_date.is_none() {
223 obligations.push(TripObligation::SetTravelDate);
224 }
225 obligations
226 }
227
228 #[must_use]
230 pub fn is_complete(&self) -> bool {
231 self.status.is_editable() && self.open_obligations().is_empty()
232 }
233
234 #[must_use]
236 pub fn extras_total_cents(&self) -> i64 {
237 self.extras.iter().fold(0_i64, |total, extra| {
238 total.saturating_add(extra.total_cents())
239 })
240 }
241
242 #[must_use]
244 pub fn extra(&self, extra_id: Uuid) -> Option<&Extra> {
245 self.extras.iter().find(|extra| extra.extra_id == extra_id)
246 }
247
248 #[must_use]
250 pub fn leg(&self, number: u32) -> Option<&Leg> {
251 self.legs.iter().find(|leg| leg.number == number)
252 }
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258#[non_exhaustive]
259pub enum TripPhase {
260 PreDraft,
262 Collecting,
264 AwaitingRebookingConfirmation,
266 Dispatching,
268 Refused,
270 Ticketed,
272 Notified,
274 NotNotified,
276 Withdrawn,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
282#[serde(rename_all = "snake_case")]
283#[non_exhaustive]
284pub enum TripObligation {
285 SelectTraveler,
287 AssignPayer {
289 extra_id: Uuid,
291 },
292 SetName,
294 SetTravelDate,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
300#[serde(rename_all = "snake_case")]
301#[non_exhaustive]
302pub enum TripOutcome {
303 Withdrawn,
305 Notified,
307 NotNotified,
309}