Compare commits
12 commits
fc5f8b2f11
...
01a6013093
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a6013093 | ||
|
|
94ddf55639 | ||
|
|
afb733608d | ||
|
|
82633908a4 | ||
|
|
5b109226e6 | ||
|
|
82ff838359 | ||
|
|
f0d4207880 | ||
|
|
b6e2ea4c5a | ||
|
|
25efd61c99 | ||
|
|
671084dc7b | ||
|
|
25fb2c9bdc | ||
|
|
9e9b0194da |
13 changed files with 230 additions and 114 deletions
43
.github/workflows/main.yml
vendored
Normal file
43
.github/workflows/main.yml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
name: Push to main
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/hydrogen
|
||||
- run: npm ci
|
||||
- run: npm run test:unit
|
||||
build-image:
|
||||
needs:
|
||||
- test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GHCR_USERNAME }}
|
||||
password: ${{ secrets.GHCR_PASSWORD }}
|
||||
- name: Declare GIT_COMMIT
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GIT_COMMIT=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV"
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/server/Dockerfile
|
||||
push: true
|
||||
tags: ghcr.io/cgduncan7/autobaan:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: GIT_COMMIT=${{ env.GIT_COMMIT }}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
when:
|
||||
- branch: main
|
||||
event: push
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: docker.io/node:hydrogen-slim
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run test:unit
|
||||
31
database/migrations/1739366336570-ReservationStatuses.ts
Normal file
31
database/migrations/1739366336570-ReservationStatuses.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class ReservationStatuses1739366336570 implements MigrationInterface {
|
||||
name = 'ReservationStatuses1739366336570'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "status" varchar(32) NOT NULL DEFAULT ('pending'), "waitingListId" integer, "ownerId" varchar(32) NOT NULL, "opponents" json NOT NULL)`,
|
||||
)
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_reservations"("id", "dateRangeStart", "dateRangeEnd", "status", "waitingListId", "ownerId", "opponents") SELECT "id", "dateRangeStart", "dateRangeEnd", CASE "waitListed" WHEN true THEN "on_waiting_list" ELSE "pending" END, "waitingListId", "ownerId", "opponents" 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, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0), "waitingListId" integer, "ownerId" varchar(32) NOT NULL, "opponents" json NOT NULL)`,
|
||||
)
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "reservations"("id", "dateRangeStart", "dateRangeEnd", "waitListed", "waitingListId", "ownerId", "opponents") SELECT "id", "dateRangeStart", "dateRangeEnd", CASE "status" WHEN "on_waiting_list" THEN true ELSE false, "waitingListId", "ownerId", "opponents" FROM "temporary_reservations"`,
|
||||
)
|
||||
await queryRunner.query(`DROP TABLE "temporary_reservations"`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dayjs/locale/nl'
|
||||
|
||||
import { TransformationType, TransformFnParams } from 'class-transformer'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import * as isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
||||
|
|
@ -41,4 +42,20 @@ const dayjsTz = (
|
|||
return dayjs(date, format).tz('Europe/Amsterdam')
|
||||
}
|
||||
|
||||
export const DayjsTransformer = ({ value, type }: TransformFnParams) => {
|
||||
switch (type) {
|
||||
case TransformationType.PLAIN_TO_CLASS:
|
||||
return dayjsTz(value)
|
||||
case TransformationType.CLASS_TO_PLAIN:
|
||||
return value.format()
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export const DayjsColumnTransformer = {
|
||||
to: (value: dayjs.Dayjs) => value.format(),
|
||||
from: (value: Date) => dayjsTz(value),
|
||||
}
|
||||
|
||||
export default dayjsTz
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export class Monitor {
|
|||
|
||||
@Column('datetime', {
|
||||
nullable: false,
|
||||
default: dayjs(),
|
||||
transformer: {
|
||||
to: (value?: Dayjs) => (value ?? dayjs()).format(),
|
||||
from: (value: Date) => dayjs(value),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'
|
|||
import { InjectRepository } from '@nestjs/typeorm'
|
||||
import { Repository } from 'typeorm'
|
||||
|
||||
import dayjsTz from '../common/dayjs'
|
||||
import { Monitor, MonitorType } from './entity'
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -13,7 +14,7 @@ export class MonitorsService {
|
|||
|
||||
async performMonitor(type: MonitorType, data: string) {
|
||||
await this.monitorsRepository.save(
|
||||
this.monitorsRepository.create({ type, data }),
|
||||
this.monitorsRepository.create({ type, data, createdAt: dayjsTz() }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,14 +121,14 @@ export class NtfyProvider implements OnApplicationBootstrap {
|
|||
)
|
||||
}
|
||||
|
||||
async sendReservationWaitlistedNotification(
|
||||
async sendReservationOnWaitingListNotification(
|
||||
reservationId: string,
|
||||
startTime: Dayjs,
|
||||
endTime: Dayjs,
|
||||
) {
|
||||
await this.publishQueue.add(
|
||||
...NtfyProvider.defaultJob({
|
||||
title: 'Reservation waitlisted',
|
||||
title: 'Reservation added to waiting list',
|
||||
message: `${reservationId} - ${startTime.format()} to ${endTime.format()}`,
|
||||
tags: [MessageTags.badminton, MessageTags.hourglass],
|
||||
}),
|
||||
|
|
@ -138,7 +138,7 @@ export class NtfyProvider implements OnApplicationBootstrap {
|
|||
async sendWaitListEmailReceivedNotification(subject: string) {
|
||||
await this.publishQueue.add(
|
||||
...NtfyProvider.defaultJob({
|
||||
title: 'Wait listed reservation available',
|
||||
title: 'Reservation on waiting list has become available',
|
||||
message: `${subject}`,
|
||||
tags: [MessageTags.badminton, MessageTags.hourglass],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -12,73 +12,61 @@ import {
|
|||
Query,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common'
|
||||
import { Transform, TransformationType } from 'class-transformer'
|
||||
import { Transform } from 'class-transformer'
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator'
|
||||
import { Dayjs } from 'dayjs'
|
||||
|
||||
import dayjs from '../common/dayjs'
|
||||
import { DayjsTransformer } from '../common/dayjs'
|
||||
import { LoggerService } from '../logger/service.logger'
|
||||
import { RESERVATIONS_QUEUE_NAME, ReservationsQueue } from './config'
|
||||
import { ReservationStatus } from './entity'
|
||||
import { ReservationsService } from './service'
|
||||
|
||||
export class GetReservationsQueryParams {
|
||||
@IsOptional()
|
||||
@Transform(() => Dayjs)
|
||||
date?: Dayjs
|
||||
readonly date?: Dayjs
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === 'true')
|
||||
readonly schedulable?: boolean
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ReservationStatus)
|
||||
readonly status?: ReservationStatus
|
||||
}
|
||||
|
||||
export class CreateReservationOpponent {
|
||||
@IsString()
|
||||
id: string
|
||||
readonly id: string
|
||||
|
||||
@IsString()
|
||||
name: string
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
export class CreateReservationRequest {
|
||||
@IsString()
|
||||
ownerId: string
|
||||
readonly ownerId: string
|
||||
|
||||
@Transform(({ value, type }) => {
|
||||
switch (type) {
|
||||
case TransformationType.PLAIN_TO_CLASS:
|
||||
return dayjs(value)
|
||||
case TransformationType.CLASS_TO_PLAIN:
|
||||
return value.format()
|
||||
default:
|
||||
return value
|
||||
}
|
||||
})
|
||||
dateRangeStart: Dayjs
|
||||
@Transform(DayjsTransformer)
|
||||
readonly dateRangeStart: Dayjs
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value, type }) => {
|
||||
switch (type) {
|
||||
case TransformationType.PLAIN_TO_CLASS:
|
||||
return dayjs(value)
|
||||
case TransformationType.CLASS_TO_PLAIN:
|
||||
return value.format()
|
||||
default:
|
||||
return value
|
||||
}
|
||||
})
|
||||
dateRangeEnd?: Dayjs
|
||||
@Transform(DayjsTransformer)
|
||||
readonly dateRangeEnd?: Dayjs
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested()
|
||||
opponents?: CreateReservationOpponent[]
|
||||
readonly opponents?: CreateReservationOpponent[]
|
||||
}
|
||||
|
||||
@Controller('reservations')
|
||||
|
|
@ -97,14 +85,14 @@ export class ReservationsController {
|
|||
|
||||
@Get()
|
||||
getReservations(@Query() params: GetReservationsQueryParams) {
|
||||
const { schedulable, date } = params
|
||||
const { schedulable, date, status } = params
|
||||
if (schedulable) {
|
||||
return this.reservationsService.getSchedulable()
|
||||
}
|
||||
if (date) {
|
||||
return this.reservationsService.getByDate(date)
|
||||
return this.reservationsService.getByDate(date, status)
|
||||
}
|
||||
return this.reservationsService.getAll()
|
||||
return this.reservationsService.getAll(status)
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
import { Exclude, Transform, Type } from 'class-transformer'
|
||||
import { TransformationType } from 'class-transformer'
|
||||
import { IsEnum } from 'class-validator'
|
||||
import { Dayjs } from 'dayjs'
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
import dayjs from '../common/dayjs'
|
||||
import dayjs, {
|
||||
DayjsColumnTransformer,
|
||||
DayjsTransformer,
|
||||
} from '../common/dayjs'
|
||||
|
||||
export interface Opponent {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export enum ReservationStatus {
|
||||
Pending = 'pending',
|
||||
OnWaitingList = 'on_waiting_list',
|
||||
Booked = 'booked',
|
||||
}
|
||||
|
||||
@Entity({ name: 'reservations' })
|
||||
export class Reservation {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
|
@ -20,49 +29,30 @@ export class Reservation {
|
|||
|
||||
@Column('datetime', {
|
||||
nullable: false,
|
||||
transformer: {
|
||||
to: (value: Dayjs) => value.format(),
|
||||
from: (value: Date) => dayjs(value),
|
||||
},
|
||||
transformer: DayjsColumnTransformer,
|
||||
})
|
||||
@Type(() => Dayjs)
|
||||
@Transform(({ value, type }) => {
|
||||
switch (type) {
|
||||
case TransformationType.PLAIN_TO_CLASS:
|
||||
return dayjs(value)
|
||||
case TransformationType.CLASS_TO_PLAIN:
|
||||
return value.format()
|
||||
default:
|
||||
return value
|
||||
}
|
||||
})
|
||||
@Transform(DayjsTransformer)
|
||||
dateRangeStart: Dayjs
|
||||
|
||||
@Column('datetime', {
|
||||
nullable: false,
|
||||
transformer: {
|
||||
to: (value: Dayjs) => value.format(),
|
||||
from: (value: Date) => dayjs(value),
|
||||
},
|
||||
transformer: DayjsColumnTransformer,
|
||||
})
|
||||
@Type(() => Dayjs)
|
||||
@Transform(({ value, type }) => {
|
||||
switch (type) {
|
||||
case TransformationType.PLAIN_TO_CLASS:
|
||||
return dayjs(value)
|
||||
case TransformationType.CLASS_TO_PLAIN:
|
||||
return value.format()
|
||||
default:
|
||||
return value
|
||||
}
|
||||
})
|
||||
@Transform(DayjsTransformer)
|
||||
dateRangeEnd: Dayjs
|
||||
|
||||
@Column('json', { nullable: false })
|
||||
opponents: Opponent[]
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
waitListed: boolean
|
||||
@Column('varchar', {
|
||||
length: 32,
|
||||
nullable: false,
|
||||
default: ReservationStatus.Pending,
|
||||
})
|
||||
@IsEnum(ReservationStatus)
|
||||
status: ReservationStatus
|
||||
|
||||
@Column('int', { nullable: true })
|
||||
waitingListId: number
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Repository } from 'typeorm'
|
|||
import dayjs from '../common/dayjs'
|
||||
import { LoggerService } from '../logger/service.logger'
|
||||
import { BaanReserverenService } from '../runner/baanreserveren/service'
|
||||
import { Reservation } from './entity'
|
||||
import { Reservation, ReservationStatus } from './entity'
|
||||
|
||||
@Injectable()
|
||||
export class ReservationsService {
|
||||
|
|
@ -21,19 +21,24 @@ export class ReservationsService {
|
|||
private readonly loggerService: LoggerService,
|
||||
) {}
|
||||
|
||||
async getAll() {
|
||||
return await this.reservationsRepository.find()
|
||||
async getAll(status?: ReservationStatus) {
|
||||
return await this.reservationsRepository.find({ where: { status } })
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return await this.reservationsRepository.findOneBy({ id })
|
||||
}
|
||||
|
||||
async getByDate(date = dayjs()) {
|
||||
return await this.reservationsRepository
|
||||
async getByDate(date = dayjs(), status?: ReservationStatus) {
|
||||
let qb = this.reservationsRepository
|
||||
.createQueryBuilder()
|
||||
.where(`DATE(dateRangeStart) = DATE(:date)`, { date: date.toISOString() })
|
||||
.getMany()
|
||||
|
||||
if (status != null) {
|
||||
qb = qb.andWhere(`status = :status`, { status })
|
||||
}
|
||||
|
||||
return await qb.orderBy('dateRangeStart', 'ASC').getMany()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,13 +49,15 @@ export class ReservationsService {
|
|||
const query = this.reservationsRepository
|
||||
.createQueryBuilder()
|
||||
.where(
|
||||
`DATE(dateRangeStart) BETWEEN DATE(:startDate) AND DATE(:endDate)`,
|
||||
`(DATE(dateRangeStart) >= DATE(:startDate) OR DATE(dateRangeStart) <= DATE(:endDate))`,
|
||||
{
|
||||
startDate: dayjs().add(1, 'days').toISOString(),
|
||||
endDate: dayjs().add(7, 'days').toISOString(),
|
||||
},
|
||||
)
|
||||
.andWhere(`waitListed = false`)
|
||||
.andWhere('status = :status', {
|
||||
statuses: ReservationStatus.Pending,
|
||||
})
|
||||
.orderBy('dateRangeStart', 'ASC')
|
||||
|
||||
return await query.getMany()
|
||||
|
|
@ -59,13 +66,10 @@ export class ReservationsService {
|
|||
async getByDateOnWaitingList(date = dayjs()) {
|
||||
return await this.reservationsRepository
|
||||
.createQueryBuilder()
|
||||
.where(`DATE(dateRangeStart) <= DATE(:date)`, {
|
||||
.where(`DATE(dateRangeStart) = DATE(:date)`, {
|
||||
date: date.toISOString(),
|
||||
})
|
||||
.andWhere(`DATE(dateRangeEnd) >= DATE(:date)`, {
|
||||
date: date.toISOString(),
|
||||
})
|
||||
.andWhere('waitListed = true')
|
||||
.andWhere('status = :status', { status: ReservationStatus.OnWaitingList })
|
||||
.getMany()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Process, Processor } from '@nestjs/bull'
|
||||
import { Inject } from '@nestjs/common'
|
||||
import { Job } from 'bull'
|
||||
import { instanceToPlain, plainToInstance } from 'class-transformer'
|
||||
|
||||
import { LoggerService } from '../logger/service.logger'
|
||||
|
|
@ -11,7 +10,7 @@ import {
|
|||
} from '../runner/baanreserveren/service'
|
||||
import { RESERVATIONS_QUEUE_NAME, ReservationsJob } from './config'
|
||||
import { DAILY_RESERVATIONS_ATTEMPTS } from './cron'
|
||||
import { Reservation } from './entity'
|
||||
import { Reservation, ReservationStatus } from './entity'
|
||||
import { ReservationsService } from './service'
|
||||
|
||||
@Processor(RESERVATIONS_QUEUE_NAME)
|
||||
|
|
@ -63,10 +62,10 @@ export class ReservationsWorker {
|
|||
}
|
||||
if (
|
||||
(shouldWaitlist || attemptsMade === DAILY_RESERVATIONS_ATTEMPTS) &&
|
||||
!reservation.waitListed
|
||||
reservation.status !== ReservationStatus.OnWaitingList
|
||||
) {
|
||||
this.loggerService.log('Adding reservation to waiting list')
|
||||
await this.ntfyProvider.sendReservationWaitlistedNotification(
|
||||
await this.ntfyProvider.sendReservationOnWaitingListNotification(
|
||||
reservation.id,
|
||||
reservation.dateRangeStart,
|
||||
reservation.dateRangeEnd,
|
||||
|
|
@ -89,7 +88,9 @@ export class ReservationsWorker {
|
|||
} else {
|
||||
await this.brService.performReservation(reservation, timeSensitive)
|
||||
}
|
||||
await this.reservationsService.deleteById(reservation.id)
|
||||
await this.reservationsService.update(reservation.id, {
|
||||
status: ReservationStatus.Booked,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
await this.handleReservationErrors(
|
||||
error as Error,
|
||||
|
|
@ -105,12 +106,12 @@ export class ReservationsWorker {
|
|||
timeSensitive = true,
|
||||
) {
|
||||
try {
|
||||
const waitingListId = await this.brService.addReservationToWaitList(
|
||||
const waitingListId = await this.brService.addReservationToWaitingList(
|
||||
reservation,
|
||||
timeSensitive,
|
||||
)
|
||||
await this.reservationsService.update(reservation.id, {
|
||||
waitListed: true,
|
||||
status: ReservationStatus.OnWaitingList,
|
||||
waitingListId,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import dayjs from '../../common/dayjs'
|
|||
import { LoggerService } from '../../logger/service.logger'
|
||||
import { MONITORING_QUEUE_NAME, MonitoringQueue } from '../../monitoring/config'
|
||||
import { MonitorType } from '../../monitoring/entity'
|
||||
import { Opponent, Reservation } from '../../reservations/entity'
|
||||
import {
|
||||
Opponent,
|
||||
Reservation,
|
||||
ReservationStatus,
|
||||
} from '../../reservations/entity'
|
||||
import { EmptyPage } from '../pages/empty'
|
||||
|
||||
export const BAAN_RESERVEREN_ROOT_URL = 'https://squashcity.baanreserveren.nl'
|
||||
|
|
@ -67,12 +71,12 @@ const CourtSlotToNumber: Record<CourtSlot, number> = {
|
|||
|
||||
// Lower is better
|
||||
const CourtRank: Record<CourtSlot, number> = {
|
||||
[CourtSlot.One]: 2,
|
||||
[CourtSlot.Two]: 1, // team at squash city has this pre-booked at 19.15 on Tuesday :sad:
|
||||
[CourtSlot.Three]: 1, // team at squash city has this pre-booked at 19.15 on Tuesday :sad:
|
||||
[CourtSlot.One]: 0,
|
||||
[CourtSlot.Two]: 2, // team at squash city has this pre-booked at 19.15 on Tuesday :sad:
|
||||
[CourtSlot.Three]: 2, // team at squash city has this pre-booked at 19.15 on Tuesday :sad:
|
||||
[CourtSlot.Four]: 0,
|
||||
[CourtSlot.Five]: 99, // shitty
|
||||
[CourtSlot.Six]: 99, // shitty
|
||||
[CourtSlot.Five]: 0,
|
||||
[CourtSlot.Six]: 0,
|
||||
[CourtSlot.Seven]: 0,
|
||||
[CourtSlot.Eight]: 0,
|
||||
[CourtSlot.Nine]: 0,
|
||||
|
|
@ -82,13 +86,16 @@ const CourtRank: Record<CourtSlot, number> = {
|
|||
[CourtSlot.Thirteen]: 9, // no one likes upstairs
|
||||
} as const
|
||||
|
||||
enum StartTimeClass {
|
||||
export enum StartTimeClass {
|
||||
First = 'first',
|
||||
Second = 'second',
|
||||
Third = 'third',
|
||||
}
|
||||
|
||||
const StartTimeClassCourtSlots: Record<StartTimeClass, readonly CourtSlot[]> = {
|
||||
export const StartTimeClassCourtSlots: Record<
|
||||
StartTimeClass,
|
||||
readonly CourtSlot[]
|
||||
> = {
|
||||
[StartTimeClass.First]: [
|
||||
CourtSlot.One,
|
||||
CourtSlot.Two,
|
||||
|
|
@ -676,7 +683,7 @@ export class BaanReserverenService {
|
|||
return courtStatuses
|
||||
}
|
||||
|
||||
private getCourtSlotsForDate(date: Dayjs) {
|
||||
public getCourtSlotsForDate(date: Dayjs) {
|
||||
const time = date.format('HH:mm')
|
||||
for (const [timeClass, times] of Object.entries(StartTimeClassStartTimes)) {
|
||||
if (times.includes(time)) {
|
||||
|
|
@ -741,7 +748,7 @@ export class BaanReserverenService {
|
|||
throw new NoCourtAvailableError('Could not reserve court')
|
||||
}
|
||||
|
||||
public async addReservationToWaitList(
|
||||
public async addReservationToWaitingList(
|
||||
reservation: Reservation,
|
||||
timeSensitive = true,
|
||||
) {
|
||||
|
|
@ -774,7 +781,11 @@ export class BaanReserverenService {
|
|||
|
||||
public async removeReservationFromWaitList(reservation: Reservation) {
|
||||
try {
|
||||
if (!reservation.waitListed || !reservation.waitingListId) return
|
||||
if (
|
||||
reservation.status !== ReservationStatus.OnWaitingList ||
|
||||
!reservation.waitingListId
|
||||
)
|
||||
return
|
||||
await this.init()
|
||||
await this.navigateToWaitingList()
|
||||
await this.deleteWaitingListEntryRowById(reservation.waitingListId)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
BAAN_RESERVEREN_ROOT_URL,
|
||||
BaanReserverenService,
|
||||
CourtSlot,
|
||||
StartTimeClass,
|
||||
StartTimeClassCourtSlots,
|
||||
} from '../../../src/runner/baanreserveren/service'
|
||||
import { EmptyPage } from '../../../src/runner/pages/empty'
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ describe('baanreserveren.service', () => {
|
|||
beforeAll(async () => {
|
||||
pageGotoSpy = jest
|
||||
.fn()
|
||||
.mockClear()
|
||||
.mockImplementation(() => Promise.resolve({ status: () => 200 }))
|
||||
module = await Test.createTestingModule({
|
||||
providers: [
|
||||
|
|
@ -54,10 +57,12 @@ describe('baanreserveren.service', () => {
|
|||
brService = module.get<BaanReserverenService>(BaanReserverenService)
|
||||
})
|
||||
|
||||
beforeEach(() => pageGotoSpy.mockClear())
|
||||
|
||||
describe('performSpeedyReservation', () => {
|
||||
it.each([
|
||||
[18, 15, CourtSlot.Seven, CourtSlot.Six],
|
||||
[18, 30, CourtSlot.Four, CourtSlot.Two],
|
||||
[18, 15, CourtSlot.Six, CourtSlot.Seven],
|
||||
[18, 30, CourtSlot.One, CourtSlot.Four],
|
||||
[18, 45, CourtSlot.Twelve, CourtSlot.Thirteen],
|
||||
])(
|
||||
'should try highest ranked court first',
|
||||
|
|
@ -89,8 +94,8 @@ describe('baanreserveren.service', () => {
|
|||
)
|
||||
|
||||
it.each([
|
||||
[18, 15, CourtSlot.Seven, CourtSlot.Eight],
|
||||
[18, 30, CourtSlot.Four, CourtSlot.Two],
|
||||
[18, 15, CourtSlot.Six, CourtSlot.Seven],
|
||||
[18, 30, CourtSlot.One, CourtSlot.Four],
|
||||
[18, 45, CourtSlot.Twelve, CourtSlot.Thirteen],
|
||||
])(
|
||||
'should try backup if first rank is taken',
|
||||
|
|
@ -132,4 +137,40 @@ describe('baanreserveren.service', () => {
|
|||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe.only('getCourtSlotsForDate', () => {
|
||||
it.each([
|
||||
{
|
||||
date: '2025-04-10T16:30:00.000Z',
|
||||
expectedCourtSlots: StartTimeClassCourtSlots[StartTimeClass.First],
|
||||
},
|
||||
{
|
||||
date: '2025-04-10T16:45:00.000Z',
|
||||
expectedCourtSlots: StartTimeClassCourtSlots[StartTimeClass.Third],
|
||||
},
|
||||
{
|
||||
date: '2025-04-10T17:00:00.000Z',
|
||||
expectedCourtSlots: StartTimeClassCourtSlots[StartTimeClass.Second],
|
||||
},
|
||||
{
|
||||
date: '2025-01-10T17:30:00.000Z',
|
||||
expectedCourtSlots: StartTimeClassCourtSlots[StartTimeClass.First],
|
||||
},
|
||||
{
|
||||
date: '2025-01-10T17:45:00.000Z',
|
||||
expectedCourtSlots: StartTimeClassCourtSlots[StartTimeClass.Third],
|
||||
},
|
||||
{
|
||||
date: '2025-01-10T18:00:00.000Z',
|
||||
expectedCourtSlots: StartTimeClassCourtSlots[StartTimeClass.Second],
|
||||
},
|
||||
])(
|
||||
'should get correct court slots for $date',
|
||||
({ date, expectedCourtSlots }) => {
|
||||
expect(brService.getCourtSlotsForDate(dayjs(date))).toEqual(
|
||||
expect.arrayContaining(expectedCourtSlots as CourtSlot[]),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue