use chrono::NaiveDate; use serde::Serialize; use sqlx::SqlitePool; use super::{ planned_event::{PlannedEvent, Registration}, tripdetails::TripDetails, triptype::TripType, user::CoxUser, }; #[derive(Serialize, Clone, Debug)] pub struct Trip { id: i64, cox_id: i64, cox_name: String, trip_details_id: Option, planned_starting_time: String, pub max_people: i64, day: String, pub notes: Option, pub allow_guests: bool, trip_type_id: Option, always_show: bool, } #[derive(Serialize)] pub struct TripWithUserAndType { #[serde(flatten)] pub trip: Trip, rower: Vec, trip_type: Option, } impl Trip { /// Cox decides to create own trip. pub async fn new_own(db: &SqlitePool, cox: &CoxUser, trip_details: TripDetails) { let _ = sqlx::query!( "INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)", cox.id, trip_details.id ) .execute(db) .await; } 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, notes, allow_guests, trip_type_id, always_show 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 planned event. pub async fn new_join( db: &SqlitePool, cox: &CoxUser, planned_event: &PlannedEvent, ) -> Result<(), CoxHelpError> { if planned_event.is_rower_registered(db, cox).await { return Err(CoxHelpError::AlreadyRegisteredAsRower); } match sqlx::query!( "INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)", cox.id, planned_event.id ) .execute(db) .await { Ok(_) => Ok(()), Err(_) => Err(CoxHelpError::AlreadyRegisteredAsCox), } } 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, notes, allow_guests, trip_type_id, always_show 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 { 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; } ret.push(TripWithUserAndType { rower: trip.get_all_rower(db).await, trip, trip_type, }); } ret } async fn get_all_rower(&self, db: &SqlitePool) -> Vec { sqlx::query_as!( Registration, " SELECT (SELECT name FROM user WHERE user_trip.user_id = user.id) as name, (SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at, (SELECT is_guest FROM user WHERE user_trip.user_id = user.id) as is_guest FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM trip WHERE id = ?)", self.id ) .fetch_all(db) .await .unwrap() //Okay, as Trip can only be created with proper DB backing } /// Cox decides to update own trip. pub async fn update_own( db: &SqlitePool, cox: &CoxUser, trip: &Trip, max_people: i32, notes: Option<&str>, trip_type: Option, //TODO: Move to `TripType` always_show: bool, ) -> Result<(), TripUpdateError> { if !trip.is_trip_from_user(cox.id) { return Err(TripUpdateError::NotYourTrip); } let trip_details = sqlx::query!( "SELECT trip_details_id as id FROM trip WHERE id = ?", trip.id ) .fetch_one(db) .await .unwrap(); //Okay, as trip can only be created with proper DB backing let Some(trip_details_id) = trip_details.id else { return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? }; sqlx::query!( "UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ? WHERE id = ?", max_people, notes, trip_type, always_show, trip_details_id ) .execute(db) .await .unwrap(); //Okay, as trip_details can only be created with proper DB backing Ok(()) } pub async fn delete_by_planned_event( db: &SqlitePool, cox: &CoxUser, planned_event: &PlannedEvent, ) -> bool { sqlx::query!( "DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?", cox.id, planned_event.id ) .execute(db) .await .unwrap() .rows_affected() > 0 } pub(crate) async fn delete( &self, db: &SqlitePool, user: &CoxUser, ) -> Result<(), TripDeleteError> { let registered_rower = self.get_all_rower(db).await; if !registered_rower.is_empty() { return Err(TripDeleteError::SomebodyAlreadyRegistered); } if !self.is_trip_from_user(user.id) { return Err(TripDeleteError::NotYourTrip); } sqlx::query!( "DELETE FROM trip WHERE cox_id = ? AND id = ?", user.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 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 } } #[derive(Debug)] pub enum CoxHelpError { AlreadyRegisteredAsRower, AlreadyRegisteredAsCox, } #[derive(Debug, PartialEq)] pub enum TripDeleteError { SomebodyAlreadyRegistered, NotYourTrip, } #[derive(Debug)] pub enum TripUpdateError { NotYourTrip, TripDetailsDoesNotExist, } #[cfg(test)] mod test { use crate::{ model::{ planned_event::PlannedEvent, trip::TripDeleteError, tripdetails::TripDetails, user::{CoxUser, User}, usertrip::UserTrip, }, testdb, }; use chrono::NaiveDate; use sqlx::SqlitePool; use super::Trip; #[sqlx::test] fn test_new_own() { let pool = testdb!(); let cox: CoxUser = User::find_by_name(&pool, "cox".into()) .await .unwrap() .try_into() .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_get_day_cox_trip() { let pool = testdb!(); let res = Trip::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()).await; assert_eq!(res.len(), 1); } #[sqlx::test] fn test_new_succ_join() { let pool = testdb!(); let cox: CoxUser = User::find_by_name(&pool, "cox2".into()) .await .unwrap() .try_into() .unwrap(); let planned_event = PlannedEvent::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: CoxUser = User::find_by_name(&pool, "cox2".into()) .await .unwrap() .try_into() .unwrap(); let planned_event = PlannedEvent::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: CoxUser = User::find_by_name(&pool, "cox".into()) .await .unwrap() .try_into() .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); assert!(Trip::update_own(&pool, &cox, &trip, 10, None, None, false) .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: CoxUser = User::find_by_name(&pool, "cox".into()) .await .unwrap() .try_into() .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); assert!( Trip::update_own(&pool, &cox, &trip, 10, None, Some(1), false) .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: CoxUser = User::find_by_name(&pool, "cox2".into()) .await .unwrap() .try_into() .unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap(); assert!(Trip::update_own(&pool, &cox, &trip, 10, None, None, false) .await .is_err()); assert_eq!(trip.max_people, 1); } #[sqlx::test] fn test_succ_delete_by_planned_event() { let pool = testdb!(); let cox: CoxUser = User::find_by_name(&pool, "cox".into()) .await .unwrap() .try_into() .unwrap(); let planned_event = PlannedEvent::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; assert!(Trip::find_by_id(&pool, 2).await.is_none()); } #[sqlx::test] fn test_succ_delete() { let pool = testdb!(); let cox: CoxUser = User::find_by_name(&pool, "cox".into()) .await .unwrap() .try_into() .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: CoxUser = User::find_by_name(&pool, "cox2".into()) .await .unwrap() .try_into() .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: CoxUser = User::find_by_name(&pool, "cox".into()) .await .unwrap() .try_into() .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).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); } }