Skip to content

Commit

Permalink
Restrict alterations to tours and availabilites (#221)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nilspenzel authored Mar 10, 2025
1 parent 954f238 commit b515392
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 25 deletions.
13 changes: 8 additions & 5 deletions src/lib/server/booking/evaluateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 [];
}
Expand All @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/routes/api/blacklist/viableBusStops.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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]);
Expand Down
55 changes: 38 additions & 17 deletions src/routes/taxi/availability/api/availability/+server.ts
Original file line number Diff line number Diff line change
@@ -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<Database['availability']>;
type NewAvailability = Insertable<Database['availability']>;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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({});
};
75 changes: 74 additions & 1 deletion src/routes/taxi/availability/api/tour/+server.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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),
Expand All @@ -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)
Expand Down

0 comments on commit b515392

Please sign in to comment.