dispatch page
This commit is contained in:
135
src/api/optimiser.ts
Normal file
135
src/api/optimiser.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { OrderRow } from './types';
|
||||
|
||||
/**
|
||||
* The route optimiser.
|
||||
*
|
||||
* A SEPARATE SERVICE from Fiesta — `routes.workolik.com`, "Route Optimization
|
||||
* API v2.0.0" — so it does not go through `client.ts`, which exists to talk to
|
||||
* one backend. Road routing is real (a Valhalla backend, not straight lines)
|
||||
* and the assignment model is trained: 3,627 records, tuned to 20 orders per
|
||||
* rider and an ideal load of 4.
|
||||
*
|
||||
* ── What it is and is not ───────────────────────────────────────────────────
|
||||
*
|
||||
* `optimization/createdeliveries` is NOT a create, despite the name it shares
|
||||
* with Fiesta's. It is a pure function: send an array of orders, get the same
|
||||
* array back reordered nearest-neighbour with `step`, `previouskms`,
|
||||
* `cumulativekms`, `actualkms` and `eta` added. Its own docs say forwarding is
|
||||
* paused, and the verified behaviour matches — it writes nothing anywhere.
|
||||
*
|
||||
* So the sequence is ours to commit: we take its answer and post it to Fiesta's
|
||||
* `deliveries/createdeliveries` ourselves. That is also what the xpress console
|
||||
* does, which is the only reason its two identically-named endpoints do not
|
||||
* collide.
|
||||
*
|
||||
* ── What we deliberately do not call ────────────────────────────────────────
|
||||
*
|
||||
* `optimization/riderassign` works and is useless to us: it assigns against its
|
||||
* OWN fleet. Sending our orders returned them assigned to `rider_id 883,
|
||||
* "Rajan A"` — not one of ours, and no parameter changes that. Auto-assignment
|
||||
* needs either a riders-inline variant of that endpoint or a mapping onto
|
||||
* `routemate`'s `doormile/assign`, which does accept `milers` inline. Neither
|
||||
* is wired here until somebody decides which.
|
||||
*/
|
||||
|
||||
const OPTIMISER_BASE = 'https://routes.workolik.com/api/v1';
|
||||
|
||||
/** An order as the optimiser hands it back — ours, plus the routing it added. */
|
||||
export interface SequencedStop extends OrderRow {
|
||||
/** 1..N. The order to visit in. */
|
||||
step?: number;
|
||||
/** Kilometres from the previous stop. */
|
||||
previouskms?: number;
|
||||
/** Running total for the round. */
|
||||
cumulativekms?: number;
|
||||
/**
|
||||
* Direct pickup-to-delivery distance, as a string.
|
||||
*
|
||||
* The service returns these as strings ("1.23"), which is also how Fiesta's
|
||||
* `deliveries.kms` / `actualkms` columns are typed — so they carry across
|
||||
* unconverted. Those columns are exactly the ones found holding the literal
|
||||
* text "null" in production, which broke the rider summary; a sequence run is
|
||||
* what should be filling them with real numbers.
|
||||
*/
|
||||
actualkms?: string;
|
||||
kms?: string;
|
||||
/** Minutes for this leg, and cumulative. Strings, as sent. */
|
||||
eta?: string;
|
||||
cumulative_eta?: string;
|
||||
ordertype?: string;
|
||||
}
|
||||
|
||||
/** One rider's leg of a plan, in the shape reconcile expects back. */
|
||||
export interface PlannedRider {
|
||||
rider_id: string | number;
|
||||
rider_name?: string;
|
||||
orders: SequencedStop[];
|
||||
}
|
||||
|
||||
export class OptimiserError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'OptimiserError';
|
||||
}
|
||||
}
|
||||
|
||||
async function post<T>(path: string, body: unknown): Promise<T> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${OPTIMISER_BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
} catch {
|
||||
// A separate host means a separate failure mode: the optimiser can be down
|
||||
// while Fiesta is fine. Said plainly so nobody debugs the wrong service.
|
||||
throw new OptimiserError('Could not reach the route optimiser');
|
||||
}
|
||||
|
||||
let payload: { code?: number; details?: T; message?: string; error?: { message?: string } };
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
throw new OptimiserError(`The optimiser sent a malformed reply (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new OptimiserError(payload?.error?.message || payload?.message || `Optimiser refused the request (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
// It answers `{code, details}` on the sequencing route and a bare object
|
||||
// elsewhere, so both shapes are unwrapped here rather than at each call site.
|
||||
return (payload.details ?? (payload as unknown)) as T;
|
||||
}
|
||||
|
||||
export const optimiserApi = {
|
||||
/**
|
||||
* Put a set of orders in a sensible order.
|
||||
*
|
||||
* Send them in any order; they come back sorted with a step number and the
|
||||
* distance and time between each. Verified against the live service with our
|
||||
* own field names — it reads `pickuplat`/`pickuplong` and
|
||||
* `deliverylat`/`deliverylong`, which order rows already carry.
|
||||
*/
|
||||
sequence: (orders: OrderRow[]) =>
|
||||
post<SequencedStop[]>('/optimization/createdeliveries', orders),
|
||||
|
||||
/**
|
||||
* Repair step numbers after somebody moved a stop by hand.
|
||||
*
|
||||
* Moving one order between riders breaks two rounds at once: the rider who
|
||||
* lost it has a hole in its sequence (1,2,3,5,6) and the one who gained it
|
||||
* has a step that collides or is missing. This fixes both.
|
||||
*
|
||||
* It MUST run before the plan is committed. The team who built the page this
|
||||
* came from call skipping it "the single biggest production bug to avoid in
|
||||
* this area" — it corrupts route sequences in the database, and nothing at
|
||||
* the point of the write can tell.
|
||||
*
|
||||
* Send only the riders that were edited; the response carries those riders
|
||||
* back and the rest of the plan is left alone.
|
||||
*/
|
||||
reconcile: (riders: PlannedRider[]) =>
|
||||
post<{ riders: PlannedRider[] }>('/optimization/reconcile-steps', { riders }),
|
||||
};
|
||||
Reference in New Issue
Block a user