Adding clean-up of waiting list entries when deleting a reservation that has been wait-listed
This commit is contained in:
parent
c26868a49a
commit
8f4b6bca44
5 changed files with 155 additions and 18 deletions
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddWaitingListIdToReservation1693469174909
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddWaitingListIdToReservation1693469174909'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_reservations" ("id" varchar PRIMARY KEY NOT NULL, "username" varchar(64) NOT NULL, "password" varchar(255) NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0), "waitingListId" integer)`,
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_reservations"("id", "username", "password", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed") SELECT "id", "username", "password", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed" FROM "reservations"`,
|
||||||
|
)
|
||||||
|
await queryRunner.query(`DROP TABLE "reservations"`)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_reservations" RENAME TO "reservations"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "reservations" RENAME TO "temporary_reservations"`,
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "reservations" ("id" varchar PRIMARY KEY NOT NULL, "username" varchar(64) NOT NULL, "password" varchar(255) NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0))`,
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "reservations"("id", "username", "password", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed") SELECT "id", "username", "password", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed" FROM "temporary_reservations"`,
|
||||||
|
)
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_reservations"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,6 +68,9 @@ export class Reservation {
|
||||||
@Column('boolean', { default: false })
|
@Column('boolean', { default: false })
|
||||||
waitListed: boolean
|
waitListed: boolean
|
||||||
|
|
||||||
|
@Column('int', { nullable: true })
|
||||||
|
waitingListId: number
|
||||||
|
|
||||||
constructor(partial: Partial<Reservation>) {
|
constructor(partial: Partial<Reservation>) {
|
||||||
Object.assign(this, partial)
|
Object.assign(this, partial)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Inject, Injectable } from '@nestjs/common'
|
||||||
import { InjectRepository } from '@nestjs/typeorm'
|
import { InjectRepository } from '@nestjs/typeorm'
|
||||||
import { Repository } from 'typeorm'
|
import { Repository } from 'typeorm'
|
||||||
|
|
||||||
import dayjs from '../common/dayjs'
|
import dayjs from '../common/dayjs'
|
||||||
|
import { BaanReserverenService } from '../runner/baanreserveren/service'
|
||||||
import { Reservation } from './entity'
|
import { Reservation } from './entity'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -10,6 +11,9 @@ export class ReservationsService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Reservation)
|
@InjectRepository(Reservation)
|
||||||
private reservationsRepository: Repository<Reservation>,
|
private reservationsRepository: Repository<Reservation>,
|
||||||
|
|
||||||
|
@Inject(BaanReserverenService)
|
||||||
|
private readonly brService: BaanReserverenService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getAll() {
|
async getAll() {
|
||||||
|
|
@ -45,6 +49,10 @@ export class ReservationsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteById(id: string) {
|
async deleteById(id: string) {
|
||||||
return await this.reservationsRepository.delete({ id })
|
const reservation = await this.getById(id)
|
||||||
|
if (!reservation) return
|
||||||
|
|
||||||
|
await this.brService.removeReservationFromWaitList(reservation)
|
||||||
|
return await this.reservationsRepository.delete({ id: reservation.id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,12 @@ export class ReservationsWorker {
|
||||||
|
|
||||||
async addReservationToWaitList(reservation: Reservation) {
|
async addReservationToWaitList(reservation: Reservation) {
|
||||||
try {
|
try {
|
||||||
await this.brService.addReservationToWaitList(reservation)
|
const waitingListId = await this.brService.addReservationToWaitList(
|
||||||
|
reservation,
|
||||||
|
)
|
||||||
await this.reservationsService.update(reservation.id, {
|
await this.reservationsService.update(reservation.id, {
|
||||||
waitListed: true,
|
waitListed: true,
|
||||||
|
waitingListId,
|
||||||
})
|
})
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.loggerService.error(
|
this.loggerService.error(
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export class BaanReserverenService {
|
||||||
private startSession(username: string) {
|
private startSession(username: string) {
|
||||||
this.loggerService.debug('Starting session', { username })
|
this.loggerService.debug('Starting session', { username })
|
||||||
if (this.session && this.session.username !== username) {
|
if (this.session && this.session.username !== username) {
|
||||||
throw new Error('Session already started')
|
throw new SessionStartError('Session already started')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.session?.username === username) {
|
if (this.session?.username === username) {
|
||||||
|
|
@ -160,7 +160,7 @@ export class BaanReserverenService {
|
||||||
this.loggerService.debug('Navigating to day', { date })
|
this.loggerService.debug('Navigating to day', { date })
|
||||||
if (this.getLastVisibleDay().isBefore(date)) {
|
if (this.getLastVisibleDay().isBefore(date)) {
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector('td.month.next')
|
.waitForSelector('td.month.next')
|
||||||
.then((d) => d?.click())
|
.then((d) => d?.click())
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
this.loggerService.error('Failed to switch months', { error: e })
|
this.loggerService.error('Failed to switch months', { error: e })
|
||||||
|
|
@ -168,7 +168,7 @@ export class BaanReserverenService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector(
|
.waitForSelector(
|
||||||
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
||||||
'date',
|
'date',
|
||||||
)}`,
|
)}`,
|
||||||
|
|
@ -179,7 +179,7 @@ export class BaanReserverenService {
|
||||||
throw new RunnerNavigationDayError(e)
|
throw new RunnerNavigationDayError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector(
|
.waitForSelector(
|
||||||
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
||||||
'date',
|
'date',
|
||||||
)}.selected`,
|
)}.selected`,
|
||||||
|
|
@ -210,6 +210,53 @@ export class BaanReserverenService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async recordWaitingListEntries(): Promise<number[]> {
|
||||||
|
const waitingListEntriesElements = await this.page.$$(
|
||||||
|
'#content tbody tr td:nth-child(1)',
|
||||||
|
)
|
||||||
|
if (!waitingListEntriesElements) return []
|
||||||
|
|
||||||
|
const waitingListIds = (
|
||||||
|
await Promise.all(
|
||||||
|
waitingListEntriesElements.map(async (e) => {
|
||||||
|
const elementTextContent = await (
|
||||||
|
await e.getProperty('textContent')
|
||||||
|
).jsonValue()
|
||||||
|
|
||||||
|
if (elementTextContent) {
|
||||||
|
return Number.parseInt(elementTextContent)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((id): id is number => id != null && typeof id === 'number')
|
||||||
|
|
||||||
|
return waitingListIds
|
||||||
|
}
|
||||||
|
|
||||||
|
private findNewWaitingListEntryId(
|
||||||
|
previous: number[],
|
||||||
|
current: number[],
|
||||||
|
): number | null {
|
||||||
|
const previousSet = new Set(previous)
|
||||||
|
for (const c of current) {
|
||||||
|
if (!previousSet.has(c)) return c
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteWaitingListEntryRowById(id: number) {
|
||||||
|
const rows = await this.page.$x(`//td[text()="${id}"]/parent::tr`)
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new WaitingListEntryDeletionError(
|
||||||
|
'Cannot find waiting list entry to delete',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteButton = await rows[0].$('a.wl-delete')
|
||||||
|
await deleteButton?.click()
|
||||||
|
}
|
||||||
|
|
||||||
private async openWaitingListDialog() {
|
private async openWaitingListDialog() {
|
||||||
this.loggerService.debug('Opening waiting list dialog')
|
this.loggerService.debug('Opening waiting list dialog')
|
||||||
await this.page.waitForNetworkIdle()
|
await this.page.waitForNetworkIdle()
|
||||||
|
|
@ -231,7 +278,7 @@ export class BaanReserverenService {
|
||||||
const timeString = possibleDate.format('HH:mm')
|
const timeString = possibleDate.format('HH:mm')
|
||||||
const selector =
|
const selector =
|
||||||
`tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']`
|
`tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']`
|
||||||
freeCourt = await this.page?.$(selector)
|
freeCourt = await this.page.$(selector)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,7 +296,7 @@ export class BaanReserverenService {
|
||||||
private async selectOpponent(id: string, name: string) {
|
private async selectOpponent(id: string, name: string) {
|
||||||
this.loggerService.debug('Selecting opponent', { id, name })
|
this.loggerService.debug('Selecting opponent', { id, name })
|
||||||
const player2Search = await this.page
|
const player2Search = await this.page
|
||||||
?.waitForSelector('input:has(~ select[name="players[2]"])')
|
.waitForSelector('input:has(~ select[name="players[2]"])')
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchError(e)
|
throw new RunnerOpponentSearchError(e)
|
||||||
})
|
})
|
||||||
|
|
@ -258,11 +305,11 @@ export class BaanReserverenService {
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchInputError(e)
|
throw new RunnerOpponentSearchInputError(e)
|
||||||
})
|
})
|
||||||
await this.page?.waitForNetworkIdle().catch((e: Error) => {
|
await this.page.waitForNetworkIdle().catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchNetworkError(e)
|
throw new RunnerOpponentSearchNetworkError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
?.$('select.br-user-select[name="players[2]"]')
|
.$('select.br-user-select[name="players[2]"]')
|
||||||
.then((d) => d?.select(id))
|
.then((d) => d?.select(id))
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchSelectionError(e)
|
throw new RunnerOpponentSearchSelectionError(e)
|
||||||
|
|
@ -272,13 +319,13 @@ export class BaanReserverenService {
|
||||||
private async confirmReservation() {
|
private async confirmReservation() {
|
||||||
this.loggerService.debug('Confirming reservation')
|
this.loggerService.debug('Confirming reservation')
|
||||||
await this.page
|
await this.page
|
||||||
?.$('input#__make_submit')
|
.$('input#__make_submit')
|
||||||
.then((b) => b?.click())
|
.then((b) => b?.click())
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerReservationConfirmButtonError(e)
|
throw new RunnerReservationConfirmButtonError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector('input#__make_submit2')
|
.waitForSelector('input#__make_submit2')
|
||||||
.then((b) => b?.click())
|
.then((b) => b?.click())
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerReservationConfirmSubmitError(e)
|
throw new RunnerReservationConfirmSubmitError(e)
|
||||||
|
|
@ -287,7 +334,7 @@ export class BaanReserverenService {
|
||||||
|
|
||||||
private async inputWaitingListDetails(reservation: Reservation) {
|
private async inputWaitingListDetails(reservation: Reservation) {
|
||||||
this.loggerService.debug('Inputting waiting list details')
|
this.loggerService.debug('Inputting waiting list details')
|
||||||
const startDateInput = await this.page?.$('input[name="start_date"]')
|
const startDateInput = await this.page.$('input[name="start_date"]')
|
||||||
// Click 3 times to select all existing text
|
// Click 3 times to select all existing text
|
||||||
await startDateInput?.click({ count: 3, delay: 10 }).catch((e) => {
|
await startDateInput?.click({ count: 3, delay: 10 }).catch((e) => {
|
||||||
throw new RunnerWaitingListInputError(e)
|
throw new RunnerWaitingListInputError(e)
|
||||||
|
|
@ -300,7 +347,7 @@ export class BaanReserverenService {
|
||||||
throw new RunnerWaitingListInputError(e)
|
throw new RunnerWaitingListInputError(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
const endDateInput = await this.page?.$('input[name="end_date"]')
|
const endDateInput = await this.page.$('input[name="end_date"]')
|
||||||
await endDateInput
|
await endDateInput
|
||||||
?.type(reservation.dateRangeEnd.format('DD-MM-YYYY'), {
|
?.type(reservation.dateRangeEnd.format('DD-MM-YYYY'), {
|
||||||
delay: this.getTypingDelay(),
|
delay: this.getTypingDelay(),
|
||||||
|
|
@ -309,7 +356,7 @@ export class BaanReserverenService {
|
||||||
throw new RunnerWaitingListInputError(e)
|
throw new RunnerWaitingListInputError(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
const startTimeInput = await this.page?.$('input[name="start_time"]')
|
const startTimeInput = await this.page.$('input[name="start_time"]')
|
||||||
await startTimeInput
|
await startTimeInput
|
||||||
?.type(reservation.dateRangeStart.format('HH:mm'), {
|
?.type(reservation.dateRangeStart.format('HH:mm'), {
|
||||||
delay: this.getTypingDelay(),
|
delay: this.getTypingDelay(),
|
||||||
|
|
@ -319,7 +366,7 @@ export class BaanReserverenService {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Use the same time for start and end so that the waiting list only notifies for start time
|
// Use the same time for start and end so that the waiting list only notifies for start time
|
||||||
const endTimeInput = await this.page?.$('input[name="end_time"]')
|
const endTimeInput = await this.page.$('input[name="end_time"]')
|
||||||
await endTimeInput
|
await endTimeInput
|
||||||
?.type(reservation.dateRangeStart.add(1, 'minutes').format('HH:mm'), {
|
?.type(reservation.dateRangeStart.add(1, 'minutes').format('HH:mm'), {
|
||||||
delay: this.getTypingDelay(),
|
delay: this.getTypingDelay(),
|
||||||
|
|
@ -331,7 +378,7 @@ export class BaanReserverenService {
|
||||||
|
|
||||||
private async confirmWaitingListDetails() {
|
private async confirmWaitingListDetails() {
|
||||||
this.loggerService.debug('Confirming waiting list details')
|
this.loggerService.debug('Confirming waiting list details')
|
||||||
const saveButton = await this.page?.$('input[type="submit"][value="Save"]')
|
const saveButton = await this.page.$('input[type="submit"][value="Save"]')
|
||||||
await saveButton?.click().catch((e) => {
|
await saveButton?.click().catch((e) => {
|
||||||
throw new RunnerWaitingListConfirmError(e)
|
throw new RunnerWaitingListConfirmError(e)
|
||||||
})
|
})
|
||||||
|
|
@ -348,9 +395,31 @@ export class BaanReserverenService {
|
||||||
public async addReservationToWaitList(reservation: Reservation) {
|
public async addReservationToWaitList(reservation: Reservation) {
|
||||||
await this.init(reservation)
|
await this.init(reservation)
|
||||||
await this.navigateToWaitingList()
|
await this.navigateToWaitingList()
|
||||||
|
const previousWaitingListIds = await this.recordWaitingListEntries()
|
||||||
await this.openWaitingListDialog()
|
await this.openWaitingListDialog()
|
||||||
await this.inputWaitingListDetails(reservation)
|
await this.inputWaitingListDetails(reservation)
|
||||||
await this.confirmWaitingListDetails()
|
await this.confirmWaitingListDetails()
|
||||||
|
const currentWaitingListIds = await this.recordWaitingListEntries()
|
||||||
|
const waitingListId = this.findNewWaitingListEntryId(
|
||||||
|
previousWaitingListIds,
|
||||||
|
currentWaitingListIds,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (waitingListId == null) {
|
||||||
|
throw new WaitingListSubmissionError(
|
||||||
|
'Failed to find new waiting list entry',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return waitingListId
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeReservationFromWaitList(reservation: Reservation) {
|
||||||
|
if (!reservation.waitListed || !reservation.waitingListId) return
|
||||||
|
|
||||||
|
await this.init(reservation)
|
||||||
|
await this.navigateToWaitingList()
|
||||||
|
await this.deleteWaitingListEntryRowById(reservation.waitingListId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,3 +567,24 @@ export class RunnerReservationConfirmSubmitError extends RunnerError {
|
||||||
super(error, 'RunnerReservationConfirmSubmitError')
|
super(error, 'RunnerReservationConfirmSubmitError')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SessionStartError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'SessionStartError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WaitingListSubmissionError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'WaitingListSubmissionError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WaitingListEntryDeletionError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'WaitingListEntryDeletionError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue