Add an accommodations reservations module (#188)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Add a new module to enable accommodations reservation  Reviewed-on: #188
This commit is contained in:
115
geneit_backend/src/controllers/accommodations_list_controller.rs
Normal file
115
geneit_backend/src/controllers/accommodations_list_controller.rs
Normal file
@ -0,0 +1,115 @@
|
||||
use crate::constants::StaticConstraints;
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
|
||||
use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
|
||||
use crate::models::Accommodation;
|
||||
use crate::services::accommodations_list_service;
|
||||
use actix_web::{web, HttpResponse};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum AccommodationListControllerErr {
|
||||
#[error("Invalid name length!")]
|
||||
InvalidNameLength,
|
||||
#[error("Invalid description length!")]
|
||||
InvalidDescriptionLength,
|
||||
#[error("Malformed color!")]
|
||||
MalformedColor,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
pub struct AccommodationRequest {
|
||||
pub name: String,
|
||||
pub need_validation: bool,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub open_to_reservations: bool,
|
||||
}
|
||||
|
||||
impl AccommodationRequest {
|
||||
pub async fn to_accommodation(self, accommodation: &mut Accommodation) -> anyhow::Result<()> {
|
||||
let c = StaticConstraints::default();
|
||||
|
||||
if !c.accommodation_name_len.validate(&self.name) {
|
||||
return Err(AccommodationListControllerErr::InvalidNameLength.into());
|
||||
}
|
||||
accommodation.name = self.name;
|
||||
|
||||
if let Some(d) = &self.description {
|
||||
if !c.accommodation_description_len.validate(d) {
|
||||
return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
|
||||
}
|
||||
}
|
||||
accommodation.description.clone_from(&self.description);
|
||||
|
||||
if let Some(c) = &self.color {
|
||||
if !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) {
|
||||
return Err(AccommodationListControllerErr::MalformedColor.into());
|
||||
}
|
||||
}
|
||||
accommodation.color.clone_from(&self.color);
|
||||
|
||||
accommodation.need_validation = self.need_validation;
|
||||
accommodation.open_to_reservations = self.open_to_reservations;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new accommodation
|
||||
pub async fn create(
|
||||
m: FamilyInPathWithAdminMembership,
|
||||
req: web::Json<AccommodationRequest>,
|
||||
) -> HttpResult {
|
||||
let mut accommodation = accommodations_list_service::create(m.family_id()).await?;
|
||||
|
||||
if let Err(e) = req.0.to_accommodation(&mut accommodation).await {
|
||||
log::error!("Failed to apply accommodation information! {e}");
|
||||
accommodations_list_service::delete(&mut accommodation).await?;
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
}
|
||||
|
||||
if let Err(e) = accommodations_list_service::update(&mut accommodation).await {
|
||||
log::error!("Failed to update accommodation information! {e}");
|
||||
accommodations_list_service::delete(&mut accommodation).await?;
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(accommodation))
|
||||
}
|
||||
|
||||
/// Get the full list of accommodations
|
||||
pub async fn get_full_list(m: FamilyInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(accommodations_list_service::get_all_of_family(m.family_id()).await?))
|
||||
}
|
||||
|
||||
/// Get the information of a single accommodation
|
||||
pub async fn get_single(m: FamilyAndAccommodationInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok().json(&m.to_accommodation()))
|
||||
}
|
||||
|
||||
/// Update an accommodation
|
||||
pub async fn update(
|
||||
m: FamilyAndAccommodationInPath,
|
||||
req: web::Json<AccommodationRequest>,
|
||||
_admin: FamilyInPathWithAdminMembership,
|
||||
) -> HttpResult {
|
||||
let mut accommodation = m.to_accommodation();
|
||||
|
||||
if let Err(e) = req.0.to_accommodation(&mut accommodation).await {
|
||||
log::error!("Failed to parse accommodation information! {e}");
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
}
|
||||
|
||||
accommodations_list_service::update(&mut accommodation).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
/// Delete an accommodation
|
||||
pub async fn delete(
|
||||
m: FamilyAndAccommodationInPath,
|
||||
_admin: FamilyInPathWithAdminMembership,
|
||||
) -> HttpResult {
|
||||
accommodations_list_service::delete(&mut m.to_accommodation()).await?;
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
use ical::{generator::*, *};
|
||||
|
||||
use actix_web::{web, HttpResponse};
|
||||
use chrono::DateTime;
|
||||
|
||||
use crate::constants::StaticConstraints;
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::accommodation_reservation_calendar_extractor::FamilyAndAccommodationReservationCalendarInPath;
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{AccommodationID, ReservationStatus};
|
||||
use crate::services::{
|
||||
accommodations_list_service, accommodations_reservations_calendars_service,
|
||||
accommodations_reservations_service, families_service,
|
||||
};
|
||||
use crate::utils::time_utils::time;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateCalendarQuery {
|
||||
accommodation_id: Option<AccommodationID>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// Create a calendar
|
||||
pub async fn create(a: FamilyInPath, req: web::Json<CreateCalendarQuery>) -> HttpResult {
|
||||
let accommodation_id = match req.accommodation_id {
|
||||
Some(i) => {
|
||||
let accommodation = match accommodations_list_service::get_by_id(i).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get accommodation information! {e}");
|
||||
return Ok(HttpResponse::NotFound()
|
||||
.json("The accommodation was not found in the family!"));
|
||||
}
|
||||
};
|
||||
|
||||
if accommodation.family_id() != a.family_id() {
|
||||
return Ok(
|
||||
HttpResponse::NotFound().json("The accommodation was not found in the family!")
|
||||
);
|
||||
}
|
||||
|
||||
Some(accommodation.id())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let conf = StaticConstraints::default();
|
||||
if !conf.accommodation_calendar_name_len.validate(&req.name) {
|
||||
return Ok(HttpResponse::BadRequest().json("Invalid accommodation name!"));
|
||||
}
|
||||
|
||||
let calendar = accommodations_reservations_calendars_service::create(
|
||||
a.user_id(),
|
||||
a.family_id(),
|
||||
accommodation_id,
|
||||
&req.name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(calendar))
|
||||
}
|
||||
|
||||
/// Get the list of calendars of a user
|
||||
pub async fn get_list(a: FamilyInPath) -> HttpResult {
|
||||
let users =
|
||||
accommodations_reservations_calendars_service::get_all_of_user(a.user_id(), a.family_id())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(users))
|
||||
}
|
||||
|
||||
/// Delete a calendar
|
||||
pub async fn delete(resa: FamilyAndAccommodationReservationCalendarInPath) -> HttpResult {
|
||||
accommodations_reservations_calendars_service::delete(resa.to_reservation()).await?;
|
||||
Ok(HttpResponse::Ok().json("Calendar successfully deleted"))
|
||||
}
|
||||
|
||||
fn fmt_date(time: i64) -> String {
|
||||
let res = DateTime::from_timestamp(time, 0).expect("Failed to parse date");
|
||||
|
||||
/*format!(
|
||||
"{:0>4}{:0>2}{:0>2}T{:0>2}{:0>2}",
|
||||
res.year(),
|
||||
res.month(),
|
||||
res.day(),
|
||||
res.minute(),
|
||||
res.second()
|
||||
)*/
|
||||
|
||||
res.format("%Y%m%dT%H%M%S").to_string()
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct AnonymousAccessURL {
|
||||
token: String,
|
||||
}
|
||||
|
||||
/// Get the content of the calendar
|
||||
pub async fn anonymous_access(req: web::Path<AnonymousAccessURL>) -> HttpResult {
|
||||
let mut calendar =
|
||||
match accommodations_reservations_calendars_service::get_by_token(&req.token).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!("Calendar information could not be retrieved: {e}");
|
||||
return Ok(HttpResponse::NotFound().body("Calendar not found!"));
|
||||
}
|
||||
};
|
||||
|
||||
let accommodations =
|
||||
accommodations_list_service::get_all_of_family(calendar.family_id()).await?;
|
||||
let members = families_service::get_memberships_of_family(calendar.family_id()).await?;
|
||||
|
||||
// Get calendar associated events
|
||||
let events = match calendar.accommodation_id() {
|
||||
None => {
|
||||
accommodations_reservations_service::get_all_of_family(calendar.family_id()).await?
|
||||
}
|
||||
Some(a) => accommodations_reservations_service::get_all_of_accommodation(a).await?,
|
||||
};
|
||||
|
||||
let mut cal = IcalCalendarBuilder::version("2.0")
|
||||
.gregorian()
|
||||
.prodid("-//geneit//")
|
||||
.build();
|
||||
|
||||
for ev in events {
|
||||
let accommodation = accommodations
|
||||
.iter()
|
||||
.find(|a| a.id() == ev.accommodation_id())
|
||||
.unwrap();
|
||||
let member_name = members
|
||||
.iter()
|
||||
.find(|a| a.membership.user_id() == ev.user_id())
|
||||
.map(|m| m.user_name.as_str())
|
||||
.unwrap_or("other user");
|
||||
|
||||
let event = IcalEventBuilder::tzid("Europe/Paris")
|
||||
.uid(format!("resa-{}", ev.id().0))
|
||||
.changed(fmt_date(ev.time_update))
|
||||
.start(fmt_date(ev.reservation_start))
|
||||
.end(fmt_date(ev.reservation_end))
|
||||
.set(ical_property!("SUMMARY", member_name))
|
||||
.set(ical_property!("LOCATION", &accommodation.name))
|
||||
.set(ical_property!(
|
||||
"STATUS",
|
||||
match ev.status() {
|
||||
ReservationStatus::Pending => "TENTATIVE",
|
||||
ReservationStatus::Accepted => "CONFIRMED",
|
||||
ReservationStatus::Rejected => "CANCELLED",
|
||||
}
|
||||
))
|
||||
.build();
|
||||
cal.events.push(event);
|
||||
}
|
||||
|
||||
if calendar.should_update_last_used() {
|
||||
calendar.time_used = time() as i64;
|
||||
accommodations_reservations_calendars_service::update(&calendar).await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/calendar")
|
||||
.body(cal.generate()))
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
|
||||
use crate::extractors::accommodation_reservation_extractor::FamilyAndAccommodationReservationInPath;
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{Accommodation, AccommodationReservationID, NewAccommodationReservation};
|
||||
use crate::services::accommodations_reservations_service;
|
||||
use crate::utils::time_utils::time;
|
||||
use actix_web::{web, HttpResponse};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UpdateReservationQuery {
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl UpdateReservationQuery {
|
||||
/// Check whether a reservation request is valid or not
|
||||
async fn validate(
|
||||
&self,
|
||||
a: &Accommodation,
|
||||
resa_id: Option<AccommodationReservationID>,
|
||||
) -> anyhow::Result<Option<&str>> {
|
||||
if !a.open_to_reservations {
|
||||
return Ok(Some(
|
||||
"The accommodation is not open to reservations creation / update!",
|
||||
));
|
||||
}
|
||||
|
||||
if (self.start as i64) < (time() as i64 - 3600 * 24 * 30) {
|
||||
return Ok(Some("Start time is too far in the past!"));
|
||||
}
|
||||
|
||||
if self.start == self.end {
|
||||
return Ok(Some("Start and end time must be different!"));
|
||||
}
|
||||
|
||||
if self.start > self.end {
|
||||
return Ok(Some("End time happens before start time!"));
|
||||
}
|
||||
|
||||
let existing = accommodations_reservations_service::get_reservations_for_time_interval(
|
||||
a.id(),
|
||||
self.start,
|
||||
self.end,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if existing
|
||||
.iter()
|
||||
.any(|r| r.validated != Some(false) && resa_id != Some(r.id()))
|
||||
{
|
||||
return Ok(Some("This reservation is in conflict with another one!"));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a reservation
|
||||
pub async fn create_reservation(
|
||||
a: FamilyAndAccommodationInPath,
|
||||
req: web::Json<UpdateReservationQuery>,
|
||||
) -> HttpResult {
|
||||
if let Some(err) = req.validate(&a, None).await? {
|
||||
return Ok(HttpResponse::BadRequest().json(err));
|
||||
}
|
||||
|
||||
let mut reservation =
|
||||
accommodations_reservations_service::create(&NewAccommodationReservation {
|
||||
family_id: a.family_id().0,
|
||||
accommodation_id: a.id().0,
|
||||
user_id: a.membership().user_id().0,
|
||||
time_create: time() as i64,
|
||||
time_update: time() as i64,
|
||||
reservation_start: req.start as i64,
|
||||
reservation_end: req.end as i64,
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Auto validate reservation if requested
|
||||
if !a.need_validation {
|
||||
reservation.validated = Some(true);
|
||||
|
||||
accommodations_reservations_service::update(&mut reservation).await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(reservation))
|
||||
}
|
||||
|
||||
/// Get the reservations for a given accommodation
|
||||
pub async fn get_accommodation_reservations(a: FamilyAndAccommodationInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(accommodations_reservations_service::get_all_of_accommodation(a.id()).await?))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CheckAvailabilityQuery {
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
/// Check reservation availability
|
||||
pub async fn get_accommodation_reservations_for_interval(
|
||||
a: FamilyAndAccommodationInPath,
|
||||
req: web::Query<CheckAvailabilityQuery>,
|
||||
) -> HttpResult {
|
||||
if req.start > req.end {
|
||||
return Ok(HttpResponse::BadRequest().json("start should be smaller than end!"));
|
||||
}
|
||||
|
||||
let res = accommodations_reservations_service::get_reservations_for_time_interval(
|
||||
a.id(),
|
||||
req.start,
|
||||
req.end,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(res))
|
||||
}
|
||||
|
||||
/// Get the full list of accommodations reservations for a family
|
||||
pub async fn full_list(m: FamilyInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(accommodations_reservations_service::get_all_of_family(m.family_id()).await?))
|
||||
}
|
||||
|
||||
/// Get a single accommodation reservation
|
||||
pub async fn get_single(m: FamilyAndAccommodationReservationInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok().json(m.to_reservation()))
|
||||
}
|
||||
|
||||
/// Update a reservation
|
||||
pub async fn update_single(
|
||||
m: FamilyAndAccommodationReservationInPath,
|
||||
req: web::Json<UpdateReservationQuery>,
|
||||
) -> HttpResult {
|
||||
if let Some(err) = req.validate(m.as_accommodation(), Some(m.id())).await? {
|
||||
return Ok(HttpResponse::BadRequest().json(err));
|
||||
}
|
||||
|
||||
if m.membership().user_id() != m.user_id() {
|
||||
return Ok(
|
||||
HttpResponse::BadRequest().json("Only the owner of a reservation can change it!")
|
||||
);
|
||||
}
|
||||
|
||||
let need_validation = m.as_accommodation().need_validation;
|
||||
|
||||
let mut reservation = m.to_reservation();
|
||||
reservation.reservation_start = req.start as i64;
|
||||
reservation.reservation_end = req.end as i64;
|
||||
|
||||
if need_validation {
|
||||
reservation.validated = None;
|
||||
}
|
||||
|
||||
accommodations_reservations_service::update(&mut reservation).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
/// Delete a reservation
|
||||
pub async fn delete(m: FamilyAndAccommodationReservationInPath) -> HttpResult {
|
||||
if m.membership().user_id() != m.user_id() {
|
||||
return Ok(
|
||||
HttpResponse::BadRequest().json("Only the owner of a reservation can delete it!")
|
||||
);
|
||||
}
|
||||
|
||||
accommodations_reservations_service::delete(m.to_reservation()).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ValidateQuery {
|
||||
validate: bool,
|
||||
}
|
||||
|
||||
/// Validate or reject a reservation
|
||||
pub async fn validate_or_reject(
|
||||
m: FamilyAndAccommodationReservationInPath,
|
||||
q: web::Json<ValidateQuery>,
|
||||
) -> HttpResult {
|
||||
if !m.membership().is_admin {
|
||||
return Ok(
|
||||
HttpResponse::BadRequest().json("Only a family admin can validate a reservation!")
|
||||
);
|
||||
}
|
||||
|
||||
if m.validated == Some(q.validate) {
|
||||
return Ok(
|
||||
HttpResponse::AlreadyReported().json("This reservation has already been processed!")
|
||||
);
|
||||
}
|
||||
|
||||
// In case of re-validation, check that the time is still available
|
||||
if m.validated == Some(false) && q.validate {
|
||||
let potential_conflicts =
|
||||
accommodations_reservations_service::get_reservations_for_time_interval(
|
||||
m.accommodation_id(),
|
||||
m.reservation_start as usize,
|
||||
m.reservation_end as usize,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if potential_conflicts
|
||||
.iter()
|
||||
.any(|a| a.validated != Some(false))
|
||||
{
|
||||
return Ok(HttpResponse::Conflict().json(
|
||||
"This cannot be accepted as it would create a conflict with another reservation!",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Update reservation validation status
|
||||
let mut reservation = m.to_reservation();
|
||||
reservation.validated = Some(q.validate);
|
||||
accommodations_reservations_service::update(&mut reservation).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
@ -80,6 +80,7 @@ struct RichFamilyInfo {
|
||||
#[serde(flatten)]
|
||||
membership: FamilyMembership,
|
||||
enable_genealogy: bool,
|
||||
enable_accommodations: bool,
|
||||
disable_couple_photos: bool,
|
||||
}
|
||||
|
||||
@ -90,6 +91,7 @@ pub async fn single_info(f: FamilyInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok().json(RichFamilyInfo {
|
||||
membership,
|
||||
enable_genealogy: family.enable_genealogy,
|
||||
enable_accommodations: family.enable_accommodations,
|
||||
disable_couple_photos: family.disable_couple_photos,
|
||||
}))
|
||||
}
|
||||
@ -105,6 +107,7 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
|
||||
pub struct UpdateFamilyBody {
|
||||
name: Option<String>,
|
||||
enable_genealogy: Option<bool>,
|
||||
enable_accommodations: Option<bool>,
|
||||
disable_couple_photos: Option<bool>,
|
||||
}
|
||||
|
||||
@ -127,6 +130,10 @@ pub async fn update(
|
||||
family.enable_genealogy = enable_genealogy;
|
||||
}
|
||||
|
||||
if let Some(enable_accommodations) = req.enable_accommodations {
|
||||
family.enable_accommodations = enable_accommodations;
|
||||
}
|
||||
|
||||
if let Some(disable_couple_photos) = req.disable_couple_photos {
|
||||
family.disable_couple_photos = disable_couple_photos;
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ use actix_web::HttpResponse;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use zip::result::ZipError;
|
||||
|
||||
pub mod accommodations_list_controller;
|
||||
pub mod accommodations_reservations_calendars_controller;
|
||||
pub mod accommodations_reservations_controller;
|
||||
pub mod auth_controller;
|
||||
pub mod couples_controller;
|
||||
pub mod data_controller;
|
||||
|
Reference in New Issue
Block a user