import { CreateBooking , Booking , mapBooking , RestoreBooking } from './model' ; import { Config } from '../app-config' ; import { createBooking as dbCreate , BookingsModel } from '../db/bookings' ; import { parse , format , parseISO } from 'date-fns' ; import { getAvailableDates , dateStartOfWeek } from '../dates' ; import { incrementOfficeBookingCount , decrementOfficeBookingCount , getOfficeBookings , } from '../db/officeBookings' ; import { incrementUserBookingCount , decrementUserBookingCount } from '../db/userBookings' ; import { Forbidden , HttpError } from '../errors' ; import { User , getUser } from '../users/model' ; import { SES } from 'aws-sdk' ; const audit = ( step , details ? ) => console . info ( JSON . stringify ( { level : 'AUDIT' , action : 'CreateBooking' , step , details , } ) ) ; const sendNotificationEmail = async ( emailAddress , fromAddress , date , user , office , reasonToBook ) => { const formattedDate = format ( parseISO ( date ) , 'dd/MM/yyyy' ) ; const params = { Destination : { ToAddresses : [ emailAddress ] } , Message : { Body : { Text : { Charset : 'UTF-8' , Data : `Date: ${ formattedDate }\nUser: ${ user }\nOffice: ${ office }\n\nReason for booking:\n${ reasonToBook }` , } , } , Subject : { Charset : 'UTF-8' , Data : `Office Booker - ${ formattedDate } | ${ user } | ${ office }` , } , } , Source : fromAddress , } ; const ses = new SES ( ) ; return await ses . sendEmail ( params ) . promise ( ) ; } ; export const createBooking = async ( config , currentUser , request ) : Promise < Booking > => { const isAuthorised = request . user === currentUser . email || currentUser . permissions . canManageAllBookings || currentUser . permissions . officesCanManageBookingsFor . find ( ( office ) => office . id === request . office . id ) !== undefined ; if ( ! isAuthorised ) { throw new Forbidden ( ) ; } const parsed = parse ( request . date , 'yyyy-MM-dd' , new Date ( ) ) ; if ( Number . isNaN ( parsed . getTime ( ) ) ) { throw new HttpError ( { internalMessage : `Invalid date format: ${ request . date }` , status : 400 , httpMessage : 'Invalid date format' , } ) ; } if ( ! getAvailableDates ( config ) . includes ( request . date ) ) { throw new HttpError ( { internalMessage : `Date out of range: ${ request . date }` , status : 400 , httpMessage : 'Date out of range' , } ) ; } const requestedOffice = config . officeQuotas . find ( ( office ) => office . id === request . office . id ) ; if ( ! requestedOffice ) { throw new HttpError ( { internalMessage : `Office not found: ${ request . office }` , status : 400 , httpMessage : 'Office not found' , } ) ; } const configureSendNotification = async ( ) => { const { fromAddress , notificationToAddress , reasonToBookRequired } = config ; if ( ! notificationToAddress ) { return async ( ) => { } ; } const dbUser = await getUser ( config , request . user . toLocaleLowerCase ( ) ) ; if ( dbUser . autoApproved ) { return async ( ) => { } ; } if ( fromAddress === undefined ) { throw Error ( `Missing required env parameters for reason notifications: FROM_ADDRESS` ) ; } if ( reasonToBookRequired && ! request . reasonToBook ) { throw new HttpError ( { httpMessage : `Invalid reason to book given` , status : 400 , internalMessage : `No reason to book provided` , } ) ; } const reasonToBook = request . reasonToBook ? request . reasonToBook : 'No reason provided' ; return async ( ) => { if ( config . env !== 'test' ) { await sendNotificationEmail ( notificationToAddress , fromAddress , request . date , request . user , requestedOffice . name , reasonToBook ) ; } } ; } ; const sendNotificationIfRequired = await configureSendNotification ( ) ; const id = requestedOffice . id + '_' + request . date . replace ( /-/gi , '' ) ; const newBooking = < BookingsModel > { id , parking : request . parking ?? false , officeId : requestedOffice . id , date : request . date , user : request . user , ... ( 'created' in request ? { id : request . id , created : request . created } : { } ) , } ; const userEmail = newBooking . user . toLocaleLowerCase ( ) ; const startOfWeek = dateStartOfWeek ( newBooking . date ) ; const officeBookings = await getOfficeBookings ( config , requestedOffice . name , [ newBooking . date ] ) ; const isQuotaExceeded = officeBookings [ 0 ] ?. bookingCount >= requestedOffice . quota ; const isParkingExceeded = newBooking . parking && officeBookings [ 0 ] ?. parkingCount >= requestedOffice . parkingQuota ; if ( isQuotaExceeded || isParkingExceeded ) { const whichExceeded = isQuotaExceeded && isParkingExceeded ? 'Office and parking quota' : isQuotaExceeded ? 'Office quota' : 'Office parking quota' ; throw new HttpError ( { internalMessage : `${ whichExceeded } has exceeded for ${ requestedOffice . name } on date: ${ newBooking . date }` , status : 409 , httpMessage : `${ whichExceeded } exceeded` , } ) ; } audit ( '1:IncrementingOfficeBookingCount' , { newBooking , startOfWeek , currentUser } ) ; const officeBookedSuccessfully = await incrementOfficeBookingCount ( config , requestedOffice , newBooking . date , newBooking . parking ) ; if ( ! officeBookedSuccessfully ) { const parkingInternalMessageAddition = newBooking . parking ? ` or parking quota of ${ requestedOffice . parkingQuota }` : '' ; const parkingMessageAddition = newBooking . parking ? ` or parking quota` : '' ; throw new HttpError ( { internalMessage : `Office quota of ${ requestedOffice . quota }${ parkingInternalMessageAddition } has exceeded for ${ requestedOffice . name } on date: ${ newBooking . date }` , status : 409 , httpMessage : `Office quota${ parkingMessageAddition } exceeded` , } ) ; } audit ( '2:IncrementingUserBookingCount' ) ; const dbUser = await getUser ( config , userEmail ) ; const userBookedSuccessfully = await incrementUserBookingCount ( config , userEmail , dbUser . quota , startOfWeek ) ; if ( ! userBookedSuccessfully ) { audit ( '2.1:DecrementingOfficeBookingCount' ) ; await decrementOfficeBookingCount ( config , requestedOffice . id , newBooking . date , newBooking . parking ) ; throw new HttpError ( { internalMessage : `User quota of ${ dbUser . quota } has exceeded for ${ userEmail } on date: ${ newBooking . date }` , status : 409 , httpMessage : 'User quota exceeded' , } ) ; } audit ( '3:CreatingBooking' ) ; const createdBooking = await dbCreate ( config , newBooking ) ; if ( createdBooking === undefined ) { try { audit ( '3.1:DecremetingUserBookingCount' ) ; await decrementUserBookingCount ( config , newBooking . user , startOfWeek ) ; audit ( '3.1:DecremetingOfficeBookingCount' ) ; await decrementOfficeBookingCount ( config , requestedOffice . id , newBooking . date , newBooking . parking ) ; throw new HttpError ( { internalMessage : `Duplicate booking found for ${ userEmail } on date: ${ newBooking . date }` , status : 409 , httpMessage : `Can't have multiple bookings per day` , } ) ; } catch ( err ) { if ( err instanceof HttpError ) { throw err ; } throw new HttpError ( { internalMessage : `Failed while rollowing back duplicate booking found for ${ userEmail } on date: ${ newBooking . date }\n${ err . message }` , level : 'ERROR' , status : 409 , httpMessage : `Can't have multiple bookings per day` , } ) ; } } audit ( '4:Completed' ) ; await sendNotificationIfRequired ( ) ; return mapBooking ( config , createdBooking ) ; } ;