use chrono::NaiveDate;
use turnframe_core::event::ExternalStatus;
use uuid::Uuid;
use crate::explore::{SimulatedTransition, WorkflowModel};
use crate::workflows::simulate;
use crate::workflows::trip::apply::{MAX_EXTRAS, extra_id_for};
use crate::workflows::trip::command::{TripCommand, TripEvent};
use crate::workflows::trip::definition::TripWorkflow;
use crate::workflows::trip::state::{
Extra, Leg, LegStatus, NewExtra, Offer, Payer, TripOutcome, TripState, TripStatus, TripTraveler,
};
pub const SAMPLE_TRAVELER_ID: Uuid = Uuid::from_u128(0x7a1e_0000_0000_4000_8000_0000_0000_0001);
pub const OTHER_TRAVELER_ID: Uuid = Uuid::from_u128(0x7a1e_0000_0000_4000_8000_0000_0000_0002);
pub const SAMPLE_NAME: &str = "Lisbon offsite";
pub const SAMPLE_EXTRA_DESCRIPTIONS: [&str; 4] = [
"Checked bag",
"Seat with extra legroom",
"Airport meal",
"Hotel night",
];
pub const SAMPLE_TICKET_NUMBER: &str = "055-2100000123";
pub const SAMPLE_REFUSAL_CODE: &str = "SEAT_GONE";
pub const SAMPLE_FARE_DIFFERENCE_CENTS: i64 = 8_400;
pub const REQUOTED_FARE_DIFFERENCE_CENTS: i64 = 13_200;
pub const MODEL_EXTRA_BUDGET: usize = MAX_EXTRAS;
#[must_use]
pub fn sample_travel_date() -> NaiveDate {
NaiveDate::from_ymd_opt(2026, 10, 6).unwrap_or(NaiveDate::MIN)
}
#[must_use]
pub fn sample_legs() -> Vec<Leg> {
vec![
Leg {
number: 1,
flight: "AZ610".to_owned(),
from: "FCO".to_owned(),
to: "LIS".to_owned(),
departs: "2026-10-05 07:40".to_owned(),
status: LegStatus::Cancelled,
protected: false,
},
Leg {
number: 2,
flight: "AZ611".to_owned(),
from: "LIS".to_owned(),
to: "FCO".to_owned(),
departs: "2026-10-09 18:20".to_owned(),
status: LegStatus::OnTime,
protected: false,
},
]
}
#[must_use]
pub fn sample_offer(leg: u32) -> Offer {
Offer {
leg,
flight: if leg == 1 { "AZ612" } else { "AZ613" }.to_owned(),
departs: if leg == 1 {
"2026-10-05 13:10"
} else {
"2026-10-10 09:30"
}
.to_owned(),
fare_difference_cents: SAMPLE_FARE_DIFFERENCE_CENTS,
}
}
#[must_use]
pub fn sample_quote(leg: u32) -> TripCommand {
let offer = sample_offer(leg);
TripCommand::Requote {
leg: offer.leg,
flight: offer.flight,
departs: offer.departs,
fare_difference_cents: offer.fare_difference_cents,
}
}
#[must_use]
pub fn sample_traveler() -> TripTraveler {
TripTraveler {
traveler_id: SAMPLE_TRAVELER_ID,
display_name: "Marta Bianchi".to_owned(),
}
}
#[must_use]
pub fn other_traveler() -> TripTraveler {
TripTraveler {
traveler_id: OTHER_TRAVELER_ID,
display_name: "Luca Ferri".to_owned(),
}
}
#[must_use]
pub fn sample_new_extra(position: usize) -> NewExtra {
let description = SAMPLE_EXTRA_DESCRIPTIONS
.get(position)
.copied()
.unwrap_or("Other extra");
NewExtra {
description: description.to_owned(),
quantity: u32::try_from(position).unwrap_or(0) + 1,
unit_price_cents: 4_000,
}
}
#[must_use]
pub fn incomplete_case() -> TripState {
let extra = sample_new_extra(0);
TripState {
traveler: Some(sample_traveler()),
legs: sample_legs(),
extras: vec![Extra {
extra_id: extra_id_for(0, &extra.description),
description: extra.description,
quantity: extra.quantity,
unit_price_cents: extra.unit_price_cents,
payer: Some(Payer::Airline),
}],
..TripState::default()
}
}
#[must_use]
pub fn unassigned_case() -> TripState {
let mut state = incomplete_case();
for extra in &mut state.extras {
extra.payer = None;
}
state
}
#[must_use]
pub fn complete_case() -> TripState {
TripState {
name: Some(SAMPLE_NAME.to_owned()),
travel_date: Some(sample_travel_date()),
..incomplete_case()
}
}
#[must_use]
pub fn with_offer(leg: u32) -> TripState {
TripState {
offer: Some(sample_offer(leg)),
..complete_case()
}
}
#[must_use]
pub fn awaiting_rebooking_confirmation() -> TripState {
awaiting_rebooking_confirmation_at(SAMPLE_FARE_DIFFERENCE_CENTS)
}
#[must_use]
pub fn awaiting_rebooking_confirmation_at(fare_difference_cents: i64) -> TripState {
let mut offer = sample_offer(1);
offer.fare_difference_cents = fare_difference_cents;
TripState {
status: TripStatus::AwaitingRebookingConfirmation,
external_status: Some(ExternalStatus::AwaitingConfirmation),
offer: Some(offer),
..complete_case()
}
}
impl TripState {
#[must_use]
pub fn protected(mut self, leg: u32) -> Self {
for found in &mut self.legs {
if found.number == leg {
found.protected = true;
}
}
self
}
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct TripModel {
pub workflow: TripWorkflow,
}
impl TripModel {
#[must_use]
pub const fn new() -> Self {
Self {
workflow: TripWorkflow::new(),
}
}
fn editing_commands(state: &TripState, commands: &mut Vec<TripCommand>) {
commands.push(TripCommand::ChangeTraveler {
traveler: match state.traveler.as_ref() {
Some(current) if current.traveler_id == SAMPLE_TRAVELER_ID => other_traveler(),
_ => sample_traveler(),
},
});
if state.name.is_none() {
commands.push(TripCommand::SetName {
value: SAMPLE_NAME.to_owned(),
});
}
if state.travel_date.is_none() {
commands.push(TripCommand::SetTravelDate {
value: sample_travel_date(),
});
}
if state.extras.len() < MODEL_EXTRA_BUDGET || state.extras.len() >= MAX_EXTRAS {
commands.push(TripCommand::AddExtra {
extra: sample_new_extra(state.extras.len()),
});
}
if let Some(extra) = state.extras.iter().find(|e| e.payer.is_none()) {
commands.push(TripCommand::AssignPayer {
extra_id: extra.extra_id,
payer: Payer::Airline,
});
}
if state.offer.is_none() {
commands.push(sample_quote(1));
}
if state
.legs
.iter()
.any(|leg| leg.number == 2 && !leg.protected)
{
commands.push(TripCommand::ProtectLeg { leg: 2 });
}
}
}
impl WorkflowModel<TripWorkflow> for TripModel {
fn initial_states(&self) -> Vec<Option<TripState>> {
vec![None]
}
fn candidate_commands(&self, state: Option<&TripState>) -> Vec<TripCommand> {
let Some(state) = state else {
return vec![TripCommand::Open];
};
let mut commands = Vec::new();
match state.status {
TripStatus::Draft | TripStatus::Refused => {
Self::editing_commands(state, &mut commands);
if let Some(offer) = &state.offer
&& state.is_complete()
{
commands.push(TripCommand::RequestRebooking { leg: offer.leg });
}
commands.push(TripCommand::Withdraw);
}
TripStatus::AwaitingRebookingConfirmation => {
Self::editing_commands(state, &mut commands);
commands.push(TripCommand::Rebook);
commands.push(TripCommand::Withdraw);
}
TripStatus::Rebooking => {
for status in [
ExternalStatus::ReceivedByIntermediary,
ExternalStatus::Accepted,
ExternalStatus::Rejected,
] {
commands.push(TripCommand::RecordAirlineOutcome {
status,
ticket_number: (status == ExternalStatus::Accepted)
.then(|| SAMPLE_TICKET_NUMBER.to_owned()),
reason_code: (status == ExternalStatus::Rejected)
.then(|| SAMPLE_REFUSAL_CODE.to_owned()),
});
}
}
TripStatus::Ticketed => {
for status in [ExternalStatus::Delivered, ExternalStatus::NotDelivered] {
commands.push(TripCommand::RecordAirlineOutcome {
status,
ticket_number: None,
reason_code: None,
});
}
}
TripStatus::Notified | TripStatus::NotNotified | TripStatus::Withdrawn => {}
}
if state.status.is_editable() {
commands.push(TripCommand::SetName {
value: String::new(),
});
}
commands
}
fn simulate(
&self,
state: Option<&TripState>,
command: &TripCommand,
) -> SimulatedTransition<TripState, TripEvent> {
simulate(&self.workflow, state, command)
}
fn declared_outcomes(&self) -> Vec<TripOutcome> {
vec![
TripOutcome::Withdrawn,
TripOutcome::Notified,
TripOutcome::NotNotified,
]
}
}