slotlock 0.3.0 → 0.5.0
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.
- package/CHANGELOG.md +252 -0
- package/README.md +226 -17
- package/agent-server.d.ts +51 -8
- package/agent-server.js +478 -36
- package/agent-store-backend.d.ts +6 -0
- package/agent-store-backend.js +771 -30
- package/application-api.d.ts +22 -1
- package/application-api.js +412 -3
- package/booking-rules.d.ts +84 -0
- package/booking-rules.js +149 -0
- package/config.d.ts +20 -1
- package/config.js +33 -0
- package/dashboard.d.ts +61 -5
- package/dashboard.js +995 -139
- package/ddl.d.ts +5 -5
- package/ddl.js +590 -5
- package/engine.d.ts +5 -0
- package/engine.js +7 -4
- package/event-feed.d.ts +178 -0
- package/event-feed.js +435 -0
- package/index.d.ts +8 -2
- package/index.js +6 -1
- package/interop.d.ts +117 -0
- package/interop.js +356 -0
- package/maintenance.d.ts +14 -2
- package/maintenance.js +35 -3
- package/mcp-app.d.ts +7 -1
- package/mcp-app.js +259 -40
- package/metrics.d.ts +24 -0
- package/metrics.js +167 -0
- package/openapi.json +8346 -3400
- package/package.json +1 -1
- package/rate-limit.d.ts +10 -1
- package/rate-limit.js +25 -2
- package/rest-api.d.ts +22 -0
- package/rest-api.js +160 -15
- package/self-host.d.ts +26 -3
- package/self-host.js +316 -19
- package/slotlock-manifest.schema.json +1 -0
- package/slotlock.manifest.json +2 -1
- package/store.d.ts +317 -8
- package/store.js +1005 -89
- package/types.d.ts +87 -0
- package/types.js +0 -2
- package/webhooks.d.ts +91 -0
- package/webhooks.js +300 -0
package/application-api.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { SlotlockAgentPrincipal } from './agent-server.js';
|
|
3
3
|
import type { SlotlockTraceContext } from './mcp-modern.js';
|
|
4
|
-
export type SlotlockApplicationOperation = 'slotlock_create_resource' | 'slotlock_get_resource' | 'slotlock_archive_resource' | 'slotlock_restore_resource' | 'slotlock_delete_resource' | 'slotlock_list_busy' | 'slotlock_set_resource_availability' | 'slotlock_set_tenant_availability' | 'slotlock_acquire_hold' | 'slotlock_get_hold' | 'slotlock_list_holds' | 'slotlock_confirm_hold' | 'slotlock_release_hold' | 'slotlock_get_reservation' | 'slotlock_list_reservations' | 'slotlock_put_reservation' | 'slotlock_cancel_reservation' | 'slotlock_record_coverage' | 'slotlock_list_tenant_events' | 'slotlock_get_tenant_event' | 'slotlock_cancel_tenant_event' | 'slotlock_get_tenant' | 'slotlock_erase_tenant' | 'slotlock_run_maintenance';
|
|
4
|
+
export type SlotlockApplicationOperation = 'slotlock_create_resource' | 'slotlock_get_resource' | 'slotlock_archive_resource' | 'slotlock_restore_resource' | 'slotlock_delete_resource' | 'slotlock_list_busy' | 'slotlock_set_resource_availability' | 'slotlock_set_tenant_availability' | 'slotlock_export_resource_availability' | 'slotlock_import_resource_availability' | 'slotlock_set_resource_booking_rules' | 'slotlock_set_resource_pool' | 'slotlock_list_pools' | 'slotlock_get_resource_history' | 'slotlock_get_coverage' | 'slotlock_record_coverage_failure' | 'slotlock_delete_coverage' | 'slotlock_get_reservation_history' | 'slotlock_get_tenant_event_history' | 'slotlock_acquire_hold' | 'slotlock_get_hold' | 'slotlock_list_holds' | 'slotlock_confirm_hold' | 'slotlock_release_hold' | 'slotlock_get_reservation' | 'slotlock_list_reservations' | 'slotlock_put_reservation' | 'slotlock_cancel_reservation' | 'slotlock_check_in_reservation' | 'slotlock_join_waitlist' | 'slotlock_list_waitlist' | 'slotlock_leave_waitlist' | 'slotlock_record_coverage' | 'slotlock_list_tenant_events' | 'slotlock_get_tenant_event' | 'slotlock_export_tenant_event' | 'slotlock_cancel_tenant_event' | 'slotlock_get_tenant' | 'slotlock_erase_tenant' | 'slotlock_run_maintenance' | 'slotlock_list_changes' | 'slotlock_create_webhook' | 'slotlock_list_webhooks' | 'slotlock_delete_webhook' | 'slotlock_enable_webhook';
|
|
5
5
|
/** The scope an application operation needs, whichever operation it is. */
|
|
6
6
|
export declare const SLOTLOCK_APPLICATION_SCOPE = "admin";
|
|
7
7
|
export interface SlotlockApplicationInvocationContext {
|
|
@@ -26,6 +26,17 @@ export interface SlotlockApplicationBackend {
|
|
|
26
26
|
listBusy: Handler;
|
|
27
27
|
setResourceAvailability: Handler;
|
|
28
28
|
setTenantAvailability: Handler;
|
|
29
|
+
exportResourceAvailability: Handler;
|
|
30
|
+
importResourceAvailability: Handler;
|
|
31
|
+
setResourceBookingRules: Handler;
|
|
32
|
+
setResourcePool: Handler;
|
|
33
|
+
listPools: Handler;
|
|
34
|
+
getResourceHistory: Handler;
|
|
35
|
+
getCoverage: Handler;
|
|
36
|
+
recordCoverageFailure: Handler;
|
|
37
|
+
deleteCoverage: Handler;
|
|
38
|
+
getReservationHistory: Handler;
|
|
39
|
+
getTenantEventHistory: Handler;
|
|
29
40
|
acquireHold: Handler;
|
|
30
41
|
getHold: Handler;
|
|
31
42
|
listHolds: Handler;
|
|
@@ -35,13 +46,23 @@ export interface SlotlockApplicationBackend {
|
|
|
35
46
|
listReservations: Handler;
|
|
36
47
|
putReservation: Handler;
|
|
37
48
|
cancelReservation: Handler;
|
|
49
|
+
checkInReservation: Handler;
|
|
50
|
+
joinWaitlist: Handler;
|
|
51
|
+
listWaitlist: Handler;
|
|
52
|
+
leaveWaitlist: Handler;
|
|
38
53
|
recordCoverage: Handler;
|
|
39
54
|
listTenantEvents: Handler;
|
|
40
55
|
getTenantEvent: Handler;
|
|
56
|
+
exportTenantEvent: Handler;
|
|
41
57
|
cancelTenantEvent: Handler;
|
|
42
58
|
getTenant: Handler;
|
|
43
59
|
eraseTenant: Handler;
|
|
44
60
|
runMaintenance: Handler;
|
|
61
|
+
listChanges: Handler;
|
|
62
|
+
createWebhook: Handler;
|
|
63
|
+
listWebhooks: Handler;
|
|
64
|
+
deleteWebhook: Handler;
|
|
65
|
+
enableWebhook: Handler;
|
|
45
66
|
}
|
|
46
67
|
export interface SlotlockApplicationOperationDefinition {
|
|
47
68
|
name: SlotlockApplicationOperation;
|
package/application-api.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// This module holds only the contract (names, schemas, routes); it imports nothing from the agent
|
|
11
11
|
// server at runtime. The store-backed implementation is createSlotlockStoreApplicationBackend.
|
|
12
|
+
import { SLOTLOCK_CHANGE_TYPES } from './event-feed.js';
|
|
12
13
|
import { z } from 'zod';
|
|
13
14
|
import { SLOTLOCK_EVENT_OUTPUT } from './event-schema.js';
|
|
14
15
|
import { SLOTLOCK_CALENDAR_HORIZON_DAYS } from './rules.js';
|
|
@@ -59,9 +60,9 @@ const VISIBILITY = z.enum(['shared', 'private']);
|
|
|
59
60
|
/**
|
|
60
61
|
* A retry identity, as the `Idempotency-Key` request header or this field (the same value, if both
|
|
61
62
|
* are sent): the same key with the same request answers what the first did, and with another
|
|
62
|
-
* request is
|
|
63
|
+
* request is 422 idempotency_conflict.
|
|
63
64
|
*/
|
|
64
|
-
const IDEMPOTENCY_KEY = IDENTITY.describe('A retry identity, also accepted as the Idempotency-Key header: the same key with the same request answers what the first did; with another request,
|
|
65
|
+
const IDEMPOTENCY_KEY = IDENTITY.describe('A retry identity, also accepted as the Idempotency-Key header: the same key with the same request answers what the first did; with another request, 422 idempotency_conflict.');
|
|
65
66
|
/** Minutes after the end kept busy as well, for turnaround: at most 30 days. */
|
|
66
67
|
const BUFFER_AFTER_MINUTES = z
|
|
67
68
|
.number()
|
|
@@ -76,6 +77,19 @@ const AVAILABILITY_RULE = z
|
|
|
76
77
|
})
|
|
77
78
|
.strict();
|
|
78
79
|
const AVAILABILITY_RULES = z.array(AVAILABILITY_RULE).max(50);
|
|
80
|
+
/** What a booking on a resource must satisfy; each rule optional (`SlotlockBookingRules`). */
|
|
81
|
+
const BOOKING_RULES = z
|
|
82
|
+
.object({
|
|
83
|
+
min_notice_minutes: z.number().int().min(0).max(525_600).optional(),
|
|
84
|
+
max_advance_days: z.number().int().min(1).max(3_660).optional(),
|
|
85
|
+
slot_step_minutes: z.number().int().min(1).max(1_440).optional(),
|
|
86
|
+
turnaround_minutes: z.number().int().min(0).max(1_440).optional(),
|
|
87
|
+
enforce_hours: z.boolean().optional(),
|
|
88
|
+
check_in_minutes: z.number().int().min(1).max(1_440).optional(),
|
|
89
|
+
require_certain: z.boolean().optional(),
|
|
90
|
+
})
|
|
91
|
+
.strict();
|
|
92
|
+
const POOL = z.string().regex(/^[a-z0-9][a-z0-9._-]{0,99}$/);
|
|
79
93
|
const RESOURCE = z
|
|
80
94
|
.object({
|
|
81
95
|
id: RESOURCE_ID,
|
|
@@ -83,10 +97,76 @@ const RESOURCE = z
|
|
|
83
97
|
timezone: TIMEZONE,
|
|
84
98
|
/** The resource's own weekly hours; `null` when it uses the default. */
|
|
85
99
|
availability: AVAILABILITY_RULES.nullable(),
|
|
100
|
+
/** What a booking on it must satisfy; `null` when it has no rules. */
|
|
101
|
+
booking_rules: BOOKING_RULES.nullable(),
|
|
86
102
|
/** `private` resources are never shown to agents; fixed when the resource is created. */
|
|
87
103
|
visibility: VISIBILITY,
|
|
88
104
|
/** When it was archived; `null` while it is in use. */
|
|
89
105
|
archived_at: DATE_TIME.nullable(),
|
|
106
|
+
/** The pool it is a member of (a vehicle class, a room type); `null` when none. */
|
|
107
|
+
pool: POOL.nullable(),
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
const HISTORY_ENTRY = z
|
|
111
|
+
.object({
|
|
112
|
+
id: z.string().max(40),
|
|
113
|
+
type: z.string().max(100),
|
|
114
|
+
time: DATE_TIME,
|
|
115
|
+
/** Who made it: an API key, an agent's subject; `null` when not recorded. */
|
|
116
|
+
actor: z.string().max(200).nullable(),
|
|
117
|
+
/** The record as the change left it, with `before` for a change of an existing one. */
|
|
118
|
+
data: z.record(z.string(), z.unknown()),
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
const HISTORY_OUTPUT = z
|
|
122
|
+
.object({ history: z.array(HISTORY_ENTRY).max(100), next_before: z.string().max(40).nullable() })
|
|
123
|
+
.strict();
|
|
124
|
+
const HISTORY_PAGE = {
|
|
125
|
+
limit: z.number().int().min(1).max(100).default(50),
|
|
126
|
+
before: z.string().regex(/^msg_[0-9]{1,19}$/).optional(),
|
|
127
|
+
};
|
|
128
|
+
const CHANGE = z
|
|
129
|
+
.object({
|
|
130
|
+
specversion: z.literal('1.0'),
|
|
131
|
+
id: z.string().max(40),
|
|
132
|
+
source: z.string().max(300),
|
|
133
|
+
type: z.string().max(100),
|
|
134
|
+
subject: z.string().max(500),
|
|
135
|
+
time: DATE_TIME,
|
|
136
|
+
datacontenttype: z.literal('application/json'),
|
|
137
|
+
data: z.record(z.string(), z.unknown()),
|
|
138
|
+
})
|
|
139
|
+
.strict();
|
|
140
|
+
const WEBHOOK = z
|
|
141
|
+
.object({
|
|
142
|
+
id: z.string().uuid(),
|
|
143
|
+
url: z.string().max(2048),
|
|
144
|
+
event_types: z.array(z.enum(SLOTLOCK_CHANGE_TYPES)).nullable(),
|
|
145
|
+
created_at: DATE_TIME,
|
|
146
|
+
attempts: z.number().int().min(0),
|
|
147
|
+
failing_since: DATE_TIME.nullable(),
|
|
148
|
+
last_status: z.number().int().nullable(),
|
|
149
|
+
last_error: z.string().max(500).nullable(),
|
|
150
|
+
disabled_at: DATE_TIME.nullable(),
|
|
151
|
+
disabled_reason: z.enum(['gone', 'failing']).nullable(),
|
|
152
|
+
})
|
|
153
|
+
.strict();
|
|
154
|
+
const WAITLIST_ENTRY = z
|
|
155
|
+
.object({
|
|
156
|
+
id: z.string().uuid(),
|
|
157
|
+
/** The resource it waits on; `null` when it waits on a pool. */
|
|
158
|
+
resource_id: RESOURCE_ID.nullable(),
|
|
159
|
+
pool: POOL.nullable(),
|
|
160
|
+
start: DATE_TIME,
|
|
161
|
+
end: DATE_TIME,
|
|
162
|
+
holder_ref: z.string().min(1).max(500),
|
|
163
|
+
offer_ttl_seconds: z.number().int().min(60).max(86_400),
|
|
164
|
+
/** waiting; offered (hold_id holds the window on offered_resource_id); expired (it started first). */
|
|
165
|
+
status: z.enum(['waiting', 'offered', 'expired']),
|
|
166
|
+
hold_id: z.string().uuid().nullable(),
|
|
167
|
+
offered_resource_id: RESOURCE_ID.nullable(),
|
|
168
|
+
offered_at: DATE_TIME.nullable(),
|
|
169
|
+
created_at: DATE_TIME,
|
|
90
170
|
})
|
|
91
171
|
.strict();
|
|
92
172
|
const HOLD = z
|
|
@@ -116,6 +196,21 @@ const RESERVATION = z
|
|
|
116
196
|
* tenant-event routes take, `event_ref` the id its owner reads it by, and `owner_ref` who that is
|
|
117
197
|
* (`internal` for the tenant's own code, `agent:<digest>` for an API key).
|
|
118
198
|
*/
|
|
199
|
+
/** A JSCalendar Event (RFC 8984 §5.1); interop.ts's JSCalendarEvent names every property. */
|
|
200
|
+
const JSCALENDAR_EVENT = z
|
|
201
|
+
.object({
|
|
202
|
+
'@type': z.literal('Event'),
|
|
203
|
+
uid: z.string().max(200),
|
|
204
|
+
updated: DATE_TIME,
|
|
205
|
+
sequence: z.number().int().min(0),
|
|
206
|
+
title: z.string(),
|
|
207
|
+
start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/),
|
|
208
|
+
timeZone: TIMEZONE,
|
|
209
|
+
duration: z.string().regex(/^PT/),
|
|
210
|
+
status: z.enum(['confirmed', 'tentative']),
|
|
211
|
+
freeBusyStatus: z.enum(['free', 'busy']),
|
|
212
|
+
})
|
|
213
|
+
.passthrough();
|
|
119
214
|
const TENANT_EVENT = SLOTLOCK_EVENT_OUTPUT.extend({
|
|
120
215
|
event_ref: z.string().max(500),
|
|
121
216
|
owner_ref: z.string().max(500),
|
|
@@ -222,6 +317,112 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
222
317
|
invoke: 'setResourceAvailability',
|
|
223
318
|
example: { resource_id: EXAMPLE_RESOURCE_ID, rules: EXAMPLE_RULES },
|
|
224
319
|
},
|
|
320
|
+
{
|
|
321
|
+
name: 'slotlock_export_resource_availability',
|
|
322
|
+
title: "Export a resource's bookable hours as iCalendar",
|
|
323
|
+
description: "One resource's own weekly bookable hours as an iCalendar VAVAILABILITY (RFC 7953) for a calendar or scheduling tool: an AVAILABLE per rule, its DTSTART's TZID the resource's IANA time zone. `null` when the resource uses the server's default.",
|
|
324
|
+
method: 'GET',
|
|
325
|
+
path: '/v1/resources/{resource_id}/availability/ical',
|
|
326
|
+
input: z.object({ resource_id: RESOURCE_ID }).strict(),
|
|
327
|
+
output: z.object({ icalendar: z.string().max(200_000).nullable() }).strict(),
|
|
328
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
329
|
+
invoke: 'exportResourceAvailability',
|
|
330
|
+
example: { resource_id: EXAMPLE_RESOURCE_ID },
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: 'slotlock_import_resource_availability',
|
|
334
|
+
title: "Import a resource's bookable hours from iCalendar",
|
|
335
|
+
description: "Set one resource's own weekly bookable hours from an iCalendar VAVAILABILITY (RFC 7953), as slotlock_set_resource_availability does from rules: each AVAILABLE a weekly RRULE with a start time and DTEND or DURATION. Times must be in the resource's time zone (its TZID) or floating. 400 invalid_availability names what weekly hours cannot say: a bounded VAVAILABILITY, RDATE, EXDATE, a single window, another zone.",
|
|
336
|
+
method: 'PUT',
|
|
337
|
+
path: '/v1/resources/{resource_id}/availability/ical',
|
|
338
|
+
input: z
|
|
339
|
+
.object({ resource_id: RESOURCE_ID, icalendar: z.string().min(1).max(65_536) })
|
|
340
|
+
.strict(),
|
|
341
|
+
output: z.object({ resource: RESOURCE }).strict(),
|
|
342
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
343
|
+
invoke: 'importResourceAvailability',
|
|
344
|
+
example: {
|
|
345
|
+
resource_id: EXAMPLE_RESOURCE_ID,
|
|
346
|
+
icalendar: 'BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Example//EN\r\nBEGIN:VAVAILABILITY\r\nUID:hours-1\r\nDTSTAMP:20260101T000000Z\r\nBEGIN:AVAILABLE\r\nUID:hours-1-0\r\nDTSTAMP:20260101T000000Z\r\nDTSTART;TZID=Europe/London:20260105T090000\r\nDTEND;TZID=Europe/London:20260105T170000\r\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\r\nEND:AVAILABLE\r\nEND:VAVAILABILITY\r\nEND:VCALENDAR\r\n',
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: 'slotlock_set_resource_booking_rules',
|
|
351
|
+
title: "Set a resource's booking rules",
|
|
352
|
+
description: "Set what a booking on one resource must satisfy from now on: minimum notice, how far ahead, the step starts fall on (from local midnight in the resource's time zone), the turnaround kept free after each booking, and whether bookings must lie inside its hours. `null` clears them. Holds, reservations, moves and agents' events are refused with the rule they break; searches offer only what they allow.",
|
|
353
|
+
method: 'PUT',
|
|
354
|
+
path: '/v1/resources/{resource_id}/booking-rules',
|
|
355
|
+
input: z.object({ resource_id: RESOURCE_ID, rules: BOOKING_RULES.nullable() }).strict(),
|
|
356
|
+
output: z.object({ resource: RESOURCE }).strict(),
|
|
357
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
358
|
+
invoke: 'setResourceBookingRules',
|
|
359
|
+
example: {
|
|
360
|
+
resource_id: EXAMPLE_RESOURCE_ID,
|
|
361
|
+
rules: { min_notice_minutes: 120, slot_step_minutes: 30, turnaround_minutes: 30, enforce_hours: true },
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'slotlock_set_resource_pool',
|
|
366
|
+
title: "Set a resource's pool",
|
|
367
|
+
description: "Make a resource a member of a pool, a class of interchangeable resources such as a vehicle class or a room type, or take it out (`null`). Agents book the class with slotlock_book_pool, which takes the first member free; the class's capacity is its number of members. A resource is in one pool at most; pool names are 1-100 of a-z, 0-9, '.', '_' and '-'.",
|
|
368
|
+
method: 'PUT',
|
|
369
|
+
path: '/v1/resources/{resource_id}/pool',
|
|
370
|
+
input: z.object({ resource_id: RESOURCE_ID, pool: POOL.nullable() }).strict(),
|
|
371
|
+
output: z.object({ resource: RESOURCE }).strict(),
|
|
372
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
373
|
+
invoke: 'setResourcePool',
|
|
374
|
+
example: { resource_id: EXAMPLE_RESOURCE_ID, pool: 'van' },
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'slotlock_list_pools',
|
|
378
|
+
title: 'List pools',
|
|
379
|
+
description: "The tenant's pools by name, each with its capacity: how many members it has that are not archived.",
|
|
380
|
+
method: 'GET',
|
|
381
|
+
path: '/v1/pools',
|
|
382
|
+
input: z.object({}).strict(),
|
|
383
|
+
output: z
|
|
384
|
+
.object({ pools: z.array(z.object({ pool: POOL, capacity: z.number().int().min(1) }).strict()).max(1000) })
|
|
385
|
+
.strict(),
|
|
386
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
387
|
+
invoke: 'listPools',
|
|
388
|
+
example: {},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'slotlock_get_resource_history',
|
|
392
|
+
title: "A resource's history",
|
|
393
|
+
description: "What happened on one of the tenant's resources, newest first: each hold, booking and event made, moved, confirmed, released or cancelled, when, by whom (the API key or agent), and for a change what it was before. Kept 30 days. Pass next_before as before for older entries.",
|
|
394
|
+
method: 'GET',
|
|
395
|
+
path: '/v1/resources/{resource_id}/history',
|
|
396
|
+
input: z.object({ resource_id: RESOURCE_ID, ...HISTORY_PAGE }).strict(),
|
|
397
|
+
output: HISTORY_OUTPUT,
|
|
398
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
399
|
+
invoke: 'getResourceHistory',
|
|
400
|
+
example: { resource_id: EXAMPLE_RESOURCE_ID, limit: 20 },
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'slotlock_get_reservation_history',
|
|
404
|
+
title: "A booking's history",
|
|
405
|
+
description: 'What happened to one booking, by its reference, newest first: made, moved, cancelled, when and by whom, as slotlock_get_resource_history.',
|
|
406
|
+
method: 'GET',
|
|
407
|
+
path: '/v1/reservations/{reservation_ref}/history',
|
|
408
|
+
input: z.object({ reservation_ref: z.string().min(1).max(500), ...HISTORY_PAGE }).strict(),
|
|
409
|
+
output: HISTORY_OUTPUT,
|
|
410
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
411
|
+
invoke: 'getReservationHistory',
|
|
412
|
+
example: { reservation_ref: 'BK-42', limit: 20 },
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: 'slotlock_get_tenant_event_history',
|
|
416
|
+
title: "An event's history",
|
|
417
|
+
description: 'What happened to one calendar event of the tenant, by its id, newest first: created, changed, cancelled, when and by whom, as slotlock_get_resource_history.',
|
|
418
|
+
method: 'GET',
|
|
419
|
+
path: '/v1/tenant/events/{event_id}/history',
|
|
420
|
+
input: z.object({ event_id: z.string().uuid(), ...HISTORY_PAGE }).strict(),
|
|
421
|
+
output: HISTORY_OUTPUT,
|
|
422
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
423
|
+
invoke: 'getTenantEventHistory',
|
|
424
|
+
example: { event_id: '5e8ff9bf-55ba-4508-9d22-e984129be6b5', limit: 20 },
|
|
425
|
+
},
|
|
225
426
|
{
|
|
226
427
|
name: 'slotlock_set_tenant_availability',
|
|
227
428
|
title: "Set every resource's bookable hours",
|
|
@@ -237,7 +438,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
237
438
|
{
|
|
238
439
|
name: 'slotlock_acquire_hold',
|
|
239
440
|
title: 'Hold time',
|
|
240
|
-
description: 'Occupy a window without booking it, for example while a customer pays. The hold blocks every other booking until it expires (the database clock, ttl_seconds from now), is confirmed, or is released. buffer_after_minutes keeps the time after it busy as well, and a confirmed hold keeps it. A taken window answers 409 overlap. With an idempotency key (the Idempotency-Key header or idempotency_key), a retry answers the hold the first request made, until that hold would have expired, is released or is confirmed; the same key with another request is
|
|
441
|
+
description: 'Occupy a window without booking it, for example while a customer pays. The hold blocks every other booking until it expires (the database clock, ttl_seconds from now), is confirmed, or is released. buffer_after_minutes keeps the time after it busy as well, and a confirmed hold keeps it. A taken window answers 409 overlap. With an idempotency key (the Idempotency-Key header or idempotency_key), a retry answers the hold the first request made, until that hold would have expired, is released or is confirmed; the same key with another request is 422 idempotency_conflict.',
|
|
241
442
|
method: 'POST',
|
|
242
443
|
path: '/v1/holds',
|
|
243
444
|
input: z
|
|
@@ -396,6 +597,78 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
396
597
|
invoke: 'cancelReservation',
|
|
397
598
|
example: { reservation_ref: 'booking-BK-42', revision: 2 },
|
|
398
599
|
},
|
|
600
|
+
{
|
|
601
|
+
name: 'slotlock_check_in_reservation',
|
|
602
|
+
title: 'Check in a booking',
|
|
603
|
+
description: "Claim a booking by its reference: the customer has arrived or picked up. On a resource whose booking rules set check_in_minutes, a booking not checked in that long after its start is released by maintenance (reservation.no_show on the feed) and offered to the waitlist. Check-in opens an hour before the start (409 check_in_not_open before then) and moving the booking to another start clears it. Checking in again keeps the first time; a booking that has ended answers 409 reservation_ended.",
|
|
604
|
+
method: 'POST',
|
|
605
|
+
path: '/v1/reservations/{reservation_ref}/check-in',
|
|
606
|
+
input: z.object({ reservation_ref: IDENTITY }).strict(),
|
|
607
|
+
output: z.object({ reference: IDENTITY, checked_in_at: DATE_TIME }).strict(),
|
|
608
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
609
|
+
invoke: 'checkInReservation',
|
|
610
|
+
example: { reservation_ref: 'booking-BK-42' },
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: 'slotlock_join_waitlist',
|
|
614
|
+
title: 'Join the waitlist',
|
|
615
|
+
description: "Wait for a window on one resource (resource_id) or on any member of a pool (pool) that is taken now. When it comes free (a booking cancelled, a hold released or expired, a no-show), the entries waiting for it are offered in the order they joined: the first gets a hold of it for holder_ref, for offer_ttl_seconds (default 900), and the feed says so (waitlist.offered, with hold_id): confirm the hold to book it. An entry whose window starts first expires (waitlist.expired). A holder may wait for 20 windows at once (409 waitlist_limit_reached); a window is at most 31 days.",
|
|
616
|
+
method: 'POST',
|
|
617
|
+
path: '/v1/waitlist',
|
|
618
|
+
input: z
|
|
619
|
+
.object({
|
|
620
|
+
resource_id: RESOURCE_ID.optional(),
|
|
621
|
+
pool: POOL.optional(),
|
|
622
|
+
start: DATE_TIME,
|
|
623
|
+
end: DATE_TIME,
|
|
624
|
+
holder_ref: z.string().trim().min(1).max(500),
|
|
625
|
+
offer_ttl_seconds: z.number().int().min(60).max(86_400).optional(),
|
|
626
|
+
})
|
|
627
|
+
.strict()
|
|
628
|
+
.refine((input) => (input.resource_id === undefined) !== (input.pool === undefined), {
|
|
629
|
+
message: 'Name resource_id or pool, not both',
|
|
630
|
+
}),
|
|
631
|
+
output: z.object({ entry: WAITLIST_ENTRY }).strict(),
|
|
632
|
+
risk: { readOnly: false, destructive: false, idempotent: false },
|
|
633
|
+
invoke: 'joinWaitlist',
|
|
634
|
+
example: {
|
|
635
|
+
pool: 'van',
|
|
636
|
+
start: '2027-03-29T09:00:00Z',
|
|
637
|
+
end: '2027-03-29T17:00:00Z',
|
|
638
|
+
holder_ref: 'customer-42',
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
name: 'slotlock_list_waitlist',
|
|
643
|
+
title: 'List the waitlist',
|
|
644
|
+
description: "The tenant's waitlist entries in the order they are offered (oldest first), with what each was offered: optionally only those of one resource, pool or holder.",
|
|
645
|
+
method: 'GET',
|
|
646
|
+
path: '/v1/waitlist',
|
|
647
|
+
input: z
|
|
648
|
+
.object({
|
|
649
|
+
resource_id: RESOURCE_ID.optional(),
|
|
650
|
+
pool: POOL.optional(),
|
|
651
|
+
holder_ref: z.string().trim().min(1).max(500).optional(),
|
|
652
|
+
limit: z.number().int().min(1).max(1_000).default(100),
|
|
653
|
+
})
|
|
654
|
+
.strict(),
|
|
655
|
+
output: z.object({ entries: z.array(WAITLIST_ENTRY).max(1_000) }).strict(),
|
|
656
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
657
|
+
invoke: 'listWaitlist',
|
|
658
|
+
example: { pool: 'van' },
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: 'slotlock_leave_waitlist',
|
|
662
|
+
title: 'Leave the waitlist',
|
|
663
|
+
description: 'Stop waiting. A hold offered and not yet confirmed is released with the entry, and the time offered to the next who waits; one confirmed is a booking and stays. left is false when there was no such entry.',
|
|
664
|
+
method: 'DELETE',
|
|
665
|
+
path: '/v1/waitlist/{entry_id}',
|
|
666
|
+
input: z.object({ entry_id: z.string().uuid() }).strict(),
|
|
667
|
+
output: z.object({ left: z.boolean() }).strict(),
|
|
668
|
+
risk: { readOnly: false, destructive: true, idempotent: true },
|
|
669
|
+
invoke: 'leaveWaitlist',
|
|
670
|
+
example: { entry_id: '1b4e28ba-2fa1-41d2-883f-0016d3cca427' },
|
|
671
|
+
},
|
|
399
672
|
{
|
|
400
673
|
name: 'slotlock_record_coverage',
|
|
401
674
|
title: 'Record calendar coverage',
|
|
@@ -423,6 +696,59 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
423
696
|
revision: 7,
|
|
424
697
|
},
|
|
425
698
|
},
|
|
699
|
+
{
|
|
700
|
+
name: 'slotlock_get_coverage',
|
|
701
|
+
title: "A resource's coverage and its health",
|
|
702
|
+
description: "Each external calendar (source) that records coverage of a resource: the window it covers, when it was last read, its last error, and whether it is stale (not read for a day, so the time it covers is unproven, not free). A source that goes stale is also put on the change feed once, as coverage.stale, until it is read again.",
|
|
703
|
+
method: 'GET',
|
|
704
|
+
path: '/v1/resources/{resource_id}/coverage',
|
|
705
|
+
input: z.object({ resource_id: RESOURCE_ID }).strict(),
|
|
706
|
+
output: z
|
|
707
|
+
.object({
|
|
708
|
+
sources: z
|
|
709
|
+
.array(z
|
|
710
|
+
.object({
|
|
711
|
+
source: SOURCE,
|
|
712
|
+
start: DATE_TIME,
|
|
713
|
+
end: DATE_TIME,
|
|
714
|
+
revision: REVISION,
|
|
715
|
+
observed_at: DATE_TIME,
|
|
716
|
+
last_error: z.string().max(500).nullable(),
|
|
717
|
+
last_error_at: DATE_TIME.nullable(),
|
|
718
|
+
stale: z.boolean(),
|
|
719
|
+
})
|
|
720
|
+
.strict())
|
|
721
|
+
.max(100),
|
|
722
|
+
})
|
|
723
|
+
.strict(),
|
|
724
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
725
|
+
invoke: 'getCoverage',
|
|
726
|
+
example: { resource_id: EXAMPLE_RESOURCE_ID },
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
name: 'slotlock_record_coverage_failure',
|
|
730
|
+
title: 'Record a failed read of a source',
|
|
731
|
+
description: "Record that reading an external calendar failed (it answered 500, a token expired): kept as the source's last error, shown by slotlock_get_coverage, while its coverage stays as it was. 404 coverage_not_found for a source that has never recorded coverage of the resource.",
|
|
732
|
+
method: 'POST',
|
|
733
|
+
path: '/v1/resources/{resource_id}/coverage/{source}/failure',
|
|
734
|
+
input: z.object({ resource_id: RESOURCE_ID, source: SOURCE, error: z.string().min(1).max(500) }).strict(),
|
|
735
|
+
output: z.object({ recorded: z.literal(true) }).strict(),
|
|
736
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
737
|
+
invoke: 'recordCoverageFailure',
|
|
738
|
+
example: { resource_id: EXAMPLE_RESOURCE_ID, source: 'google:fleet@example.com', error: 'HTTP 401 from the calendar API' },
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: 'slotlock_delete_coverage',
|
|
742
|
+
title: 'Retire a source',
|
|
743
|
+
description: "Forget a source's coverage of the resource, for a calendar sync that is disconnected or replaced: free/busy and a resource whose booking rules require certainty stop waiting on it. 404 coverage_not_found for a source with no coverage of the resource.",
|
|
744
|
+
method: 'DELETE',
|
|
745
|
+
path: '/v1/resources/{resource_id}/coverage/{source}',
|
|
746
|
+
input: z.object({ resource_id: RESOURCE_ID, source: SOURCE }).strict(),
|
|
747
|
+
output: z.object({ deleted: z.literal(true) }).strict(),
|
|
748
|
+
risk: { readOnly: false, destructive: true, idempotent: false },
|
|
749
|
+
invoke: 'deleteCoverage',
|
|
750
|
+
example: { resource_id: EXAMPLE_RESOURCE_ID, source: 'google:fleet@example.com' },
|
|
751
|
+
},
|
|
426
752
|
{
|
|
427
753
|
name: 'slotlock_list_tenant_events',
|
|
428
754
|
title: "List a resource's events, every key's",
|
|
@@ -458,6 +784,18 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
458
784
|
invoke: 'getTenantEvent',
|
|
459
785
|
example: { event_id: EXAMPLE_EVENT_ID },
|
|
460
786
|
},
|
|
787
|
+
{
|
|
788
|
+
name: 'slotlock_export_tenant_event',
|
|
789
|
+
title: 'Export an event as JSCalendar',
|
|
790
|
+
description: "One of the tenant's events as a JSCalendar Event (RFC 8984), the JSON form of a calendar event: uid is its id, sequence its revision, start a local time in timeZone with an exact duration, its people participants, its reminders alerts, its recurrence recurrenceRules and recurrenceOverrides (and the RRULE verbatim as slotlock.pylota.io:rrule). A cancelled or unknown one answers 404 event_not_found.",
|
|
791
|
+
method: 'GET',
|
|
792
|
+
path: '/v1/tenant/events/{event_id}/jscalendar',
|
|
793
|
+
input: z.object({ event_id: EVENT_ID }).strict(),
|
|
794
|
+
output: z.object({ event: JSCALENDAR_EVENT }).strict(),
|
|
795
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
796
|
+
invoke: 'exportTenantEvent',
|
|
797
|
+
example: { event_id: EXAMPLE_EVENT_ID },
|
|
798
|
+
},
|
|
461
799
|
{
|
|
462
800
|
name: 'slotlock_cancel_tenant_event',
|
|
463
801
|
title: 'Cancel any event of the tenant',
|
|
@@ -535,6 +873,10 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
535
873
|
conflicts: z.number().int().min(0),
|
|
536
874
|
refused: z.number().int().min(0),
|
|
537
875
|
pruned: z.number().int().min(0),
|
|
876
|
+
zone_shifts: z.number().int().min(0),
|
|
877
|
+
stale_sources: z.number().int().min(0),
|
|
878
|
+
no_shows: z.number().int().min(0),
|
|
879
|
+
waitlist_offers: z.number().int().min(0),
|
|
538
880
|
capped: z.boolean(),
|
|
539
881
|
})
|
|
540
882
|
.strict(),
|
|
@@ -542,6 +884,73 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
542
884
|
invoke: 'runMaintenance',
|
|
543
885
|
example: {},
|
|
544
886
|
},
|
|
887
|
+
{
|
|
888
|
+
name: 'slotlock_list_changes',
|
|
889
|
+
title: 'List changes',
|
|
890
|
+
description: "What changed in the tenant, oldest first, as CloudEvents: holds made, confirmed, released or expired; bookings made, moved or cancelled; calendar events created, changed or cancelled (ids, times and states, never titles or attendees). Pass next_cursor as cursor to read on; it is returned even when nothing is new, so poll with it. Only changes of transactions that have ended are listed, so following the cursor never skips one. Changes are kept 30 days: an older cursor answers 410 cursor_expired, so start again without one.",
|
|
891
|
+
method: 'GET',
|
|
892
|
+
path: '/v1/changes',
|
|
893
|
+
input: z
|
|
894
|
+
.object({ cursor: CURSOR.optional(), limit: z.number().int().min(1).max(500).default(100) })
|
|
895
|
+
.strict(),
|
|
896
|
+
output: z.object({ changes: z.array(CHANGE).max(500), next_cursor: z.string().max(500) }).strict(),
|
|
897
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
898
|
+
invoke: 'listChanges',
|
|
899
|
+
example: { limit: 100 },
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
name: 'slotlock_create_webhook',
|
|
903
|
+
title: 'Add a webhook',
|
|
904
|
+
description: 'Send the changes slotlock_list_changes lists from now on to an https URL, each as a CloudEvent signed as Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature), in order, until a 2xx. A failure is retried with backoff; after 24 hours of failures, or a 410, the webhook is disabled and the feed records webhook.disabled. The secret is returned here only. At most 10 per tenant.',
|
|
905
|
+
method: 'POST',
|
|
906
|
+
path: '/v1/webhooks',
|
|
907
|
+
input: z
|
|
908
|
+
.object({
|
|
909
|
+
url: z.string().url().max(2048),
|
|
910
|
+
event_types: z.array(z.enum(SLOTLOCK_CHANGE_TYPES)).min(1).max(20).optional(),
|
|
911
|
+
})
|
|
912
|
+
.strict(),
|
|
913
|
+
output: z.object({ webhook: WEBHOOK, secret: z.string().regex(/^whsec_/) }).strict(),
|
|
914
|
+
risk: { readOnly: false, destructive: false, idempotent: false },
|
|
915
|
+
invoke: 'createWebhook',
|
|
916
|
+
example: { url: 'https://example.com/slotlock-webhook', event_types: ['reservation.created'] },
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
name: 'slotlock_list_webhooks',
|
|
920
|
+
title: 'List webhooks',
|
|
921
|
+
description: "The tenant's webhooks with how delivery is going: failed attempts, the last status, and whether and why one is disabled.",
|
|
922
|
+
method: 'GET',
|
|
923
|
+
path: '/v1/webhooks',
|
|
924
|
+
input: z.object({}).strict(),
|
|
925
|
+
output: z.object({ webhooks: z.array(WEBHOOK).max(10) }).strict(),
|
|
926
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
927
|
+
invoke: 'listWebhooks',
|
|
928
|
+
example: {},
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
name: 'slotlock_delete_webhook',
|
|
932
|
+
title: 'Delete a webhook',
|
|
933
|
+
description: 'Stop sending to a webhook. deleted is false when the tenant has no such webhook.',
|
|
934
|
+
method: 'DELETE',
|
|
935
|
+
path: '/v1/webhooks/{webhook_id}',
|
|
936
|
+
input: z.object({ webhook_id: z.string().uuid() }).strict(),
|
|
937
|
+
output: z.object({ deleted: z.boolean() }).strict(),
|
|
938
|
+
risk: { readOnly: false, destructive: true, idempotent: true },
|
|
939
|
+
invoke: 'deleteWebhook',
|
|
940
|
+
example: { webhook_id: '9b2f4c1e-6d3a-4e8b-a1c7-5f0e2d4b6a8c' },
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
name: 'slotlock_enable_webhook',
|
|
944
|
+
title: 'Enable a webhook',
|
|
945
|
+
description: 'Send to a disabled webhook again, from the first change it did not accept. 404 webhook_not_found when the tenant has no such webhook; 409 changes_pruned when what it missed is older than the 30 days the feed keeps, since it would skip those unseen: delete it, add it again, and read what it missed from slotlock_list_changes.',
|
|
946
|
+
method: 'POST',
|
|
947
|
+
path: '/v1/webhooks/{webhook_id}/enable',
|
|
948
|
+
input: z.object({ webhook_id: z.string().uuid() }).strict(),
|
|
949
|
+
output: z.object({ webhook: WEBHOOK }).strict(),
|
|
950
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
951
|
+
invoke: 'enableWebhook',
|
|
952
|
+
example: { webhook_id: '9b2f4c1e-6d3a-4e8b-a1c7-5f0e2d4b6a8c' },
|
|
953
|
+
},
|
|
545
954
|
]);
|
|
546
955
|
const BY_NAME = new Map(SLOTLOCK_APPLICATION_OPERATIONS.map((operation) => [operation.name, operation]));
|
|
547
956
|
export function isSlotlockApplicationOperation(name) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Interval, WeeklyAvailabilityRule } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A resource's booking rules: what a booking made through Slotlock (a hold, a reservation, an
|
|
4
|
+
* agent's event) must satisfy, and what a search offers. Each is optional; none set is no rule.
|
|
5
|
+
* Imported calendar events are what happened elsewhere and are not checked.
|
|
6
|
+
*/
|
|
7
|
+
export interface SlotlockBookingRules {
|
|
8
|
+
/** A booking starts at least this many minutes after it is made: 0-525,600 (a year). */
|
|
9
|
+
minNoticeMinutes?: number;
|
|
10
|
+
/** A booking starts at most this many days after it is made: 1-3,660. */
|
|
11
|
+
maxAdvanceDays?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Bookings start on a step of this many minutes from local midnight, in the resource's time zone
|
|
14
|
+
* (15: on the quarter hour): 1-1,440.
|
|
15
|
+
*/
|
|
16
|
+
slotStepMinutes?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Minutes kept free after each booking made under the rule (cleaning, handover, travel), as its
|
|
19
|
+
* trailing buffer, so the database refuses a booking that would start inside it: 0-1,440. A
|
|
20
|
+
* booking made before the rule, or imported, keeps the buffer it has.
|
|
21
|
+
*/
|
|
22
|
+
turnaroundMinutes?: number;
|
|
23
|
+
/** A booking lies inside the resource's bookable hours. */
|
|
24
|
+
enforceHours?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* A booking is claimed (checked in) within this many minutes of its start, or maintenance
|
|
27
|
+
* releases it for others (and offers it to the waitlist): 1-1,440. Only bookings made once the
|
|
28
|
+
* rule is set are released.
|
|
29
|
+
*/
|
|
30
|
+
checkInMinutes?: number;
|
|
31
|
+
/**
|
|
32
|
+
* A booking lies in time every calendar sync recording coverage for the resource has read within
|
|
33
|
+
* the last day (`recordCalendarCoverage`): time a stale or failing sync leaves unproven is
|
|
34
|
+
* refused (`uncertain`), not booked over what it may hide. A resource no sync covers is certain.
|
|
35
|
+
*/
|
|
36
|
+
requireCertain?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Which rule a write broke, as its refusal names it. */
|
|
39
|
+
export type SlotlockBookingRule = 'min_notice' | 'max_advance' | 'slot_step' | 'outside_hours' | 'uncertain';
|
|
40
|
+
/** Each numeric rule's least and greatest value. */
|
|
41
|
+
export declare const SLOTLOCK_BOOKING_RULE_LIMITS: Readonly<{
|
|
42
|
+
readonly minNoticeMinutes: readonly [0, 525600];
|
|
43
|
+
readonly maxAdvanceDays: readonly [1, 3660];
|
|
44
|
+
readonly slotStepMinutes: readonly [1, number];
|
|
45
|
+
readonly turnaroundMinutes: readonly [0, number];
|
|
46
|
+
readonly checkInMinutes: readonly [1, number];
|
|
47
|
+
}>;
|
|
48
|
+
/** Booking rules from untrusted input, or a TypeError naming the field that is wrong. */
|
|
49
|
+
export declare function parseBookingRules(value: unknown): SlotlockBookingRules;
|
|
50
|
+
/** The trailing buffer a booking made under the rules keeps: the larger of its own and the rule's. */
|
|
51
|
+
export declare function bookingBufferAfterMs(rules: SlotlockBookingRules | undefined, ownMs?: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* The first instant at or after `from` that starts on a step in `timezone`. A step that does not
|
|
54
|
+
* divide the day starts again at local midnight. Across a clock change it moves forward until the
|
|
55
|
+
* local time is on a step.
|
|
56
|
+
*/
|
|
57
|
+
export declare function nextStepStart(from: Date, stepMinutes: number, timezone: string): Date;
|
|
58
|
+
/**
|
|
59
|
+
* The rule a booking of `interval` made at `now` breaks, or null. `hours` are the resource's
|
|
60
|
+
* bookable hours (its own, or the default), read only when the rules enforce them.
|
|
61
|
+
*/
|
|
62
|
+
export declare function bookingRuleBroken(params: {
|
|
63
|
+
rules: SlotlockBookingRules | undefined;
|
|
64
|
+
timezone: string;
|
|
65
|
+
hours: readonly WeeklyAvailabilityRule[];
|
|
66
|
+
interval: Interval;
|
|
67
|
+
now: Date;
|
|
68
|
+
}): SlotlockBookingRule | null;
|
|
69
|
+
/**
|
|
70
|
+
* How a search honours the rules: the part of `window` a booking made at `now` may start in, the
|
|
71
|
+
* trailing buffer each slot keeps, and how a candidate start moves to a step. `null` when no start
|
|
72
|
+
* in the window is allowed.
|
|
73
|
+
*/
|
|
74
|
+
export declare function bookingSearch(params: {
|
|
75
|
+
rules: SlotlockBookingRules | undefined;
|
|
76
|
+
timezone: string;
|
|
77
|
+
window: Interval;
|
|
78
|
+
now: Date;
|
|
79
|
+
bufferAfterMs?: number;
|
|
80
|
+
}): {
|
|
81
|
+
window: Interval;
|
|
82
|
+
bufferAfterMs: number;
|
|
83
|
+
align?: (start: Date) => Date;
|
|
84
|
+
} | null;
|