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:
37
geneit_backend/Cargo.lock
generated
37
geneit_backend/Cargo.lock
generated
@ -711,8 +711,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
@ -1404,13 +1406,16 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"clap",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"httpdate",
|
||||
"ical",
|
||||
"image",
|
||||
"lazy-regex",
|
||||
"lazy_static",
|
||||
"lettre",
|
||||
"light-openid",
|
||||
@ -1774,6 +1779,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ical"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@ -1939,6 +1953,29 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "lazy-regex"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c"
|
||||
dependencies = [
|
||||
"lazy-regex-proc_macros",
|
||||
"once_cell",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-regex-proc_macros"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.63",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
@ -10,6 +10,7 @@ log = "0.4.21"
|
||||
env_logger = "0.11.3"
|
||||
clap = { version = "4.5.4", features = ["derive", "env"] }
|
||||
lazy_static = "1.4.0"
|
||||
lazy-regex = "3.1.0"
|
||||
anyhow = "1.0.83"
|
||||
actix-web = "4.5.1"
|
||||
actix-cors = "0.7.0"
|
||||
@ -38,3 +39,5 @@ zip = "2.0.0"
|
||||
mime_guess = "2.0.4"
|
||||
tempfile = "3.10.1"
|
||||
base64 = "0.22.0"
|
||||
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
|
||||
chrono = "0.4.38"
|
@ -0,0 +1,6 @@
|
||||
ALTER TABLE public.families
|
||||
DROP COLUMN enable_accommodations;
|
||||
|
||||
DROP TABLE IF EXISTS accommodations_reservations_cals_urls;
|
||||
DROP TABLE IF EXISTS accommodations_reservations;
|
||||
DROP TABLE IF EXISTS accommodations_list;
|
@ -0,0 +1,52 @@
|
||||
-- Add column to toggle accommodations module
|
||||
ALTER TABLE public.families
|
||||
ADD enable_accommodations boolean NOT NULL DEFAULT false;
|
||||
COMMENT
|
||||
ON COLUMN public.families.enable_accommodations IS 'Specify whether accommodations feature is enabled for the family';
|
||||
|
||||
|
||||
-- Create tables
|
||||
CREATE TABLE IF NOT EXISTS accommodations_list
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
family_id integer NOT NULL REFERENCES families,
|
||||
time_create BIGINT NOT NULL,
|
||||
time_update BIGINT NOT NULL,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
need_validation BOOLEAN NOT NULL DEFAULT true,
|
||||
description text NULL,
|
||||
color VARCHAR(6) NULL,
|
||||
open_to_reservations BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN accommodations_list.need_validation is 'true if family admin review is required for validation. False otherwise';
|
||||
COMMENT ON COLUMN accommodations_list.open_to_reservations is 'true if reservations can be created / updated. False otherwise';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accommodations_reservations
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
family_id integer NOT NULL REFERENCES families ON DELETE CASCADE,
|
||||
accommodation_id integer NOT NULL REFERENCES accommodations_list ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
time_create BIGINT NOT NULL,
|
||||
time_update BIGINT NOT NULL,
|
||||
reservation_start BIGINT NOT NULL,
|
||||
reservation_end BIGINT NOT NULL,
|
||||
validated BOOLEAN NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN accommodations_reservations.validated is 'null if not reviewed yet. true if reservation is accepted. false if reservation is rejected';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accommodations_reservations_cals_urls
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
family_id integer NOT NULL REFERENCES families ON DELETE CASCADE,
|
||||
accommodation_id integer NULL REFERENCES accommodations_list ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
token VARCHAR(50) NOT NULL,
|
||||
time_create BIGINT NOT NULL,
|
||||
time_used BIGINT NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN accommodations_reservations_cals_urls.accommodation_id is 'null to get reservations of all accommodations. otherwise get the reservations of the specified accommodation only';
|
@ -60,6 +60,10 @@ pub struct StaticConstraints {
|
||||
pub member_country: SizeConstraint,
|
||||
pub member_sex: SizeConstraint,
|
||||
pub member_note: SizeConstraint,
|
||||
|
||||
pub accommodation_name_len: SizeConstraint,
|
||||
pub accommodation_description_len: SizeConstraint,
|
||||
pub accommodation_calendar_name_len: SizeConstraint,
|
||||
}
|
||||
|
||||
impl Default for StaticConstraints {
|
||||
@ -91,6 +95,10 @@ impl Default for StaticConstraints {
|
||||
member_country: SizeConstraint::new(0, 2),
|
||||
member_sex: SizeConstraint::new(0, 1),
|
||||
member_note: SizeConstraint::new(0, 35000),
|
||||
|
||||
accommodation_name_len: SizeConstraint::new(1, 50),
|
||||
accommodation_description_len: SizeConstraint::new(0, 500),
|
||||
accommodation_calendar_name_len: SizeConstraint::new(2, 50),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -134,3 +142,10 @@ pub const THUMB_WIDTH: u32 = 350;
|
||||
|
||||
/// Thumbnail height
|
||||
pub const THUMB_HEIGHT: u32 = 350;
|
||||
|
||||
/// Accommodations reservations calendars tokens len
|
||||
pub const ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN: usize = 50;
|
||||
|
||||
/// Minimum interval before calendar used time update
|
||||
pub const ACCOMMODATIONS_RESERVATIONS_CAL_URL_TIME_USED_UPDATE_MIN_INTERVAL: Duration =
|
||||
Duration::from_secs(60);
|
||||
|
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;
|
||||
|
83
geneit_backend/src/extractors/accommodation_extractor.rs
Normal file
83
geneit_backend/src/extractors/accommodation_extractor.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{Accommodation, AccommodationID, FamilyID, Membership};
|
||||
use crate::services::accommodations_list_service;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum AccommodationExtractorErr {
|
||||
#[error("Accommodation {0:?} does not belong to family {1:?}!")]
|
||||
AccommodationNotInFamily(AccommodationID, FamilyID),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FamilyAndAccommodationInPath(Membership, Accommodation);
|
||||
|
||||
impl FamilyAndAccommodationInPath {
|
||||
async fn load_accommodation_from_path(
|
||||
family: FamilyInPath,
|
||||
accommodation_id: AccommodationID,
|
||||
) -> anyhow::Result<Self> {
|
||||
let accommodation = accommodations_list_service::get_by_id(accommodation_id).await?;
|
||||
if accommodation.family_id() != family.family_id() {
|
||||
return Err(AccommodationExtractorErr::AccommodationNotInFamily(
|
||||
accommodation.id(),
|
||||
family.family_id(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(Self(family.into(), accommodation))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FamilyAndAccommodationInPath {
|
||||
type Target = Accommodation;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl FamilyAndAccommodationInPath {
|
||||
pub fn membership(&self) -> &Membership {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_accommodation(self) -> Accommodation {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AccommodationIDInPath {
|
||||
accommodation_id: AccommodationID,
|
||||
}
|
||||
|
||||
impl FromRequest for FamilyAndAccommodationInPath {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
Box::pin(async move {
|
||||
let family = FamilyInPath::extract(&req).await?;
|
||||
|
||||
let accommodation_id = actix_web::web::Path::<AccommodationIDInPath>::from_request(
|
||||
&req,
|
||||
&mut Payload::None,
|
||||
)
|
||||
.await?
|
||||
.accommodation_id;
|
||||
|
||||
Self::load_accommodation_from_path(family, accommodation_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to extract accommodation ID from URL! {}", e);
|
||||
actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{
|
||||
AccommodationReservationCalendar, AccommodationReservationCalendarID, FamilyID, Membership,
|
||||
};
|
||||
use crate::services::accommodations_reservations_calendars_service;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum AccommodationCalendarExtractorErr {
|
||||
#[error("Calendar {0:?} does not belong to user or family {1:?}!")]
|
||||
CalendarNotOfUserOrFamily(AccommodationReservationCalendarID, FamilyID),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FamilyAndAccommodationReservationCalendarInPath(
|
||||
Membership,
|
||||
AccommodationReservationCalendar,
|
||||
);
|
||||
|
||||
impl FamilyAndAccommodationReservationCalendarInPath {
|
||||
async fn load_calendar_from_path(
|
||||
family: FamilyInPath,
|
||||
calendar_id: AccommodationReservationCalendarID,
|
||||
) -> anyhow::Result<Self> {
|
||||
let accommodation =
|
||||
accommodations_reservations_calendars_service::get_by_id(calendar_id).await?;
|
||||
if accommodation.family_id() != family.family_id()
|
||||
|| accommodation.user_id() != family.user_id()
|
||||
{
|
||||
return Err(
|
||||
AccommodationCalendarExtractorErr::CalendarNotOfUserOrFamily(
|
||||
accommodation.id(),
|
||||
family.family_id(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self(family.into(), accommodation))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FamilyAndAccommodationReservationCalendarInPath {
|
||||
type Target = AccommodationReservationCalendar;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl FamilyAndAccommodationReservationCalendarInPath {
|
||||
pub fn membership(&self) -> &Membership {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_reservation(self) -> AccommodationReservationCalendar {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AccommodationIDInPath {
|
||||
cal_id: AccommodationReservationCalendarID,
|
||||
}
|
||||
|
||||
impl FromRequest for FamilyAndAccommodationReservationCalendarInPath {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
Box::pin(async move {
|
||||
let family = FamilyInPath::extract(&req).await?;
|
||||
|
||||
let accommodation_id = actix_web::web::Path::<AccommodationIDInPath>::from_request(
|
||||
&req,
|
||||
&mut Payload::None,
|
||||
)
|
||||
.await?
|
||||
.cal_id;
|
||||
|
||||
Self::load_calendar_from_path(family, accommodation_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to extract calendar ID from URL! {}", e);
|
||||
actix_web::error::ErrorNotFound("Could not fetch calendar information!")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{
|
||||
Accommodation, AccommodationReservation, AccommodationReservationID, FamilyID, Membership,
|
||||
};
|
||||
use crate::services::{accommodations_list_service, accommodations_reservations_service};
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum AccommodationReservationExtractorErr {
|
||||
#[error("Accommodation reservation {0:?} does not belong to family {1:?}!")]
|
||||
AccommodationNotInFamily(AccommodationReservationID, FamilyID),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FamilyAndAccommodationReservationInPath(
|
||||
Membership,
|
||||
Accommodation,
|
||||
AccommodationReservation,
|
||||
);
|
||||
|
||||
impl FamilyAndAccommodationReservationInPath {
|
||||
async fn load_accommodation_reservation_from_path(
|
||||
family: FamilyInPath,
|
||||
reservation_id: AccommodationReservationID,
|
||||
) -> anyhow::Result<Self> {
|
||||
let reservation = accommodations_reservations_service::get_by_id(reservation_id).await?;
|
||||
let accommodation =
|
||||
accommodations_list_service::get_by_id(reservation.accommodation_id()).await?;
|
||||
|
||||
if accommodation.family_id() != family.family_id()
|
||||
|| reservation.family_id() != family.family_id()
|
||||
{
|
||||
return Err(
|
||||
AccommodationReservationExtractorErr::AccommodationNotInFamily(
|
||||
reservation.id(),
|
||||
family.family_id(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self(family.into(), accommodation, reservation))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FamilyAndAccommodationReservationInPath {
|
||||
type Target = AccommodationReservation;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.2
|
||||
}
|
||||
}
|
||||
|
||||
impl FamilyAndAccommodationReservationInPath {
|
||||
pub fn membership(&self) -> &Membership {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn as_accommodation(&self) -> &Accommodation {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn to_accommodation(self) -> Accommodation {
|
||||
self.1
|
||||
}
|
||||
|
||||
pub fn to_reservation(self) -> AccommodationReservation {
|
||||
self.2
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReservationIDInPath {
|
||||
reservation_id: AccommodationReservationID,
|
||||
}
|
||||
|
||||
impl FromRequest for FamilyAndAccommodationReservationInPath {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
Box::pin(async move {
|
||||
let family = FamilyInPath::extract(&req).await?;
|
||||
|
||||
let reservation_id =
|
||||
actix_web::web::Path::<ReservationIDInPath>::from_request(&req, &mut Payload::None)
|
||||
.await?
|
||||
.reservation_id;
|
||||
|
||||
Self::load_accommodation_reservation_from_path(family, reservation_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to extract accommodation ID from URL! {}", e);
|
||||
actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
pub mod accommodation_extractor;
|
||||
pub mod accommodation_reservation_calendar_extractor;
|
||||
pub mod accommodation_reservation_extractor;
|
||||
pub mod couple_extractor;
|
||||
pub mod family_extractor;
|
||||
pub mod member_extractor;
|
||||
|
@ -6,8 +6,10 @@ use actix_web::{web, App, HttpServer};
|
||||
use geneit_backend::app_config::AppConfig;
|
||||
use geneit_backend::connections::{db_connection, s3_connection};
|
||||
use geneit_backend::controllers::{
|
||||
auth_controller, couples_controller, data_controller, families_controller, members_controller,
|
||||
photos_controller, server_controller, users_controller,
|
||||
accommodations_list_controller, accommodations_reservations_calendars_controller,
|
||||
accommodations_reservations_controller, auth_controller, couples_controller, data_controller,
|
||||
families_controller, members_controller, photos_controller, server_controller,
|
||||
users_controller,
|
||||
};
|
||||
|
||||
#[actix_web::main]
|
||||
@ -204,6 +206,79 @@ async fn main() -> std::io::Result<()> {
|
||||
"/family/{id}/genealogy/data/import",
|
||||
web::put().to(data_controller::import_family),
|
||||
)
|
||||
// [ACCOMODATIONS] List controller
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/create",
|
||||
web::post().to(accommodations_list_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/list",
|
||||
web::get().to(accommodations_list_controller::get_full_list),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::get().to(accommodations_list_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::put().to(accommodations_list_controller::update),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::delete().to(accommodations_list_controller::delete),
|
||||
)
|
||||
// [ACCOMODATIONS] Reservations controller
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}",
|
||||
web::get()
|
||||
.to(accommodations_reservations_controller::get_accommodation_reservations),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval",
|
||||
web::get()
|
||||
.to(accommodations_reservations_controller::get_accommodation_reservations_for_interval),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations/full_list",
|
||||
web::get().to(accommodations_reservations_controller::full_list),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/create",
|
||||
web::post().to(accommodations_reservations_controller::create_reservation),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::get().to(accommodations_reservations_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::patch().to(accommodations_reservations_controller::update_single),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::delete().to(accommodations_reservations_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}/validate",
|
||||
web::post().to(accommodations_reservations_controller::validate_or_reject),
|
||||
)
|
||||
// [ACCOMMODATIONS] Calendars controller
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations_calendars/create",
|
||||
web::post().to(accommodations_reservations_calendars_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations_calendars/list",
|
||||
web::get().to(accommodations_reservations_calendars_controller::get_list),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations_calendars/{cal_id}",
|
||||
web::delete().to(accommodations_reservations_calendars_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/acccommodations_calendar/{token}",
|
||||
web::get().to(accommodations_reservations_calendars_controller::anonymous_access),
|
||||
)
|
||||
// Photos controller
|
||||
.route(
|
||||
"/photo/{id}",
|
||||
|
@ -1,6 +1,11 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::schema::{couples, families, members, memberships, photos, users};
|
||||
use crate::constants;
|
||||
use crate::schema::{
|
||||
accommodations_list, accommodations_reservations, accommodations_reservations_cals_urls,
|
||||
couples, families, members, memberships, photos, users,
|
||||
};
|
||||
use crate::utils::crypt_utils::sha256;
|
||||
use crate::utils::time_utils::time;
|
||||
use diesel::prelude::*;
|
||||
|
||||
/// User ID holder
|
||||
@ -66,6 +71,7 @@ pub struct Family {
|
||||
pub invitation_code: String,
|
||||
pub disable_couple_photos: bool,
|
||||
pub enable_genealogy: bool,
|
||||
pub enable_accommodations: bool,
|
||||
}
|
||||
|
||||
impl Family {
|
||||
@ -308,7 +314,7 @@ pub struct NewMember {
|
||||
pub time_update: i64,
|
||||
}
|
||||
|
||||
/// Member ID holder
|
||||
/// Couple ID holder
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||
pub struct CoupleID(pub i32);
|
||||
|
||||
@ -441,3 +447,153 @@ pub struct NewCouple {
|
||||
pub time_create: i64,
|
||||
pub time_update: i64,
|
||||
}
|
||||
|
||||
/// Accommodation ID holder
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||
pub struct AccommodationID(pub i32);
|
||||
|
||||
#[derive(Queryable, Debug, serde::Serialize)]
|
||||
pub struct Accommodation {
|
||||
id: i32,
|
||||
family_id: i32,
|
||||
time_create: i64,
|
||||
pub time_update: i64,
|
||||
pub name: String,
|
||||
pub need_validation: bool,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub open_to_reservations: bool,
|
||||
}
|
||||
|
||||
impl Accommodation {
|
||||
pub fn id(&self) -> AccommodationID {
|
||||
AccommodationID(self.id)
|
||||
}
|
||||
|
||||
pub fn family_id(&self) -> FamilyID {
|
||||
FamilyID(self.family_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = accommodations_list)]
|
||||
pub struct NewAccommodation {
|
||||
pub family_id: i32,
|
||||
pub name: String,
|
||||
pub time_create: i64,
|
||||
pub time_update: i64,
|
||||
}
|
||||
|
||||
/// Accommodation reservation ID holder
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||
pub struct AccommodationReservationID(pub i32);
|
||||
|
||||
pub enum ReservationStatus {
|
||||
Pending,
|
||||
Accepted,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Debug, serde::Serialize)]
|
||||
pub struct AccommodationReservation {
|
||||
id: i32,
|
||||
family_id: i32,
|
||||
accommodation_id: i32,
|
||||
user_id: i32,
|
||||
time_create: i64,
|
||||
pub time_update: i64,
|
||||
pub reservation_start: i64,
|
||||
pub reservation_end: i64,
|
||||
pub validated: Option<bool>,
|
||||
}
|
||||
|
||||
impl AccommodationReservation {
|
||||
pub fn id(&self) -> AccommodationReservationID {
|
||||
AccommodationReservationID(self.id)
|
||||
}
|
||||
|
||||
pub fn accommodation_id(&self) -> AccommodationID {
|
||||
AccommodationID(self.accommodation_id)
|
||||
}
|
||||
|
||||
pub fn family_id(&self) -> FamilyID {
|
||||
FamilyID(self.family_id)
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> UserID {
|
||||
UserID(self.user_id)
|
||||
}
|
||||
|
||||
pub fn status(&self) -> ReservationStatus {
|
||||
match self.validated {
|
||||
None => ReservationStatus::Pending,
|
||||
Some(true) => ReservationStatus::Accepted,
|
||||
Some(false) => ReservationStatus::Rejected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = accommodations_reservations)]
|
||||
pub struct NewAccommodationReservation {
|
||||
pub family_id: i32,
|
||||
pub accommodation_id: i32,
|
||||
pub user_id: i32,
|
||||
pub time_create: i64,
|
||||
pub time_update: i64,
|
||||
pub reservation_start: i64,
|
||||
pub reservation_end: i64,
|
||||
}
|
||||
|
||||
/// Accommodation reservation calendar ID holder
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||
pub struct AccommodationReservationCalendarID(pub i32);
|
||||
|
||||
#[derive(Queryable, Debug, serde::Serialize)]
|
||||
pub struct AccommodationReservationCalendar {
|
||||
id: i32,
|
||||
family_id: i32,
|
||||
accommodation_id: Option<i32>,
|
||||
user_id: i32,
|
||||
name: String,
|
||||
token: String,
|
||||
pub time_create: i64,
|
||||
pub time_used: i64,
|
||||
}
|
||||
|
||||
impl AccommodationReservationCalendar {
|
||||
pub fn id(&self) -> AccommodationReservationCalendarID {
|
||||
AccommodationReservationCalendarID(self.id)
|
||||
}
|
||||
|
||||
pub fn accommodation_id(&self) -> Option<AccommodationID> {
|
||||
self.accommodation_id.map(AccommodationID)
|
||||
}
|
||||
|
||||
pub fn family_id(&self) -> FamilyID {
|
||||
FamilyID(self.family_id)
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> UserID {
|
||||
UserID(self.user_id)
|
||||
}
|
||||
|
||||
pub fn should_update_last_used(&self) -> bool {
|
||||
(self.time_used
|
||||
+ constants::ACCOMMODATIONS_RESERVATIONS_CAL_URL_TIME_USED_UPDATE_MIN_INTERVAL.as_secs()
|
||||
as i64)
|
||||
< time() as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = accommodations_reservations_cals_urls)]
|
||||
pub struct NewAccommodationReservationCalendar {
|
||||
pub family_id: i32,
|
||||
pub accommodation_id: Option<i32>,
|
||||
pub user_id: i32,
|
||||
pub name: String,
|
||||
pub token: String,
|
||||
pub time_create: i64,
|
||||
pub time_used: i64,
|
||||
}
|
||||
|
@ -1,5 +1,50 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
accommodations_list (id) {
|
||||
id -> Int4,
|
||||
family_id -> Int4,
|
||||
time_create -> Int8,
|
||||
time_update -> Int8,
|
||||
#[max_length = 50]
|
||||
name -> Varchar,
|
||||
need_validation -> Bool,
|
||||
description -> Nullable<Text>,
|
||||
#[max_length = 6]
|
||||
color -> Nullable<Varchar>,
|
||||
open_to_reservations -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
accommodations_reservations (id) {
|
||||
id -> Int4,
|
||||
family_id -> Int4,
|
||||
accommodation_id -> Int4,
|
||||
user_id -> Int4,
|
||||
time_create -> Int8,
|
||||
time_update -> Int8,
|
||||
reservation_start -> Int8,
|
||||
reservation_end -> Int8,
|
||||
validated -> Nullable<Bool>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
accommodations_reservations_cals_urls (id) {
|
||||
id -> Int4,
|
||||
family_id -> Int4,
|
||||
accommodation_id -> Nullable<Int4>,
|
||||
user_id -> Int4,
|
||||
#[max_length = 50]
|
||||
name -> Varchar,
|
||||
#[max_length = 50]
|
||||
token -> Varchar,
|
||||
time_create -> Int8,
|
||||
time_used -> Int8,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
couples (id) {
|
||||
id -> Int4,
|
||||
@ -30,6 +75,7 @@ diesel::table! {
|
||||
invitation_code -> Varchar,
|
||||
disable_couple_photos -> Bool,
|
||||
enable_genealogy -> Bool,
|
||||
enable_accommodations -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +165,13 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(accommodations_list -> families (family_id));
|
||||
diesel::joinable!(accommodations_reservations -> accommodations_list (accommodation_id));
|
||||
diesel::joinable!(accommodations_reservations -> families (family_id));
|
||||
diesel::joinable!(accommodations_reservations -> users (user_id));
|
||||
diesel::joinable!(accommodations_reservations_cals_urls -> accommodations_list (accommodation_id));
|
||||
diesel::joinable!(accommodations_reservations_cals_urls -> families (family_id));
|
||||
diesel::joinable!(accommodations_reservations_cals_urls -> users (user_id));
|
||||
diesel::joinable!(couples -> families (family_id));
|
||||
diesel::joinable!(couples -> photos (photo_id));
|
||||
diesel::joinable!(members -> families (family_id));
|
||||
@ -127,6 +180,9 @@ diesel::joinable!(memberships -> families (family_id));
|
||||
diesel::joinable!(memberships -> users (user_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
accommodations_list,
|
||||
accommodations_reservations,
|
||||
accommodations_reservations_cals_urls,
|
||||
couples,
|
||||
families,
|
||||
members,
|
||||
|
103
geneit_backend/src/services/accommodations_list_service.rs
Normal file
103
geneit_backend/src/services/accommodations_list_service.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use crate::connections::db_connection;
|
||||
use crate::models::{Accommodation, AccommodationID, FamilyID, NewAccommodation};
|
||||
use crate::schema::accommodations_list;
|
||||
use crate::utils::time_utils::time;
|
||||
use diesel::prelude::*;
|
||||
|
||||
/// Create a new accommodation
|
||||
pub async fn create(family_id: FamilyID) -> anyhow::Result<Accommodation> {
|
||||
db_connection::execute(|conn| {
|
||||
let res: Accommodation = diesel::insert_into(accommodations_list::table)
|
||||
.values(&NewAccommodation {
|
||||
family_id: family_id.0,
|
||||
name: "".to_string(),
|
||||
time_create: time() as i64,
|
||||
time_update: time() as i64,
|
||||
})
|
||||
.get_result(conn)?;
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the information of an accommodation
|
||||
pub async fn get_by_id(id: AccommodationID) -> anyhow::Result<Accommodation> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_list::table
|
||||
.filter(accommodations_list::dsl::id.eq(id.0))
|
||||
.first(conn)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all the accommodations of a family
|
||||
pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result<Vec<Accommodation>> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_list::table
|
||||
.filter(accommodations_list::dsl::family_id.eq(id.0))
|
||||
.get_results(conn)
|
||||
})
|
||||
}
|
||||
|
||||
/// Check whether accommodation with a given id exists or not
|
||||
pub async fn exists(
|
||||
family_id: FamilyID,
|
||||
accommodation_id: AccommodationID,
|
||||
) -> anyhow::Result<bool> {
|
||||
db_connection::execute(|conn| {
|
||||
let count: i64 = accommodations_list::table
|
||||
.filter(
|
||||
accommodations_list::id
|
||||
.eq(accommodation_id.0)
|
||||
.and(accommodations_list::family_id.eq(family_id.0)),
|
||||
)
|
||||
.count()
|
||||
.get_result(conn)?;
|
||||
|
||||
Ok(count != 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the information of an accommodation
|
||||
pub async fn update(accommodation: &mut Accommodation) -> anyhow::Result<()> {
|
||||
accommodation.time_update = time() as i64;
|
||||
|
||||
db_connection::execute(|conn| {
|
||||
diesel::update(
|
||||
accommodations_list::dsl::accommodations_list
|
||||
.filter(accommodations_list::dsl::id.eq(accommodation.id().0)),
|
||||
)
|
||||
.set((
|
||||
accommodations_list::dsl::time_update.eq(accommodation.time_update),
|
||||
accommodations_list::dsl::name.eq(accommodation.name.to_string()),
|
||||
accommodations_list::dsl::need_validation.eq(accommodation.need_validation),
|
||||
accommodations_list::dsl::description.eq(accommodation.description.clone()),
|
||||
accommodations_list::dsl::color.eq(accommodation.color.clone()),
|
||||
accommodations_list::dsl::open_to_reservations.eq(accommodation.open_to_reservations),
|
||||
))
|
||||
.execute(conn)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete an accommodation
|
||||
pub async fn delete(accommodation: &mut Accommodation) -> anyhow::Result<()> {
|
||||
// Remove the accommodation
|
||||
db_connection::execute(|conn| {
|
||||
diesel::delete(
|
||||
accommodations_list::dsl::accommodations_list
|
||||
.filter(accommodations_list::dsl::id.eq(accommodation.id().0)),
|
||||
)
|
||||
.execute(conn)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all the accommodations of a family
|
||||
pub async fn delete_all_family(family_id: FamilyID) -> anyhow::Result<()> {
|
||||
for mut m in get_all_of_family(family_id).await? {
|
||||
delete(&mut m).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
use crate::connections::db_connection;
|
||||
use crate::constants;
|
||||
use crate::models::{
|
||||
AccommodationID, AccommodationReservationCalendar, AccommodationReservationCalendarID,
|
||||
FamilyID, NewAccommodationReservationCalendar, UserID,
|
||||
};
|
||||
use crate::schema::accommodations_reservations_cals_urls;
|
||||
use crate::utils::string_utils::rand_str;
|
||||
use crate::utils::time_utils::time;
|
||||
use diesel::prelude::*;
|
||||
|
||||
/// Create a new reservation calendar entry
|
||||
pub async fn create(
|
||||
user_id: UserID,
|
||||
family_id: FamilyID,
|
||||
accommodation_id: Option<AccommodationID>,
|
||||
name: &str,
|
||||
) -> anyhow::Result<AccommodationReservationCalendar> {
|
||||
db_connection::execute(|conn| {
|
||||
let res: AccommodationReservationCalendar =
|
||||
diesel::insert_into(accommodations_reservations_cals_urls::table)
|
||||
.values(&NewAccommodationReservationCalendar {
|
||||
family_id: family_id.0,
|
||||
accommodation_id: accommodation_id.map(|i| i.0),
|
||||
user_id: user_id.0,
|
||||
name: name.to_string(),
|
||||
token: rand_str(constants::ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN),
|
||||
time_create: time() as i64,
|
||||
time_used: time() as i64,
|
||||
})
|
||||
.get_result(conn)?;
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the information of a reservations calendar
|
||||
pub async fn update(cal: &AccommodationReservationCalendar) -> anyhow::Result<()> {
|
||||
db_connection::execute(|conn| {
|
||||
diesel::update(
|
||||
accommodations_reservations_cals_urls::dsl::accommodations_reservations_cals_urls
|
||||
.filter(accommodations_reservations_cals_urls::dsl::id.eq(cal.id().0)),
|
||||
)
|
||||
.set((accommodations_reservations_cals_urls::dsl::time_used.eq(cal.time_used),))
|
||||
.execute(conn)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all the calendars of a user
|
||||
pub async fn get_all_of_user(
|
||||
user: UserID,
|
||||
family: FamilyID,
|
||||
) -> anyhow::Result<Vec<AccommodationReservationCalendar>> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_reservations_cals_urls::table
|
||||
.filter(
|
||||
accommodations_reservations_cals_urls::dsl::family_id
|
||||
.eq(family.0)
|
||||
.and(accommodations_reservations_cals_urls::dsl::user_id.eq(user.0)),
|
||||
)
|
||||
.get_results(conn)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a single calendar by its id
|
||||
pub async fn get_by_id(
|
||||
id: AccommodationReservationCalendarID,
|
||||
) -> anyhow::Result<AccommodationReservationCalendar> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_reservations_cals_urls::table
|
||||
.filter(accommodations_reservations_cals_urls::dsl::id.eq(id.0))
|
||||
.get_result(conn)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a single calendar by its token
|
||||
pub async fn get_by_token(token: &str) -> anyhow::Result<AccommodationReservationCalendar> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_reservations_cals_urls::table
|
||||
.filter(accommodations_reservations_cals_urls::dsl::token.eq(token))
|
||||
.get_result(conn)
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete a calendar
|
||||
pub async fn delete(r: AccommodationReservationCalendar) -> anyhow::Result<()> {
|
||||
db_connection::execute(|conn| {
|
||||
diesel::delete(
|
||||
accommodations_reservations_cals_urls::dsl::accommodations_reservations_cals_urls
|
||||
.filter(accommodations_reservations_cals_urls::dsl::id.eq(r.id().0)),
|
||||
)
|
||||
.execute(conn)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
use crate::connections::db_connection;
|
||||
use crate::models::{
|
||||
AccommodationID, AccommodationReservation, AccommodationReservationID, FamilyID,
|
||||
NewAccommodationReservation,
|
||||
};
|
||||
use crate::schema::accommodations_reservations;
|
||||
use crate::utils::time_utils::time;
|
||||
use diesel::prelude::*;
|
||||
|
||||
/// Create a new reservation
|
||||
pub async fn create(new: &NewAccommodationReservation) -> anyhow::Result<AccommodationReservation> {
|
||||
db_connection::execute(|conn| {
|
||||
let res: AccommodationReservation = diesel::insert_into(accommodations_reservations::table)
|
||||
.values(new)
|
||||
.get_result(conn)?;
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
/// Update a reservation
|
||||
pub async fn update(r: &mut AccommodationReservation) -> anyhow::Result<()> {
|
||||
r.time_update = time() as i64;
|
||||
|
||||
db_connection::execute(|conn| {
|
||||
diesel::update(
|
||||
accommodations_reservations::dsl::accommodations_reservations
|
||||
.filter(accommodations_reservations::dsl::id.eq(r.id().0)),
|
||||
)
|
||||
.set((
|
||||
accommodations_reservations::dsl::time_update.eq(r.time_update),
|
||||
accommodations_reservations::dsl::validated.eq(r.validated),
|
||||
accommodations_reservations::dsl::reservation_start.eq(r.reservation_start),
|
||||
accommodations_reservations::dsl::reservation_end.eq(r.reservation_end),
|
||||
))
|
||||
.execute(conn)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a reservation
|
||||
pub async fn delete(r: AccommodationReservation) -> anyhow::Result<()> {
|
||||
// Remove the reservation
|
||||
db_connection::execute(|conn| {
|
||||
diesel::delete(
|
||||
accommodations_reservations::dsl::accommodations_reservations
|
||||
.filter(accommodations_reservations::dsl::id.eq(r.id().0)),
|
||||
)
|
||||
.execute(conn)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all the reservations of an accommodation
|
||||
pub async fn get_all_of_accommodation(
|
||||
id: AccommodationID,
|
||||
) -> anyhow::Result<Vec<AccommodationReservation>> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_reservations::table
|
||||
.filter(accommodations_reservations::dsl::accommodation_id.eq(id.0))
|
||||
.get_results(conn)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all the reservations of a family
|
||||
pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result<Vec<AccommodationReservation>> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_reservations::table
|
||||
.filter(accommodations_reservations::dsl::family_id.eq(id.0))
|
||||
.get_results(conn)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a single accommodation reservation by its id
|
||||
pub async fn get_by_id(id: AccommodationReservationID) -> anyhow::Result<AccommodationReservation> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_reservations::table
|
||||
.filter(accommodations_reservations::dsl::id.eq(id.0))
|
||||
.get_result(conn)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the reservations that are between a given interval of time for a given accommodation
|
||||
pub async fn get_reservations_for_time_interval(
|
||||
id: AccommodationID,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> anyhow::Result<Vec<AccommodationReservation>> {
|
||||
db_connection::execute(|conn| {
|
||||
accommodations_reservations::table
|
||||
.filter(
|
||||
accommodations_reservations::dsl::accommodation_id
|
||||
.eq(id.0)
|
||||
.and(accommodations_reservations::dsl::reservation_start.lt((end) as i64))
|
||||
.and(accommodations_reservations::dsl::reservation_end.gt((start) as i64)),
|
||||
)
|
||||
.get_results(conn)
|
||||
})
|
||||
}
|
@ -5,7 +5,9 @@ use crate::models::{
|
||||
Family, FamilyID, FamilyMembership, Membership, NewFamily, NewMembership, UserID,
|
||||
};
|
||||
use crate::schema::{families, memberships};
|
||||
use crate::services::{couples_service, members_service, users_service};
|
||||
use crate::services::{
|
||||
accommodations_list_service, couples_service, members_service, users_service,
|
||||
};
|
||||
use crate::utils::string_utils::rand_str;
|
||||
use crate::utils::time_utils::time;
|
||||
use diesel::prelude::*;
|
||||
@ -127,9 +129,9 @@ pub async fn update_membership(membership: &Membership) -> anyhow::Result<()> {
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct FamilyMember {
|
||||
#[serde(flatten)]
|
||||
membership: Membership,
|
||||
user_name: String,
|
||||
user_mail: String,
|
||||
pub membership: Membership,
|
||||
pub user_name: String,
|
||||
pub user_mail: String,
|
||||
}
|
||||
|
||||
/// Get information about the users of a family
|
||||
@ -175,6 +177,7 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> {
|
||||
families::dsl::name.eq(family.name.clone()),
|
||||
families::dsl::invitation_code.eq(family.invitation_code.clone()),
|
||||
families::dsl::enable_genealogy.eq(family.enable_genealogy),
|
||||
families::dsl::enable_accommodations.eq(family.enable_accommodations),
|
||||
families::dsl::disable_couple_photos.eq(family.disable_couple_photos),
|
||||
))
|
||||
.execute(conn)
|
||||
@ -185,6 +188,9 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> {
|
||||
|
||||
/// Delete a family
|
||||
pub async fn delete_family(family_id: FamilyID) -> anyhow::Result<()> {
|
||||
// Delete all family accommodations
|
||||
accommodations_list_service::delete_all_family(family_id).await?;
|
||||
|
||||
// Delete all family couples
|
||||
couples_service::delete_all_family(family_id).await?;
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
//! # Backend services
|
||||
|
||||
pub mod accommodations_list_service;
|
||||
pub mod accommodations_reservations_calendars_service;
|
||||
pub mod accommodations_reservations_service;
|
||||
pub mod couples_service;
|
||||
pub mod families_service;
|
||||
pub mod login_token_service;
|
||||
|
Reference in New Issue
Block a user