@proveanything/smartlinks 2.0.70 → 2.1.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/dist/api/asset.d.ts +5 -0
- package/dist/api/asset.js +11 -0
- package/dist/api/bookings.d.ts +89 -0
- package/dist/api/bookings.js +179 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/docs/API_SUMMARY.md +463 -1
- package/dist/docs/app-objects.md +7 -0
- package/dist/docs/assets.md +63 -2
- package/dist/docs/bookings.md +351 -0
- package/dist/docs/overview.md +1 -0
- package/dist/docs/server-functions.md +29 -1
- package/dist/http.js +3 -0
- package/dist/openapi.yaml +3825 -2774
- package/dist/testing/bookingsMemory.d.ts +75 -0
- package/dist/testing/bookingsMemory.js +413 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.js +31 -10
- package/dist/types/appManifest.d.ts +55 -0
- package/dist/types/asset.d.ts +25 -1
- package/dist/types/bookings.d.ts +301 -0
- package/dist/types/bookings.js +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/freeTimes.d.ts +57 -0
- package/dist/utils/freeTimes.js +206 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/API_SUMMARY.md +463 -1
- package/docs/app-objects.md +7 -0
- package/docs/assets.md +63 -2
- package/docs/bookings.md +351 -0
- package/docs/overview.md +1 -0
- package/docs/server-functions.md +29 -1
- package/openapi.yaml +3825 -2774
- package/package.json +1 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Bookings: slots, bookings and allocations
|
|
2
|
+
|
|
3
|
+
Generic booking objects for **any** booking app: event tickets, classes, tours, table reservations,
|
|
4
|
+
one-to-one appointments, room and equipment hire. The platform gives you the objects and the
|
|
5
|
+
guarantees. Your app owns the rest: schedule, policy, pricing, payment and UI.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { bookings, utils } from '@proveanything/smartlinks'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What the platform guarantees (and what it doesn't)
|
|
14
|
+
|
|
15
|
+
| The platform | Your app |
|
|
16
|
+
|---|---|
|
|
17
|
+
| **No double-booking** of a person / room / table, however many requests arrive at once | The schedule (opening hours) and its UI |
|
|
18
|
+
| **No overselling** a slot's capacity | Booking policy: notice, durations, who may book |
|
|
19
|
+
| **Atomic** writes: a booking with several parts (a stylist *and* a chair) succeeds whole or not at all | Pricing, ticket types, payment or credits, refunds |
|
|
20
|
+
| **Idempotent** creates (`idempotencyKey`): a retried request never books twice | Queues, waiting rooms, throttling for high-demand sales |
|
|
21
|
+
| **Holds** that expire and give the place back | Confirmation / reminder content (flows on booking events) |
|
|
22
|
+
| Who may book publicly, by **per-app rules** (like records / cases) | |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## The three objects
|
|
27
|
+
|
|
28
|
+
- **Slot** — a time window with a **capacity**: an event performance, a class, a tour at 2pm. Seats
|
|
29
|
+
are counted (`booked`, `remaining`).
|
|
30
|
+
- **Booking** — the customer's reservation and its lifecycle. This is what "my bookings" lists.
|
|
31
|
+
- **Allocation** — what a booking actually claims: seats on a slot (`mode: 'capacity'`) or
|
|
32
|
+
exclusive time on a resource (`mode: 'exclusive'`). Most apps never touch these directly: they're
|
|
33
|
+
returned on `booking.allocations`.
|
|
34
|
+
|
|
35
|
+
**Resources are not a new object.** Staff, rooms, tables and equipment are whatever you already use —
|
|
36
|
+
app records, contacts, products — referenced by type + id: `{ resourceType: 'record', resourceId: 'staff-sam' }`.
|
|
37
|
+
|
|
38
|
+
### Booking statuses
|
|
39
|
+
|
|
40
|
+
| Status | Holds a place? | Meaning |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `held` | yes, until `holdExpiresAt` | Claimed while the customer pays / confirms; expires automatically |
|
|
43
|
+
| `pending` | yes | Claimed, waiting for the business to approve |
|
|
44
|
+
| `confirmed` | yes | Booked |
|
|
45
|
+
| `completed` / `no_show` | yes (the time was used) | Outcome after the event (admin) |
|
|
46
|
+
| `cancelled` | no | `cancelReason`: `admin`, `owner`, `hold_expired` or your own |
|
|
47
|
+
| `waitlisted` | no | Waiting; confirming it claims the place if one is free |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Events and classes — slots
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// Admin (Studio, or your app's server function): publish the sessions
|
|
55
|
+
const slot = await bookings.slots.create(collectionId, appId, {
|
|
56
|
+
startsAt: '2026-11-14T19:30:00Z', endsAt: '2026-11-14T21:00:00Z',
|
|
57
|
+
capacity: 200,
|
|
58
|
+
perContactLimit: 4, // max 4 seats per person on this slot, enforced atomically
|
|
59
|
+
data: { title: 'Autumn concert' },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// A class that also needs its instructor free: the slot blocks the instructor's time too
|
|
63
|
+
await bookings.slots.create(collectionId, appId, {
|
|
64
|
+
startsAt, endsAt, capacity: 12,
|
|
65
|
+
resourceType: 'record', resourceId: 'instructor-ana', blockResource: true,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// A series, all-or-nothing (max 500)
|
|
69
|
+
await bookings.slots.createMany(collectionId, appId, sessions, { seriesId })
|
|
70
|
+
|
|
71
|
+
// Public: open slots with places left
|
|
72
|
+
const { data } = await bookings.slots.list(collectionId, appId, { from, to, available: true })
|
|
73
|
+
data[0].remaining
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Book seats:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const booking = await bookings.create(collectionId, appId, { slotId: slot.id, quantity: 2, idempotencyKey })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Recurring events are **expanded into concrete slots** (`seriesId`); there's no recurrence rule
|
|
83
|
+
stored on a slot.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Appointments — exclusive time on a resource
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await bookings.create(collectionId, appId, {
|
|
91
|
+
resourceType: 'record', resourceId: 'staff-sam',
|
|
92
|
+
startsAt: '2026-11-03T10:00:00Z', endsAt: '2026-11-03T10:30:00Z',
|
|
93
|
+
bufferAfter: 15, // admin: blocks 10:30–10:45 too (clean-up); the booking still shows 10:00–10:30
|
|
94
|
+
}, true)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Several resources at once — all-or-nothing:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
await bookings.create(collectionId, appId, {
|
|
101
|
+
startsAt, endsAt,
|
|
102
|
+
allocations: [
|
|
103
|
+
{ resourceType: 'record', resourceId: 'stylist-jo' },
|
|
104
|
+
{ resourceType: 'record', resourceId: 'chair-2' },
|
|
105
|
+
],
|
|
106
|
+
}, true)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A pool with no fixed times (3 kayaks, bookable any time) is three resources; your app picks a free one.
|
|
110
|
+
|
|
111
|
+
### Blocked time
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
await bookings.blocks.create(collectionId, appId, { resourceType: 'record', resourceId: 'staff-sam', startsAt, endsAt, data: { reason: 'training' } })
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Planned time off usually belongs in your schedule (an override); blocks are for ad-hoc blocked time.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Schedules and free times
|
|
122
|
+
|
|
123
|
+
**Your app owns the schedule.** Store it however you like — usually an app record. For the time maths,
|
|
124
|
+
the SDK ships a pure helper:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const schedule = {
|
|
128
|
+
timezone: 'Europe/London',
|
|
129
|
+
weekly: { mon: [{ start: '09:00', end: '12:30' }, { start: '13:30', end: '17:00' }], tue: [{ start: '09:00', end: '17:00' }] },
|
|
130
|
+
overrides: [{ from: '2026-12-24', to: '2026-12-28', hours: [] }, { date: '2026-11-07', hours: [{ start: '10:00', end: '13:00' }] }],
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const fb = await bookings.freebusy(collectionId, appId, { resources: ['record:staff-sam'], from, to }, true)
|
|
134
|
+
const times = utils.freeTimes({
|
|
135
|
+
hours: schedule, busy: fb.resources[0].busy,
|
|
136
|
+
from, to, duration: 30, step: 30, bufferAfter: 15, minNotice: 240,
|
|
137
|
+
})
|
|
138
|
+
// → [{ start, end }, …] in UTC; render in the booker's timezone
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- Times are **local wall-clock** in `timezone`, so "9 to 5" stays 9 to 5 across daylight-saving changes.
|
|
142
|
+
- A missing day is closed; hours never cross midnight (split them); `end` may be `'24:00'`.
|
|
143
|
+
- `overrides` win over `weekly` (`hours: []` = closed).
|
|
144
|
+
- Also exported: `utils.withinSchedule`, `utils.openIntervals`, `utils.subtractIntervals`, `utils.validateSchedule`.
|
|
145
|
+
|
|
146
|
+
`freebusy` returns busy intervals only (bookings, blocks, slots that block a resource) — never who
|
|
147
|
+
booked. It's collection-wide: a resource is busy whichever app booked it.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Who can book — per-app rules
|
|
152
|
+
|
|
153
|
+
Exactly like records, cases and threads:
|
|
154
|
+
|
|
155
|
+
- **Admin calls are unconditional** — Studio users, and **your app's own server functions** (they call
|
|
156
|
+
the admin routes with your app's authority). Pass `admin = true`.
|
|
157
|
+
- **Public calls** (`admin = false`, the default for `create`) follow your app's config in
|
|
158
|
+
`sites/{collectionId}/apps/{appId}` (`POST /admin/collection/:cId/apps/:appId`). With no config,
|
|
159
|
+
**public booking is refused** — only admins (and your functions) can book.
|
|
160
|
+
|
|
161
|
+
Pick a mode per app:
|
|
162
|
+
|
|
163
|
+
| Mode | Config | Suits |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| **Public booking** | `publicCreate.bookings` allows anonymous and/or signed-in callers | "Book an appointment with me", simple events — no server code |
|
|
166
|
+
| **Admin-only booking** | no `publicCreate.bookings` | Extra validation, queues, payment-first, high demand: book through your server function |
|
|
167
|
+
|
|
168
|
+
### `publicCreate.bookings`
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"publicCreate": {
|
|
173
|
+
"bookings": {
|
|
174
|
+
"anonymous": {
|
|
175
|
+
"allow": true,
|
|
176
|
+
"enforce": { "status": "held" },
|
|
177
|
+
"limits": { "maxQuantity": 4, "holdMinutes": 15 },
|
|
178
|
+
"rules": { "targets": ["slots"], "minNoticeMinutes": 60, "horizonDays": 90 },
|
|
179
|
+
"edit": { "editToken": true }
|
|
180
|
+
},
|
|
181
|
+
"authenticated": {
|
|
182
|
+
"allow": true,
|
|
183
|
+
"enforce": { "status": "confirmed", "visibility": "owner" },
|
|
184
|
+
"rules": {
|
|
185
|
+
"targets": ["slots", "resources"],
|
|
186
|
+
"schedule": true,
|
|
187
|
+
"resourceTypes": ["record"],
|
|
188
|
+
"durations": [30, 60],
|
|
189
|
+
"minNoticeMinutes": 240,
|
|
190
|
+
"bufferAfter": 15
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Field | Meaning |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `enforce.status` | `confirmed` (instant), `pending` (business approves), `held` (pay / confirm next). Default `pending`. The caller can't override it. |
|
|
201
|
+
| `enforce.visibility` | Default: `owner` for signed-in, `admin` for anonymous |
|
|
202
|
+
| `limits.maxQuantity` | Max seats / claims per booking |
|
|
203
|
+
| `limits.holdMinutes` | Hold length for public `held` bookings (default 15, max 60) |
|
|
204
|
+
| `rules.targets` | `slots` (only your published slots) and/or `resources` (free-form time). Default `['slots']` |
|
|
205
|
+
| `rules.schedule` | **Required for `resources`.** The platform checks the requested time against the resource's schedule: an app record of **your app** with `recordType: 'availability'`, `parentType` / `parentId` = the resource, `data` in the standard shape above |
|
|
206
|
+
| `rules.resourceTypes` | Which resource types the public may book |
|
|
207
|
+
| `rules.durations` | Allowed lengths in minutes |
|
|
208
|
+
| `rules.minNoticeMinutes`, `rules.horizonDays` | How soon / how far ahead |
|
|
209
|
+
| `rules.bufferBefore`, `rules.bufferAfter` | Applied to every public booking (the caller can't set buffers) |
|
|
210
|
+
| `edit.editToken` | Anonymous only: the create response carries `editToken` **once** — store it so the guest can manage the booking. `edit.windowMinutes` limits how long it works |
|
|
211
|
+
|
|
212
|
+
Public callers can't set `contactId` (signed-in callers get their own contact; guests none), the
|
|
213
|
+
admin zone, buffers, `customId` or `sourceSystem`.
|
|
214
|
+
|
|
215
|
+
### `ownerActions.bookings` — what the booker may do afterwards
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"ownerActions": {
|
|
220
|
+
"bookings": {
|
|
221
|
+
"cancel": { "allow": true, "cutoffMinutes": 1440 },
|
|
222
|
+
"move": { "allow": true, "cutoffMinutes": 1440 },
|
|
223
|
+
"confirm": { "allow": true }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Default: nothing. `cutoffMinutes` = not within that time of the start (`CUTOFF_PASSED`). `move`
|
|
230
|
+
re-checks the create rules for the new time. `confirm` turns the booker's own **held** booking into
|
|
231
|
+
`confirmed` — only for apps that take no payment; `pending` and `waitlisted` are the business's to
|
|
232
|
+
confirm.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// Signed-in booker
|
|
236
|
+
await bookings.cancel(collectionId, appId, bookingId)
|
|
237
|
+
const mine = await bookings.mine(collectionId, appId, { from: new Date() })
|
|
238
|
+
|
|
239
|
+
// Guest, with the token from create
|
|
240
|
+
const b = await bookings.get(collectionId, appId, bookingId, false, { editToken })
|
|
241
|
+
await bookings.move(collectionId, appId, bookingId, { slotId: other.id }, false, { editToken })
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `publicRead.bookings.freebusy`
|
|
245
|
+
|
|
246
|
+
`{ "publicRead": { "bookings": { "freebusy": true } } }` lets public pages call `bookings.freebusy`.
|
|
247
|
+
Off by default — most apps serve free times through their own function.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## In a server function — `ctx.sl.bookings`
|
|
252
|
+
|
|
253
|
+
When public booking is off, your app books through its own server function. `ctx.sl.bookings` is
|
|
254
|
+
the same API, admin-side, for **your app's** bookings only (`sl:bookings:read` / `sl:bookings:write`):
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// manifest: { name: 'book', trigger: { type: 'http' }, visibility: 'public', authority: 'collection',
|
|
258
|
+
// capabilities: ['sl:bookings:write', 'sl:contacts:write'] }
|
|
259
|
+
export async function book(ctx, req) {
|
|
260
|
+
const me = await ctx.sl.contacts.forCaller() // null when signed out
|
|
261
|
+
// … your rules: hours (utils.withinSchedule), a queue token, a member level, a price …
|
|
262
|
+
const b = await ctx.sl.bookings.create({
|
|
263
|
+
resourceType: 'record', resourceId: req.body.staffId,
|
|
264
|
+
startsAt: req.body.startsAt, endsAt: req.body.endsAt, bufferAfter: 10,
|
|
265
|
+
contactId: me?.contactId, status: 'confirmed', idempotencyKey: req.body.requestId,
|
|
266
|
+
})
|
|
267
|
+
return Response.json({ id: b.id, startsAt: b.startsAt })
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`create`, `get`, `query`, `mine` (the caller's own), `update`, `confirm`, `cancel`, `move`,
|
|
272
|
+
`extendHold`, `complete`, `noShow`, `freebusy`, `slots.*`, `blocks.*`. Errors carry `code`,
|
|
273
|
+
`status` and `details` exactly as over HTTP. The local test harness
|
|
274
|
+
(`@proveanything/smartlinks/testing`) runs bookings in memory with the same rules.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Payments and high-demand sales
|
|
279
|
+
|
|
280
|
+
The platform takes no payments. It gives you what a payment or credits flow needs:
|
|
281
|
+
|
|
282
|
+
1. Create the booking **`held`** (claims the place; `holdMinutes`).
|
|
283
|
+
2. Take payment. Slow step? `bookings.extendHold(…, minutes)` (admin; capped at 24h from creation).
|
|
284
|
+
3. **Confirm after payment** — from your function or payment webhook handler. Confirm is idempotent, so
|
|
285
|
+
a repeated webhook is harmless. A run-out hold fails with `HOLD_EXPIRED`.
|
|
286
|
+
4. If it's never confirmed, the hold expires within a minute: the booking becomes `cancelled`
|
|
287
|
+
(`cancelReason: 'hold_expired'`) and the place goes back. Void the payment intent on the
|
|
288
|
+
`booking.hold_expired` event.
|
|
289
|
+
|
|
290
|
+
Keep payment references, amounts, credits and refunds in the booking's `admin` / `owner` zones.
|
|
291
|
+
|
|
292
|
+
**High demand** (queues, lotteries, bot checks): turn public booking **off** so the only way in is
|
|
293
|
+
your server function; it checks the queue token / per-person limit / throttle, then creates a short
|
|
294
|
+
`held` booking. The platform guarantees nothing is oversold however many requests arrive.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Errors
|
|
299
|
+
|
|
300
|
+
Booking calls fail with HTTP 409 and an `errorCode` you can show or act on:
|
|
301
|
+
|
|
302
|
+
| Code | When |
|
|
303
|
+
|---|---|
|
|
304
|
+
| `SLOT_FULL` | Not enough places left (`details.remaining`) |
|
|
305
|
+
| `SLOT_CLOSED` | The slot isn't open |
|
|
306
|
+
| `RESOURCE_TAKEN` | The time isn't free on that resource (`details` names it) |
|
|
307
|
+
| `PER_CONTACT_LIMIT` / `QUANTITY_LIMIT` | Per-person / per-booking limit |
|
|
308
|
+
| `HOLD_EXPIRED` | The hold ran out before confirm / move |
|
|
309
|
+
| `INVALID_STATE` | e.g. confirming a cancelled booking |
|
|
310
|
+
| `SLOT_OVERBOOKED` / `SLOT_HAS_BOOKINGS` | Lowering capacity below what's booked / changing a booked slot's time or deleting it |
|
|
311
|
+
| `TOO_SOON` / `TOO_FAR_AHEAD` / `IN_THE_PAST` / `DURATION_NOT_ALLOWED` | Public time rules |
|
|
312
|
+
| `OUTSIDE_SCHEDULE` / `NO_SCHEDULE` / `SCHEDULE_INVALID` | Public schedule check |
|
|
313
|
+
| `CUTOFF_PASSED` | Owner action inside its cutoff |
|
|
314
|
+
|
|
315
|
+
`403 FORBIDDEN` when the app's rules don't allow the call; `404` for unknown ids.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Events
|
|
320
|
+
|
|
321
|
+
Recorded after each write commits, for flows and app functions (`trigger: { type: 'event', eventTypes: [...] }`):
|
|
322
|
+
|
|
323
|
+
`booking.created`, `booking.confirmed`, `booking.cancelled`, `booking.moved`, `booking.completed`,
|
|
324
|
+
`booking.no_show`, `booking.hold_expired`, `slot.full`, `slot.place_freed` (for waitlist promotion).
|
|
325
|
+
Context: `{ appId, status, slotIds }` (bookings) / `{ appId, remaining }` (slots).
|
|
326
|
+
|
|
327
|
+
Reminders before a booking (`booking.starts_in`) are coming; until then, use a cron function that lists
|
|
328
|
+
upcoming bookings.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## API reference
|
|
333
|
+
|
|
334
|
+
All take `(collectionId, appId, …)`; `admin` is the last parameter where both exist.
|
|
335
|
+
|
|
336
|
+
| Call | Admin | Public |
|
|
337
|
+
|---|---|---|
|
|
338
|
+
| `bookings.create(input, admin?)` | any status, any field | per `publicCreate.bookings` |
|
|
339
|
+
| `bookings.list(params?)` | ✓ | — (use `mine`) |
|
|
340
|
+
| `bookings.mine(params?)` | — | signed-in booker |
|
|
341
|
+
| `bookings.get(id, admin?, { editToken }?)` | ✓ | booker / token |
|
|
342
|
+
| `bookings.update(id, input)` | ✓ | — |
|
|
343
|
+
| `bookings.confirm / cancel / move(…, admin?, { editToken }?)` | ✓ | per `ownerActions.bookings` |
|
|
344
|
+
| `bookings.extendHold / complete / noShow` | ✓ | — |
|
|
345
|
+
| `bookings.freebusy(q, admin?)` | ✓ | if `publicRead.bookings.freebusy` |
|
|
346
|
+
| `bookings.slots.create / createMany / update / remove` | ✓ | — |
|
|
347
|
+
| `bookings.slots.list / get(…, admin?)` | ✓ | open, public slots |
|
|
348
|
+
| `bookings.blocks.create / list / remove` | ✓ | — |
|
|
349
|
+
| `utils.freeTimes / withinSchedule / openIntervals / subtractIntervals / validateSchedule` | pure helpers | |
|
|
350
|
+
|
|
351
|
+
REST: `/{admin|public}/collection/:cId/app/:appId/bookings[/slots|/blocks|/freebusy|/mine|/:id[/confirm|/cancel|/move|/extend-hold|/complete|/no-show]]`.
|
package/dist/docs/overview.md
CHANGED
|
@@ -88,6 +88,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
88
88
|
| **Proof Ownership Transfer** | `docs/proof-ownership-transfer.md` | Moving a proof's single owner (directed transfer / open release), accept/cancel, and the state machine |
|
|
89
89
|
| **appConfig / Feature Flags** | `docs/appConfig.md` | `appConfig` settings contract — installed apps, `system.features`/`entitledAppGroups`/`meters`, `isFeatureEnabled()` helper |
|
|
90
90
|
| **App Objects** | `docs/app-objects.md` | Queryable domain objects — `app.records` / `app.cases` / `app.threads`: JSONB zones, visibility, policies, aggregations |
|
|
91
|
+
| **Bookings** | `docs/bookings.md` | Slots, bookings and allocations for events and appointments: no double-booking, holds, per-app public rules, `utils.freeTimes` |
|
|
91
92
|
| **App Data Storage** | `docs/app-data-storage.md` | Decision guide: pick between `appConfiguration` / `userAppData` / `app.records` |
|
|
92
93
|
| **Attestations** | `docs/attestations.md` | Postgres attestations API (proof-level user/admin data; replaces the legacy Firestore path) |
|
|
93
94
|
| **Caching** | `docs/caching.md` | SDK GET cache tiers + TTLs, `invalidateCache({ exact })`, and `force`/push guidance |
|
|
@@ -167,7 +167,7 @@ Declare the minimum you need.
|
|
|
167
167
|
|
|
168
168
|
| Capability | Grants |
|
|
169
169
|
|---|---|
|
|
170
|
-
| `sl:<resource>:read` / `sl:<resource>:write` | SDK access to that resource, e.g. `sl:products:read`, `sl:records:write`, `sl:attestations:write`, `sl:contacts:write`. |
|
|
170
|
+
| `sl:<resource>:read` / `sl:<resource>:write` | SDK access to that resource, e.g. `sl:products:read`, `sl:records:write`, `sl:attestations:write`, `sl:contacts:write`, `sl:bookings:write`. |
|
|
171
171
|
| `network` | `ctx.fetch` to any host. |
|
|
172
172
|
| `network:<host>` | `ctx.fetch` to that host only (repeat for several). Prefer this over blanket `network`. |
|
|
173
173
|
| `network:*.<domain>` | `ctx.fetch` to any subdomain of `<domain>` (not `<domain>` itself), for a customer's own endpoint at a provider, e.g. `network:*.servicebus.windows.net`. `<domain>` needs at least two labels (`*.com` is not honoured). |
|
|
@@ -517,6 +517,7 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
|
|
|
517
517
|
| Check a tapped tag is real | `ctx.sl.tags.resolve(proofId)` → `{ id, claimSetId, code }` or `null` | `sl:tags:read` |
|
|
518
518
|
| Put a visitor's details on a contact | `ctx.sl.contacts.upsert({ email, firstName, … })` → `{ contactId, created }` (fills empty fields only) | `sl:contacts:write` |
|
|
519
519
|
| Record "they did X" | `ctx.sl.interactions.record({ interactionId, contactId, outcome, metadata })` | `sl:interactions:write` |
|
|
520
|
+
| Take a booking with your own checks (hours, queue token, payment) | `ctx.sl.bookings.create({ slotId \| resource + time, contactId, status, idempotencyKey })` — never oversold, never double-booked; also `confirm` / `cancel` / `move` / `mine` / `freebusy` / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
|
|
520
521
|
|
|
521
522
|
Notes:
|
|
522
523
|
- **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
|
|
@@ -611,6 +612,33 @@ await ctx.sl.interactions.record({
|
|
|
611
612
|
})
|
|
612
613
|
```
|
|
613
614
|
|
|
615
|
+
### `bookings.*` — places and time that can't be oversold
|
|
616
|
+
|
|
617
|
+
`ctx.sl.bookings` is your app's own bookings, slots and blocks (`sl:bookings:read` / `write`). A
|
|
618
|
+
function books **admin-side**: any status, the admin zone, any contact — your app's public booking
|
|
619
|
+
rules (`publicCreate.bookings`) don't apply, because the function *is* your rules. The platform still
|
|
620
|
+
guarantees no overbooking and no overlapping time, so a clash throws (`code`: `SLOT_FULL`,
|
|
621
|
+
`RESOURCE_TAKEN`, `PER_CONTACT_LIMIT`, `HOLD_EXPIRED`, …; with `status` and `details`). Pass an
|
|
622
|
+
`idempotencyKey` so a retried request returns the first booking (`replayed: true`).
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
const me = await ctx.sl.contacts.forCaller()
|
|
626
|
+
if (!me) return Response.json({ error: 'sign in' }, { status: 401 })
|
|
627
|
+
try {
|
|
628
|
+
const booking = await ctx.sl.bookings.create({
|
|
629
|
+
slotId: body.slotId, quantity: body.quantity, contactId: me.contactId,
|
|
630
|
+
status: 'held', holdMinutes: 15, idempotencyKey: body.requestId,
|
|
631
|
+
})
|
|
632
|
+
return Response.json({ id: booking.id, holdExpiresAt: booking.holdExpiresAt })
|
|
633
|
+
} catch (e: any) {
|
|
634
|
+
if (e.code === 'SLOT_FULL') return Response.json({ error: 'sold out', remaining: e.details?.remaining }, { status: 409 })
|
|
635
|
+
throw e
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
The local harness runs bookings in memory with the same rules (capacity, per-person limits, no
|
|
640
|
+
overlaps, idempotency, holds): `ctx.memory.bookings`.
|
|
641
|
+
|
|
614
642
|
---
|
|
615
643
|
|
|
616
644
|
## Platform tools — PDFs, labels, barcodes
|
package/dist/http.js
CHANGED
|
@@ -216,6 +216,9 @@ function shouldSkipCache(path) {
|
|
|
216
216
|
// Never cache /me endpoints (contact/me, loyalty/me, etc.) — always user-specific.
|
|
217
217
|
if (/\/me(\?|$)/i.test(path))
|
|
218
218
|
return true;
|
|
219
|
+
// Bookings change by the second (places left, busy times, a booking's status) — never serve them stale.
|
|
220
|
+
if (/\/app\/[^/]+\/bookings(\/|\?|$)/i.test(path))
|
|
221
|
+
return true;
|
|
219
222
|
return false;
|
|
220
223
|
}
|
|
221
224
|
/** Evict the oldest (LRU) entry when the cache is at capacity. */
|