import * as mongoose from 'mongoose' import { v4 as uuidv4 } from 'uuid' import { dateFormatBackend, getDays, nowInTz } from '../helpers/date' import { createCalendarEvent, deleteCalendarEvent } from '../lib/googlecalendar' import { Bill } from './bill' import { BOOKING_STATUS, VALIDATION_ERRORS } from './enums' import { getBookedDays } from './index' export type Booking = { uuid: string name: string email: string phone: string street: string zip: string city: string bill?: Bill startDate: string endDate: string status?: BOOKING_STATUS purpose?: string org?: string destination?: string days?: string[], calendarEventId: string, } export type BookingDocument = Booking & mongoose.Document & mongoose.SchemaTimestampsConfig export type BookingModel = mongoose.Model & { findBookedDays(uuidsToIngore?: string[]): Promise } const BookingSchema = new mongoose.Schema( { // need a seperate uuid to be able to target a booking anonimously uuid: { type: String, default: uuidv4, index: true, }, name: { type: String, required: true }, email: { type: String, required: true, minlength: 5 }, phone: { type: String, required: false }, street: { type: String, required: true }, zip: { type: String, required: true }, city: { type: String, required: true }, bill: { type: mongoose.Schema.Types.ObjectId, ref: 'Bill', required: false, }, startDate: { type: Date, required: true, get: dateFormatBackend, validate: { validator: function (v: Date): boolean { return v >= nowInTz() }, message: (props: { value: Date }): string => `${props.value} is in the past`, }, }, endDate: { type: Date, required: false, get: dateFormatBackend, validate: { validator: function (v: Date): boolean { return v >= nowInTz() }, message: (props: { value: Date }): string => `${props.value} is in the past`, }, }, days: { type: [String], required: true, validate: { validator: async function (days: string[]): Promise { const booking = this as Booking const uuid = booking.uuid && [booking.uuid] const bookedDays = await getBookedDays(uuid) const doubleBookedDays = days.filter((day: string): boolean => bookedDays.includes(day) ) return doubleBookedDays.length === 0 }, message: (props: { value: string[] }): string => `At least one day is of ${props.value.join(',')} is already booked`, type: VALIDATION_ERRORS.AT_LEAST_ONE_DAY_BOOKED, }, }, status: { type: String, enum: Object.values(BOOKING_STATUS), required: true, default: 'requested', }, purpose: { type: String, required: false }, org: { type: String, required: false }, destination: { type: String, required: false }, calendarEventId: { type: String, required: false }, }, { timestamps: true, toJSON: { virtuals: true, getters: true }, toObject: { virtuals: true, getters: true }, } ) BookingSchema.pre('validate', function (next: () => void): void { const booking = this as BookingDocument booking.days = getDays({ startDate: new Date(booking.startDate), endDate: new Date(booking.endDate), }) next() }) BookingSchema.pre('save', async function (next: () => void): Promise { const booking = this as BookingDocument if (!booking.calendarEventId) { // create calendar event before saving to database await createCalendarEvent(booking); } else if ([BOOKING_STATUS.CANCELED, BOOKING_STATUS.REJECTED].includes(booking.status)) { // event has been canceled or rejected, delete calendar event again to free up the slot await deleteCalendarEvent(booking); } next(); }); BookingSchema.static('findBookedDays', async function ( uuidsToIngore: string[] = [] ): Promise { const model = this as BookingModel const now = nowInTz() const bookedDays = await model .find( { status: { $in: [BOOKING_STATUS.REQUESTED, BOOKING_STATUS.CONFIRMED] }, uuid: { $nin: uuidsToIngore }, // dateFormatBackend uses YYYY-MM-DD, which is startOfDay anyway endDate: { $gt: dateFormatBackend(now) }, }, 'days' ) .exec() return bookedDays .map((b) => b.days) .flat() .sort() }) export default (mongoose.models.Booking || mongoose.model('Booking', BookingSchema)) as BookingModel