Skip to content

Commit

Permalink
Concurrent place reservation (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlueHorn07 authored Dec 27, 2023
1 parent 17f113c commit fd492fd
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 24 deletions.
1 change: 1 addition & 0 deletions src/popo/place/place.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class PlaceDto {
readonly region: PlaceRegion;
readonly staff_email: string;
readonly max_minutes: number;
readonly max_concurrent_reservation: number;
readonly opening_hours: string;
readonly enable_auto_accept: PlaceEnableAutoAccept;
}
Expand Down
3 changes: 3 additions & 0 deletions src/popo/place/place.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export class Place extends BaseEntity {
@Column({ default: 24 * 60 })
max_minutes: number;

@Column({ default: 1 })
max_concurrent_reservation: number;

@Column('text', { default: '{"Everyday":"00:00-24:00"}' })
opening_hours: string;
// if null, there's no rule for opening hours.
Expand Down
11 changes: 4 additions & 7 deletions src/popo/reservation/equip/reserve.equip.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class ReserveEquipController {
}
return `Sync Done: ${equipmentList.length} Equipments`;
}

@Get('count')
count() {
return this.reserveEquipService.count();
Expand Down Expand Up @@ -186,13 +186,10 @@ export class ReserveEquipController {
@UseGuards(JwtAuthGuard, RolesGuard)
async patchStatus(
@Param('uuid') uuid: string,
@Param('status') status: string,
@Param('status') status: ReservationStatus,
@Query('sendEmail') sendEmail?: boolean,
) {
const response = await this.reserveEquipService.updateStatus(
uuid,
ReservationStatus[status],
);
const response = await this.reserveEquipService.updateStatus(uuid, status);

if (sendEmail) {
// Send e-mail to client.
Expand All @@ -201,7 +198,7 @@ export class ReserveEquipController {
await this.mailService.sendReservationPatchMail(
response.email,
response.title,
ReservationStatus[status],
status,
);
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/popo/reservation/place/reserve.place.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { Roles } from '../../../auth/authroization/roles.decorator';
import { RolesGuard } from '../../../auth/authroization/roles.guard';
import { PlaceService } from '../../place/place.service';
import {JwtPayload} from "../../../auth/strategies/jwt.payload";
import { ReservePlace } from './reserve.place.entity';

@ApiTags('Place Reservation')
@Controller('reservation-place')
Expand Down Expand Up @@ -196,8 +197,16 @@ export class ReservePlaceController {
@Body() body: AcceptPlaceReservationListDto,
@Query('sendEmail') sendEmail?: string,
) {
const reservations: ReservePlace[] = [];
for (const reservation_uuid of body.uuid_list) {
const reservation = await this.reservePlaceService.findOneByUuidOrFail(reservation_uuid);
reservations.push(reservation);
}

// early created reservation should be processed first
reservations.sort((a, b) => (a.created_at > b.created_at ? 1 : -1));

for (const reservation of reservations) {
await this.reservePlaceService.checkReservationPossible(
{
place_id: reservation.place_id,
Expand All @@ -208,7 +217,7 @@ export class ReservePlaceController {
reservation.booker_id,
)
const response = await this.reservePlaceService.updateStatus(
reservation_uuid,
reservation.uuid,
ReservationStatus.accept,
);

Expand Down
104 changes: 88 additions & 16 deletions src/popo/reservation/place/reserve.place.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ReservePlace } from './reserve.place.entity';
import { DeepPartial, In, LessThan, MoreThan, MoreThanOrEqual, Repository } from 'typeorm'
import { DeepPartial, In, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, Repository } from 'typeorm'
import { CreateReservePlaceDto } from './reserve.place.dto';
import { UserService } from '../../user/user.service';
import { PlaceService } from '../../place/place.service';
Expand Down Expand Up @@ -29,6 +29,7 @@ export class ReservePlaceService {
private readonly placeService: PlaceService,
) {}

// TODO: delete this code, after concurrent check logic is fully validated
async isReservationOverlap(
place_id: string,
date: string,
Expand Down Expand Up @@ -56,6 +57,68 @@ export class ReservePlaceService {
return null;
}

async isReservationConcurrent(
place_id: string,
max_concurrent_reservation: number,
date: string,
start_time: string,
end_time: string
): Promise<boolean> {
const booked_reservations = await this.reservePlaceRepo.find({
where: {
place_id: place_id,
date: date,
status: ReservationStatus.accept,
start_time: LessThan(end_time),
end_time: MoreThan(start_time),
},
order: {
start_time: 'ASC',
}
});

function _get_concurrent_cnt_at_time(time: string) {
let cnt = 0;
for (const reservation of booked_reservations) {
if (reservation.start_time <= time && time <= reservation.end_time) {
cnt += 1;
}
}
return cnt;
}

// 1. check start time reservation is possible
if (_get_concurrent_cnt_at_time(start_time) >= max_concurrent_reservation) {
return false;
}

// 2. check end time reservation is possible
if (_get_concurrent_cnt_at_time(end_time) >= max_concurrent_reservation) {
return false;
}

// 3. check middle time reservation is possible: they should be less than max_concurrent_reservation
for (const reservation of booked_reservations) {
// handled on case 1
if (reservation.start_time < start_time)
continue;

// handled on case 2
if (reservation.end_time > end_time)
continue;

if (_get_concurrent_cnt_at_time(reservation.start_time) >= max_concurrent_reservation) {
return false;
}

if (_get_concurrent_cnt_at_time(reservation.end_time) >= max_concurrent_reservation) {
return false;
}
}

return true;
}

async checkReservationPossible(dto: DeepPartial<CreateReservePlaceDto>, booker_id: string) {
const { place_id, date, start_time, end_time } = dto;

Expand All @@ -69,17 +132,26 @@ export class ReservePlaceService {

const targetPlace = await this.placeService.findOneByUuidOrFail(place_id);

// TODO: delete this code, after concurrent check logic is fully validated
// Reservation Overlap Check
const isReservationOverlap = await this.isReservationOverlap(
place_id,
date,
start_time,
end_time,
);
if (isReservationOverlap) {
// const isReservationOverlap = await this.isReservationOverlap(
// place_id,
// date,
// start_time,
// end_time,
// );
// if (isReservationOverlap) {
// throw new BadRequestException(
// `${Message.OVERLAP_RESERVATION}: ${isReservationOverlap.date} ${isReservationOverlap.start_time} ~ ${isReservationOverlap.end_time}`
// );
// }

// Reservation Concurrent Check
const isConcurrentPossible = await this.isReservationConcurrent(place_id, targetPlace.max_concurrent_reservation, date, start_time, end_time);
if (!isConcurrentPossible) {
throw new BadRequestException(
`${Message.OVERLAP_RESERVATION}: ${isReservationOverlap.date} ${isReservationOverlap.start_time} ~ ${isReservationOverlap.end_time}`
);
`"${targetPlace.name}" 장소에 이미 승인된 ${targetPlace.max_concurrent_reservation}개 예약이 있어 ${date} ${start_time} ~ ${end_time}에는 예약이 불가능 합니다.`
)
}

// Reservation Duration Check
Expand All @@ -92,18 +164,18 @@ export class ReservePlaceService {
newReservationMinutes > targetPlace.max_minutes
) {
throw new BadRequestException(
`${Message.OVER_MAX_RESERVATION_TIME}: max ${targetPlace.max_minutes} mins, new ${newReservationMinutes} mins`,
`${Message.OVER_MAX_RESERVATION_TIME}: "${targetPlace.name}" 장소는 하루 최대 ${targetPlace.max_minutes}분 동안 예약할 수 있습니다. 신규 예약은 ${newReservationMinutes}분으로 최대 예약 시간을 초과합니다.`,
);
}

const booker = await this.userService.findOneByUuidOrFail(booker_id);

if (
targetPlace.region === PlaceRegion.residential_college &&
targetPlace.region === PlaceRegion.residential_college &&
!(booker.userType === UserType.rc_student || booker.userType === UserType.admin)
) {
throw new BadRequestException(
`This place is only available for RC students.`
`"${targetPlace.name}" 장소는 RC 학생만 예약할 수 있습니다.`
)
}

Expand Down Expand Up @@ -131,9 +203,9 @@ export class ReservePlaceService {
) {
throw new BadRequestException(
`${Message.OVER_MAX_RESERVATION_TIME}: `
+ `최대 예약 가능 ${targetPlace.max_minutes}분 중에서 `
+ `"${targetPlace.name}" 장소에 대해 하루 최대 예약 가능한 ${targetPlace.max_minutes}분 중에서 `
+ `오늘(${date}) ${totalReservationMinutes}분을 이미 예약했습니다. `
+ `신규로 ${newReservationMinutes} 예약하는 것은 불가능합니다.`,
+ `신규로 ${newReservationMinutes}분을 예약하는 것은 불가능합니다.`,
);
}
}
Expand Down

0 comments on commit fd492fd

Please sign in to comment.