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 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub payer: Option<Payer>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Extra {
41 pub extra_id: Uuid,
43 pub description: String,
45 pub quantity: u32,
47 pub unit_price_cents: i64,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub payer: Option<Payer>,
52}
53
54impl Extra {
55 #[must_use]
57 pub fn total_cents(&self) -> i64 {
58 i64::from(self.quantity).saturating_mul(self.unit_price_cents)
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65#[non_exhaustive]
66pub enum LegStatus {
67 #[default]
69 OnTime,
70 Delayed,
72 Cancelled,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct Leg {
79 pub number: u32,
81 pub flight: String,
83 pub from: String,
85 pub to: String,
87 pub departs: String,
89 pub status: LegStatus,
91 #[serde(default)]
93 pub protected: bool,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct Offer {
99 pub leg: u32,
101 pub flight: String,
103 pub departs: String,
105 pub fare_difference_cents: i64,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct TripTraveler {
112 pub traveler_id: Uuid,
114 pub display_name: String,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124#[non_exhaustive]
125pub enum TripStatus {
126 #[default]
128 Draft,
129 AwaitingRebookingConfirmation,
131 Rebooking,
133 Ticketed,
135 Refused,
137 Notified,
139 NotNotified,
141 Withdrawn,
143}
144
145impl TripStatus {
146 #[must_use]
148 pub fn is_editable(self) -> bool {
149 matches!(
150 self,
151 Self::Draft | Self::AwaitingRebookingConfirmation | Self::Refused
152 )
153 }
154
155 #[must_use]
158 pub fn is_sent(self) -> bool {
159 matches!(
160 self,
161 Self::Rebooking | Self::Ticketed | Self::Notified | Self::NotNotified
162 )
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
168#[serde(deny_unknown_fields)]
169pub struct TripState {
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub traveler: Option<TripTraveler>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub name: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub travel_date: Option<NaiveDate>,
179 #[serde(default)]
181 pub legs: Vec<Leg>,
182 #[serde(default)]
184 pub extras: Vec<Extra>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub offer: Option<Offer>,
188 pub status: TripStatus,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub external_status: Option<ExternalStatus>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub ticket_number: Option<String>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub refusal_code: Option<String>,
199}
200
201impl TripState {
202 #[must_use]
207 pub fn open_obligations(&self) -> Vec<TripObligation> {
208 if !self.status.is_editable() {
209 return Vec::new();
210 }
211 let mut obligations = Vec::new();
212 if self.traveler.is_none() {
213 obligations.push(TripObligation::SelectTraveler);
214 }
215 for extra in &self.extras {
216 if extra.payer.is_none() {
217 obligations.push(TripObligation::AssignPayer {
218 extra_id: extra.extra_id,
219 });
220 }
221 }
222 if self.name.is_none() {
223 obligations.push(TripObligation::SetName);
224 }
225 if self.travel_date.is_none() {
226 obligations.push(TripObligation::SetTravelDate);
227 }
228 obligations
229 }
230
231 #[must_use]
233 pub fn is_complete(&self) -> bool {
234 self.status.is_editable() && self.open_obligations().is_empty()
235 }
236
237 #[must_use]
239 pub fn extras_total_cents(&self) -> i64 {
240 self.extras.iter().fold(0_i64, |total, extra| {
241 total.saturating_add(extra.total_cents())
242 })
243 }
244
245 #[must_use]
247 pub fn extra(&self, extra_id: Uuid) -> Option<&Extra> {
248 self.extras.iter().find(|extra| extra.extra_id == extra_id)
249 }
250
251 #[must_use]
253 pub fn leg(&self, number: u32) -> Option<&Leg> {
254 self.legs.iter().find(|leg| leg.number == number)
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
260#[serde(rename_all = "snake_case")]
261#[non_exhaustive]
262pub enum TripPhase {
263 PreDraft,
265 Collecting,
267 AwaitingRebookingConfirmation,
269 Dispatching,
271 Refused,
273 Ticketed,
275 Notified,
277 NotNotified,
279 Withdrawn,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286#[non_exhaustive]
287pub enum TripObligation {
288 SelectTraveler,
290 AssignPayer {
292 extra_id: Uuid,
294 },
295 SetName,
297 SetTravelDate,
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
303#[serde(rename_all = "snake_case")]
304#[non_exhaustive]
305pub enum TripOutcome {
306 Withdrawn,
308 Notified,
310 NotNotified,
312}