@proveanything/smartlinks 2.1.0 → 2.1.2
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/bookings.d.ts +7 -1
- package/dist/api/bookings.js +9 -0
- package/dist/docs/API_SUMMARY.md +31 -1
- package/dist/docs/bookings.md +75 -2
- package/dist/docs/server-functions.md +13 -1
- package/dist/openapi.yaml +82 -0
- package/dist/testing/bookingsMemory.d.ts +11 -0
- package/dist/testing/bookingsMemory.js +22 -0
- package/dist/testing/index.js +1 -1
- package/dist/types/appManifest.d.ts +3 -1
- package/dist/types/bookings.d.ts +31 -0
- package/docs/API_SUMMARY.md +31 -1
- package/docs/bookings.md +75 -2
- package/docs/server-functions.md +13 -1
- package/openapi.yaml +82 -0
- package/package.json +1 -1
package/dist/api/bookings.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse, BookingAccessOptions } from '../types/bookings.js';
|
|
1
|
+
import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse, BookingAccessOptions, MessageBookersInput, MessageBookersResult } from '../types/bookings.js';
|
|
2
2
|
import type { PaginatedResponse } from '../types/appObjects.js';
|
|
3
3
|
export declare namespace bookings {
|
|
4
4
|
/**
|
|
@@ -55,6 +55,12 @@ export declare namespace bookings {
|
|
|
55
55
|
* GET /bookings/freebusy
|
|
56
56
|
*/
|
|
57
57
|
function freebusy(collectionId: string, appId: string, q: FreeBusyQuery, admin?: boolean): Promise<FreeBusyResponse>;
|
|
58
|
+
/**
|
|
59
|
+
* Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date
|
|
60
|
+
* range (optionally with one person / room), each send hydrated with their booking ({{ booking }}).
|
|
61
|
+
* No segment to make. POST /bookings/message
|
|
62
|
+
*/
|
|
63
|
+
function message(collectionId: string, appId: string, input: MessageBookersInput): Promise<MessageBookersResult>;
|
|
58
64
|
namespace slots {
|
|
59
65
|
/** Create a slot (admin). POST /bookings/slots */
|
|
60
66
|
function create(collectionId: string, appId: string, input: CreateSlotInput): Promise<BookingSlot>;
|
package/dist/api/bookings.js
CHANGED
|
@@ -122,6 +122,15 @@ export var bookings;
|
|
|
122
122
|
return request(`${basePath(collectionId, appId, admin)}/freebusy${query(params)}`);
|
|
123
123
|
}
|
|
124
124
|
bookings.freebusy = freebusy;
|
|
125
|
+
/**
|
|
126
|
+
* Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date
|
|
127
|
+
* range (optionally with one person / room), each send hydrated with their booking ({{ booking }}).
|
|
128
|
+
* No segment to make. POST /bookings/message
|
|
129
|
+
*/
|
|
130
|
+
async function message(collectionId, appId, input) {
|
|
131
|
+
return post(`${basePath(collectionId, appId, true)}/message`, input);
|
|
132
|
+
}
|
|
133
|
+
bookings.message = message;
|
|
125
134
|
// ==================== SLOTS ====================
|
|
126
135
|
let slots;
|
|
127
136
|
(function (slots_1) {
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2 | Generated: 2026-10-26T14:58:53.408Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2816,6 +2816,7 @@ interface ServerFunctionBookings {
|
|
|
2816
2816
|
complete(bookingId: string): Promise<Booking>;
|
|
2817
2817
|
noShow(bookingId: string): Promise<Booking>;
|
|
2818
2818
|
freebusy(q: FreeBusyQuery): Promise<FreeBusyResponse>;
|
|
2819
|
+
message(input: MessageBookersInput): Promise<MessageBookersResult>;
|
|
2819
2820
|
slots: {
|
|
2820
2821
|
create(input: CreateSlotInput): Promise<BookingSlot>;
|
|
2821
2822
|
createMany(inputs: CreateSlotInput[], opts?: { seriesId?: string }): Promise<BookingSlot[]>;
|
|
@@ -5366,6 +5367,32 @@ interface FreeBusyResponse {
|
|
|
5366
5367
|
}
|
|
5367
5368
|
```
|
|
5368
5369
|
|
|
5370
|
+
**MessageBookersInput** (interface)
|
|
5371
|
+
```typescript
|
|
5372
|
+
interface MessageBookersInput {
|
|
5373
|
+
templateId: string
|
|
5374
|
+
channel?: 'email' | 'sms' | 'whatsapp' | 'push' | 'preferred'
|
|
5375
|
+
props?: Record<string, any>
|
|
5376
|
+
slotId?: string
|
|
5377
|
+
from?: string | Date
|
|
5378
|
+
to?: string | Date
|
|
5379
|
+
resourceType?: string
|
|
5380
|
+
resourceId?: string
|
|
5381
|
+
statuses?: BookingStatus[]
|
|
5382
|
+
ref?: string
|
|
5383
|
+
}
|
|
5384
|
+
```
|
|
5385
|
+
|
|
5386
|
+
**MessageBookersResult** (interface)
|
|
5387
|
+
```typescript
|
|
5388
|
+
interface MessageBookersResult {
|
|
5389
|
+
matched: number
|
|
5390
|
+
sent: number
|
|
5391
|
+
skipped: { noContact: number; duplicate: number; failed: number }
|
|
5392
|
+
failures: Array<{ bookingId: string; error: string }>
|
|
5393
|
+
}
|
|
5394
|
+
```
|
|
5395
|
+
|
|
5369
5396
|
**BookingAccessOptions** (interface)
|
|
5370
5397
|
```typescript
|
|
5371
5398
|
interface BookingAccessOptions {
|
|
@@ -11902,6 +11929,9 @@ confirmed → no_show (admin).
|
|
|
11902
11929
|
**freebusy**(collectionId: string, appId: string, q: FreeBusyQuery, admin: boolean = false) → `Promise<FreeBusyResponse>`
|
|
11903
11930
|
Busy intervals per resource (bookings, blocks, slots' own blocks) — never booking details. Public only when the app sets `publicRead.bookings.freebusy: true`. GET /bookings/freebusy
|
|
11904
11931
|
|
|
11932
|
+
**message**(collectionId: string, appId: string, input: MessageBookersInput) → `Promise<MessageBookersResult>`
|
|
11933
|
+
Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date range (optionally with one person / room), each send hydrated with their booking ({{ booking }}). No segment to make. POST /bookings/message
|
|
11934
|
+
|
|
11905
11935
|
### bookings.blocks
|
|
11906
11936
|
|
|
11907
11937
|
**create**(collectionId: string, appId: string, input: CreateBlockInput) → `Promise<BookingAllocation>`
|
package/dist/docs/bookings.md
CHANGED
|
@@ -324,8 +324,80 @@ Recorded after each write commits, for flows and app functions (`trigger: { type
|
|
|
324
324
|
`booking.no_show`, `booking.hold_expired`, `slot.full`, `slot.place_freed` (for waitlist promotion).
|
|
325
325
|
Context: `{ appId, status, slotIds }` (bookings) / `{ appId, remaining }` (slots).
|
|
326
326
|
|
|
327
|
-
Reminders before a booking (`booking.starts_in`) are coming; until then,
|
|
328
|
-
|
|
327
|
+
Reminders before a booking (`booking.starts_in`) are coming; until then, send a reminder template to
|
|
328
|
+
tomorrow's bookers with `bookings.message` (below) from a cron function.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Confirmations and messages
|
|
333
|
+
|
|
334
|
+
Nothing is emailed by default: your app decides what goes out, in **flows on booking events**, with
|
|
335
|
+
**templates the business can reword in Studio** (Comms → Templates). The platform does the sending,
|
|
336
|
+
the hydration and the consent.
|
|
337
|
+
|
|
338
|
+
### Flows on booking events
|
|
339
|
+
|
|
340
|
+
A site, or an app on behalf of a business, sets them up through the cloud blueprint
|
|
341
|
+
(`cloud/flows.json` in a site; `POST /admin/collection/:cId/blueprint/apply` with
|
|
342
|
+
`{ appId, blueprint: { templates, flows } }` from an app's admin screen). Additive: re-applying never
|
|
343
|
+
removes what is there, and a template edited in Studio keeps its edits unless the blueprint sends new
|
|
344
|
+
wording.
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"templates": {
|
|
349
|
+
"booking-confirmed": { "name": "You're booked", "email": { "subject": "{{ booking.serviceName }} on {{ booking.when.date }}", "body": "<p>Hi {{ contact.firstName }}, see you at {{ booking.when.time }}.</p>" }, "sms": { "body": "Booked: {{ booking.when.shortDate }} {{ booking.when.time }}" } },
|
|
350
|
+
"booking-requested": { "name": "Request received", "email": { "subject": "We got your request", "body": "<p>We'll confirm shortly.</p>" } },
|
|
351
|
+
"staff-new-booking": { "name": "You've been booked", "email": { "subject": "New appointment", "body": "<p>{{ booking.customer.name }}, {{ booking.when.dateTime }}</p>" } }
|
|
352
|
+
},
|
|
353
|
+
"flows": [
|
|
354
|
+
{ "when": { "event": "booking.confirmed" }, "do": [{ "send": "booking-confirmed", "to": "contact" }, { "send": "staff-new-booking", "to": "resource" }, { "send": "team-new-booking", "to": "team" }] },
|
|
355
|
+
{ "when": { "event": "booking.created", "status": "pending" }, "do": [{ "send": "booking-requested", "to": "contact" }] }
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
- `when.event`: `booking.created` / `confirmed` / `cancelled` / `moved` / `completed` / `no_show` /
|
|
361
|
+
`hold_expired`, `slot.full` / `slot.place_freed`; `when.status` narrows to the booking's status at the
|
|
362
|
+
time (`"pending"` for "request received").
|
|
363
|
+
- `to`: `contact` — the booker, by their preferred channel (`channel` to force one; a template needs an
|
|
364
|
+
`sms` part to go by text); `team` — the collection's team addresses; `resource` — the booked
|
|
365
|
+
person or room: a resource that is a contact, or a record with `data.email` / `data.contactId`.
|
|
366
|
+
A **public** resource record (a staff profile the booking page lists) keeps its address on a
|
|
367
|
+
**private child record** instead (`visibility: 'admin'`, `parentType: 'record'`, `parentId` = the
|
|
368
|
+
resource) — the platform looks there too, and the address never ships with the public listing.
|
|
369
|
+
- Who gets what, in templates: `{{ booking }}` is the booking **ready-formatted**: its fields and
|
|
370
|
+
`data`, plus `serviceName`, `staffName`, `customer`, `durationMinutes`, `when` (`date`,
|
|
371
|
+
`shortDate`, `time`, `endTime`, `dateTime`, `timezone` — in the business's timezone) and, when the
|
|
372
|
+
recipient's timezone is known and differs, `whenFor` (the same in theirs); `editToken` only when the
|
|
373
|
+
recipient is the booker (a guest's manage link). `{{ contact }}` is the recipient; `{{ appConfig }}`
|
|
374
|
+
the app's public config (a booking page's address, the business name) for sends to the contact.
|
|
375
|
+
|
|
376
|
+
### Who is reachable
|
|
377
|
+
|
|
378
|
+
A **guest** who books with an email or phone gets a **contact** (found by that identity or made,
|
|
379
|
+
filling only empty fields: a form's details never overwrite a known contact), and so does a customer
|
|
380
|
+
**staff book in by hand** (`customer: { name, email, phone }` on the body or in `data`). Admin
|
|
381
|
+
creates may pass `contactId` instead. A booking with no contact gets no messages: show the booker why
|
|
382
|
+
an email is worth giving.
|
|
383
|
+
|
|
384
|
+
### Messaging everyone booked — `bookings.message`
|
|
385
|
+
|
|
386
|
+
No list to build, no segment to save: send a template to everyone on a slot, or everyone booked in a
|
|
387
|
+
window (optionally with one resource), each hydrated with **their own** booking.
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
// Everyone on tomorrow's 10:00 class
|
|
391
|
+
await SL.bookings.message(collectionId, appId, { templateId, slotId })
|
|
392
|
+
// Everyone booked with Sam next week, by email even if they prefer texts
|
|
393
|
+
await SL.bookings.message(collectionId, appId, { templateId, channel: 'email', from, to, resourceType: 'record', resourceId: samId })
|
|
394
|
+
// → { matched, sent, skipped: { noContact, duplicate, failed }, failures: [{ bookingId, reason }] }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Statuses default to held, pending and confirmed; at most 500 recipients per call (narrow the window
|
|
398
|
+
for more). One message per contact on a slot, one per booking in a window. Also
|
|
399
|
+
`ctx.sl.bookings.message(input)` in a server function (`sl:bookings:write`) — a cron function that
|
|
400
|
+
messages tomorrow's bookers is a reminder system in five lines.
|
|
329
401
|
|
|
330
402
|
---
|
|
331
403
|
|
|
@@ -343,6 +415,7 @@ All take `(collectionId, appId, …)`; `admin` is the last parameter where both
|
|
|
343
415
|
| `bookings.confirm / cancel / move(…, admin?, { editToken }?)` | ✓ | per `ownerActions.bookings` |
|
|
344
416
|
| `bookings.extendHold / complete / noShow` | ✓ | — |
|
|
345
417
|
| `bookings.freebusy(q, admin?)` | ✓ | if `publicRead.bookings.freebusy` |
|
|
418
|
+
| `bookings.message(input)` | ✓ | — |
|
|
346
419
|
| `bookings.slots.create / createMany / update / remove` | ✓ | — |
|
|
347
420
|
| `bookings.slots.list / get(…, admin?)` | ✓ | open, public slots |
|
|
348
421
|
| `bookings.blocks.create / list / remove` | ✓ | — |
|
|
@@ -517,7 +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
|
+
| 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` / `message` (everyone booked, hydrated) / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
|
|
521
521
|
|
|
522
522
|
Notes:
|
|
523
523
|
- **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
|
|
@@ -636,6 +636,18 @@ try {
|
|
|
636
636
|
}
|
|
637
637
|
```
|
|
638
638
|
|
|
639
|
+
`ctx.sl.bookings.message({ templateId, slotId | from, to, resourceType?, resourceId?, channel? })` sends a
|
|
640
|
+
template to everyone booked (each hydrated with their own booking) — the five-line reminder cron:
|
|
641
|
+
|
|
642
|
+
```ts
|
|
643
|
+
// manifest: { name: 'remind', trigger: { type: 'cron', schedule: '0 17 * * *' }, capabilities: ['sl:bookings:write'] }
|
|
644
|
+
export async function remind(ctx) {
|
|
645
|
+
const from = new Date(); from.setDate(from.getDate() + 1); from.setHours(0, 0, 0, 0)
|
|
646
|
+
const to = new Date(from); to.setDate(to.getDate() + 1)
|
|
647
|
+
return ctx.sl.bookings.message({ templateId: 'bp.' + ctx.collectionId + '.reminder', from, to })
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
639
651
|
The local harness runs bookings in memory with the same rules (capacity, per-person limits, no
|
|
640
652
|
overlaps, idempotency, holds): `ctx.memory.bookings`.
|
|
641
653
|
|
package/dist/openapi.yaml
CHANGED
|
@@ -10291,6 +10291,32 @@ paths:
|
|
|
10291
10291
|
description: Unauthorized
|
|
10292
10292
|
404:
|
|
10293
10293
|
description: Not found
|
|
10294
|
+
/message:
|
|
10295
|
+
post:
|
|
10296
|
+
tags:
|
|
10297
|
+
- bookings
|
|
10298
|
+
summary: "Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date range (optionally with one person / room), each send hydrated with their booking ({{ booking }})."
|
|
10299
|
+
operationId: bookings_message
|
|
10300
|
+
security: []
|
|
10301
|
+
responses:
|
|
10302
|
+
200:
|
|
10303
|
+
description: Success
|
|
10304
|
+
content:
|
|
10305
|
+
application/json:
|
|
10306
|
+
schema:
|
|
10307
|
+
$ref: "#/components/schemas/MessageBookersResult"
|
|
10308
|
+
400:
|
|
10309
|
+
description: Bad request
|
|
10310
|
+
401:
|
|
10311
|
+
description: Unauthorized
|
|
10312
|
+
404:
|
|
10313
|
+
description: Not found
|
|
10314
|
+
requestBody:
|
|
10315
|
+
required: true
|
|
10316
|
+
content:
|
|
10317
|
+
application/json:
|
|
10318
|
+
schema:
|
|
10319
|
+
$ref: "#/components/schemas/MessageBookersInput"
|
|
10294
10320
|
/platform/location:
|
|
10295
10321
|
post:
|
|
10296
10322
|
tags:
|
|
@@ -24706,6 +24732,62 @@ components:
|
|
|
24706
24732
|
- from
|
|
24707
24733
|
- to
|
|
24708
24734
|
- resources
|
|
24735
|
+
MessageBookersInput:
|
|
24736
|
+
type: object
|
|
24737
|
+
properties:
|
|
24738
|
+
templateId:
|
|
24739
|
+
type: string
|
|
24740
|
+
channel:
|
|
24741
|
+
type: string
|
|
24742
|
+
enum:
|
|
24743
|
+
- email
|
|
24744
|
+
- sms
|
|
24745
|
+
- whatsapp
|
|
24746
|
+
- push
|
|
24747
|
+
- preferred
|
|
24748
|
+
props:
|
|
24749
|
+
type: object
|
|
24750
|
+
additionalProperties: true
|
|
24751
|
+
slotId:
|
|
24752
|
+
type: string
|
|
24753
|
+
from:
|
|
24754
|
+
type: object
|
|
24755
|
+
additionalProperties: true
|
|
24756
|
+
to:
|
|
24757
|
+
type: object
|
|
24758
|
+
additionalProperties: true
|
|
24759
|
+
resourceType:
|
|
24760
|
+
type: string
|
|
24761
|
+
resourceId:
|
|
24762
|
+
type: string
|
|
24763
|
+
statuses:
|
|
24764
|
+
type: array
|
|
24765
|
+
items:
|
|
24766
|
+
$ref: "#/components/schemas/BookingStatus"
|
|
24767
|
+
ref:
|
|
24768
|
+
type: string
|
|
24769
|
+
required:
|
|
24770
|
+
- templateId
|
|
24771
|
+
MessageBookersResult:
|
|
24772
|
+
type: object
|
|
24773
|
+
properties:
|
|
24774
|
+
matched:
|
|
24775
|
+
type: number
|
|
24776
|
+
sent:
|
|
24777
|
+
type: number
|
|
24778
|
+
skipped:
|
|
24779
|
+
type: object
|
|
24780
|
+
additionalProperties: true
|
|
24781
|
+
failures:
|
|
24782
|
+
type: array
|
|
24783
|
+
items:
|
|
24784
|
+
type: object
|
|
24785
|
+
additionalProperties: true
|
|
24786
|
+
required:
|
|
24787
|
+
- matched
|
|
24788
|
+
- sent
|
|
24789
|
+
- skipped
|
|
24790
|
+
- failures
|
|
24709
24791
|
BookingAccessOptions:
|
|
24710
24792
|
type: object
|
|
24711
24793
|
properties:
|
|
@@ -32,6 +32,17 @@ export declare function bookingsMemoryImpl(mem: BookingsMemory, caller?: {
|
|
|
32
32
|
cancel(bookingId: string, opts?: any): Promise<any>;
|
|
33
33
|
move(bookingId: string, input?: any): Promise<any>;
|
|
34
34
|
extendHold(bookingId: string, holdMinutes: number): Promise<any>;
|
|
35
|
+
/** Counts who would be messaged (no sends in memory); inject `sl.bookings.message` to assert sends. */
|
|
36
|
+
message(input?: any): Promise<{
|
|
37
|
+
matched: number;
|
|
38
|
+
sent: number;
|
|
39
|
+
skipped: {
|
|
40
|
+
noContact: number;
|
|
41
|
+
duplicate: number;
|
|
42
|
+
failed: number;
|
|
43
|
+
};
|
|
44
|
+
failures: any[];
|
|
45
|
+
}>;
|
|
35
46
|
complete(bookingId: string): Promise<any>;
|
|
36
47
|
noShow(bookingId: string): Promise<any>;
|
|
37
48
|
freebusy(q?: any): Promise<{
|
|
@@ -280,6 +280,28 @@ export function bookingsMemoryImpl(mem, caller = {}) {
|
|
|
280
280
|
b.holdExpiresAt = iso(Math.min(now() + (holdMinutes !== null && holdMinutes !== void 0 ? holdMinutes : 15) * 60000, ms(b.createdAt) + 24 * 3600000));
|
|
281
281
|
return copy(withAllocations(b));
|
|
282
282
|
},
|
|
283
|
+
/** Counts who would be messaged (no sends in memory); inject `sl.bookings.message` to assert sends. */
|
|
284
|
+
async message(input = {}) {
|
|
285
|
+
const list = input.slotId
|
|
286
|
+
? (await this.query({ slotId: input.slotId, status: (input.statuses || ['held', 'pending', 'confirmed']).join(',') })).data
|
|
287
|
+
: (await this.query({ from: input.from, to: input.to, status: (input.statuses || ['held', 'pending', 'confirmed']).join(',') })).data;
|
|
288
|
+
const seen = new Set();
|
|
289
|
+
const out = { matched: list.length, sent: 0, skipped: { noContact: 0, duplicate: 0, failed: 0 }, failures: [] };
|
|
290
|
+
for (const b of list) {
|
|
291
|
+
if (!b.contactId) {
|
|
292
|
+
out.skipped.noContact++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const key = input.slotId ? b.contactId : `${b.contactId}:${b.id}`;
|
|
296
|
+
if (seen.has(key)) {
|
|
297
|
+
out.skipped.duplicate++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
seen.add(key);
|
|
301
|
+
out.sent++;
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
},
|
|
283
305
|
async complete(bookingId) { return outcome(find(bookingId), 'completed'); },
|
|
284
306
|
async noShow(bookingId) { return outcome(find(bookingId), 'no_show'); },
|
|
285
307
|
async freebusy(q = {}) {
|
package/dist/testing/index.js
CHANGED
|
@@ -274,7 +274,7 @@ export function createFunctionTestContext(opts) {
|
|
|
274
274
|
const bkFn = (obj, name) => (obj && typeof obj[name] === 'function' ? obj[name].bind(obj) : undefined);
|
|
275
275
|
const BOOKING_READS = new Set(['get', 'query', 'mine', 'freebusy']);
|
|
276
276
|
sl.bookings = {};
|
|
277
|
-
for (const m of ['create', 'get', 'query', 'mine', 'update', 'confirm', 'cancel', 'move', 'extendHold', 'complete', 'noShow', 'freebusy']) {
|
|
277
|
+
for (const m of ['create', 'get', 'query', 'mine', 'update', 'confirm', 'cancel', 'move', 'extendHold', 'complete', 'noShow', 'freebusy', 'message']) {
|
|
278
278
|
sl.bookings[m] = gated('bookings', BOOKING_READS.has(m) ? 'read' : 'write', bkFn(bk, m), `bookings.${m}`);
|
|
279
279
|
}
|
|
280
280
|
sl.bookings.slots = {};
|
|
@@ -4,7 +4,7 @@ import type { AppDataDeclaration, AppHeadlessDeclaration } from './headless.js';
|
|
|
4
4
|
import type { RealtimeChannelDeclaration } from './realtime.js';
|
|
5
5
|
import type { AppManifestAdminCards } from './adminCards.js';
|
|
6
6
|
import type { PaginatedResponse } from './appObjects.js';
|
|
7
|
-
import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse } from './bookings.js';
|
|
7
|
+
import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse, MessageBookersInput, MessageBookersResult } from './bookings.js';
|
|
8
8
|
/**
|
|
9
9
|
* A bundle (widget or container) as returned by the collection widgets endpoint.
|
|
10
10
|
*
|
|
@@ -449,6 +449,8 @@ export interface ServerFunctionBookings {
|
|
|
449
449
|
noShow(bookingId: string): Promise<Booking>;
|
|
450
450
|
/** Busy intervals per resource — feed them to `utils.freeTimes` with your schedule. */
|
|
451
451
|
freebusy(q: FreeBusyQuery): Promise<FreeBusyResponse>;
|
|
452
|
+
/** A template to everyone booked on a slot / in a range, each hydrated with their booking. */
|
|
453
|
+
message(input: MessageBookersInput): Promise<MessageBookersResult>;
|
|
452
454
|
slots: {
|
|
453
455
|
create(input: CreateSlotInput): Promise<BookingSlot>;
|
|
454
456
|
createMany(inputs: CreateSlotInput[], opts?: {
|
package/dist/types/bookings.d.ts
CHANGED
|
@@ -266,6 +266,37 @@ export interface FreeBusyResponse {
|
|
|
266
266
|
busy: FreeBusyInterval[];
|
|
267
267
|
}>;
|
|
268
268
|
}
|
|
269
|
+
/** `bookings.message`: a template to everyone booked on a slot, or in a date range. */
|
|
270
|
+
export interface MessageBookersInput {
|
|
271
|
+
/** A comms template of the collection (edited in Studio). */
|
|
272
|
+
templateId: string;
|
|
273
|
+
channel?: 'email' | 'sms' | 'whatsapp' | 'push' | 'preferred';
|
|
274
|
+
/** Extra Liquid variables, on top of {{ booking }}, {{ contact }} and {{ appConfig }}. */
|
|
275
|
+
props?: Record<string, any>;
|
|
276
|
+
/** Everyone on this slot … */
|
|
277
|
+
slotId?: string;
|
|
278
|
+
/** … or everyone booked in [from, to), optionally with one resource. */
|
|
279
|
+
from?: string | Date;
|
|
280
|
+
to?: string | Date;
|
|
281
|
+
resourceType?: string;
|
|
282
|
+
resourceId?: string;
|
|
283
|
+
/** Default: held, pending, confirmed. */
|
|
284
|
+
statuses?: BookingStatus[];
|
|
285
|
+
ref?: string;
|
|
286
|
+
}
|
|
287
|
+
export interface MessageBookersResult {
|
|
288
|
+
matched: number;
|
|
289
|
+
sent: number;
|
|
290
|
+
skipped: {
|
|
291
|
+
noContact: number;
|
|
292
|
+
duplicate: number;
|
|
293
|
+
failed: number;
|
|
294
|
+
};
|
|
295
|
+
failures: Array<{
|
|
296
|
+
bookingId: string;
|
|
297
|
+
error: string;
|
|
298
|
+
}>;
|
|
299
|
+
}
|
|
269
300
|
/** Public calls on a booking made by an anonymous guest: pass the edit token returned at create. */
|
|
270
301
|
export interface BookingAccessOptions {
|
|
271
302
|
editToken?: string;
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2 | Generated: 2026-10-26T14:58:53.408Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2816,6 +2816,7 @@ interface ServerFunctionBookings {
|
|
|
2816
2816
|
complete(bookingId: string): Promise<Booking>;
|
|
2817
2817
|
noShow(bookingId: string): Promise<Booking>;
|
|
2818
2818
|
freebusy(q: FreeBusyQuery): Promise<FreeBusyResponse>;
|
|
2819
|
+
message(input: MessageBookersInput): Promise<MessageBookersResult>;
|
|
2819
2820
|
slots: {
|
|
2820
2821
|
create(input: CreateSlotInput): Promise<BookingSlot>;
|
|
2821
2822
|
createMany(inputs: CreateSlotInput[], opts?: { seriesId?: string }): Promise<BookingSlot[]>;
|
|
@@ -5366,6 +5367,32 @@ interface FreeBusyResponse {
|
|
|
5366
5367
|
}
|
|
5367
5368
|
```
|
|
5368
5369
|
|
|
5370
|
+
**MessageBookersInput** (interface)
|
|
5371
|
+
```typescript
|
|
5372
|
+
interface MessageBookersInput {
|
|
5373
|
+
templateId: string
|
|
5374
|
+
channel?: 'email' | 'sms' | 'whatsapp' | 'push' | 'preferred'
|
|
5375
|
+
props?: Record<string, any>
|
|
5376
|
+
slotId?: string
|
|
5377
|
+
from?: string | Date
|
|
5378
|
+
to?: string | Date
|
|
5379
|
+
resourceType?: string
|
|
5380
|
+
resourceId?: string
|
|
5381
|
+
statuses?: BookingStatus[]
|
|
5382
|
+
ref?: string
|
|
5383
|
+
}
|
|
5384
|
+
```
|
|
5385
|
+
|
|
5386
|
+
**MessageBookersResult** (interface)
|
|
5387
|
+
```typescript
|
|
5388
|
+
interface MessageBookersResult {
|
|
5389
|
+
matched: number
|
|
5390
|
+
sent: number
|
|
5391
|
+
skipped: { noContact: number; duplicate: number; failed: number }
|
|
5392
|
+
failures: Array<{ bookingId: string; error: string }>
|
|
5393
|
+
}
|
|
5394
|
+
```
|
|
5395
|
+
|
|
5369
5396
|
**BookingAccessOptions** (interface)
|
|
5370
5397
|
```typescript
|
|
5371
5398
|
interface BookingAccessOptions {
|
|
@@ -11902,6 +11929,9 @@ confirmed → no_show (admin).
|
|
|
11902
11929
|
**freebusy**(collectionId: string, appId: string, q: FreeBusyQuery, admin: boolean = false) → `Promise<FreeBusyResponse>`
|
|
11903
11930
|
Busy intervals per resource (bookings, blocks, slots' own blocks) — never booking details. Public only when the app sets `publicRead.bookings.freebusy: true`. GET /bookings/freebusy
|
|
11904
11931
|
|
|
11932
|
+
**message**(collectionId: string, appId: string, input: MessageBookersInput) → `Promise<MessageBookersResult>`
|
|
11933
|
+
Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date range (optionally with one person / room), each send hydrated with their booking ({{ booking }}). No segment to make. POST /bookings/message
|
|
11934
|
+
|
|
11905
11935
|
### bookings.blocks
|
|
11906
11936
|
|
|
11907
11937
|
**create**(collectionId: string, appId: string, input: CreateBlockInput) → `Promise<BookingAllocation>`
|
package/docs/bookings.md
CHANGED
|
@@ -324,8 +324,80 @@ Recorded after each write commits, for flows and app functions (`trigger: { type
|
|
|
324
324
|
`booking.no_show`, `booking.hold_expired`, `slot.full`, `slot.place_freed` (for waitlist promotion).
|
|
325
325
|
Context: `{ appId, status, slotIds }` (bookings) / `{ appId, remaining }` (slots).
|
|
326
326
|
|
|
327
|
-
Reminders before a booking (`booking.starts_in`) are coming; until then,
|
|
328
|
-
|
|
327
|
+
Reminders before a booking (`booking.starts_in`) are coming; until then, send a reminder template to
|
|
328
|
+
tomorrow's bookers with `bookings.message` (below) from a cron function.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Confirmations and messages
|
|
333
|
+
|
|
334
|
+
Nothing is emailed by default: your app decides what goes out, in **flows on booking events**, with
|
|
335
|
+
**templates the business can reword in Studio** (Comms → Templates). The platform does the sending,
|
|
336
|
+
the hydration and the consent.
|
|
337
|
+
|
|
338
|
+
### Flows on booking events
|
|
339
|
+
|
|
340
|
+
A site, or an app on behalf of a business, sets them up through the cloud blueprint
|
|
341
|
+
(`cloud/flows.json` in a site; `POST /admin/collection/:cId/blueprint/apply` with
|
|
342
|
+
`{ appId, blueprint: { templates, flows } }` from an app's admin screen). Additive: re-applying never
|
|
343
|
+
removes what is there, and a template edited in Studio keeps its edits unless the blueprint sends new
|
|
344
|
+
wording.
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"templates": {
|
|
349
|
+
"booking-confirmed": { "name": "You're booked", "email": { "subject": "{{ booking.serviceName }} on {{ booking.when.date }}", "body": "<p>Hi {{ contact.firstName }}, see you at {{ booking.when.time }}.</p>" }, "sms": { "body": "Booked: {{ booking.when.shortDate }} {{ booking.when.time }}" } },
|
|
350
|
+
"booking-requested": { "name": "Request received", "email": { "subject": "We got your request", "body": "<p>We'll confirm shortly.</p>" } },
|
|
351
|
+
"staff-new-booking": { "name": "You've been booked", "email": { "subject": "New appointment", "body": "<p>{{ booking.customer.name }}, {{ booking.when.dateTime }}</p>" } }
|
|
352
|
+
},
|
|
353
|
+
"flows": [
|
|
354
|
+
{ "when": { "event": "booking.confirmed" }, "do": [{ "send": "booking-confirmed", "to": "contact" }, { "send": "staff-new-booking", "to": "resource" }, { "send": "team-new-booking", "to": "team" }] },
|
|
355
|
+
{ "when": { "event": "booking.created", "status": "pending" }, "do": [{ "send": "booking-requested", "to": "contact" }] }
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
- `when.event`: `booking.created` / `confirmed` / `cancelled` / `moved` / `completed` / `no_show` /
|
|
361
|
+
`hold_expired`, `slot.full` / `slot.place_freed`; `when.status` narrows to the booking's status at the
|
|
362
|
+
time (`"pending"` for "request received").
|
|
363
|
+
- `to`: `contact` — the booker, by their preferred channel (`channel` to force one; a template needs an
|
|
364
|
+
`sms` part to go by text); `team` — the collection's team addresses; `resource` — the booked
|
|
365
|
+
person or room: a resource that is a contact, or a record with `data.email` / `data.contactId`.
|
|
366
|
+
A **public** resource record (a staff profile the booking page lists) keeps its address on a
|
|
367
|
+
**private child record** instead (`visibility: 'admin'`, `parentType: 'record'`, `parentId` = the
|
|
368
|
+
resource) — the platform looks there too, and the address never ships with the public listing.
|
|
369
|
+
- Who gets what, in templates: `{{ booking }}` is the booking **ready-formatted**: its fields and
|
|
370
|
+
`data`, plus `serviceName`, `staffName`, `customer`, `durationMinutes`, `when` (`date`,
|
|
371
|
+
`shortDate`, `time`, `endTime`, `dateTime`, `timezone` — in the business's timezone) and, when the
|
|
372
|
+
recipient's timezone is known and differs, `whenFor` (the same in theirs); `editToken` only when the
|
|
373
|
+
recipient is the booker (a guest's manage link). `{{ contact }}` is the recipient; `{{ appConfig }}`
|
|
374
|
+
the app's public config (a booking page's address, the business name) for sends to the contact.
|
|
375
|
+
|
|
376
|
+
### Who is reachable
|
|
377
|
+
|
|
378
|
+
A **guest** who books with an email or phone gets a **contact** (found by that identity or made,
|
|
379
|
+
filling only empty fields: a form's details never overwrite a known contact), and so does a customer
|
|
380
|
+
**staff book in by hand** (`customer: { name, email, phone }` on the body or in `data`). Admin
|
|
381
|
+
creates may pass `contactId` instead. A booking with no contact gets no messages: show the booker why
|
|
382
|
+
an email is worth giving.
|
|
383
|
+
|
|
384
|
+
### Messaging everyone booked — `bookings.message`
|
|
385
|
+
|
|
386
|
+
No list to build, no segment to save: send a template to everyone on a slot, or everyone booked in a
|
|
387
|
+
window (optionally with one resource), each hydrated with **their own** booking.
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
// Everyone on tomorrow's 10:00 class
|
|
391
|
+
await SL.bookings.message(collectionId, appId, { templateId, slotId })
|
|
392
|
+
// Everyone booked with Sam next week, by email even if they prefer texts
|
|
393
|
+
await SL.bookings.message(collectionId, appId, { templateId, channel: 'email', from, to, resourceType: 'record', resourceId: samId })
|
|
394
|
+
// → { matched, sent, skipped: { noContact, duplicate, failed }, failures: [{ bookingId, reason }] }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Statuses default to held, pending and confirmed; at most 500 recipients per call (narrow the window
|
|
398
|
+
for more). One message per contact on a slot, one per booking in a window. Also
|
|
399
|
+
`ctx.sl.bookings.message(input)` in a server function (`sl:bookings:write`) — a cron function that
|
|
400
|
+
messages tomorrow's bookers is a reminder system in five lines.
|
|
329
401
|
|
|
330
402
|
---
|
|
331
403
|
|
|
@@ -343,6 +415,7 @@ All take `(collectionId, appId, …)`; `admin` is the last parameter where both
|
|
|
343
415
|
| `bookings.confirm / cancel / move(…, admin?, { editToken }?)` | ✓ | per `ownerActions.bookings` |
|
|
344
416
|
| `bookings.extendHold / complete / noShow` | ✓ | — |
|
|
345
417
|
| `bookings.freebusy(q, admin?)` | ✓ | if `publicRead.bookings.freebusy` |
|
|
418
|
+
| `bookings.message(input)` | ✓ | — |
|
|
346
419
|
| `bookings.slots.create / createMany / update / remove` | ✓ | — |
|
|
347
420
|
| `bookings.slots.list / get(…, admin?)` | ✓ | open, public slots |
|
|
348
421
|
| `bookings.blocks.create / list / remove` | ✓ | — |
|
package/docs/server-functions.md
CHANGED
|
@@ -517,7 +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
|
+
| 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` / `message` (everyone booked, hydrated) / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
|
|
521
521
|
|
|
522
522
|
Notes:
|
|
523
523
|
- **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
|
|
@@ -636,6 +636,18 @@ try {
|
|
|
636
636
|
}
|
|
637
637
|
```
|
|
638
638
|
|
|
639
|
+
`ctx.sl.bookings.message({ templateId, slotId | from, to, resourceType?, resourceId?, channel? })` sends a
|
|
640
|
+
template to everyone booked (each hydrated with their own booking) — the five-line reminder cron:
|
|
641
|
+
|
|
642
|
+
```ts
|
|
643
|
+
// manifest: { name: 'remind', trigger: { type: 'cron', schedule: '0 17 * * *' }, capabilities: ['sl:bookings:write'] }
|
|
644
|
+
export async function remind(ctx) {
|
|
645
|
+
const from = new Date(); from.setDate(from.getDate() + 1); from.setHours(0, 0, 0, 0)
|
|
646
|
+
const to = new Date(from); to.setDate(to.getDate() + 1)
|
|
647
|
+
return ctx.sl.bookings.message({ templateId: 'bp.' + ctx.collectionId + '.reminder', from, to })
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
639
651
|
The local harness runs bookings in memory with the same rules (capacity, per-person limits, no
|
|
640
652
|
overlaps, idempotency, holds): `ctx.memory.bookings`.
|
|
641
653
|
|
package/openapi.yaml
CHANGED
|
@@ -10291,6 +10291,32 @@ paths:
|
|
|
10291
10291
|
description: Unauthorized
|
|
10292
10292
|
404:
|
|
10293
10293
|
description: Not found
|
|
10294
|
+
/message:
|
|
10295
|
+
post:
|
|
10296
|
+
tags:
|
|
10297
|
+
- bookings
|
|
10298
|
+
summary: "Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date range (optionally with one person / room), each send hydrated with their booking ({{ booking }})."
|
|
10299
|
+
operationId: bookings_message
|
|
10300
|
+
security: []
|
|
10301
|
+
responses:
|
|
10302
|
+
200:
|
|
10303
|
+
description: Success
|
|
10304
|
+
content:
|
|
10305
|
+
application/json:
|
|
10306
|
+
schema:
|
|
10307
|
+
$ref: "#/components/schemas/MessageBookersResult"
|
|
10308
|
+
400:
|
|
10309
|
+
description: Bad request
|
|
10310
|
+
401:
|
|
10311
|
+
description: Unauthorized
|
|
10312
|
+
404:
|
|
10313
|
+
description: Not found
|
|
10314
|
+
requestBody:
|
|
10315
|
+
required: true
|
|
10316
|
+
content:
|
|
10317
|
+
application/json:
|
|
10318
|
+
schema:
|
|
10319
|
+
$ref: "#/components/schemas/MessageBookersInput"
|
|
10294
10320
|
/platform/location:
|
|
10295
10321
|
post:
|
|
10296
10322
|
tags:
|
|
@@ -24706,6 +24732,62 @@ components:
|
|
|
24706
24732
|
- from
|
|
24707
24733
|
- to
|
|
24708
24734
|
- resources
|
|
24735
|
+
MessageBookersInput:
|
|
24736
|
+
type: object
|
|
24737
|
+
properties:
|
|
24738
|
+
templateId:
|
|
24739
|
+
type: string
|
|
24740
|
+
channel:
|
|
24741
|
+
type: string
|
|
24742
|
+
enum:
|
|
24743
|
+
- email
|
|
24744
|
+
- sms
|
|
24745
|
+
- whatsapp
|
|
24746
|
+
- push
|
|
24747
|
+
- preferred
|
|
24748
|
+
props:
|
|
24749
|
+
type: object
|
|
24750
|
+
additionalProperties: true
|
|
24751
|
+
slotId:
|
|
24752
|
+
type: string
|
|
24753
|
+
from:
|
|
24754
|
+
type: object
|
|
24755
|
+
additionalProperties: true
|
|
24756
|
+
to:
|
|
24757
|
+
type: object
|
|
24758
|
+
additionalProperties: true
|
|
24759
|
+
resourceType:
|
|
24760
|
+
type: string
|
|
24761
|
+
resourceId:
|
|
24762
|
+
type: string
|
|
24763
|
+
statuses:
|
|
24764
|
+
type: array
|
|
24765
|
+
items:
|
|
24766
|
+
$ref: "#/components/schemas/BookingStatus"
|
|
24767
|
+
ref:
|
|
24768
|
+
type: string
|
|
24769
|
+
required:
|
|
24770
|
+
- templateId
|
|
24771
|
+
MessageBookersResult:
|
|
24772
|
+
type: object
|
|
24773
|
+
properties:
|
|
24774
|
+
matched:
|
|
24775
|
+
type: number
|
|
24776
|
+
sent:
|
|
24777
|
+
type: number
|
|
24778
|
+
skipped:
|
|
24779
|
+
type: object
|
|
24780
|
+
additionalProperties: true
|
|
24781
|
+
failures:
|
|
24782
|
+
type: array
|
|
24783
|
+
items:
|
|
24784
|
+
type: object
|
|
24785
|
+
additionalProperties: true
|
|
24786
|
+
required:
|
|
24787
|
+
- matched
|
|
24788
|
+
- sent
|
|
24789
|
+
- skipped
|
|
24790
|
+
- failures
|
|
24709
24791
|
BookingAccessOptions:
|
|
24710
24792
|
type: object
|
|
24711
24793
|
properties:
|