slotlock 0.8.0 → 0.9.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 +237 -0
- package/agent-server.d.ts +25 -5
- package/agent-server.js +152 -7
- package/agent-store-backend.d.ts +6 -1
- package/agent-store-backend.js +861 -55
- package/application-api.d.ts +19 -1
- package/application-api.js +578 -21
- package/approvals.d.ts +2 -0
- package/approvals.js +12 -5
- package/assistant.d.ts +34 -8
- package/assistant.js +81 -26
- package/availability-store.d.ts +193 -0
- package/availability-store.js +558 -0
- package/booking-rules.d.ts +92 -5
- package/booking-rules.js +243 -15
- package/dashboard-assistant-ui.d.ts +1 -1
- package/dashboard-assistant-ui.js +109 -8
- package/dashboard.d.ts +55 -6
- package/dashboard.js +790 -105
- package/ddl.d.ts +5 -1
- package/ddl.js +760 -9
- package/event-feed.d.ts +1 -1
- package/event-feed.js +18 -0
- package/holiday-import.d.ts +38 -0
- package/holiday-import.js +57 -0
- package/holidays.d.ts +38 -0
- package/holidays.js +150 -0
- package/hours.d.ts +125 -0
- package/hours.js +224 -0
- package/index.d.ts +14 -3
- package/index.js +10 -2
- package/interop.d.ts +25 -7
- package/interop.js +145 -17
- package/lifecycle.d.ts +31 -0
- package/lifecycle.js +115 -0
- package/maintenance.d.ts +19 -0
- package/maintenance.js +55 -1
- package/metrics.js +3 -0
- package/openapi.json +16237 -7795
- package/package.json +1 -1
- package/ranges.d.ts +28 -0
- package/ranges.js +135 -0
- package/resource-search.d.ts +19 -0
- package/resource-search.js +153 -0
- package/rest-api.js +7 -0
- package/rules.d.ts +2 -0
- package/rules.js +2 -0
- package/self-host.d.ts +1 -1
- package/self-host.js +221 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +367 -24
- package/store.js +1567 -97
- package/types.d.ts +198 -0
package/application-api.js
CHANGED
|
@@ -13,7 +13,7 @@ import { z } from 'zod';
|
|
|
13
13
|
import { SLOTLOCK_APPROVAL_KINDS, SLOTLOCK_MAX_APPROVAL_MINUTES, SLOTLOCK_MAX_APPROVAL_POLICIES, } from './approvals.js';
|
|
14
14
|
import { SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_PLAN } from './partner-names.js';
|
|
15
15
|
import { SLOTLOCK_EVENT_OUTPUT } from './event-schema.js';
|
|
16
|
-
import { SLOTLOCK_CALENDAR_HORIZON_DAYS } from './rules.js';
|
|
16
|
+
import { SLOTLOCK_CALENDAR_HORIZON_DAYS, SLOTLOCK_SOURCE_BUSY_MAX } from './rules.js';
|
|
17
17
|
import { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_POOL_ORDERS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_LIMITS, } from './resource-profile.js';
|
|
18
18
|
/** The scope an application operation needs, but the partner API's (`slotlockApplicationScope`). */
|
|
19
19
|
export const SLOTLOCK_APPLICATION_SCOPE = 'admin';
|
|
@@ -162,6 +162,10 @@ const AVAILABILITY_RULE = z
|
|
|
162
162
|
})
|
|
163
163
|
.strict();
|
|
164
164
|
const AVAILABILITY_RULES = z.array(AVAILABILITY_RULE).max(50);
|
|
165
|
+
/** A window of a day: minutes after local midnight and how long (it may run past midnight). */
|
|
166
|
+
const DAY_WINDOW = z
|
|
167
|
+
.object({ start_minutes: z.number().int().min(0).max(1_439), duration_minutes: z.number().int().min(1).max(1_440) })
|
|
168
|
+
.strict();
|
|
165
169
|
/** What a booking on a resource must satisfy; each rule optional (`SlotlockBookingRules`). */
|
|
166
170
|
const BOOKING_RULES = z
|
|
167
171
|
.object({
|
|
@@ -170,10 +174,39 @@ const BOOKING_RULES = z
|
|
|
170
174
|
slot_step_minutes: z.number().int().min(1).max(1_440).optional(),
|
|
171
175
|
step_offset_minutes: z.number().int().min(0).max(1_439).optional(),
|
|
172
176
|
buffer_before_minutes: z.number().int().min(0).max(1_440).optional(),
|
|
173
|
-
turnaround_minutes: z.number().int().min(0).max(
|
|
177
|
+
turnaround_minutes: z.number().int().min(0).max(10_080).optional(),
|
|
174
178
|
enforce_hours: z.boolean().optional(),
|
|
175
179
|
check_in_minutes: z.number().int().min(1).max(1_440).optional(),
|
|
176
180
|
require_certain: z.boolean().optional(),
|
|
181
|
+
/** A booking lasts at least, and at most, this many minutes (RFC 0004 B3). */
|
|
182
|
+
min_duration_minutes: z.number().int().min(1).max(525_600).optional(),
|
|
183
|
+
max_duration_minutes: z.number().int().min(1).max(525_600).optional(),
|
|
184
|
+
/** A booking lasts exactly one of these many minutes. */
|
|
185
|
+
allowed_durations: z.array(z.number().int().min(1).max(525_600)).min(1).max(20).optional(),
|
|
186
|
+
/** The length a search offers when it names none. */
|
|
187
|
+
default_duration_minutes: z.number().int().min(1).max(525_600).optional(),
|
|
188
|
+
/** A booking from one local day to another starts in one of these windows of the day. */
|
|
189
|
+
pickup_times: z.array(DAY_WINDOW).min(1).max(4).optional(),
|
|
190
|
+
/** And ends in one of these. */
|
|
191
|
+
return_times: z.array(DAY_WINDOW).min(1).max(4).optional(),
|
|
192
|
+
/**
|
|
193
|
+
* How the hours hold a booking from one local day to another: whole (all of it inside them) or
|
|
194
|
+
* ends (only its start and end); by default ends with pickup_times or return_times, else whole.
|
|
195
|
+
* Writes under enforce_hours, and every search, follow it.
|
|
196
|
+
*/
|
|
197
|
+
hours_apply: z.enum(['whole', 'ends']).optional(),
|
|
198
|
+
/** Cancelling, or moving, is refused this many minutes before the start (RFC 0004 B5). */
|
|
199
|
+
cancel_cutoff_minutes: z.number().int().min(0).max(525_600).optional(),
|
|
200
|
+
reschedule_cutoff_minutes: z.number().int().min(0).max(525_600).optional(),
|
|
201
|
+
/** What a cancellation costs (within within_minutes of the start, when given), for you to charge. */
|
|
202
|
+
cancellation_fee: z
|
|
203
|
+
.object({
|
|
204
|
+
amount_minor: z.number().int().min(0).max(1_000_000_000_000),
|
|
205
|
+
currency: z.string().regex(/^[A-Z]{3}$/),
|
|
206
|
+
within_minutes: z.number().int().min(0).max(525_600).optional(),
|
|
207
|
+
})
|
|
208
|
+
.strict()
|
|
209
|
+
.optional(),
|
|
177
210
|
})
|
|
178
211
|
.strict();
|
|
179
212
|
/** Default booking rules for the tenant or a pool: a resource's own rules but check-in. */
|
|
@@ -231,6 +264,7 @@ const APPROVAL_POLICY = z
|
|
|
231
264
|
outcome: z.enum(['require_approval', 'refuse']),
|
|
232
265
|
approval_ttl_minutes: z.number().int().min(1).max(SLOTLOCK_MAX_APPROVAL_MINUTES),
|
|
233
266
|
lead_minutes: z.number().int().min(0).max(SLOTLOCK_MAX_APPROVAL_MINUTES),
|
|
267
|
+
include_application: z.boolean(),
|
|
234
268
|
created_at: DATE_TIME,
|
|
235
269
|
updated_at: DATE_TIME,
|
|
236
270
|
})
|
|
@@ -315,8 +349,73 @@ const RESOURCE = z
|
|
|
315
349
|
parent_id: z.string().uuid().nullable(),
|
|
316
350
|
/** Its place among its parent's parts, from 0; `null` when it is no part. */
|
|
317
351
|
part_index: z.number().int().min(0).nullable(),
|
|
352
|
+
/**
|
|
353
|
+
* The hours it inherits when it has none of its own (RFC 0004 B2): its location's (the nearest
|
|
354
|
+
* up the chain), else the tenant's; `null` when it has its own or uses the server's default.
|
|
355
|
+
*/
|
|
356
|
+
inherited_availability: z
|
|
357
|
+
.object({ source: z.enum(['location', 'tenant']), location: GROUPING_REF.nullable(), rules: AVAILABILITY_RULES })
|
|
358
|
+
.strict()
|
|
359
|
+
.nullable(),
|
|
318
360
|
})
|
|
319
361
|
.strict();
|
|
362
|
+
/** An admin's reason for going past a cancel or reschedule cutoff (RFC 0004 B5), kept on the feed. */
|
|
363
|
+
const OVERRIDE_REASON = z.string().trim().min(1).max(500);
|
|
364
|
+
/** A cancellation's fee as recorded: an amount in the currency's minor unit, ISO 4217 currency. */
|
|
365
|
+
const RECORDED_FEE = z.object({ amount_minor: z.number().int().min(0), currency: z.string().regex(/^[A-Z]{3}$/) }).strict();
|
|
366
|
+
/** A day in the scope's calendar, YYYY-MM-DD. */
|
|
367
|
+
const LOCAL_DATE = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
|
368
|
+
/** One window of an exception's day: minutes after local midnight and how long. */
|
|
369
|
+
const EXCEPTION_WINDOW = z
|
|
370
|
+
.object({ start_minutes: z.number().int().min(0).max(1_439), duration_minutes: z.number().int().min(1).max(1_440) })
|
|
371
|
+
.strict();
|
|
372
|
+
/** A closure or a day of special hours (RFC 0004 B1). */
|
|
373
|
+
const AVAILABILITY_EXCEPTION = z
|
|
374
|
+
.object({
|
|
375
|
+
id: z.string().uuid(),
|
|
376
|
+
/** The location it applies to (and every location inside it); `null` when not one location's. */
|
|
377
|
+
location: GROUPING_REF.nullable(),
|
|
378
|
+
/** The resource it applies to; `null` when not one resource's. Both null: the whole tenant. */
|
|
379
|
+
resource_id: z.string().uuid().nullable(),
|
|
380
|
+
starts_on: LOCAL_DATE,
|
|
381
|
+
/** The last day, inclusive. */
|
|
382
|
+
ends_on: LOCAL_DATE,
|
|
383
|
+
outcome: z.enum(['closed', 'open']),
|
|
384
|
+
/** For open: the windows each of its days has instead of its weekly hours; `null` when closed. */
|
|
385
|
+
hours: z.array(EXCEPTION_WINDOW).min(1).max(4).nullable(),
|
|
386
|
+
label: z.string().max(200),
|
|
387
|
+
/** manual, ics (a holiday calendar) or vavailability (a resource's imported hours). */
|
|
388
|
+
source: z.enum(['manual', 'ics', 'vavailability']),
|
|
389
|
+
/** The holiday calendar it was imported from (slotlock_list_holiday_calendars); `null` when none. */
|
|
390
|
+
calendar_id: z.string().uuid().nullable(),
|
|
391
|
+
created_at: DATE_TIME,
|
|
392
|
+
})
|
|
393
|
+
.strict();
|
|
394
|
+
/** A holiday calendar imported into closures (RFC 0004 B1). */
|
|
395
|
+
const HOLIDAY_CALENDAR = z
|
|
396
|
+
.object({
|
|
397
|
+
calendar_id: z.string().uuid(),
|
|
398
|
+
/** Unique at its scope: importing under the same name replaces what that calendar said. */
|
|
399
|
+
name: z.string().min(1).max(200),
|
|
400
|
+
location: GROUPING_REF.nullable(),
|
|
401
|
+
resource_id: z.string().uuid().nullable(),
|
|
402
|
+
/** A subscription's URL, imported again every 30 days; `null` for one pasted or uploaded. */
|
|
403
|
+
url: z.string().max(2_048).nullable(),
|
|
404
|
+
imported_at: DATE_TIME.nullable(),
|
|
405
|
+
next_import_at: DATE_TIME.nullable(),
|
|
406
|
+
/** Why a subscription's last import failed; it is tried again a day later. */
|
|
407
|
+
last_error: z.string().max(500).nullable(),
|
|
408
|
+
/** Its closures. */
|
|
409
|
+
closures: z.number().int().min(0),
|
|
410
|
+
created_at: DATE_TIME,
|
|
411
|
+
})
|
|
412
|
+
.strict();
|
|
413
|
+
const AVAILABILITY_DEFAULTS_RESULT = {
|
|
414
|
+
/** Resources in use the hours now apply to. */
|
|
415
|
+
resources_updated: z.number().int().min(0),
|
|
416
|
+
/** Resources in use the hours reach that keep hours of their own. */
|
|
417
|
+
resources_with_own_hours: z.number().int().min(0),
|
|
418
|
+
};
|
|
320
419
|
const HISTORY_ENTRY = z
|
|
321
420
|
.object({
|
|
322
421
|
id: z.string().max(40),
|
|
@@ -389,12 +488,29 @@ const WAITLIST_ENTRY = z
|
|
|
389
488
|
end: DATE_TIME,
|
|
390
489
|
holder_ref: z.string().min(1).max(500),
|
|
391
490
|
offer_ttl_seconds: z.number().int().min(60).max(86_400),
|
|
392
|
-
/**
|
|
393
|
-
|
|
491
|
+
/**
|
|
492
|
+
* waiting; offered (hold_id holds the time on offered_resource_id); then accepted (the hold was
|
|
493
|
+
* confirmed), declined (let go) or lapsed (it expired); or expired (no slot could be offered in
|
|
494
|
+
* the window).
|
|
495
|
+
*/
|
|
496
|
+
status: z.enum(['waiting', 'offered', 'accepted', 'declined', 'lapsed', 'expired']),
|
|
394
497
|
hold_id: z.string().uuid().nullable(),
|
|
395
498
|
offered_resource_id: RESOURCE_ID.nullable(),
|
|
396
499
|
offered_at: DATE_TIME.nullable(),
|
|
397
500
|
created_at: DATE_TIME,
|
|
501
|
+
/** The length it waits for, any slot of it within the window; null: the whole window. */
|
|
502
|
+
duration_minutes: z.number().int().min(1).nullable(),
|
|
503
|
+
/** When its offer was accepted, declined or lapsed. */
|
|
504
|
+
decided_at: DATE_TIME.nullable(),
|
|
505
|
+
})
|
|
506
|
+
.strict();
|
|
507
|
+
/** A payment's reference at the integrator's processor (RFC 0004 C3): a PaymentIntent id, say. */
|
|
508
|
+
const PAYMENT_REF = z.string().regex(/^[\x21-\x7e]{1,255}$/, 'must be 1-255 printable ASCII characters');
|
|
509
|
+
/** An amount in its currency's minor unit (cents), and the ISO 4217 code, upper case. */
|
|
510
|
+
const PAYMENT_AMOUNT = z
|
|
511
|
+
.object({
|
|
512
|
+
amount_minor: z.number().int().min(0).max(1_000_000_000_000),
|
|
513
|
+
currency: z.string().regex(/^[A-Z]{3}$/, 'must be an ISO 4217 code in capitals, like GBP'),
|
|
398
514
|
})
|
|
399
515
|
.strict();
|
|
400
516
|
const HOLD = z
|
|
@@ -405,6 +521,36 @@ const HOLD = z
|
|
|
405
521
|
end: DATE_TIME,
|
|
406
522
|
expires_at: DATE_TIME,
|
|
407
523
|
source: z.string().max(200).nullable(),
|
|
524
|
+
/** Present when it waits for payment (RFC 0004 C3), with the payment once recorded. */
|
|
525
|
+
payment_required: z.literal(true).optional(),
|
|
526
|
+
payment_ref: PAYMENT_REF.nullable().optional(),
|
|
527
|
+
paid_at: DATE_TIME.nullable().optional(),
|
|
528
|
+
/** What the payment must be, when the hold names it; and what was received, once paid. */
|
|
529
|
+
payment_amount: PAYMENT_AMOUNT.nullable().optional(),
|
|
530
|
+
paid_amount: PAYMENT_AMOUNT.nullable().optional(),
|
|
531
|
+
})
|
|
532
|
+
.strict();
|
|
533
|
+
/** A person a booking may name to be told about it (RFC 0004 P3), by the tenant's reference. */
|
|
534
|
+
const CONTACT_REF = z
|
|
535
|
+
.string()
|
|
536
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/, 'must be 1-200 letters, digits, . _ : or -, starting with a letter or digit');
|
|
537
|
+
const E164_NUMBER = z.string().regex(/^\+[1-9][0-9]{6,14}$/, 'must be an E.164 number, like +447700900123');
|
|
538
|
+
const CONSENT = z
|
|
539
|
+
.object({ email: z.boolean().optional(), sms: z.boolean().optional(), whatsapp: z.boolean().optional() })
|
|
540
|
+
.strict();
|
|
541
|
+
const CONTACT = z
|
|
542
|
+
.object({
|
|
543
|
+
contact_ref: CONTACT_REF,
|
|
544
|
+
name: z.string().max(200).nullable(),
|
|
545
|
+
email: z.string().max(320).nullable(),
|
|
546
|
+
phone: E164_NUMBER.nullable(),
|
|
547
|
+
whatsapp: E164_NUMBER.nullable(),
|
|
548
|
+
consent: CONSENT,
|
|
549
|
+
/** When each answer in consent was given, kept while it stands. */
|
|
550
|
+
consent_at: z.object({ email: DATE_TIME.optional(), sms: DATE_TIME.optional(), whatsapp: DATE_TIME.optional() }).strict(),
|
|
551
|
+
locale: z.string().max(35).nullable(),
|
|
552
|
+
created_at: DATE_TIME,
|
|
553
|
+
updated_at: DATE_TIME,
|
|
408
554
|
})
|
|
409
555
|
.strict();
|
|
410
556
|
const RESERVATION = z
|
|
@@ -417,6 +563,12 @@ const RESERVATION = z
|
|
|
417
563
|
buffer_after_minutes: z.number().int().min(0),
|
|
418
564
|
revision: REVISION,
|
|
419
565
|
source: z.string().max(200).nullable(),
|
|
566
|
+
/** Whom to tell about it (RFC 0004 P3), when it names someone. */
|
|
567
|
+
contact_ref: CONTACT_REF.optional(),
|
|
568
|
+
/** Present, and false, when no one should be notified about it. */
|
|
569
|
+
notify: z.literal(false).optional(),
|
|
570
|
+
/** Minutes before its start it is reminded (reminder.due), latest first, when it is. */
|
|
571
|
+
reminder_minutes: z.array(z.number().int().min(0).max(527_040)).max(5).optional(),
|
|
420
572
|
})
|
|
421
573
|
.strict();
|
|
422
574
|
/** A JSCalendar Event (RFC 8984 §5.1); interop.ts's JSCalendarEvent names every property. */
|
|
@@ -686,7 +838,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
686
838
|
{
|
|
687
839
|
name: 'slotlock_set_resource_availability',
|
|
688
840
|
title: "Set a resource's bookable hours",
|
|
689
|
-
description: "Give one resource its own weekly bookable hours. `null` makes it
|
|
841
|
+
description: "Give one resource its own weekly bookable hours. `null` makes it inherit: its location's hours (the nearest up the chain), else the tenant's, else the server's; `[]` makes it never bookable. Each rule is weekly with BYDAY, in the resource's time zone: start_minutes after midnight, lasting duration_minutes.",
|
|
690
842
|
method: 'PUT',
|
|
691
843
|
path: '/v1/resources/{resource_id}/availability',
|
|
692
844
|
input: z.object({ resource_id: RESOURCE_ID, rules: AVAILABILITY_RULES.nullable() }).strict(),
|
|
@@ -698,7 +850,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
698
850
|
{
|
|
699
851
|
name: 'slotlock_export_resource_availability',
|
|
700
852
|
title: "Export a resource's bookable hours as iCalendar",
|
|
701
|
-
description: "
|
|
853
|
+
description: "The weekly bookable hours that apply to one resource (its own, or those it inherits) 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. The closures and special hours of the next year are there too: an EXDATE takes a weekly window off a day, and an AVAILABLE with RDATEs gives a day its own hours. `null` when the resource uses the server's default.",
|
|
702
854
|
method: 'GET',
|
|
703
855
|
path: '/v1/resources/{resource_id}/availability/ical',
|
|
704
856
|
input: z.object({ resource_id: RESOURCE_ID }).strict(),
|
|
@@ -710,7 +862,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
710
862
|
{
|
|
711
863
|
name: 'slotlock_import_resource_availability',
|
|
712
864
|
title: "Import a resource's bookable hours from iCalendar",
|
|
713
|
-
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
|
|
865
|
+
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. EXDATE, RDATE and single AVAILABLEs become the resource's exceptions, replacing those an earlier import made: each day they name keeps the windows left on it, or is closed with none. Times must be in the resource's time zone (its TZID) or floating. 400 invalid_availability names what cannot be said: a bounded VAVAILABILITY, RECURRENCE-ID, another zone, more than four windows on a day.",
|
|
714
866
|
method: 'PUT',
|
|
715
867
|
path: '/v1/resources/{resource_id}/availability/ical',
|
|
716
868
|
input: z
|
|
@@ -727,7 +879,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
727
879
|
{
|
|
728
880
|
name: 'slotlock_set_resource_booking_rules',
|
|
729
881
|
title: "Set a resource's booking rules",
|
|
730
|
-
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, offset by step_offset_minutes), the time kept free before and after each booking,
|
|
882
|
+
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, offset by step_offset_minutes), the time kept free before and after each booking (turnaround up to a week), whether bookings must lie inside its hours, how long a booking may be (min_duration_minutes, max_duration_minutes, allowed_durations, and default_duration_minutes for a search that names no length), and for a booking from one local day to another, the pickup_times it starts in and the return_times it ends in. `null` clears them. A rule it does not set comes from its pool's or the tenant's defaults (slotlock_set_pool_booking_rules, slotlock_set_tenant_booking_rules). Holds, reservations, moves and agents' events are refused with the rule they break; searches offer only what they allow.",
|
|
731
883
|
method: 'PUT',
|
|
732
884
|
path: '/v1/resources/{resource_id}/booking-rules',
|
|
733
885
|
input: z.object({ resource_id: RESOURCE_ID, rules: BOOKING_RULES.nullable() }).strict(),
|
|
@@ -872,20 +1024,163 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
872
1024
|
},
|
|
873
1025
|
{
|
|
874
1026
|
name: 'slotlock_set_tenant_availability',
|
|
875
|
-
title: "Set
|
|
876
|
-
description: "
|
|
1027
|
+
title: "Set the tenant's bookable hours",
|
|
1028
|
+
description: "Store the tenant's weekly bookable hours: every resource without hours of its own, and with no location hours above it, uses them, resources added later too. `null` hands them back to the server's default. Nothing is copied: a resource with its own hours keeps them (override_resources none, the default), unless override_resources is all, which clears them so every resource inherits, or matching, which clears only those whose hours are the same as the tenant's were before or as these: the copies versions up to 0.8.0 made when setting the tenant's hours, while hours set apart on purpose stay. resources_updated counts the resources in use these hours now apply to; resources_with_own_hours, those that keep their own.",
|
|
877
1029
|
method: 'PUT',
|
|
878
1030
|
path: '/v1/availability',
|
|
879
|
-
input: z
|
|
880
|
-
|
|
1031
|
+
input: z
|
|
1032
|
+
.object({ rules: AVAILABILITY_RULES.nullable(), override_resources: z.enum(['none', 'matching', 'all']).optional() })
|
|
1033
|
+
.strict(),
|
|
1034
|
+
output: z.object(AVAILABILITY_DEFAULTS_RESULT).strict(),
|
|
881
1035
|
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
882
1036
|
invoke: 'setTenantAvailability',
|
|
883
1037
|
example: { rules: EXAMPLE_RULES },
|
|
884
1038
|
},
|
|
1039
|
+
{
|
|
1040
|
+
name: 'slotlock_get_availability_defaults',
|
|
1041
|
+
title: 'Read stored bookable hours',
|
|
1042
|
+
description: "The tenant's stored weekly hours (`null` when it uses the server's default) and each location's that has its own. A resource without its own uses its location's, the nearest up the chain, then the tenant's, then the server's.",
|
|
1043
|
+
method: 'GET',
|
|
1044
|
+
path: '/v1/availability',
|
|
1045
|
+
input: z.object({}).strict(),
|
|
1046
|
+
output: z
|
|
1047
|
+
.object({
|
|
1048
|
+
tenant: AVAILABILITY_RULES.nullable(),
|
|
1049
|
+
locations: z.array(z.object({ location: GROUPING_REF, rules: AVAILABILITY_RULES }).strict()),
|
|
1050
|
+
})
|
|
1051
|
+
.strict(),
|
|
1052
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
1053
|
+
invoke: 'getAvailabilityDefaults',
|
|
1054
|
+
example: {},
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
name: 'slotlock_set_location_availability',
|
|
1058
|
+
title: "Set a location's bookable hours",
|
|
1059
|
+
description: "Store a location's weekly bookable hours, each read in a resource's own time zone: resources at it, or at a location inside it, without hours of their own use them, unless a location nearer them has its own. `null` clears them, so they inherit from further up. 404 location_not_found for a location the tenant does not have.",
|
|
1060
|
+
method: 'PUT',
|
|
1061
|
+
path: '/v1/locations/{location}/availability',
|
|
1062
|
+
input: z.object({ location: GROUPING_REF, rules: AVAILABILITY_RULES.nullable() }).strict(),
|
|
1063
|
+
output: z.object({ location: GROUPING_REF, ...AVAILABILITY_DEFAULTS_RESULT }).strict(),
|
|
1064
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
1065
|
+
invoke: 'setLocationAvailability',
|
|
1066
|
+
example: { location: 'london-depot', rules: EXAMPLE_RULES },
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
name: 'slotlock_list_availability_exceptions',
|
|
1070
|
+
title: 'List closures and special hours',
|
|
1071
|
+
description: "The tenant's exceptions to its weekly hours, by first day: with resource_id, those that apply to that resource (the tenant's, its locations' and its own); with location, that location's own; else all of them. from and to keep those that reach the days between. At most limit (1-1,000, default 100); next is the cursor for after.",
|
|
1072
|
+
method: 'GET',
|
|
1073
|
+
path: '/v1/availability/exceptions',
|
|
1074
|
+
input: z
|
|
1075
|
+
.object({
|
|
1076
|
+
resource_id: z.string().uuid().optional(),
|
|
1077
|
+
location: GROUPING_REF.optional(),
|
|
1078
|
+
from: LOCAL_DATE.optional(),
|
|
1079
|
+
to: LOCAL_DATE.optional(),
|
|
1080
|
+
limit: z.coerce.number().int().min(1).max(1_000).optional(),
|
|
1081
|
+
after: z.string().uuid().optional(),
|
|
1082
|
+
})
|
|
1083
|
+
.strict(),
|
|
1084
|
+
output: z.object({ exceptions: z.array(AVAILABILITY_EXCEPTION), next: z.string().uuid().nullable() }).strict(),
|
|
1085
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
1086
|
+
invoke: 'listAvailabilityExceptions',
|
|
1087
|
+
example: { from: '2026-12-01', to: '2027-01-31' },
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
name: 'slotlock_create_availability_exception',
|
|
1091
|
+
title: 'Add a closure or special hours',
|
|
1092
|
+
description: "Close whole days, or give them hours of their own, for the whole tenant, one location (and every location inside it) or one resource. starts_on to ends_on (inclusive, at most 367 days) are local days, read in each resource's time zone. outcome closed empties those days; open replaces their weekly hours with hours (1-4 windows of start_minutes and duration_minutes). Where several apply to a day, the nearest wins: the resource's own, then its location's from the innermost out, then the tenant's. Search leaves the days as they say; a write is refused outside_hours only under the enforce_hours booking rule. 409 availability_exceptions_limit_reached past 5,000 a tenant.",
|
|
1093
|
+
method: 'POST',
|
|
1094
|
+
path: '/v1/availability/exceptions',
|
|
1095
|
+
input: z
|
|
1096
|
+
.object({
|
|
1097
|
+
location: GROUPING_REF.optional(),
|
|
1098
|
+
resource_id: z.string().uuid().optional(),
|
|
1099
|
+
starts_on: LOCAL_DATE,
|
|
1100
|
+
ends_on: LOCAL_DATE.optional(),
|
|
1101
|
+
outcome: z.enum(['closed', 'open']),
|
|
1102
|
+
hours: z.array(EXCEPTION_WINDOW).min(1).max(4).optional(),
|
|
1103
|
+
label: z.string().trim().min(1).max(200),
|
|
1104
|
+
})
|
|
1105
|
+
.strict(),
|
|
1106
|
+
output: z.object({ exception: AVAILABILITY_EXCEPTION }).strict(),
|
|
1107
|
+
risk: { readOnly: false, destructive: false, idempotent: false },
|
|
1108
|
+
invoke: 'createAvailabilityException',
|
|
1109
|
+
example: { starts_on: '2026-12-25', ends_on: '2026-12-26', outcome: 'closed', label: 'Christmas' },
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
name: 'slotlock_delete_availability_exception',
|
|
1113
|
+
title: 'Delete a closure or special hours',
|
|
1114
|
+
description: 'Remove an exception: its days read as the weekly hours again. 404 availability_exception_not_found.',
|
|
1115
|
+
method: 'DELETE',
|
|
1116
|
+
path: '/v1/availability/exceptions/{exception_id}',
|
|
1117
|
+
input: z.object({ exception_id: z.string().uuid() }).strict(),
|
|
1118
|
+
output: z.object({ deleted: z.boolean() }).strict(),
|
|
1119
|
+
risk: { readOnly: false, destructive: true, idempotent: true },
|
|
1120
|
+
invoke: 'deleteAvailabilityException',
|
|
1121
|
+
example: { exception_id: '7b3c1d9e-4f2a-4e8b-9c6d-1a2b3c4d5e6f' },
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
name: 'slotlock_import_holidays',
|
|
1125
|
+
title: 'Import a holiday calendar',
|
|
1126
|
+
description: "Close the days an iCalendar holiday calendar names, for the whole tenant, one location or one resource: the text in icalendar, or an https url the server fetches (at most 1 MB, 10 seconds; private addresses refused). Each all-day event closes the days it covers, and each occurrence of a recurring one within from to to (default today to two years on, at most three years); timed and cancelled events are skipped. The calendar is kept by name at its scope (name, by default its own X-WR-CALNAME, else the url's host and path, else Holidays): importing it again updates its closures instead of adding more, and deletes the ones it made that it no longer names within the span (removed), so a holiday it dropped goes too. With subscribe (and a url), the server keeps the url and imports it again every 30 days. At most 50 calendars a tenant (409 holiday_calendars_limit_reached). 400 invalid_holiday_calendar for text that is not one; 400 holiday_calendar_unreachable when the url cannot be read; 400 holiday_url_not_served where the server fetches no urls.",
|
|
1127
|
+
method: 'POST',
|
|
1128
|
+
path: '/v1/availability/holidays',
|
|
1129
|
+
input: z
|
|
1130
|
+
.object({
|
|
1131
|
+
icalendar: z.string().min(1).max(1_048_576).optional(),
|
|
1132
|
+
url: z.string().url().max(2_000).optional(),
|
|
1133
|
+
name: z.string().trim().min(1).max(200).optional(),
|
|
1134
|
+
subscribe: z.boolean().optional(),
|
|
1135
|
+
location: GROUPING_REF.optional(),
|
|
1136
|
+
resource_id: z.string().uuid().optional(),
|
|
1137
|
+
from: LOCAL_DATE.optional(),
|
|
1138
|
+
to: LOCAL_DATE.optional(),
|
|
1139
|
+
})
|
|
1140
|
+
.strict(),
|
|
1141
|
+
output: z
|
|
1142
|
+
.object({
|
|
1143
|
+
/** Closures added or updated. */
|
|
1144
|
+
imported: z.number().int().min(0),
|
|
1145
|
+
/** Events that were not all-day, were cancelled, or fell outside the span. */
|
|
1146
|
+
skipped: z.number().int().min(0),
|
|
1147
|
+
/** Closures this calendar made before that it no longer names in the span, deleted. */
|
|
1148
|
+
removed: z.number().int().min(0),
|
|
1149
|
+
calendar: HOLIDAY_CALENDAR,
|
|
1150
|
+
})
|
|
1151
|
+
.strict(),
|
|
1152
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
1153
|
+
invoke: 'importHolidays',
|
|
1154
|
+
example: { url: 'https://example.com/holidays/gb.ics', location: 'london-depot', subscribe: true },
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
name: 'slotlock_list_holiday_calendars',
|
|
1158
|
+
title: 'List holiday calendars',
|
|
1159
|
+
description: "The holiday calendars imported into the tenant's closures (slotlock_import_holidays), each with its scope, how many closures it made, and for a subscription its url, when it was last imported, when it will be again, and why the last import failed if it did.",
|
|
1160
|
+
method: 'GET',
|
|
1161
|
+
path: '/v1/availability/holiday-calendars',
|
|
1162
|
+
input: z.object({}).strict(),
|
|
1163
|
+
output: z.object({ calendars: z.array(HOLIDAY_CALENDAR).max(50) }).strict(),
|
|
1164
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
1165
|
+
invoke: 'listHolidayCalendars',
|
|
1166
|
+
example: {},
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: 'slotlock_delete_holiday_calendar',
|
|
1170
|
+
title: 'Delete a holiday calendar',
|
|
1171
|
+
description: 'Delete a holiday calendar and every closure it made, and stop importing it again. deleted is false when the tenant has no such calendar; closures counts those deleted with it.',
|
|
1172
|
+
method: 'DELETE',
|
|
1173
|
+
path: '/v1/availability/holiday-calendars/{calendar_id}',
|
|
1174
|
+
input: z.object({ calendar_id: z.string().uuid() }).strict(),
|
|
1175
|
+
output: z.object({ deleted: z.boolean(), closures: z.number().int().min(0) }).strict(),
|
|
1176
|
+
risk: { readOnly: false, destructive: true, idempotent: true },
|
|
1177
|
+
invoke: 'deleteHolidayCalendar',
|
|
1178
|
+
example: { calendar_id: '3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d' },
|
|
1179
|
+
},
|
|
885
1180
|
{
|
|
886
1181
|
name: 'slotlock_acquire_hold',
|
|
887
1182
|
title: 'Hold time',
|
|
888
|
-
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.',
|
|
1183
|
+
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. With payment_required (ttl_seconds at most 1800), it cannot be confirmed until slotlock_mark_hold_paid records the payment, which must be at least payment (amount_minor in the minor unit of its currency, and the currency) when given: Slotlock records payment state and never moves money. 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.',
|
|
889
1184
|
method: 'POST',
|
|
890
1185
|
path: '/v1/holds',
|
|
891
1186
|
input: z
|
|
@@ -902,9 +1197,21 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
902
1197
|
idempotency_key: IDEMPOTENCY_KEY.optional(),
|
|
903
1198
|
// Last, so a generated 0.2.0 client's positional arguments keep their places.
|
|
904
1199
|
buffer_after_minutes: BUFFER_AFTER_MINUTES.optional(),
|
|
1200
|
+
/** It waits for payment (RFC 0004 C3): confirmed only once slotlock_mark_hold_paid records it. */
|
|
1201
|
+
payment_required: z.boolean().optional(),
|
|
1202
|
+
/** What the payment must be: a smaller one, or in another currency, is refused. */
|
|
1203
|
+
payment: PAYMENT_AMOUNT.optional(),
|
|
905
1204
|
})
|
|
906
1205
|
.strict()
|
|
907
|
-
.refine(boundedSpan, SPAN_MESSAGE)
|
|
1206
|
+
.refine(boundedSpan, SPAN_MESSAGE)
|
|
1207
|
+
.refine((input) => input.payment_required !== true || input.ttl_seconds <= 1_800, {
|
|
1208
|
+
message: 'A hold that waits for payment lives at most 1800 seconds',
|
|
1209
|
+
path: ['ttl_seconds'],
|
|
1210
|
+
})
|
|
1211
|
+
.refine((input) => input.payment === undefined || input.payment_required === true, {
|
|
1212
|
+
message: 'payment needs payment_required',
|
|
1213
|
+
path: ['payment'],
|
|
1214
|
+
}),
|
|
908
1215
|
output: z.object({ hold: HOLD }).strict(),
|
|
909
1216
|
risk: { readOnly: false, destructive: false, idempotent: false },
|
|
910
1217
|
invoke: 'acquireHold',
|
|
@@ -946,7 +1253,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
946
1253
|
{
|
|
947
1254
|
name: 'slotlock_confirm_hold',
|
|
948
1255
|
title: 'Confirm a hold',
|
|
949
|
-
description: "Turn an unexpired hold into a confirmed reservation, whose reference is the hold's id at revision 1: move or cancel it with the reservation routes by that reference (DELETE /v1/reservations/{hold_id}?revision=2 cancels it). Confirming it again answers the same reservation, so a retry is safe. An expired hold answers 409 hold_expired; a released or unknown one, 404 hold_not_found.",
|
|
1256
|
+
description: "Turn an unexpired hold into a confirmed reservation, whose reference is the hold's id at revision 1: move or cancel it with the reservation routes by that reference (DELETE /v1/reservations/{hold_id}?revision=2 cancels it). Confirming it again answers the same reservation, so a retry is safe. An expired hold answers 409 hold_expired; a released or unknown one, 404 hold_not_found; one that waits for payment not yet recorded, 409 payment_required.",
|
|
950
1257
|
method: 'POST',
|
|
951
1258
|
path: '/v1/holds/{hold_id}/confirm',
|
|
952
1259
|
input: z.object({ hold_id: HOLD_ID }).strict(),
|
|
@@ -955,6 +1262,29 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
955
1262
|
invoke: 'confirmHold',
|
|
956
1263
|
example: { hold_id: EXAMPLE_HOLD_ID },
|
|
957
1264
|
},
|
|
1265
|
+
{
|
|
1266
|
+
name: 'slotlock_mark_hold_paid',
|
|
1267
|
+
title: 'Record a hold as paid',
|
|
1268
|
+
description: "Record that a hold made with payment_required is paid, by the payment's reference at your processor (a Stripe PaymentIntent id, say), once you have verified it: the hold may then be confirmed. Slotlock records payment state and never moves money. The same reference again answers the same hold (replayed: true), whatever became of it since (confirmed, booked as an event, expired): the payment is kept apart from the hold. Another reference answers 409 payment_conflict. When the hold names its payment, received must be at least that amount in that currency (409 payment_amount_mismatch). The feed says so (hold.paid). A hold that does not wait for payment answers 409 payment_not_required; one that expired before it was paid, 409 hold_expired (refund the payment: its time may be someone else's); one released before it was paid, or unknown, 404 hold_not_found.",
|
|
1269
|
+
method: 'POST',
|
|
1270
|
+
path: '/v1/holds/{hold_id}/paid',
|
|
1271
|
+
input: z
|
|
1272
|
+
.object({
|
|
1273
|
+
hold_id: HOLD_ID,
|
|
1274
|
+
payment_ref: PAYMENT_REF,
|
|
1275
|
+
/** What was received, as your processor says: required when the hold names its payment. */
|
|
1276
|
+
received: PAYMENT_AMOUNT.optional(),
|
|
1277
|
+
})
|
|
1278
|
+
.strict(),
|
|
1279
|
+
output: z.object({ hold: HOLD, replayed: z.boolean() }).strict(),
|
|
1280
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
1281
|
+
invoke: 'markHoldPaid',
|
|
1282
|
+
example: {
|
|
1283
|
+
hold_id: EXAMPLE_HOLD_ID,
|
|
1284
|
+
payment_ref: 'pi_3PqX9a2eZvKYlo2C1d7Hk0Lm',
|
|
1285
|
+
received: { amount_minor: 4500, currency: 'GBP' },
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
958
1288
|
{
|
|
959
1289
|
name: 'slotlock_release_hold',
|
|
960
1290
|
title: 'Release a hold',
|
|
@@ -1005,7 +1335,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1005
1335
|
{
|
|
1006
1336
|
name: 'slotlock_put_reservation',
|
|
1007
1337
|
title: 'Book or move a reservation',
|
|
1008
|
-
description: 'Book the reservation with this reference (revision 1), or move it (the next revision: the current one plus 1). Sending the same reservation at its current revision again changes nothing (replayed: true). A taken window answers 409 overlap and leaves a moved reservation where it was. buffer_after_minutes keeps the time after it busy as well.',
|
|
1338
|
+
description: 'Book the reservation with this reference (revision 1), or move it (the next revision: the current one plus 1). Sending the same reservation at its current revision again changes nothing (replayed: true). A taken window answers 409 overlap and leaves a moved reservation where it was. buffer_after_minutes keeps the time after it busy as well. A move past the resource\'s reschedule cutoff answers 409 booking_rule_reschedule_cutoff unless override_reason says why, which the feed keeps. local_start (YYYY-MM-DDTHH:MM in the resource\'s zone) records the wall time it is booked for: it must name start, and if time zone rules later move that wall time to another instant, maintenance says so (reservation.timezone_shifted) or follows it. 400 invalid_local_start otherwise. contact_ref names whom to tell about it (slotlock_put_contact) and notify: false says to tell no one; both are on the feed with each change, as sent with each revision. reminder_minutes (up to five, distinct) puts reminder.due on the feed that long before its start, once each, and again if it moves.',
|
|
1009
1339
|
method: 'PUT',
|
|
1010
1340
|
path: '/v1/reservations/{reservation_ref}',
|
|
1011
1341
|
input: z
|
|
@@ -1017,6 +1347,16 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1017
1347
|
buffer_after_minutes: BUFFER_AFTER_MINUTES.default(0),
|
|
1018
1348
|
revision: REVISION,
|
|
1019
1349
|
source: SOURCE.optional(),
|
|
1350
|
+
/** An admin's reason for moving it past its reschedule cutoff (RFC 0004 B5). */
|
|
1351
|
+
override_reason: OVERRIDE_REASON.optional(),
|
|
1352
|
+
/** The wall time it starts at in the resource's zone, kept across zone rule changes (RFC 0004 B9). */
|
|
1353
|
+
local_start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/).optional(),
|
|
1354
|
+
/** Whom to tell about it (RFC 0004 P3): a contact's reference, never the person. */
|
|
1355
|
+
contact_ref: CONTACT_REF.optional(),
|
|
1356
|
+
/** False: no one should be notified about it (default true). */
|
|
1357
|
+
notify: z.boolean().optional(),
|
|
1358
|
+
/** Minutes before its start to remind whom it names: up to five, each said once as reminder.due. */
|
|
1359
|
+
reminder_minutes: z.array(z.number().int().min(0).max(527_040)).max(5).optional(),
|
|
1020
1360
|
})
|
|
1021
1361
|
.strict()
|
|
1022
1362
|
.refine(boundedSpan, SPAN_MESSAGE),
|
|
@@ -1035,11 +1375,13 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1035
1375
|
{
|
|
1036
1376
|
name: 'slotlock_cancel_reservation',
|
|
1037
1377
|
title: 'Cancel a reservation',
|
|
1038
|
-
description: 'Cancel the reservation with this reference at the next revision (the current one plus 1). Cancelling leaves a tombstone, so a delayed booking cannot bring it back; repeating the cancel answers cancelled: false. Use a new reference for a new reservation.',
|
|
1378
|
+
description: 'Cancel the reservation with this reference at the next revision (the current one plus 1). Cancelling leaves a tombstone, so a delayed booking cannot bring it back; repeating the cancel answers cancelled: false. Use a new reference for a new reservation. Past the resource\'s cancel cutoff it answers 409 booking_rule_cancel_cutoff unless override_reason says why, which the tombstone and the feed keep. A late cancellation\'s fee (the resource\'s cancellation_fee) is answered in cancellation_fee and kept on the feed, for you to charge: Slotlock moves no money.',
|
|
1039
1379
|
method: 'DELETE',
|
|
1040
1380
|
path: '/v1/reservations/{reservation_ref}',
|
|
1041
|
-
input: z
|
|
1042
|
-
|
|
1381
|
+
input: z
|
|
1382
|
+
.object({ reservation_ref: IDENTITY, revision: REVISION, override_reason: OVERRIDE_REASON.optional() })
|
|
1383
|
+
.strict(),
|
|
1384
|
+
output: z.object({ cancelled: z.boolean(), revision: REVISION, cancellation_fee: RECORDED_FEE.nullable() }).strict(),
|
|
1043
1385
|
risk: { readOnly: false, destructive: true, idempotent: true },
|
|
1044
1386
|
invoke: 'cancelReservation',
|
|
1045
1387
|
example: { reservation_ref: 'booking-BK-42', revision: 2 },
|
|
@@ -1056,10 +1398,75 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1056
1398
|
invoke: 'checkInReservation',
|
|
1057
1399
|
example: { reservation_ref: 'booking-BK-42' },
|
|
1058
1400
|
},
|
|
1401
|
+
{
|
|
1402
|
+
name: 'slotlock_put_contact',
|
|
1403
|
+
title: 'Keep a contact',
|
|
1404
|
+
description: "Keep a person a booking may name to be told about it (contact_ref on slotlock_put_reservation), under your own reference: how to reach them (email, phone for SMS and whatsapp, both E.164) and which channels they agreed to (consent: false is an opt-out a sender must honour). Replaces what was kept under the reference. A sender reads it to send; Slotlock itself sends nothing. created says whether it is new. A tenant keeps at most 100,000 (409 contact_limit_reached).",
|
|
1405
|
+
method: 'PUT',
|
|
1406
|
+
path: '/v1/contacts/{contact_ref}',
|
|
1407
|
+
input: z
|
|
1408
|
+
.object({
|
|
1409
|
+
contact_ref: CONTACT_REF,
|
|
1410
|
+
name: z.string().min(1).max(200).nullable().optional(),
|
|
1411
|
+
email: z.string().min(3).max(320).regex(/^[^\s@]+@[^\s@]+$/, 'must be an email address').nullable().optional(),
|
|
1412
|
+
phone: E164_NUMBER.nullable().optional(),
|
|
1413
|
+
whatsapp: E164_NUMBER.nullable().optional(),
|
|
1414
|
+
consent: CONSENT.optional(),
|
|
1415
|
+
locale: z.string().max(35).regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, 'must be a BCP 47 tag, like en-GB').nullable().optional(),
|
|
1416
|
+
})
|
|
1417
|
+
.strict(),
|
|
1418
|
+
output: z.object({ contact: CONTACT, created: z.boolean() }).strict(),
|
|
1419
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
1420
|
+
invoke: 'putContact',
|
|
1421
|
+
example: {
|
|
1422
|
+
contact_ref: 'customer-42',
|
|
1423
|
+
name: 'Ada Lovelace',
|
|
1424
|
+
email: 'ada@example.com',
|
|
1425
|
+
phone: '+447700900123',
|
|
1426
|
+
consent: { email: true, sms: false },
|
|
1427
|
+
locale: 'en-GB',
|
|
1428
|
+
},
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
name: 'slotlock_get_contact',
|
|
1432
|
+
title: 'Get a contact',
|
|
1433
|
+
description: "The person kept under this reference, for a sender to reach. An unknown one answers 404 contact_not_found.",
|
|
1434
|
+
method: 'GET',
|
|
1435
|
+
path: '/v1/contacts/{contact_ref}',
|
|
1436
|
+
input: z.object({ contact_ref: CONTACT_REF }).strict(),
|
|
1437
|
+
output: z.object({ contact: CONTACT }).strict(),
|
|
1438
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
1439
|
+
invoke: 'getContact',
|
|
1440
|
+
example: { contact_ref: 'customer-42' },
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
name: 'slotlock_list_contacts',
|
|
1444
|
+
title: 'List contacts',
|
|
1445
|
+
description: 'The people the tenant keeps to tell about bookings, by reference, a page at a time: pass next_cursor as cursor for the next page, until it is null.',
|
|
1446
|
+
method: 'GET',
|
|
1447
|
+
path: '/v1/contacts',
|
|
1448
|
+
input: z.object({ limit: PAGE_LIMIT, cursor: CONTACT_REF.optional() }).strict(),
|
|
1449
|
+
output: z.object({ contacts: z.array(CONTACT).max(100), next_cursor: CONTACT_REF.nullable() }).strict(),
|
|
1450
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
1451
|
+
invoke: 'listContacts',
|
|
1452
|
+
example: { limit: 50 },
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
name: 'slotlock_delete_contact',
|
|
1456
|
+
title: 'Forget a contact',
|
|
1457
|
+
description: "Forget a person: their contact, and their reference on every booking that named it, at once (for an erasure request). deleted is false when no contact was kept; bookings counts the bookings that no longer name them. Changes already on the feed keep the reference, never the person.",
|
|
1458
|
+
method: 'DELETE',
|
|
1459
|
+
path: '/v1/contacts/{contact_ref}',
|
|
1460
|
+
input: z.object({ contact_ref: CONTACT_REF }).strict(),
|
|
1461
|
+
output: z.object({ deleted: z.boolean(), bookings: z.number().int().min(0) }).strict(),
|
|
1462
|
+
risk: { readOnly: false, destructive: true, idempotent: true },
|
|
1463
|
+
invoke: 'deleteContact',
|
|
1464
|
+
example: { contact_ref: 'customer-42' },
|
|
1465
|
+
},
|
|
1059
1466
|
{
|
|
1060
1467
|
name: 'slotlock_join_waitlist',
|
|
1061
1468
|
title: 'Join the waitlist',
|
|
1062
|
-
description: "Wait for a window on one resource (resource_id) or on any member of a pool (pool) that is taken now. When
|
|
1469
|
+
description: "Wait for a window on one resource (resource_id) or on any member of a pool (pool) that is taken now, or with duration_minutes for any slot of that length within it. When time comes free (a booking cancelled, a hold released or expired, a no-show), the entries waiting are offered in the order they joined: the first that fits gets a hold of it (the window, or the earliest slot of its length) 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 (waitlist.accepted), decline it with slotlock_decline_waitlist_offer (waitlist.declined), or let it lapse (waitlist.lapsed); either way the time goes to the next. An entry no slot can be offered to before its window closes expires (waitlist.expired). Joining and leaving are on the feed too (waitlist.joined, waitlist.left). A holder may wait for 20 windows at once (409 waitlist_limit_reached); a window is at most 31 days. With an idempotency key (the Idempotency-Key header or idempotency_key), a retry answers the entry the first request made (replayed: true) for as long as it is kept; the same key with another request is 422 idempotency_conflict.",
|
|
1063
1470
|
method: 'POST',
|
|
1064
1471
|
path: '/v1/waitlist',
|
|
1065
1472
|
input: z
|
|
@@ -1070,6 +1477,8 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1070
1477
|
end: DATE_TIME,
|
|
1071
1478
|
holder_ref: z.string().trim().min(1).max(500),
|
|
1072
1479
|
offer_ttl_seconds: z.number().int().min(60).max(86_400).optional(),
|
|
1480
|
+
/** Any slot of this length within the window (RFC 0004 B6); left out, the whole window. */
|
|
1481
|
+
duration_minutes: z.number().int().min(1).max(44_640).optional(),
|
|
1073
1482
|
idempotency_key: IDEMPOTENCY_KEY.optional(),
|
|
1074
1483
|
})
|
|
1075
1484
|
.strict()
|
|
@@ -1117,6 +1526,18 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1117
1526
|
invoke: 'leaveWaitlist',
|
|
1118
1527
|
example: { entry_id: '1b4e28ba-2fa1-41d2-883f-0016d3cca427' },
|
|
1119
1528
|
},
|
|
1529
|
+
{
|
|
1530
|
+
name: 'slotlock_decline_waitlist_offer',
|
|
1531
|
+
title: 'Decline a waitlist offer',
|
|
1532
|
+
description: 'Turn down the hold a waitlist entry was offered: the hold is let go, the entry is declined (waitlist.declined), and the time is offered to the next who waits. 404 waitlist_offer_not_found when the entry has no open offer.',
|
|
1533
|
+
method: 'POST',
|
|
1534
|
+
path: '/v1/waitlist/{entry_id}/decline',
|
|
1535
|
+
input: z.object({ entry_id: z.string().uuid() }).strict(),
|
|
1536
|
+
output: z.object({ entry: WAITLIST_ENTRY }).strict(),
|
|
1537
|
+
risk: { readOnly: false, destructive: false, idempotent: false },
|
|
1538
|
+
invoke: 'declineWaitlistOffer',
|
|
1539
|
+
example: { entry_id: '1b4e28ba-2fa1-41d2-883f-0016d3cca427' },
|
|
1540
|
+
},
|
|
1120
1541
|
{
|
|
1121
1542
|
name: 'slotlock_record_coverage',
|
|
1122
1543
|
title: 'Record calendar coverage',
|
|
@@ -1144,6 +1565,81 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1144
1565
|
revision: 7,
|
|
1145
1566
|
},
|
|
1146
1567
|
},
|
|
1568
|
+
{
|
|
1569
|
+
name: 'slotlock_put_source_busy',
|
|
1570
|
+
title: "Record a calendar's busy time",
|
|
1571
|
+
description: `For a calendar that gives busy time but not its events (Google freeBusy, Microsoft getSchedule): record its coverage of a window at the source's revision, as slotlock_record_coverage does, and the busy time in it, which replaces what the source said of that window before (what it said outside the window stays). Busy time counts in free/busy and every booking Slotlock makes on the resource is refused over it (409 overlap), as over an event. A confirmed booking the source newly reports busy time over is left as it is and put on the feed once as coverage.conflict, naming the booking and the source by its source_id: someone decides. At most ${SLOTLOCK_SOURCE_BUSY_MAX} intervals; they are clipped to the window and merged. Revisions only go forward; repeating one changes nothing (replayed: true).`,
|
|
1572
|
+
method: 'PUT',
|
|
1573
|
+
path: '/v1/resources/{resource_id}/coverage/{source}/busy',
|
|
1574
|
+
input: z
|
|
1575
|
+
.object({
|
|
1576
|
+
resource_id: RESOURCE_ID,
|
|
1577
|
+
source: SOURCE,
|
|
1578
|
+
start: DATE_TIME,
|
|
1579
|
+
end: DATE_TIME,
|
|
1580
|
+
revision: REVISION,
|
|
1581
|
+
busy: z
|
|
1582
|
+
.array(z
|
|
1583
|
+
.object({ start: DATE_TIME, end: DATE_TIME })
|
|
1584
|
+
.strict()
|
|
1585
|
+
.refine((value) => Date.parse(value.end) > Date.parse(value.start), SPAN_MESSAGE))
|
|
1586
|
+
.max(SLOTLOCK_SOURCE_BUSY_MAX),
|
|
1587
|
+
})
|
|
1588
|
+
.strict()
|
|
1589
|
+
.refine((value) => Date.parse(value.end) > Date.parse(value.start), SPAN_MESSAGE),
|
|
1590
|
+
output: z
|
|
1591
|
+
.object({
|
|
1592
|
+
revision: REVISION,
|
|
1593
|
+
replayed: z.boolean(),
|
|
1594
|
+
busy: z.number().int().min(0),
|
|
1595
|
+
conflicts: z.number().int().min(0),
|
|
1596
|
+
})
|
|
1597
|
+
.strict(),
|
|
1598
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
1599
|
+
invoke: 'putSourceBusy',
|
|
1600
|
+
example: {
|
|
1601
|
+
resource_id: EXAMPLE_RESOURCE_ID,
|
|
1602
|
+
source: 'google:fleet@example.com',
|
|
1603
|
+
start: '2027-03-01T00:00:00Z',
|
|
1604
|
+
end: '2027-03-15T00:00:00Z',
|
|
1605
|
+
revision: 8,
|
|
1606
|
+
busy: [{ start: '2027-03-02T09:00:00Z', end: '2027-03-02T11:00:00Z' }],
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
name: 'slotlock_report_coverage_conflict',
|
|
1611
|
+
title: 'Report a booking changed in a calendar',
|
|
1612
|
+
description: "For a calendar sync that writes bookings to a calendar: report that one (by reservation_ref or event_id) was changed (changed_in_calendar, with where the calendar now has it) or removed (removed_from_calendar) there. It is put on the change feed as coverage.conflict, named by the source's source_id, with the booking and the calendar's times; the booking is left as it is, for someone to decide. 404 coverage_not_found for a source that has not recorded coverage of the resource; reservation_not_found or event_not_found for a booking not on it.",
|
|
1613
|
+
method: 'POST',
|
|
1614
|
+
path: '/v1/resources/{resource_id}/coverage/{source}/conflicts',
|
|
1615
|
+
input: z
|
|
1616
|
+
.object({
|
|
1617
|
+
resource_id: RESOURCE_ID,
|
|
1618
|
+
source: SOURCE,
|
|
1619
|
+
reservation_ref: IDENTITY.optional(),
|
|
1620
|
+
event_id: z.string().uuid().optional(),
|
|
1621
|
+
reason: z.enum(['changed_in_calendar', 'removed_from_calendar']),
|
|
1622
|
+
calendar_start: DATE_TIME.optional(),
|
|
1623
|
+
calendar_end: DATE_TIME.optional(),
|
|
1624
|
+
})
|
|
1625
|
+
.strict()
|
|
1626
|
+
.refine((value) => (value.reservation_ref === undefined) !== (value.event_id === undefined), {
|
|
1627
|
+
message: 'Name reservation_ref or event_id, not both',
|
|
1628
|
+
})
|
|
1629
|
+
.refine((value) => (value.calendar_start === undefined) === (value.calendar_end === undefined) &&
|
|
1630
|
+
(value.calendar_start === undefined || Date.parse(value.calendar_end) > Date.parse(value.calendar_start)), { message: 'calendar_start and calendar_end come together, the end after the start' }),
|
|
1631
|
+
output: z.object({ reported: z.literal(true) }).strict(),
|
|
1632
|
+
risk: { readOnly: false, destructive: false, idempotent: false },
|
|
1633
|
+
invoke: 'reportCoverageConflict',
|
|
1634
|
+
example: {
|
|
1635
|
+
resource_id: EXAMPLE_RESOURCE_ID,
|
|
1636
|
+
source: 'google:fleet@example.com',
|
|
1637
|
+
reservation_ref: 'booking-BK-42',
|
|
1638
|
+
reason: 'changed_in_calendar',
|
|
1639
|
+
calendar_start: '2027-03-29T10:00:00Z',
|
|
1640
|
+
calendar_end: '2027-03-29T11:00:00Z',
|
|
1641
|
+
},
|
|
1642
|
+
},
|
|
1147
1643
|
{
|
|
1148
1644
|
name: 'slotlock_get_coverage',
|
|
1149
1645
|
title: "A resource's coverage and its health",
|
|
@@ -1249,6 +1745,60 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1249
1745
|
invoke: 'exportTenantEvent',
|
|
1250
1746
|
example: { event_id: EXAMPLE_EVENT_ID },
|
|
1251
1747
|
},
|
|
1748
|
+
{
|
|
1749
|
+
name: 'slotlock_tenant_book_together',
|
|
1750
|
+
title: 'Book resources together, as the tenant',
|
|
1751
|
+
description: "Book the same time on 2-20 resources at once, all or none, as agents' slotlock_book_together does but for the tenant's own systems (RFC 0004 B7): each is the tenant's event, not the key's (no key's quota counts it, and revoking the key leaves it), held to the resources' booking rules; approval policies hold it only when they include the application (include_application). If any is taken, nothing is booked and the refusal names that resource_id. A retry with the same idempotency_key answers the same events. 409 bundles_unavailable where the server books no bundles.",
|
|
1752
|
+
method: 'POST',
|
|
1753
|
+
path: '/v1/tenant/bundles',
|
|
1754
|
+
input: z
|
|
1755
|
+
.object({
|
|
1756
|
+
resource_ids: z.array(RESOURCE_ID).min(2).max(20),
|
|
1757
|
+
starts_at: DATE_TIME,
|
|
1758
|
+
ends_at: DATE_TIME,
|
|
1759
|
+
timezone: TIMEZONE,
|
|
1760
|
+
title: z.string().trim().min(1).max(500).optional(),
|
|
1761
|
+
idempotency_key: IDEMPOTENCY_KEY,
|
|
1762
|
+
})
|
|
1763
|
+
.strict(),
|
|
1764
|
+
output: z.object({ events: z.array(SLOTLOCK_EVENT_OUTPUT).min(2).max(20), replayed: z.boolean() }).strict(),
|
|
1765
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
1766
|
+
invoke: 'tenantBookTogether',
|
|
1767
|
+
example: {
|
|
1768
|
+
resource_ids: [EXAMPLE_RESOURCE_ID, '1f8b6c1e-2d4a-4c7e-9b1a-3e5f7a9c2d4b'],
|
|
1769
|
+
starts_at: '2027-03-02T09:00:00Z',
|
|
1770
|
+
ends_at: '2027-03-02T10:00:00Z',
|
|
1771
|
+
timezone: 'Europe/London',
|
|
1772
|
+
idempotency_key: 'desk-bundle-2027-03-02',
|
|
1773
|
+
},
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
name: 'slotlock_tenant_book_pool',
|
|
1777
|
+
title: 'Book any member of a pool, as the tenant',
|
|
1778
|
+
description: "Book any one free member of a pool for a time, as agents' slotlock_book_pool does but for the tenant's own systems (RFC 0004 B7), in the pool's order: the event names the member, and is the tenant's, not the key's; approval policies hold it only when they include the application (include_application). 409 pool_full when none is free; 404 pool_not_found. A retry with the same idempotency_key answers the member it booked.",
|
|
1779
|
+
method: 'POST',
|
|
1780
|
+
path: '/v1/tenant/pool-bookings',
|
|
1781
|
+
input: z
|
|
1782
|
+
.object({
|
|
1783
|
+
pool: POOL,
|
|
1784
|
+
starts_at: DATE_TIME,
|
|
1785
|
+
ends_at: DATE_TIME,
|
|
1786
|
+
timezone: TIMEZONE,
|
|
1787
|
+
title: z.string().trim().min(1).max(500).optional(),
|
|
1788
|
+
idempotency_key: IDEMPOTENCY_KEY,
|
|
1789
|
+
})
|
|
1790
|
+
.strict(),
|
|
1791
|
+
output: z.object({ event: SLOTLOCK_EVENT_OUTPUT, replayed: z.boolean() }).strict(),
|
|
1792
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
1793
|
+
invoke: 'tenantBookPool',
|
|
1794
|
+
example: {
|
|
1795
|
+
pool: 'van',
|
|
1796
|
+
starts_at: '2027-03-02T09:00:00Z',
|
|
1797
|
+
ends_at: '2027-03-02T10:00:00Z',
|
|
1798
|
+
timezone: 'Europe/London',
|
|
1799
|
+
idempotency_key: 'desk-pool-2027-03-02',
|
|
1800
|
+
},
|
|
1801
|
+
},
|
|
1252
1802
|
{
|
|
1253
1803
|
name: 'slotlock_cancel_tenant_event',
|
|
1254
1804
|
title: 'Cancel any event of the tenant',
|
|
@@ -1263,6 +1813,8 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1263
1813
|
// Required, so clients always send it; the Idempotency-Key header may stand in, since the
|
|
1264
1814
|
// server copies it in before validation.
|
|
1265
1815
|
idempotency_key: IDEMPOTENCY_KEY,
|
|
1816
|
+
/** An admin's reason for cancelling a one-off event past its cancel cutoff (RFC 0004 B5). */
|
|
1817
|
+
override_reason: OVERRIDE_REASON.optional(),
|
|
1266
1818
|
})
|
|
1267
1819
|
.strict(),
|
|
1268
1820
|
output: z
|
|
@@ -1271,6 +1823,8 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1271
1823
|
revision: REVISION,
|
|
1272
1824
|
cancelled: z.literal(true),
|
|
1273
1825
|
replayed: z.boolean(),
|
|
1826
|
+
/** A late cancellation's fee, for you to charge (RFC 0004 B5); null when none. */
|
|
1827
|
+
cancellation_fee: RECORDED_FEE.nullable(),
|
|
1274
1828
|
})
|
|
1275
1829
|
.strict(),
|
|
1276
1830
|
risk: { readOnly: false, destructive: true, idempotent: true },
|
|
@@ -1331,6 +1885,8 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1331
1885
|
no_shows: z.number().int().min(0),
|
|
1332
1886
|
waitlist_offers: z.number().int().min(0),
|
|
1333
1887
|
approvals_expired: z.number().int().min(0),
|
|
1888
|
+
bookings_completed: z.number().int().min(0),
|
|
1889
|
+
reminders_due: z.number().int().min(0),
|
|
1334
1890
|
capped: z.boolean(),
|
|
1335
1891
|
})
|
|
1336
1892
|
.strict(),
|
|
@@ -1449,7 +2005,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1449
2005
|
{
|
|
1450
2006
|
name: 'slotlock_put_approval_policy',
|
|
1451
2007
|
title: 'Create or replace an approval policy',
|
|
1452
|
-
description: "Which agent bookings (slotlock_create_event, slotlock_confirm_slot, slotlock_book_pool, slotlock_book_together, and moves by slotlock_update_event or slotlock_reschedule) a person approves first, or are refused. applies_to names resources, pools, locations (with the locations in them) and groups; empty, every resource. conditions narrow it: longer_than_minutes, outside_hours (outside the resource's bookable hours), kinds (single, pool, bundle, move); a booking meets the policy when it meets every condition given. outcome require_approval holds the exact time the booking would take, under the approval, and answers the agent 409 approval_pending; refuse answers 403 approval_policy_refused. A person has approval_ttl_minutes to decide (default 1,440; at most 43,200, and the server's ceiling), and must decide lead_minutes before the booking starts: the wait is the earlier of the two. Where several apply, refuse wins, then the one that must be decided soonest. At most 50 policies: one more is 409 approval_policy_limit_reached.",
|
|
2008
|
+
description: "Which agent bookings (slotlock_create_event, slotlock_confirm_slot, slotlock_book_pool, slotlock_book_together, and moves by slotlock_update_event or slotlock_reschedule) a person approves first, or are refused. applies_to names resources, pools, locations (with the locations in them) and groups; empty, every resource. conditions narrow it: longer_than_minutes, outside_hours (outside the resource's bookable hours), kinds (single, pool, bundle, move); a booking meets the policy when it meets every condition given. outcome require_approval holds the exact time the booking would take, under the approval, and answers the agent 409 approval_pending; refuse answers 403 approval_policy_refused. A person has approval_ttl_minutes to decide (default 1,440; at most 43,200, and the server's ceiling), and must decide lead_minutes before the booking starts: the wait is the earlier of the two. Where several apply, refuse wins, then the one that must be decided soonest. The tenant's own bookings through this API (slotlock_tenant_book_together, slotlock_tenant_book_pool) are held only by policies with include_application true, and are the tenant's again when granted. At most 50 policies: one more is 409 approval_policy_limit_reached.",
|
|
1453
2009
|
method: 'PUT',
|
|
1454
2010
|
path: '/v1/approval-policies/{policy_ref}',
|
|
1455
2011
|
input: z
|
|
@@ -1461,6 +2017,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
|
|
|
1461
2017
|
outcome: z.enum(['require_approval', 'refuse']),
|
|
1462
2018
|
approval_ttl_minutes: z.number().int().min(1).max(SLOTLOCK_MAX_APPROVAL_MINUTES).optional(),
|
|
1463
2019
|
lead_minutes: z.number().int().min(0).max(SLOTLOCK_MAX_APPROVAL_MINUTES).optional(),
|
|
2020
|
+
include_application: z.boolean().optional(),
|
|
1464
2021
|
})
|
|
1465
2022
|
.strict(),
|
|
1466
2023
|
output: z.object({ policy: APPROVAL_POLICY }).strict(),
|