Updated reservationScheduler to properly input/output correct data format
This commit is contained in:
parent
4f76823102
commit
6aa46fbaa9
9 changed files with 234 additions and 46 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,3 +8,6 @@ dist/
|
||||||
# terraform
|
# terraform
|
||||||
.terraform/
|
.terraform/
|
||||||
deploy/
|
deploy/
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode/settings.json
|
||||||
|
|
|
||||||
60
src/common/logger.ts
Normal file
60
src/common/logger.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG,
|
||||||
|
INFO,
|
||||||
|
ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Logger {
|
||||||
|
private readonly correlationId: string
|
||||||
|
private readonly level: LogLevel
|
||||||
|
|
||||||
|
constructor(correlationId: string, level = LogLevel.ERROR) {
|
||||||
|
this.correlationId = correlationId
|
||||||
|
this.level = level
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(logLevel: LogLevel, message: string, details?: unknown): void {
|
||||||
|
if (logLevel < this.level) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let levelString
|
||||||
|
switch (logLevel) {
|
||||||
|
case LogLevel.ERROR:
|
||||||
|
levelString = 'ERROR'
|
||||||
|
break
|
||||||
|
case LogLevel.INFO:
|
||||||
|
levelString = 'INFO'
|
||||||
|
break
|
||||||
|
case LogLevel.DEBUG:
|
||||||
|
default:
|
||||||
|
levelString = 'DEBUG'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let fmtString = '[%s] %s: %s'
|
||||||
|
const params: Array<unknown> = [this.correlationId, levelString, message]
|
||||||
|
if (details) {
|
||||||
|
params.push(details)
|
||||||
|
fmtString += ' - %O'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logLevel === LogLevel.ERROR) {
|
||||||
|
console.error(fmtString, ...params)
|
||||||
|
} else {
|
||||||
|
console.log(fmtString, ...params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(message: string, details?: unknown): void {
|
||||||
|
this.log(LogLevel.DEBUG, message, details)
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(message: string, details?: unknown): void {
|
||||||
|
this.log(LogLevel.INFO, message, details)
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(message: string, details?: unknown): void {
|
||||||
|
this.log(LogLevel.ERROR, message, details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import dayjs, { Dayjs } from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
||||||
dayjs.extend(isSameOrBefore)
|
dayjs.extend(isSameOrBefore)
|
||||||
|
|
||||||
|
|
@ -7,10 +7,7 @@ import { DateRange, Opponent } from './reservation'
|
||||||
export interface ReservationRequest {
|
export interface ReservationRequest {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
dateRange: {
|
dateRange: DateRange
|
||||||
start: Dayjs
|
|
||||||
end: Dayjs
|
|
||||||
}
|
|
||||||
opponent: Opponent
|
opponent: Opponent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,9 +34,7 @@ export class ValidationError extends Error {
|
||||||
* @param body String of request body
|
* @param body String of request body
|
||||||
* @returns ReservationRequest
|
* @returns ReservationRequest
|
||||||
*/
|
*/
|
||||||
export const validateRequest = (
|
export const validateRequest = (body: string): ReservationRequest => {
|
||||||
body: string
|
|
||||||
): ReservationRequest => {
|
|
||||||
const request = validateRequestBody(body)
|
const request = validateRequestBody(body)
|
||||||
validateRequestDateRange(request.dateRange)
|
validateRequestDateRange(request.dateRange)
|
||||||
validateRequestOpponent(request.opponent)
|
validateRequestOpponent(request.opponent)
|
||||||
|
|
@ -48,7 +43,10 @@ export const validateRequest = (
|
||||||
|
|
||||||
const validateRequestBody = (body?: string): ReservationRequest => {
|
const validateRequestBody = (body?: string): ReservationRequest => {
|
||||||
if (body === undefined) {
|
if (body === undefined) {
|
||||||
throw new ValidationError('Invalid request', ValidationErrorCode.UNDEFINED_REQUEST_BODY)
|
throw new ValidationError(
|
||||||
|
'Invalid request',
|
||||||
|
ValidationErrorCode.UNDEFINED_REQUEST_BODY
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonBody = transformRequestBody(body)
|
const jsonBody = transformRequestBody(body)
|
||||||
|
|
@ -65,7 +63,10 @@ const validateRequestBody = (body?: string): ReservationRequest => {
|
||||||
(opponent && opponent.id && opponent.id.length < 1) ||
|
(opponent && opponent.id && opponent.id.length < 1) ||
|
||||||
(opponent && opponent.name && opponent.name.length < 1)
|
(opponent && opponent.name && opponent.name.length < 1)
|
||||||
) {
|
) {
|
||||||
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_REQUEST_BODY)
|
throw new ValidationError(
|
||||||
|
'Invalid request',
|
||||||
|
ValidationErrorCode.INVALID_REQUEST_BODY
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonBody
|
return jsonBody
|
||||||
|
|
@ -76,7 +77,10 @@ const transformRequestBody = (body: string): ReservationRequest => {
|
||||||
try {
|
try {
|
||||||
json = JSON.parse(body)
|
json = JSON.parse(body)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_JSON)
|
throw new ValidationError(
|
||||||
|
'Invalid request',
|
||||||
|
ValidationErrorCode.INVALID_JSON
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const startTime = json.dateRange?.start ?? 'invalid'
|
const startTime = json.dateRange?.start ?? 'invalid'
|
||||||
const endTime = json.dateRange?.end ?? 'invalid'
|
const endTime = json.dateRange?.end ?? 'invalid'
|
||||||
|
|
@ -84,8 +88,8 @@ const transformRequestBody = (body: string): ReservationRequest => {
|
||||||
return {
|
return {
|
||||||
username: json.username,
|
username: json.username,
|
||||||
password: json.password,
|
password: json.password,
|
||||||
opponent: json.opponent,
|
|
||||||
dateRange,
|
dateRange,
|
||||||
|
opponent: json.opponent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +97,10 @@ const validateRequestDateRange = (dateRange: DateRange): void => {
|
||||||
// checking that both dates are valid
|
// checking that both dates are valid
|
||||||
const { start, end } = dateRange
|
const { start, end } = dateRange
|
||||||
if (!start.isValid() || !end.isValid()) {
|
if (!start.isValid() || !end.isValid()) {
|
||||||
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_DATE_RANGE)
|
throw new ValidationError(
|
||||||
|
'Invalid request',
|
||||||
|
ValidationErrorCode.INVALID_DATE_RANGE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checking that:
|
// checking that:
|
||||||
|
|
@ -105,7 +112,10 @@ const validateRequestDateRange = (dateRange: DateRange): void => {
|
||||||
!start.isSameOrBefore(end) ||
|
!start.isSameOrBefore(end) ||
|
||||||
start.format('YYYY MM DD') !== end.format('YYYY MM DD')
|
start.format('YYYY MM DD') !== end.format('YYYY MM DD')
|
||||||
) {
|
) {
|
||||||
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_START_OR_END_DATE)
|
throw new ValidationError(
|
||||||
|
'Invalid request',
|
||||||
|
ValidationErrorCode.INVALID_START_OR_END_DATE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,6 +129,9 @@ const validateRequestOpponent = (opponent?: Opponent): void => {
|
||||||
id.length < 1 ||
|
id.length < 1 ||
|
||||||
name.length < 1
|
name.length < 1
|
||||||
) {
|
) {
|
||||||
throw new ValidationError('Invalid request', ValidationErrorCode.INVALID_OPPONENT)
|
throw new ValidationError(
|
||||||
|
'Invalid request',
|
||||||
|
ValidationErrorCode.INVALID_OPPONENT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export class Reservation {
|
||||||
|
|
||||||
const { start, end } = this.dateRange
|
const { start, end } = this.dateRange
|
||||||
|
|
||||||
let possibleDate = dayjs(start)
|
let possibleDate = dayjs(start).second(0).millisecond(0)
|
||||||
while (possibleDate.isSameOrBefore(end)) {
|
while (possibleDate.isSameOrBefore(end)) {
|
||||||
possibleDates.push(possibleDate)
|
possibleDates.push(possibleDate)
|
||||||
possibleDate = possibleDate.add(15, 'minute')
|
possibleDate = possibleDate.add(15, 'minute')
|
||||||
|
|
@ -45,6 +45,9 @@ export class Reservation {
|
||||||
* @returns is reservation date within 7 days
|
* @returns is reservation date within 7 days
|
||||||
*/
|
*/
|
||||||
public isAvailableForReservation(): boolean {
|
public isAvailableForReservation(): boolean {
|
||||||
return Math.ceil(this.dateRange.start.diff(dayjs(), 'days', true)) <= RESERVATION_AVAILABLE_WITHIN_DAYS
|
return (
|
||||||
|
Math.ceil(this.dateRange.start.diff(dayjs(), 'days', true)) <=
|
||||||
|
RESERVATION_AVAILABLE_WITHIN_DAYS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@ import { Dayjs } from 'dayjs'
|
||||||
* @param requestedDate
|
* @param requestedDate
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const scheduleDateToRequestReservation = (requestedDate: Dayjs): Dayjs => {
|
export const scheduleDateToRequestReservation = (
|
||||||
return requestedDate.hour(0).minute(0).second(0).millisecond(0).subtract(7, 'days')
|
requestedDate: Dayjs
|
||||||
|
): Dayjs => {
|
||||||
|
return requestedDate
|
||||||
|
.hour(0)
|
||||||
|
.minute(0)
|
||||||
|
.second(0)
|
||||||
|
.millisecond(0)
|
||||||
|
.subtract(7, 'days')
|
||||||
}
|
}
|
||||||
|
|
@ -1,24 +1,64 @@
|
||||||
import { Handler } from 'aws-lambda'
|
import { Context, Handler } from 'aws-lambda'
|
||||||
import { Dayjs } from 'dayjs'
|
import { Dayjs } from 'dayjs'
|
||||||
|
|
||||||
import { InputEvent } from '../stepFunctions/event'
|
import { Logger, LogLevel } from '../common/logger'
|
||||||
import { Reservation } from '../common/reservation'
|
import { Reservation } from '../common/reservation'
|
||||||
import { validateRequest, ReservationRequest } from '../common/request'
|
import {
|
||||||
|
validateRequest,
|
||||||
|
ReservationRequest,
|
||||||
|
} from '../common/request'
|
||||||
import { scheduleDateToRequestReservation } from '../common/schedule'
|
import { scheduleDateToRequestReservation } from '../common/schedule'
|
||||||
|
|
||||||
export interface ScheduledReservationRequest {
|
export interface ScheduledReservationRequest {
|
||||||
reservationRequest: ReservationRequest
|
reservationRequest: ReservationRequest
|
||||||
availableAt: Dayjs
|
scheduledFor?: Dayjs
|
||||||
}
|
}
|
||||||
|
|
||||||
export const run: Handler<InputEvent, void> = async (input: InputEvent): Promise<void> => {
|
export interface ReservationSchedulerResult {
|
||||||
console.log(`Handling event: ${input}`)
|
scheduledReservationRequest?: ScheduledReservationRequest
|
||||||
const reservationRequest = validateRequest(JSON.stringify(input.reservationRequest))
|
}
|
||||||
console.log('Successfully validated request')
|
|
||||||
|
const handler: Handler<string, ReservationSchedulerResult> = async (
|
||||||
|
payload: string,
|
||||||
|
context: Context,
|
||||||
|
): Promise<ReservationSchedulerResult> => {
|
||||||
|
const logger = new Logger(context.awsRequestId, LogLevel.DEBUG)
|
||||||
|
logger.debug('Handling event', { payload })
|
||||||
|
let reservationRequest: ReservationRequest
|
||||||
|
try {
|
||||||
|
reservationRequest = validateRequest(payload)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to validate request', { err })
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Successfully validated request', { reservationRequest })
|
||||||
|
|
||||||
|
const res = new Reservation(
|
||||||
|
reservationRequest.dateRange,
|
||||||
|
reservationRequest.opponent
|
||||||
|
)
|
||||||
|
|
||||||
const res = new Reservation(reservationRequest.dateRange, reservationRequest.opponent)
|
|
||||||
if (!res.isAvailableForReservation()) {
|
if (!res.isAvailableForReservation()) {
|
||||||
console.log('Reservation date is more than 7 days away; scheduling for later')
|
logger.debug('Reservation date is more than 7 days away')
|
||||||
scheduleDateToRequestReservation(reservationRequest.dateRange.start)
|
const scheduledDay = scheduleDateToRequestReservation(
|
||||||
|
reservationRequest.dateRange.start
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
`Scheduling reservation request for ${scheduledDay.format('YYYY-MM-DD')}`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
scheduledReservationRequest: {
|
||||||
|
reservationRequest,
|
||||||
|
scheduledFor: scheduledDay,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('Reservation request can be performed now')
|
||||||
|
return {
|
||||||
|
scheduledReservationRequest: { reservationRequest },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { ReservationRequest } from "../common/request";
|
|
||||||
|
|
||||||
export interface RawReservationRequest extends Omit<ReservationRequest, 'dateRanges'> {
|
|
||||||
dateRanges: { start: string, end: string }[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InputEvent {
|
|
||||||
reservationRequest: RawReservationRequest
|
|
||||||
}
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { DateRange, Reservation } from '../../src/common/reservation'
|
||||||
|
|
||||||
describe('Reservation', () => {
|
describe('Reservation', () => {
|
||||||
test('will create correct possible dates', () => {
|
test('will create correct possible dates', () => {
|
||||||
const startDate = dayjs().set('hour', 12).set('minute', 0)
|
const startDate = dayjs().hour(12).minute(0).second(0).millisecond(0)
|
||||||
const endDate = dayjs().set('hour', 13).set('minute', 0)
|
const endDate = startDate.add(1, 'hour')
|
||||||
const dateRange: DateRange = {
|
const dateRange: DateRange = {
|
||||||
start: startDate,
|
start: startDate,
|
||||||
end: endDate,
|
end: endDate,
|
||||||
|
|
|
||||||
71
tests/lambdas/reservationScheduler.test.ts
Normal file
71
tests/lambdas/reservationScheduler.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { ValidationError, ValidationErrorCode } from '../../src/common/request'
|
||||||
|
import handler, { ReservationSchedulerResult } from '../../src/lambdas/reservationScheduler'
|
||||||
|
|
||||||
|
jest.mock('../../src/common/logger')
|
||||||
|
|
||||||
|
describe('reservationScheduler', () => {
|
||||||
|
test('should handle valid requests within reservation window', async () => {
|
||||||
|
const start = dayjs().add(15, 'minutes')
|
||||||
|
const end = start.add(15, 'minutes')
|
||||||
|
|
||||||
|
const payload = '{' +
|
||||||
|
'"username": "collin",' +
|
||||||
|
'"password": "password",' +
|
||||||
|
`"dateRange": { "start": "${start.toISOString()}", "end": "${end.toISOString()}" },` +
|
||||||
|
'"opponent": { "id": "123", "name": "collin" }' +
|
||||||
|
'}'
|
||||||
|
|
||||||
|
// @ts-expect-error - Stubbing AWS context
|
||||||
|
await expect(handler(payload, { awsRequestId: '1234' }, undefined)).resolves
|
||||||
|
.toMatchObject<ReservationSchedulerResult>({
|
||||||
|
scheduledReservationRequest: {
|
||||||
|
reservationRequest: {
|
||||||
|
username: 'collin',
|
||||||
|
password: 'password',
|
||||||
|
dateRange: { start, end },
|
||||||
|
opponent: { id: '123', name: 'collin' },
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle valid requests outside of reservation window', async () => {
|
||||||
|
const start = dayjs().add(15, 'days')
|
||||||
|
const end = start.add(15, 'minutes')
|
||||||
|
const payload = '{' +
|
||||||
|
'"username": "collin",' +
|
||||||
|
'"password": "password",' +
|
||||||
|
`"dateRange": { "start": "${start.toISOString()}", "end": "${end.toISOString()}" },` +
|
||||||
|
'"opponent": { "id": "123", "name": "collin" }' +
|
||||||
|
'}'
|
||||||
|
|
||||||
|
// @ts-expect-error - Stubbing AWS context
|
||||||
|
await expect(handler(payload, { awsRequestId: '1234' }, undefined)).resolves.toMatchObject<ReservationSchedulerResult>({
|
||||||
|
scheduledReservationRequest: {
|
||||||
|
reservationRequest: {
|
||||||
|
username: 'collin',
|
||||||
|
password: 'password',
|
||||||
|
dateRange: { start, end },
|
||||||
|
opponent: { id: '123', name: 'collin' },
|
||||||
|
},
|
||||||
|
scheduledFor: start.subtract(7, 'days').hour(0).minute(0).second(0).millisecond(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error for invalid requests', async () => {
|
||||||
|
const start = dayjs().add(15, 'days')
|
||||||
|
const end = start.add(15, 'minutes')
|
||||||
|
const payload = '{invalidJson' +
|
||||||
|
'"username": "collin",' +
|
||||||
|
'"password": "password",' +
|
||||||
|
`"dateRange": { "start": "${start.format()}", "end": "${end.format()}" },` +
|
||||||
|
'"opponent": { "id": "123", "name": "collin" }' +
|
||||||
|
'}'
|
||||||
|
|
||||||
|
// @ts-expect-error - Stubbing AWS context
|
||||||
|
await expect(handler(payload, { awsRequestId: '1234' }, undefined))
|
||||||
|
.rejects
|
||||||
|
.toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_JSON))
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Reference in a new issue