From b5153929e3677640733b1b984d318e78b1053846 Mon Sep 17 00:00:00 2001 From: nilspenzel <116674699+nilspenzel@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:15:42 +0100 Subject: [PATCH] Restrict alterations to tours and availabilites (#221) * wip * wip * wip close #177 close #178 * wip * wip * wip closes#177, closes#178, closes#227 * wip * wip * wip * wip * wip * wip * wip * wip close#227 * wip * wip * wip * wip * wip * wip * wip * Disallow moving tours to vehicles with insufficient capacity closes#239 * Format * wip --- src/lib/server/booking/evaluateRequest.ts | 13 ++-- src/routes/api/blacklist/viableBusStops.ts | 6 +- .../availability/api/availability/+server.ts | 55 +++++++++----- .../taxi/availability/api/tour/+server.ts | 75 ++++++++++++++++++- 4 files changed, 124 insertions(+), 25 deletions(-) diff --git a/src/lib/server/booking/evaluateRequest.ts b/src/lib/server/booking/evaluateRequest.ts index 022a69fa..0d6be8f1 100644 --- a/src/lib/server/booking/evaluateRequest.ts +++ b/src/lib/server/booking/evaluateRequest.ts @@ -84,7 +84,7 @@ export async function evaluateRequest( if (earliest >= latest) { return busStops.map((bs) => bs.times.map((_) => undefined)); } - const allowedTimes = getAllowedTimes(earliest, latest); + const allowedTimes = getAllowedTimes(earliest, latest, EARLIEST_SHIFT_START, LATEST_SHIFT_END); console.log( 'WHITELIST REQUEST: ALLOWED TIMES (RESTRICTION FROM 6 TO 21):\n', allowedTimes.map((i) => i.toString()) @@ -103,7 +103,12 @@ export async function evaluateRequest( return newTourEvaluations; } -export function getAllowedTimes(earliest: UnixtimeMs, latest: UnixtimeMs): Interval[] { +export function getAllowedTimes( + earliest: UnixtimeMs, + latest: UnixtimeMs, + startOnDay: UnixtimeMs, + endOnDay: UnixtimeMs +): Interval[] { if (earliest >= latest) { return []; } @@ -123,9 +128,7 @@ export function getAllowedTimes(earliest: UnixtimeMs, latest: UnixtimeMs): Inter timeZone: 'Europe/Berlin' }) ) - 12; - allowedTimes.push( - new Interval(t + EARLIEST_SHIFT_START - offset * HOUR, t + LATEST_SHIFT_END - offset * HOUR) - ); + allowedTimes.push(new Interval(t + startOnDay - offset * HOUR, t + endOnDay - offset * HOUR)); noonEarliestDay.setHours(noonEarliestDay.getHours() + 24); } return allowedTimes; diff --git a/src/routes/api/blacklist/viableBusStops.ts b/src/routes/api/blacklist/viableBusStops.ts index 41187baa..73321109 100644 --- a/src/routes/api/blacklist/viableBusStops.ts +++ b/src/routes/api/blacklist/viableBusStops.ts @@ -1,7 +1,9 @@ import { MAX_PASSENGER_WAITING_TIME_PICKUP, MAX_PASSENGER_WAITING_TIME_DROPOFF, - WGS84 + WGS84, + EARLIEST_SHIFT_START, + LATEST_SHIFT_END } from '$lib/constants'; import { db, type Database } from '$lib/server/db'; import { covers } from '$lib/server/db/covers'; @@ -185,7 +187,7 @@ export const getViableBusStops = async ( if (earliest >= latest) { return []; } - const allowedTimes = getAllowedTimes(earliest, latest); + const allowedTimes = getAllowedTimes(earliest, latest, EARLIEST_SHIFT_START, LATEST_SHIFT_END); busStopIntervals = busStopIntervals.map((b) => b.map((t) => { const allowed = Interval.intersect(allowedTimes, [t]); diff --git a/src/routes/taxi/availability/api/availability/+server.ts b/src/routes/taxi/availability/api/availability/+server.ts index f13d2acb..59ffc5e2 100644 --- a/src/routes/taxi/availability/api/availability/+server.ts +++ b/src/routes/taxi/availability/api/availability/+server.ts @@ -1,8 +1,14 @@ +import { EARLIEST_SHIFT_START, LATEST_SHIFT_END, MIN_PREP } from '$lib/constants'; +import { getAllowedTimes } from '$lib/server/booking/evaluateRequest'; import { db, type Database } from '$lib/server/db'; import { Interval } from '$lib/server/util/interval'; +import { HOUR, MINUTE } from '$lib/util/time'; import { json } from '@sveltejs/kit'; import { sql, type Insertable, type Selectable } from 'kysely'; +function getFirstAlterableTime() { + return Math.ceil((Date.now() + MIN_PREP) / (15 * MINUTE)) * 15 * MINUTE; +} type Availability = Selectable; type NewAvailability = Insertable; @@ -35,7 +41,11 @@ export const DELETE = async ({ locals, request }) => { throw 'invalid params'; } - const toRemove = new Interval(from, to); + const restrictedFrom = Math.max(getFirstAlterableTime(), from); + if (to <= restrictedFrom) { + return json({}); + } + const toRemove = new Interval(restrictedFrom, to); console.log('remove availability vehicle=', vehicleId, 'toRemove=', toRemove); await db.transaction().execute(async (trx) => { await sql`LOCK TABLE availability IN ACCESS EXCLUSIVE MODE;`.execute(trx); @@ -111,21 +121,32 @@ export const POST = async ({ locals, request }) => { throw 'invalid params'; } - await db - .insertInto('availability') - .columns(['startTime', 'endTime', 'vehicle']) - .expression((eb) => - eb - .selectFrom('vehicle') - .select((eb) => [ - eb.val(from).as('startTime'), - eb.val(to).as('endTime'), - 'vehicle.id as vehicle' - ]) - .where('vehicle.company', '=', companyId) - .where('vehicle.id', '=', vehicleId) - ) - .execute(); - + const restrictedFrom = Math.max(from, getFirstAlterableTime()); + if (to <= restrictedFrom) { + return json({}); + } + const interval = new Interval(restrictedFrom, to); + await Promise.all( + getAllowedTimes(restrictedFrom, to, EARLIEST_SHIFT_START - HOUR, LATEST_SHIFT_END + HOUR) + .map((allowed) => allowed.intersect(interval)) + .filter((a) => a != undefined) + .map((availability) => + db + .insertInto('availability') + .columns(['startTime', 'endTime', 'vehicle']) + .expression((eb) => + eb + .selectFrom('vehicle') + .select((eb) => [ + eb.val(availability.startTime).as('startTime'), + eb.val(availability.endTime).as('endTime'), + 'vehicle.id as vehicle' + ]) + .where('vehicle.company', '=', companyId) + .where('vehicle.id', '=', vehicleId) + ) + .execute() + ) + ); return json({}); }; diff --git a/src/routes/taxi/availability/api/tour/+server.ts b/src/routes/taxi/availability/api/tour/+server.ts index 2a38f01b..a7373ca5 100644 --- a/src/routes/taxi/availability/api/tour/+server.ts +++ b/src/routes/taxi/availability/api/tour/+server.ts @@ -1,8 +1,18 @@ import { json } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { sql } from 'kysely'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; +import { getPossibleInsertions } from '$lib/server/booking/getPossibleInsertions'; export const POST = async (event) => { + function getLatestEventTime(ev: { + communicatedTime: number; + scheduledTimeEnd: number; + scheduledTimeStart: number; + }) { + return Math.max(...[ev.scheduledTimeStart, ev.scheduledTimeEnd, ev.communicatedTime]); + } + const companyId = event.locals.session?.companyId; if (!companyId) { throw 'no company id'; @@ -13,6 +23,7 @@ export const POST = async (event) => { await sql`LOCK TABLE tour IN ACCESS EXCLUSIVE MODE;`.execute(trx); const movedTour = await trx .selectFrom('tour') + .innerJoin('vehicle', 'vehicle.id', 'tour.vehicle') .where(({ eb }) => eb.and([ eb('tour.id', '=', tourId), @@ -29,11 +40,73 @@ export const POST = async (event) => { ) ]) ) - .selectAll() + .select((eb) => [ + 'tour.departure', + 'tour.arrival', + 'vehicle.passengers', + 'vehicle.bikes', + 'vehicle.wheelchairs', + 'vehicle.luggage', + jsonArrayFrom( + eb + .selectFrom('request') + .whereRef('tour.id', '=', 'request.tour') + .select((eb) => [ + 'request.bikes', + 'request.wheelchairs', + 'request.luggage', + 'request.passengers', + jsonArrayFrom( + eb + .selectFrom('event') + .whereRef('event.request', '=', 'request.id') + .select([ + 'event.scheduledTimeStart', + 'event.scheduledTimeEnd', + 'event.communicatedTime', + 'event.isPickup' + ]) + ).as('events') + ]) + ).as('requests') + ]) .executeTakeFirst(); if (!movedTour) { return; } + console.assert(movedTour.requests.length != 0, 'Found a tour which contains no requests.'); + console.assert( + !movedTour.requests.some((r) => r.events.length == 0), + 'Found a request which contains no events.' + ); + const events = movedTour.requests.flatMap((r) => + r.events.map((e) => { + return { + ...e, + passengers: r.passengers, + bikes: r.bikes, + wheelchairs: r.wheelchairs, + luggage: r.luggage + }; + }) + ); + const possibleInsertions = getPossibleInsertions( + movedTour, + { passengers: 0, bikes: 0, wheelchairs: 0, luggage: 0 }, + events + ); + if ( + possibleInsertions.length != 1 || + possibleInsertions[0].earliestPickup != 0 || + possibleInsertions[0].latestDropoff != events.length + ) { + return; + } + + const firstEventTime = Math.min(...events.map((e) => getLatestEventTime(e))); + if (firstEventTime < Date.now()) { + return; + } const collidingTours = await trx .selectFrom('tour') .where('tour.vehicle', '=', vehicleId)