use chrono::{Local, NaiveDate}; use ics::properties::{DtStart, Summary}; use serde::Serialize; use sqlx::SqlitePool; use super::{ event::{Event, Registration}, log::Log, notification::Notification, tripdetails::TripDetails, triptype::TripType, user::{ErgoUser, SteeringUser, User}, usertrip::UserTrip, }; #[derive(Serialize, Clone, Debug)] pub struct Trip { id: i64, pub cox_id: i64, cox_name: String, trip_details_id: Option, planned_starting_time: String, pub max_people: i64, pub day: String, pub notes: Option, pub allow_guests: bool, trip_type_id: Option, always_show: bool, is_locked: bool, } #[derive(Serialize, Debug)] pub struct TripWithUserAndType { #[serde(flatten)] pub trip: Trip, pub rower: Vec, trip_type: Option, } pub struct TripUpdate<'a> { pub cox: &'a User, pub trip: &'a Trip, pub max_people: i32, pub notes: Option<&'a str>, pub trip_type: Option, //TODO: Move to `TripType` pub is_locked: bool, } impl TripWithUserAndType { pub async fn from(db: &SqlitePool, trip: Trip) -> Self { let mut trip_type = None; if let Some(trip_type_id) = trip.trip_type_id { trip_type = TripType::find_by_id(db, trip_type_id).await; } Self { rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await, trip, trip_type, } } } impl Trip { /// Cox decides to create own trip. pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) { Self::perform_new(db, &cox.user, trip_details).await } pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) { let typ = trip_details.triptype(db).await; if let Some(typ) = typ { let allowed_type = TripType::find_by_id(db, 4).await.unwrap(); if typ == allowed_type { Self::perform_new(db, &ergo.user, trip_details).await; } } } async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) { let _ = sqlx::query!( "INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)", user.id, trip_details.id ) .execute(db) .await; let same_starting_datetime = TripDetails::find_by_startingdatetime( db, trip_details.day, trip_details.planned_starting_time, ) .await; for notify in same_starting_datetime { // don't notify oneself if notify.id == trip_details.id { continue; } // don't notify people who have cancelled their trip if notify.cancelled() { continue; } if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await { let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); Notification::create( db, &user_earlier_trip, &format!( "{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt", user.name, trip.day, trip.planned_starting_time ), "Neue Ausfahrt zur selben Zeit", None, None, ) .await; } } } pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option { sqlx::query_as!( Self, " SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked FROM trip INNER JOIN trip_details ON trip.trip_details_id = trip_details.id INNER JOIN user ON trip.cox_id = user.id WHERE trip_details.id=? ", tripdetails_id ) .fetch_one(db) .await .ok() } pub(crate) async fn get_vevent(self, user: &User) -> ics::Event { let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000"); vevent.push(DtStart::new(format!( "{}T{}00", self.day.replace('-', ""), self.planned_starting_time.replace(':', "") ))); let mut name = String::new(); if self.is_cancelled() { name.push_str("ABGESAGT"); if let Some(notes) = &self.notes { if !notes.is_empty() { name.push_str(&format!(" (Grund: {notes})")) } } name.push_str("! :-( "); } if self.cox_id == user.id { name.push_str("Ruderausfahrt (selber ausgeschrieben)"); } else { name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name)); } vevent.push(Summary::new(name)); vevent } pub async fn all(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked FROM trip INNER JOIN trip_details ON trip.trip_details_id = trip_details.id INNER JOIN user ON trip.cox_id = user.id ", ) .fetch_all(db) .await .unwrap() //TODO: fixme } pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec { let mut ret = Vec::new(); let trips = Self::all(db).await; for trip in trips { if user.id == trip.cox_id { ret.push(trip.clone()); } if let Some(trip_details_id) = trip.trip_details_id { if UserTrip::find_by_userid_and_trip_detail_id(db, user.id, trip_details_id) .await .is_some() { ret.push(trip); } } } ret } pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!( Self, " SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked FROM trip INNER JOIN trip_details ON trip.trip_details_id = trip_details.id INNER JOIN user ON trip.cox_id = user.id WHERE trip.id=? ", id ) .fetch_one(db) .await .ok() } /// Cox decides to help in a event. pub async fn new_join( db: &SqlitePool, cox: &SteeringUser, event: &Event, ) -> Result<(), CoxHelpError> { if event.is_rower_registered(db, cox).await { return Err(CoxHelpError::AlreadyRegisteredAsRower); } if event.trip_details(db).await.is_locked { return Err(CoxHelpError::DetailsLocked); } if event.max_people == 0 { return Err(CoxHelpError::CanceledEvent); } match sqlx::query!( "INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)", cox.id, event.id ) .execute(db) .await { Ok(_) => Ok(()), Err(_) => Err(CoxHelpError::AlreadyRegisteredAsCox), } } pub async fn get_for_today(db: &SqlitePool) -> Vec { let today = Local::now().date_naive(); Self::get_for_day(db, today).await } pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec { let day = format!("{day}"); let trips = sqlx::query_as!( Trip, " SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked FROM trip INNER JOIN trip_details ON trip.trip_details_id = trip_details.id INNER JOIN user ON trip.cox_id = user.id WHERE day=? ", day ) .fetch_all(db) .await .unwrap(); //Okay, as Trip can only be created with proper DB backing let mut ret = Vec::new(); for trip in trips { ret.push(TripWithUserAndType::from(db, trip).await); } ret } /// Cox decides to update own trip. pub async fn update_own( db: &SqlitePool, update: &TripUpdate<'_>, ) -> Result<(), TripUpdateError> { if !update.trip.is_trip_from_user(update.cox.id) { return Err(TripUpdateError::NotYourTrip); } if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await { return Err(TripUpdateError::TripTypeNotAllowed); } let Some(trip_details_id) = update.trip.trip_details_id else { return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? }; let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); let was_already_cancelled = tripdetails.max_people == 0; let is_locked = if update.max_people == 0 { false } else { update.is_locked }; sqlx::query!( "UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?", update.max_people, update.notes, update.trip_type, is_locked, trip_details_id ) .execute(db) .await .unwrap(); //Okay, as trip_details can only be created with proper DB backing if update.max_people == 0 && !was_already_cancelled { let rowers = TripWithUserAndType::from(db, update.trip.clone()) .await .rower; for user in rowers { if let Some(user) = User::find_by_name(db, &user.name).await { let notes = match update.notes { Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"), _ => String::from(""), }; Notification::create( db, &user, &format!( "Die Ausfahrt von {} am {} um {} wurde abgesagt. {} Bitte gib Bescheid, dass du die Info erhalten hast indem du auf ✓ klickst.", update.cox.name, update.trip.day, update.trip.planned_starting_time, notes ), "Absage Ausfahrt", None, Some(&format!( "remove_user_trip_with_trip_details_id:{}", trip_details_id )), ) .await; } } } else { Notification::delete_by_action( db, &format!("remove_user_trip_with_trip_details_id:{}", trip_details_id), ) .await; } if update.max_people > 0 && was_already_cancelled { Notification::delete_by_action( db, &format!("remove_user_trip_with_trip_details_id:{}", trip_details_id), ) .await; } let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); trip_details.check_free_spaces(db).await; Ok(()) } pub async fn trip_details(&self, db: &SqlitePool) -> Option { if let Some(trip_details_id) = self.trip_type_id { return TripDetails::find_by_id(db, trip_details_id).await; } None } pub async fn delete_by_planned_event( db: &SqlitePool, cox: &SteeringUser, event: &Event, ) -> Result<(), TripHelpDeleteError> { if event.trip_details(db).await.is_locked { return Err(TripHelpDeleteError::DetailsLocked); } let affected_rows = sqlx::query!( "DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?", cox.id, event.id ) .execute(db) .await .unwrap() .rows_affected(); if affected_rows == 0 { return Err(TripHelpDeleteError::CoxNotHelping); } Ok(()) } pub(crate) async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), TripDeleteError> { let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await; if !registered_rower.is_empty() { return Err(TripDeleteError::SomebodyAlreadyRegistered); } if !self.is_trip_from_user(user.id) && !user.has_role(db, "admin").await { return Err(TripDeleteError::NotYourTrip); } Log::create(db, format!("{} deleted trip: {:#?}", user.name, self)).await; sqlx::query!("DELETE FROM trip WHERE id = ?", self.id) .execute(db) .await .unwrap(); //TODO: fixme Ok(()) } fn is_trip_from_user(&self, user_id: i64) -> bool { self.cox_id == user_id } pub(crate) async fn toggle_always_show(&self, db: &SqlitePool) { if let Some(trip_details) = self.trip_details_id { let new_state = !self.always_show; sqlx::query!( "UPDATE trip_details SET always_show = ? WHERE id = ?", new_state, trip_details ) .execute(db) .await .unwrap(); } } pub(crate) async fn get_pinned_for_day( db: &sqlx::Pool, day: NaiveDate, ) -> Vec { let mut trips = Self::get_for_day(db, day).await; trips.retain(|e| e.trip.always_show); trips } fn is_cancelled(&self) -> bool { self.max_people == 0 } } #[derive(Debug)] pub enum CoxHelpError { AlreadyRegisteredAsRower, AlreadyRegisteredAsCox, DetailsLocked, CanceledEvent, } #[derive(Debug, PartialEq)] pub enum TripHelpDeleteError { DetailsLocked, CoxNotHelping, } #[derive(Debug, PartialEq)] pub enum TripDeleteError { SomebodyAlreadyRegistered, NotYourTrip, } #[derive(Debug)] pub enum TripUpdateError { NotYourTrip, TripDetailsDoesNotExist, TripTypeNotAllowed, } #[cfg(test)] mod test { use crate::{ model::{ event::Event, notification::Notification, trip::{self, TripDeleteError}, tripdetails::TripDetails, user::{SteeringUser, User}, usertrip::UserTrip, }, testdb, }; use chrono::Local; use sqlx::SqlitePool; use super::Trip; #[sqlx::test] fn test_new_own() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox".into()).await.unwrap(), ) .await .unwrap(); let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); Trip::new_own(&pool, &cox, trip_details).await; assert!(Trip::find_by_id(&pool, 1).await.is_some()); } #[sqlx::test] fn test_notification_cox_if_same_datetime() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox".into()).await.unwrap(), ) .await .unwrap(); let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); Trip::new_own(&pool, &cox, trip_details).await; let cox2 = SteeringUser::new( &pool, User::find_by_name(&pool, "cox2".into()).await.unwrap(), ) .await .unwrap(); let trip_details = TripDetails::find_by_id(&pool, 3).await.unwrap(); Trip::new_own(&pool, &cox2, trip_details).await; let last_notification = &Notification::for_user(&pool, &cox).await[0]; assert!(last_notification .message .starts_with("cox2 hat eine Ausfahrt zur selben Zeit")); } #[sqlx::test] fn test_get_day_cox_trip() { let pool = testdb!(); let tomorrow = Local::now().date_naive() + chrono::Duration::days(1); let res = Trip::get_for_day(&pool, tomorrow).await; assert_eq!(res.len(), 1); } #[sqlx::test] fn test_new_succ_join() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox2".into()).await.unwrap(), ) .await .unwrap(); let planned_event = Event::find_by_id(&pool, 1).await.unwrap(); assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_ok()); } #[sqlx::test] fn test_new_failed_join_already_cox() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox2".into()).await.unwrap(), ) .await .unwrap(); let planned_event = Event::find_by_id(&pool, 1).await.unwrap(); Trip::new_join(&pool, &cox, &planned_event).await.unwrap(); assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err()); } #[sqlx::test] fn test_succ_update_own() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox".into()).await.unwrap(), ) .await .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let update = trip::TripUpdate { cox: &cox, trip: &trip, max_people: 10, notes: None, trip_type: None, is_locked: false, }; assert!(Trip::update_own(&pool, &update).await.is_ok()); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); assert_eq!(trip.max_people, 10); } #[sqlx::test] fn test_succ_update_own_with_triptype() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox".into()).await.unwrap(), ) .await .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let update = trip::TripUpdate { cox: &cox, trip: &trip, max_people: 10, notes: None, trip_type: Some(1), is_locked: false, }; assert!(Trip::update_own(&pool, &update).await.is_ok()); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); assert_eq!(trip.max_people, 10); assert_eq!(trip.trip_type_id, Some(1)); } #[sqlx::test] fn test_fail_update_own_not_your_trip() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox2".into()).await.unwrap(), ) .await .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let update = trip::TripUpdate { cox: &cox, trip: &trip, max_people: 10, notes: None, trip_type: None, is_locked: false, }; assert!(Trip::update_own(&pool, &update).await.is_err()); assert_eq!(trip.max_people, 1); } #[sqlx::test] fn test_succ_delete_by_planned_event() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox".into()).await.unwrap(), ) .await .unwrap(); let planned_event = Event::find_by_id(&pool, 1).await.unwrap(); Trip::new_join(&pool, &cox, &planned_event).await.unwrap(); //TODO: check why following assert fails //assert!(Trip::find_by_id(&pool, 2).await.is_some()); Trip::delete_by_planned_event(&pool, &cox, &planned_event) .await .unwrap(); assert!(Trip::find_by_id(&pool, 2).await.is_none()); } #[sqlx::test] fn test_succ_delete() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox".into()).await.unwrap(), ) .await .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); trip.delete(&pool, &cox).await.unwrap(); assert!(Trip::find_by_id(&pool, 1).await.is_none()); } #[sqlx::test] fn test_fail_delete_diff_cox() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox2".into()).await.unwrap(), ) .await .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let result = trip .delete(&pool, &cox) .await .expect_err("It should not be possible to delete trips from others"); let expected = TripDeleteError::NotYourTrip; assert_eq!(result, expected); } #[sqlx::test] fn test_fail_delete_someone_registered() { let pool = testdb!(); let cox = SteeringUser::new( &pool, User::find_by_name(&pool, "cox".into()).await.unwrap(), ) .await .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let trip_details = TripDetails::find_by_id(&pool, trip.trip_details_id.unwrap()) .await .unwrap(); let user = User::find_by_name(&pool, "rower".into()).await.unwrap(); UserTrip::create(&pool, &user, &trip_details, None) .await .unwrap(); let result = trip .delete(&pool, &cox) .await .expect_err("It should not be possible to delete trips if somebody already registered"); let expected = TripDeleteError::SomebodyAlreadyRegistered; assert_eq!(result, expected); } }