@fabriktor/fx 0.0.138 → 0.0.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ import { isValidInputDate, minutesOfDay, sameCalendarDay, } from "../core/date.js";
2
+ import { errValidation } from "../core/err.js";
3
+ import { normalizeIDs, resolvedID } from "../core/id.js";
4
+ import { routeInput, validateRouteTimeWindow } from "./validation.js";
5
+ import { PreActionBlocked } from "../core/pre-action.js";
6
+ const routePreparationBlocked = "route_preparation";
7
+ const msgPlanningLocation = "Select an active location with a client and a saved address.";
8
+ const msgPlanningEmployees = "Choose available employees who are not already in another vehicle.";
9
+ export const RouteMutationBlock = {
10
+ Busy: "route_mutation_busy",
11
+ Changed: "route_mutation_changed",
12
+ Unconfirmed: "route_mutation_unconfirmed",
13
+ };
14
+ // Compare only editable fields. Vehicle IDs and itineraries belong to Go.
15
+ export function routePlanChanged(current, input) {
16
+ return (input.override_itinerary ||
17
+ JSON.stringify({
18
+ ...routeInput(current),
19
+ override_itinerary: false,
20
+ }) !== JSON.stringify({ ...routeInput(input), override_itinerary: false }));
21
+ }
22
+ export class RoutePreparationBlocked extends PreActionBlocked {
23
+ cause;
24
+ constructor(cause) {
25
+ super(routePreparationBlocked);
26
+ this.cause = cause;
27
+ }
28
+ }
29
+ export const routePlanningDefaults = {
30
+ days_ago: 0,
31
+ duration_in_minutes: 0,
32
+ consider_traffic: false,
33
+ start_early: false,
34
+ override_itinerary: false,
35
+ };
36
+ export function newRoutePlan(opts) {
37
+ return {
38
+ timezone: opts.day.tz,
39
+ from: { ...opts.day, h: opts.company.work_day_start_hour, min: 0 },
40
+ to: { ...opts.day, h: opts.company.work_day_end_hour, min: 0 },
41
+ start_place_id: opts.start_place_id,
42
+ vehicles: [],
43
+ destinations: [],
44
+ consider_traffic: routePlanningDefaults.consider_traffic,
45
+ start_early: routePlanningDefaults.start_early,
46
+ override_itinerary: routePlanningDefaults.override_itinerary,
47
+ };
48
+ }
49
+ export function routeDestination(location, owner_id) {
50
+ if (location.owner_id !== owner_id ||
51
+ location.is_archived ||
52
+ !location.place_id.trim() ||
53
+ !resolvedID(location.client_id))
54
+ throw errValidation({
55
+ field: "destinations",
56
+ msg: msgPlanningLocation,
57
+ });
58
+ return {
59
+ location_id: location.id,
60
+ client_id: location.client_id,
61
+ place_id: location.place_id,
62
+ tags: [],
63
+ duration_in_minutes: routePlanningDefaults.duration_in_minutes,
64
+ use_time_window: false,
65
+ time_window_start_hour_in_minutes: 0,
66
+ time_window_end_hour_in_minutes: 0,
67
+ };
68
+ }
69
+ export function validateRouteEmployees(ids, allowed, occupied = []) {
70
+ const available = new Set(allowed);
71
+ const used = new Set(occupied);
72
+ if (!ids.length ||
73
+ normalizeIDs(ids).length !== ids.length ||
74
+ ids.some((id) => !available.has(id) || used.has(id)))
75
+ throw errValidation({
76
+ field: "vehicles",
77
+ msg: msgPlanningEmployees,
78
+ });
79
+ }
80
+ // Suggestions are child data in an availability response. Intersect the client's
81
+ // window with the route window; never widen the hours offered by either party.
82
+ export function routeAvailabilityWindows(input, availability, location_id, client_id) {
83
+ validateRouteTimeWindow(input);
84
+ return availability.client_availabilities
85
+ .flatMap((group) => group.location_id === location_id ? group.availabilities : [])
86
+ .flatMap((window) => {
87
+ if (window.owner_id !== client_id ||
88
+ window.location_id !== location_id ||
89
+ window.timezone !== input.timezone ||
90
+ window.from.tz !== input.timezone ||
91
+ window.to.tz !== input.timezone ||
92
+ !isValidInputDate(window.from) ||
93
+ !isValidInputDate(window.to) ||
94
+ !sameCalendarDay(window.from, input.from) ||
95
+ !sameCalendarDay(window.to, input.to))
96
+ return [];
97
+ const start = Math.max(minutesOfDay(window.from), minutesOfDay(input.from));
98
+ const end = Math.min(minutesOfDay(window.to), minutesOfDay(input.to));
99
+ return start < end ? [{ availability: window, start, end }] : [];
100
+ });
101
+ }
@@ -0,0 +1,9 @@
1
+ import type { JobsOperator, RoutesOperator } from "@fabriktor/client";
2
+ import { type Location } from "@fabriktor/schema";
3
+ import { type RoutesActor } from "./workspace.js";
4
+ export declare function loadRoutePunchLocation(routes: RoutesOperator, jobs: JobsOperator, opts: {
5
+ actor: RoutesActor;
6
+ route_id: string;
7
+ vehicle_id: string;
8
+ location_id: string;
9
+ }): Promise<Location>;
@@ -0,0 +1,23 @@
1
+ import { UserTypeEmployee } from "@fabriktor/schema";
2
+ import { errValidation } from "../core/err.js";
3
+ import { routeCanTrack } from "./workspace.js";
4
+ const msgRoutePunch = "This destination is no longer available on the assigned route.";
5
+ export async function loadRoutePunchLocation(routes, jobs, opts) {
6
+ const route = (await routes.findOneRoute({ id: opts.route_id })).value;
7
+ if (!route ||
8
+ opts.actor.active_mode !== UserTypeEmployee ||
9
+ !routeCanTrack(opts.actor, route, opts.vehicle_id, opts.actor.owner_id) ||
10
+ !route.itinerary.routes
11
+ .find((v) => v.vehicle_id === opts.vehicle_id)
12
+ ?.stops.some((s) => s.location_id === opts.location_id))
13
+ throw errValidation({ msg: msgRoutePunch });
14
+ const location = (await jobs.findOneLocation({ id: opts.location_id })).value;
15
+ const destination = route.destinations.find((value) => value.location_id === opts.location_id);
16
+ if (!location ||
17
+ location.is_archived ||
18
+ location.owner_id !== route.owner_id ||
19
+ !destination ||
20
+ location.place_id !== destination.place_id)
21
+ throw errValidation({ msg: msgRoutePunch });
22
+ return location;
23
+ }
@@ -1,5 +1,7 @@
1
1
  import type { RoutesOperator } from "@fabriktor/client";
2
- import type { ClientAvailability, Route, RouteTeam, Tag, Tracking } from "@fabriktor/schema";
2
+ import type { ClientAvailability, InRoute, Route, RouteTeam, RPRoutesAvailability, Tag, Tracking } from "@fabriktor/schema";
3
+ import { RoutesWorkspace, type PrepareRouteOptions, type RoutesActor } from "./workspace.js";
4
+ import type { MultiCreateCheckpoint, MultiCreateAttempt } from "../core/multi-create.js";
3
5
  import { type OwnerSearchOptions, type SearchPage } from "../core/search.js";
4
6
  type RouteInsertOptions = Parameters<RoutesOperator["insertOneRoute"]>[0];
5
7
  type RouteReplaceOptions = Parameters<RoutesOperator["replaceOneRoute"]>[0];
@@ -11,24 +13,39 @@ type TrackingInsertOptions = Parameters<RoutesOperator["insertOneTracking"]>[0];
11
13
  type TrackingReplaceOptions = Parameters<RoutesOperator["replaceOneTracking"]>[0];
12
14
  type RouteTeamInsertOptions = Parameters<RoutesOperator["insertOneRouteTeam"]>[0];
13
15
  type RouteTeamReplaceOptions = Parameters<RoutesOperator["replaceOneRouteTeam"]>[0];
14
- type RouteInsert = RouteInsertOptions["in"];
15
16
  type RouteAvailability = NonNullable<Awaited<ReturnType<RoutesOperator["getAvailability"]>>["value"]>;
16
- export type CreateRouteOptions = {
17
- in: RouteInsert;
17
+ export type CreateRouteOptions = RouteInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created"> & {
18
18
  availability: RouteAvailability;
19
19
  };
20
20
  export type CreateValidatedRouteResult = Awaited<ReturnType<RoutesOperator["insertOneRoute"]>>;
21
21
  export type CreateRouteResult = CreateValidatedRouteResult;
22
- export type ReplaceRouteOptions = RouteReplaceOptions;
23
- export type CreateTagOptions = TagInsertOptions;
22
+ export type CreatePlannedRouteOptions = Omit<PrepareRouteOptions, "request"> & {
23
+ attempt: MultiCreateAttempt;
24
+ };
25
+ export type ReplaceRouteOptions = RouteReplaceOptions & {
26
+ availability?: RPRoutesAvailability;
27
+ expected?: Route;
28
+ before_request?: () => void;
29
+ };
30
+ export type RouteMutationOptions = {
31
+ actor: RoutesActor;
32
+ expected: Route;
33
+ signal?: AbortSignal;
34
+ before_request?: () => void;
35
+ };
36
+ export type ReplacePlannedRouteOptions = RouteMutationOptions & {
37
+ in: InRoute;
38
+ days_ago: PrepareRouteOptions["days_ago"];
39
+ };
40
+ export type CreateTagOptions = TagInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
24
41
  export type ReplaceTagOptions = TagReplaceOptions;
25
42
  export type SearchTagsOptions = OwnerSearchOptions;
26
43
  export type SearchTagsPage = SearchPage<Tag>;
27
- export type CreateClientAvailabilityOptions = ClientAvailabilityInsertOptions;
28
- export type ReplaceClientAvailabilityOptions = ClientAvailabilityReplaceOptions;
29
- export type CreateTrackingOptions = TrackingInsertOptions;
30
- export type ReplaceTrackingOptions = TrackingReplaceOptions;
31
- export type CreateRouteTeamOptions = RouteTeamInsertOptions;
44
+ export type CreateClientAvailabilityOptions = ClientAvailabilityInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
45
+ export type ReplaceClientAvailabilityOptions = ClientAvailabilityReplaceOptions & Pick<MultiCreateCheckpoint, "before_request">;
46
+ export type CreateTrackingOptions = TrackingInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
47
+ export type ReplaceTrackingOptions = TrackingReplaceOptions & Pick<MultiCreateCheckpoint, "before_request">;
48
+ export type CreateRouteTeamOptions = RouteTeamInsertOptions & Pick<MultiCreateCheckpoint, "before_request" | "on_created">;
32
49
  export type ReplaceRouteTeamOptions = RouteTeamReplaceOptions;
33
50
  export type SearchRouteTeamsOptions = OwnerSearchOptions;
34
51
  export type SearchRouteTeamsPage = SearchPage<RouteTeam>;
@@ -36,9 +53,13 @@ export type RoutesFXOptions = {
36
53
  routes: RoutesOperator;
37
54
  };
38
55
  export interface RoutesFXOperator {
56
+ readonly workspace: RoutesWorkspace;
39
57
  createRoute(opts: CreateRouteOptions): Promise<Route>;
58
+ createPlannedRoute(opts: CreatePlannedRouteOptions): Promise<Route>;
40
59
  createValidatedRoute(opts: CreateRouteOptions): Promise<CreateValidatedRouteResult>;
41
60
  replaceRoute(opts: ReplaceRouteOptions): Promise<Route>;
61
+ replacePlannedRoute(opts: ReplacePlannedRouteOptions): Promise<Route>;
62
+ deletePlannedRoute(opts: RouteMutationOptions): Promise<void>;
42
63
  createTag(opts: CreateTagOptions): Promise<Tag>;
43
64
  replaceTag(opts: ReplaceTagOptions): Promise<Tag>;
44
65
  searchTags(opts: SearchTagsOptions): Promise<SearchTagsPage>;
@@ -52,10 +73,18 @@ export interface RoutesFXOperator {
52
73
  }
53
74
  export declare class RoutesFX implements RoutesFXOperator {
54
75
  private routes;
76
+ private routeMutationPending;
77
+ readonly workspace: RoutesWorkspace;
55
78
  constructor(opts: RoutesFXOptions);
56
79
  createRoute(opts: CreateRouteOptions): Promise<Route>;
80
+ createPlannedRoute(opts: CreatePlannedRouteOptions): Promise<Route>;
57
81
  createValidatedRoute(opts: CreateRouteOptions): Promise<CreateValidatedRouteResult>;
58
82
  replaceRoute(opts: ReplaceRouteOptions): Promise<Route>;
83
+ replacePlannedRoute(opts: ReplacePlannedRouteOptions): Promise<Route>;
84
+ deletePlannedRoute(opts: RouteMutationOptions): Promise<void>;
85
+ private checkedRoute;
86
+ private requireCurrentRoute;
87
+ private withRouteMutation;
59
88
  createTag(opts: CreateTagOptions): Promise<Tag>;
60
89
  replaceTag(opts: ReplaceTagOptions): Promise<Tag>;
61
90
  searchTags(opts: SearchTagsOptions): Promise<SearchTagsPage>;
@@ -4,11 +4,12 @@
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
6
  import { errResolveFailed, errValidation } from "../core/err.js";
7
+ import { RoutesWorkspace, routeCanManage, routeCanPlan, } from "./workspace.js";
8
+ import { RoutePreparationBlocked, RouteMutationBlock, routePlanChanged, } from "./planning.js";
9
+ import { PreActionBlocked } from "../core/pre-action.js";
10
+ import { normalizedTagInput, routeInput, validateClientAvailabilityInput, validateRouteInput, validateRouteTeamInput, validateTrackingInput, } from "./validation.js";
7
11
  import { requiredOperationID } from "../core/id.js";
8
12
  import { runOwnerSearch, } from "../core/search.js";
9
- const maxHour = 23;
10
- const maxMinute = 59;
11
- const maxMinuteOfDay = 1440;
12
13
  const resourceRoute = "route";
13
14
  const resourceTag = "tag";
14
15
  const resourceClientAvailability = "client_availability";
@@ -16,6 +17,7 @@ const resourceTracking = "tracking";
16
17
  const resourceRouteTeam = "route_team";
17
18
  const msgCreatedRouteIDMissing = "Created route response did not contain an ID.";
18
19
  const msgRouteMissing = "Route could not be resolved after the mutation.";
20
+ const msgRouteOwner = "This route does not belong to the active employer.";
19
21
  const msgCreatedTagIDMissing = "Created tag response did not contain an ID.";
20
22
  const msgTagMissing = "Tag could not be resolved after the mutation.";
21
23
  const msgCreatedClientAvailabilityIDMissing = "Created client availability response did not contain an ID.";
@@ -24,42 +26,177 @@ const msgCreatedTrackingIDMissing = "Created tracking response did not contain a
24
26
  const msgTrackingMissing = "Tracking could not be resolved after the mutation.";
25
27
  const msgCreatedRouteTeamIDMissing = "Created route team response did not contain an ID.";
26
28
  const msgRouteTeamMissing = "Route team could not be resolved after the mutation.";
27
- const msgGlobalWindowSameDay = "The global route window must stay within one calendar day.";
28
- const msgGlobalWindowInvalid = "The global route window is invalid.";
29
- const msgEmployeeDuplicate = "An employee cannot be selected in more than one vehicle.";
30
- const msgEmployeeUnavailable = "A selected employee is not available for this route.";
31
- const msgLocationDuplicate = "A location cannot be selected more than once.";
32
- const msgLocationUnavailable = "A selected location is not available for this route.";
33
- const msgDestinationWindowInvalid = "A destination time window is invalid.";
34
- const msgDestinationWindowOutsideGlobal = "A destination time window must stay within the global route window.";
35
29
  export class RoutesFX {
36
30
  routes;
31
+ routeMutationPending = false;
32
+ workspace;
37
33
  constructor(opts) {
38
34
  this.routes = opts.routes;
35
+ this.workspace = new RoutesWorkspace(opts.routes);
39
36
  }
40
37
  async createRoute(opts) {
41
38
  const out = await this.createValidatedRoute(opts);
42
39
  const id = requiredOperationID(out, msgCreatedRouteIDMissing);
43
- return await this.findRoute(id);
40
+ return await this.findRoute(id, opts.signal);
44
41
  }
45
- async createValidatedRoute(opts) {
46
- validateRoute(opts.in, opts.availability);
47
- return await this.routes.insertOneRoute({
42
+ // Recheck availability immediately before each write. An acknowledged ID
43
+ // resumes only the read, so a failed follow-up GET cannot repeat optimization.
44
+ async createPlannedRoute(opts) {
45
+ const { attempt, actor } = opts;
46
+ if (!routeCanPlan(actor))
47
+ throw errValidation({ msg: msgRouteOwner });
48
+ if (attempt.created_id) {
49
+ const route = await this.findRoute(attempt.created_id, attempt.signal);
50
+ if (!routeCanManage(actor, route))
51
+ throw errValidation({ msg: msgRouteOwner });
52
+ return route;
53
+ }
54
+ const prepared = await this.workspace
55
+ .prepareRoute({
56
+ actor,
48
57
  in: opts.in,
58
+ days_ago: opts.days_ago,
59
+ request: { signal: attempt.signal },
60
+ })
61
+ .catch((error) => {
62
+ // No creation was attempted. Keep the draft editable in the shared queue.
63
+ throw new RoutePreparationBlocked(error);
64
+ });
65
+ return this.createRoute({
66
+ in: prepared.in,
67
+ availability: prepared.availability,
68
+ signal: attempt.signal,
69
+ idempotency_key: attempt.idempotency_key,
70
+ before_request: attempt.before_request,
71
+ on_created: attempt.on_created,
72
+ });
73
+ }
74
+ async createValidatedRoute(opts) {
75
+ const input = routeInput(opts.in);
76
+ validateRouteInput(input, opts.availability);
77
+ opts.before_request?.();
78
+ const out = await this.routes.insertOneRoute({
79
+ in: input,
80
+ signal: opts.signal,
81
+ idempotency_key: opts.idempotency_key,
49
82
  });
83
+ opts.on_created?.(requiredOperationID(out, msgCreatedRouteIDMissing));
84
+ return out;
50
85
  }
51
86
  async replaceRoute(opts) {
52
- await this.routes.replaceOneRoute(opts);
53
- return await this.findRoute(opts.id);
87
+ const current = await this.findRoute(opts.id, opts.signal);
88
+ if (opts.expected)
89
+ this.requireCurrentRoute(current, opts.expected);
90
+ const input = routeInput({ ...current, ...opts.in });
91
+ validateRouteInput(input, opts.availability, opts.id);
92
+ // Recompute changed plans, but do not invalidate vehicle/tracking IDs for
93
+ // an unchanged save. An explicit override still requests reoptimization.
94
+ const override_itinerary = routePlanChanged(current, input);
95
+ if (!override_itinerary)
96
+ return current;
97
+ opts.before_request?.();
98
+ await this.routes.replaceOneRoute({
99
+ id: opts.id,
100
+ signal: opts.signal,
101
+ in: {
102
+ ...opts.in,
103
+ ...(opts.in.from !== undefined ? { from: input.from } : {}),
104
+ ...(opts.in.to !== undefined ? { to: input.to } : {}),
105
+ ...(opts.in.vehicles !== undefined ? { vehicles: input.vehicles } : {}),
106
+ ...(opts.in.destinations !== undefined
107
+ ? { destinations: input.destinations }
108
+ : {}),
109
+ ...(opts.in.start_place_id !== undefined
110
+ ? { start_place_id: input.start_place_id }
111
+ : {}),
112
+ override_itinerary,
113
+ },
114
+ });
115
+ return await this.findRoute(opts.id, opts.signal);
116
+ }
117
+ async replacePlannedRoute(opts) {
118
+ return this.withRouteMutation(async () => {
119
+ const current = await this.checkedRoute(opts);
120
+ const input = routeInput(opts.in);
121
+ validateRouteInput(input);
122
+ if (!routePlanChanged(current, input))
123
+ return current;
124
+ const availability = await this.workspace.availability({
125
+ actor: opts.actor,
126
+ employee_ids: input.vehicles.flatMap((vehicle) => vehicle.employee_ids),
127
+ date: input.from,
128
+ days_ago: opts.days_ago,
129
+ request: { signal: opts.signal },
130
+ });
131
+ // Retain this route's own assignments, but never another route's.
132
+ validateRouteInput(input, availability, current.id);
133
+ const result = await this.replaceRoute({
134
+ id: current.id,
135
+ in: input,
136
+ expected: current,
137
+ availability,
138
+ signal: opts.signal,
139
+ before_request: opts.before_request,
140
+ });
141
+ if (result.override_itinerary ||
142
+ routePlanChanged(result, {
143
+ ...input,
144
+ override_itinerary: false,
145
+ }))
146
+ throw new PreActionBlocked(RouteMutationBlock.Unconfirmed);
147
+ return result;
148
+ });
149
+ }
150
+ async deletePlannedRoute(opts) {
151
+ return this.withRouteMutation(async () => {
152
+ const current = await this.checkedRoute(opts);
153
+ opts.signal?.throwIfAborted();
154
+ opts.before_request?.();
155
+ await this.routes.deleteOneRoute({ id: current.id });
156
+ });
157
+ }
158
+ async checkedRoute(opts) {
159
+ if (!routeCanManage(opts.actor, opts.expected))
160
+ throw errValidation({ msg: msgRouteOwner });
161
+ const current = await this.findRoute(opts.expected.id, opts.signal);
162
+ if (!routeCanManage(opts.actor, current))
163
+ throw errValidation({ msg: msgRouteOwner });
164
+ this.requireCurrentRoute(current, opts.expected);
165
+ return current;
166
+ }
167
+ requireCurrentRoute(current, expected) {
168
+ if (current.id !== expected.id ||
169
+ current.updated_at !== expected.updated_at)
170
+ throw new PreActionBlocked(RouteMutationBlock.Changed);
171
+ }
172
+ async withRouteMutation(fn) {
173
+ if (this.routeMutationPending)
174
+ throw new PreActionBlocked(RouteMutationBlock.Busy);
175
+ this.routeMutationPending = true;
176
+ try {
177
+ return await fn();
178
+ }
179
+ finally {
180
+ this.routeMutationPending = false;
181
+ }
54
182
  }
55
183
  async createTag(opts) {
56
- const out = await this.routes.insertOneTag(opts);
184
+ const { on_created, before_request, ...request } = opts;
185
+ const input = normalizedTagInput(opts.in);
186
+ before_request?.();
187
+ const out = await this.routes.insertOneTag({
188
+ ...request,
189
+ in: input,
190
+ });
57
191
  const id = requiredOperationID(out, msgCreatedTagIDMissing);
58
- return await this.findTag(id);
192
+ on_created?.(id);
193
+ return await this.findTag(id, opts.signal);
59
194
  }
60
195
  async replaceTag(opts) {
61
- await this.routes.replaceOneTag(opts);
62
- return await this.findTag(opts.id);
196
+ const current = await this.findTag(opts.id, opts.signal);
197
+ const input = normalizedTagInput({ ...current, ...opts.in });
198
+ await this.routes.replaceOneTag({ ...opts, in: input });
199
+ return await this.findTag(opts.id, opts.signal);
63
200
  }
64
201
  async searchTags(opts) {
65
202
  return await runOwnerSearch({
@@ -68,31 +205,53 @@ export class RoutesFX {
68
205
  });
69
206
  }
70
207
  async createClientAvailability(opts) {
71
- const out = await this.routes.insertOneClientAvailability(opts);
208
+ validateClientAvailabilityInput(opts.in);
209
+ const { before_request, on_created, ...request } = opts;
210
+ before_request?.();
211
+ const out = await this.routes.insertOneClientAvailability(request);
72
212
  const id = requiredOperationID(out, msgCreatedClientAvailabilityIDMissing);
73
- return await this.findClientAvailability(id);
213
+ on_created?.(id);
214
+ return await this.findClientAvailability(id, opts.signal);
74
215
  }
75
216
  async replaceClientAvailability(opts) {
76
- await this.routes.replaceOneClientAvailability(opts);
77
- return await this.findClientAvailability(opts.id);
217
+ const current = await this.findClientAvailability(opts.id, opts.signal);
218
+ validateClientAvailabilityInput({ ...current, ...opts.in });
219
+ const { before_request, ...request } = opts;
220
+ before_request?.();
221
+ await this.routes.replaceOneClientAvailability(request);
222
+ return await this.findClientAvailability(opts.id, opts.signal);
78
223
  }
79
224
  async createTracking(opts) {
80
- const out = await this.routes.insertOneTracking(opts);
225
+ validateTrackingInput(opts.in);
226
+ const { before_request, on_created, ...request } = opts;
227
+ before_request?.();
228
+ const out = await this.routes.insertOneTracking(request);
81
229
  const id = requiredOperationID(out, msgCreatedTrackingIDMissing);
82
- return await this.findTracking(id);
230
+ on_created?.(id);
231
+ return await this.findTracking(id, opts.signal);
83
232
  }
84
233
  async replaceTracking(opts) {
85
- await this.routes.replaceOneTracking(opts);
86
- return await this.findTracking(opts.id);
234
+ const current = await this.findTracking(opts.id, opts.signal);
235
+ validateTrackingInput({ ...current, ...opts.in }, current);
236
+ const { before_request, ...request } = opts;
237
+ before_request?.();
238
+ await this.routes.replaceOneTracking(request);
239
+ return await this.findTracking(opts.id, opts.signal);
87
240
  }
88
241
  async createRouteTeam(opts) {
89
- const out = await this.routes.insertOneRouteTeam(opts);
242
+ const { on_created, before_request, ...request } = opts;
243
+ validateRouteTeamInput(opts.in);
244
+ before_request?.();
245
+ const out = await this.routes.insertOneRouteTeam(request);
90
246
  const id = requiredOperationID(out, msgCreatedRouteTeamIDMissing);
91
- return await this.findRouteTeam(id);
247
+ on_created?.(id);
248
+ return await this.findRouteTeam(id, opts.signal);
92
249
  }
93
250
  async replaceRouteTeam(opts) {
251
+ const current = await this.findRouteTeam(opts.id, opts.signal);
252
+ validateRouteTeamInput({ ...current, ...opts.in });
94
253
  await this.routes.replaceOneRouteTeam(opts);
95
- return await this.findRouteTeam(opts.id);
254
+ return await this.findRouteTeam(opts.id, opts.signal);
96
255
  }
97
256
  async searchRouteTeams(opts) {
98
257
  return await runOwnerSearch({
@@ -100,9 +259,10 @@ export class RoutesFX {
100
259
  search: (searchOpts) => this.routes.searchManyRouteTeams(searchOpts),
101
260
  });
102
261
  }
103
- async findRoute(id) {
262
+ async findRoute(id, signal) {
104
263
  const out = await this.routes.findOneRoute({
105
264
  id,
265
+ signal,
106
266
  });
107
267
  const route = out.value;
108
268
  if (route === undefined) {
@@ -113,9 +273,10 @@ export class RoutesFX {
113
273
  }
114
274
  return route;
115
275
  }
116
- async findTag(id) {
276
+ async findTag(id, signal) {
117
277
  const out = await this.routes.findOneTag({
118
278
  id,
279
+ signal,
119
280
  });
120
281
  const tag = out.value;
121
282
  if (tag === undefined) {
@@ -126,9 +287,10 @@ export class RoutesFX {
126
287
  }
127
288
  return tag;
128
289
  }
129
- async findClientAvailability(id) {
290
+ async findClientAvailability(id, signal) {
130
291
  const out = await this.routes.findOneClientAvailability({
131
292
  id,
293
+ signal,
132
294
  });
133
295
  const client_availability = out.value;
134
296
  if (client_availability === undefined) {
@@ -139,9 +301,10 @@ export class RoutesFX {
139
301
  }
140
302
  return client_availability;
141
303
  }
142
- async findTracking(id) {
304
+ async findTracking(id, signal) {
143
305
  const out = await this.routes.findOneTracking({
144
306
  id,
307
+ signal,
145
308
  });
146
309
  const tracking = out.value;
147
310
  if (tracking === undefined) {
@@ -152,9 +315,10 @@ export class RoutesFX {
152
315
  }
153
316
  return tracking;
154
317
  }
155
- async findRouteTeam(id) {
318
+ async findRouteTeam(id, signal) {
156
319
  const out = await this.routes.findOneRouteTeam({
157
320
  id,
321
+ signal,
158
322
  });
159
323
  const route_team = out.value;
160
324
  if (route_team === undefined) {
@@ -169,105 +333,3 @@ export class RoutesFX {
169
333
  export function newRoutesFX(opts) {
170
334
  return new RoutesFX(opts);
171
335
  }
172
- function validateRoute(input, availability) {
173
- if (!sameDay(input.from, input.to)) {
174
- throw errValidation({
175
- field: "to",
176
- msg: msgGlobalWindowSameDay,
177
- });
178
- }
179
- const fromMinute = minuteOfDay(input.from);
180
- const toMinute = minuteOfDay(input.to);
181
- if (fromMinute === undefined ||
182
- toMinute === undefined ||
183
- fromMinute >= toMinute) {
184
- throw errValidation({
185
- field: "to",
186
- msg: msgGlobalWindowInvalid,
187
- });
188
- }
189
- validateVehicles(input, availability);
190
- validateDestinations(input, availability, fromMinute, toMinute);
191
- }
192
- function validateVehicles(input, availability) {
193
- const availableEmployeeIDs = new Set(availability.availables.employees.map((id) => id.trim()));
194
- const employeeIDs = new Set();
195
- for (const vehicle of input.vehicles) {
196
- for (const rawEmployeeID of vehicle.employee_ids) {
197
- const employeeID = rawEmployeeID.trim();
198
- if (employeeID === "") {
199
- continue;
200
- }
201
- if (employeeIDs.has(employeeID)) {
202
- throw errValidation({
203
- field: "vehicles",
204
- msg: msgEmployeeDuplicate,
205
- });
206
- }
207
- if (!availableEmployeeIDs.has(employeeID)) {
208
- throw errValidation({
209
- field: "vehicles",
210
- msg: msgEmployeeUnavailable,
211
- });
212
- }
213
- employeeIDs.add(employeeID);
214
- }
215
- }
216
- }
217
- function validateDestinations(input, availability, fromMinute, toMinute) {
218
- const availableLocationIDs = new Set(availability.availables.locations.map((id) => id.trim()));
219
- const locationIDs = new Set();
220
- for (const destination of input.destinations) {
221
- const locationID = destination.location_id.trim();
222
- if (locationID !== "") {
223
- if (locationIDs.has(locationID)) {
224
- throw errValidation({
225
- field: "destinations",
226
- msg: msgLocationDuplicate,
227
- });
228
- }
229
- if (!availableLocationIDs.has(locationID)) {
230
- throw errValidation({
231
- field: "destinations",
232
- msg: msgLocationUnavailable,
233
- });
234
- }
235
- locationIDs.add(locationID);
236
- }
237
- if (!destination.use_time_window) {
238
- continue;
239
- }
240
- const start = destination.time_window_start_hour_in_minutes;
241
- const end = destination.time_window_end_hour_in_minutes;
242
- if (!Number.isInteger(start) ||
243
- !Number.isInteger(end) ||
244
- start < 0 ||
245
- end > maxMinuteOfDay ||
246
- start >= end) {
247
- throw errValidation({
248
- field: "destinations",
249
- msg: msgDestinationWindowInvalid,
250
- });
251
- }
252
- if (start < fromMinute || end > toMinute) {
253
- throw errValidation({
254
- field: "destinations",
255
- msg: msgDestinationWindowOutsideGlobal,
256
- });
257
- }
258
- }
259
- }
260
- function sameDay(a, b) {
261
- return a.y === b.y && a.m === b.m && a.d === b.d;
262
- }
263
- function minuteOfDay(v) {
264
- if (!Number.isInteger(v.h) ||
265
- !Number.isInteger(v.min) ||
266
- v.h < 0 ||
267
- v.h > maxHour ||
268
- v.min < 0 ||
269
- v.min > maxMinute) {
270
- return undefined;
271
- }
272
- return v.h * 60 + v.min;
273
- }