updats on the dispatch page
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { SolverRequest, Tuning } from '@/features/store-admin/autoAssign';
|
||||
import type { OrderRow } from './types';
|
||||
|
||||
/**
|
||||
@@ -22,18 +23,46 @@ import type { OrderRow } from './types';
|
||||
* does, which is the only reason its two identically-named endpoints do not
|
||||
* collide.
|
||||
*
|
||||
* ── What we deliberately do not call ────────────────────────────────────────
|
||||
* ── `riderassign` assigns against OUR fleet, not a foreign one ──────────────
|
||||
*
|
||||
* `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.
|
||||
* This file used to say the opposite — that `riderassign` was useless because
|
||||
* it returned orders assigned to `rider_id 883, "Rajan A"`, "not one of ours".
|
||||
* That was wrong, and it was wrong for the ordinary reason: an unfamiliar id
|
||||
* was taken for a stranger without checking the roster.
|
||||
*
|
||||
* Checked on 2026-09-10. `getriderroster?partnerid=44` lists 883 "Rajan A", and
|
||||
* so do the rider ids on tenant 916's own delivery rows — 883, 897, 950, 1111,
|
||||
* 1114, every one of them partner 44's, which is the Coimbatore fleet. The
|
||||
* solver reads the same database Fiesta does: `getallriders` on jupiter and
|
||||
* `getriders` on Fiesta return identical rosters and identical on-duty state.
|
||||
*
|
||||
* So auto-assignment works and `assign` below wires it up.
|
||||
*
|
||||
* ── What it cannot do yet, and why that is not our bug ──────────────────────
|
||||
*
|
||||
* The solver picks the riders itself, gated on `onduty = 1`, and that flag is 0
|
||||
* for all 118 riders on the platform — every region, checked the same day. So
|
||||
* `active_riders_pool` is 0 and every order comes back unassigned with "No
|
||||
* riders found (check partner online status)". Supplying riders in the body
|
||||
* does not help: `riders` and `active_riders` were both tried against a rider
|
||||
* the on-duty endpoint DOES report, and the pool stayed 0.
|
||||
*
|
||||
* Whatever is meant to set `onduty` is not setting it. That is worth asking the
|
||||
* app team about; nothing here can work around it.
|
||||
*
|
||||
* ── `routemate` is gone ─────────────────────────────────────────────────────
|
||||
*
|
||||
* The old console's second mode posted to `routemate.workolik.com/api/v1/
|
||||
* optimization/riderassign?strategy=multi_trip`, which accepted a rider list
|
||||
* inline. It answers 404 now, with and without the query string, so that route
|
||||
* around the `onduty` gate is closed too.
|
||||
*/
|
||||
|
||||
const OPTIMISER_BASE = 'https://routes.workolik.com/api/v1';
|
||||
|
||||
/** A solve can legitimately take a while. Past this, something is wrong. */
|
||||
const SOLVE_TIMEOUT_MS = 90_000;
|
||||
|
||||
/** 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. */
|
||||
@@ -103,6 +132,52 @@ async function post<T>(path: string, body: unknown): Promise<T> {
|
||||
return (payload.details ?? (payload as unknown)) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A run that is allowed to take its time, and to be cancelled.
|
||||
*
|
||||
* Separate from `post` for two reasons: the caller needs the whole envelope
|
||||
* rather than `details`, and a solve is slow enough that abandoning it has to
|
||||
* be possible. The caller's cancel and the timeout both have to be able to stop
|
||||
* it, so they are combined rather than one winning.
|
||||
*/
|
||||
async function postRaw(path: string, body: unknown, signal?: AbortSignal): Promise<unknown> {
|
||||
const timer = new AbortController();
|
||||
const stop = setTimeout(() => timer.abort(), SOLVE_TIMEOUT_MS);
|
||||
const onAbort = () => timer.abort();
|
||||
signal?.addEventListener('abort', onAbort);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${OPTIMISER_BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: timer.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
// 422 is the solver rejecting the payload and saying which field. Worth
|
||||
// showing verbatim — "422" on its own is not actionable.
|
||||
const text = await response.text().catch(() => '');
|
||||
throw new OptimiserError(
|
||||
text.trim().slice(0, 400) || `Optimiser refused the request (HTTP ${response.status})`,
|
||||
);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof OptimiserError) throw error;
|
||||
if ((error as Error)?.name === 'AbortError') {
|
||||
throw new OptimiserError(
|
||||
signal?.aborted
|
||||
? 'Cancelled.'
|
||||
: 'The optimiser did not answer in time. Nothing was assigned — the orders are untouched.',
|
||||
);
|
||||
}
|
||||
throw new OptimiserError('Could not reach the route optimiser');
|
||||
} finally {
|
||||
clearTimeout(stop);
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
export const optimiserApi = {
|
||||
/**
|
||||
* Put a set of orders in a sensible order.
|
||||
@@ -132,4 +207,38 @@ export const optimiserApi = {
|
||||
*/
|
||||
reconcile: (riders: PlannedRider[]) =>
|
||||
post<{ riders: PlannedRider[] }>('/optimization/reconcile-steps', { riders }),
|
||||
|
||||
/**
|
||||
* Propose a rider for each waiting order.
|
||||
*
|
||||
* A PLAN, not a commitment. Nothing is written anywhere until the operator
|
||||
* accepts it and the console makes its own `createdeliveries` call to Fiesta
|
||||
* through `buildDelivery` — the same path the manual assign bar uses, so
|
||||
* there is exactly one way a delivery is ever written. Safe to run twice and
|
||||
* safe to walk away from.
|
||||
*
|
||||
* ── Raw, not unwrapped ────────────────────────────────────────────────────
|
||||
*
|
||||
* `postRaw`, because the answer here IS the envelope: `zones` carries the
|
||||
* assignment, `meta` carries the accounting and the per-order reasons, and
|
||||
* `details` is only the flat fallback shape. `post` would hand back `details`
|
||||
* alone and throw the plan away — and `details` is `[]` on every run that
|
||||
* assigns nothing, which is every run today.
|
||||
*
|
||||
* ── Slow on purpose ───────────────────────────────────────────────────────
|
||||
*
|
||||
* Seven seconds for five orders, measured, and it is a solver so it grows
|
||||
* with the problem. `signal` is taken so a caller can offer to cancel; the
|
||||
* timeout is deliberately generous, since killing a run early abandons work
|
||||
* the operator is waiting on and teaches them the button is broken.
|
||||
*
|
||||
* `tuning` steers it — balanced, aggressive_speed, fuel_saver, zone_strict —
|
||||
* and the literal string `null` is a value it accepts, meaning "your default".
|
||||
*/
|
||||
assign: (request: SolverRequest, tuning: Tuning | null, signal?: AbortSignal) =>
|
||||
postRaw(
|
||||
`/optimization/riderassign?hypertuning_params=${tuning ?? 'null'}`,
|
||||
request,
|
||||
signal,
|
||||
),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user