@peektravel/app-utilities 0.1.5 → 0.2.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/README.md +34 -0
- package/dist/index.cjs +103 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -6
- package/dist/index.d.ts +98 -6
- package/dist/index.js +102 -7
- package/dist/index.js.map +1 -1
- package/docs/webhooks.md +141 -0
- package/llms.txt +18 -0
- package/package.json +3 -2
package/docs/webhooks.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Webhooks
|
|
2
|
+
|
|
3
|
+
A guide for wiring a receiver app up to Peek "backoffice" webhooks using
|
|
4
|
+
`@peektravel/app-utilities`. Two webhook types are supported today — **booking**
|
|
5
|
+
and **waiver** — and each has a parser that turns the delivered payload into a
|
|
6
|
+
clean data model. They differ in one important way: a booking webhook is
|
|
7
|
+
configured with a GraphQL query (so the package documents the exact query to
|
|
8
|
+
register), while a waiver webhook has a fixed payload (so there's nothing to
|
|
9
|
+
register beyond subscribing to the event).
|
|
10
|
+
|
|
11
|
+
| Webhook | Register | Parse the delivery |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| Booking | paste a GraphQL query into the external config (below) | `parseBookingWebhook(body)` → `Booking` |
|
|
14
|
+
| Waiver | subscribe to the event — **no query needed** | `parseWaiverWebhook(body)` → `Waiver` |
|
|
15
|
+
|
|
16
|
+
Both parsers are pure transforms — no auth, no network, construct nothing.
|
|
17
|
+
|
|
18
|
+
# Booking webhooks
|
|
19
|
+
|
|
20
|
+
## The problem this solves
|
|
21
|
+
|
|
22
|
+
A Peek booking webhook is unusual: its payload shape is **not fixed**. The
|
|
23
|
+
webhook is configured with a GraphQL field selection (`output_fields_gql_query`);
|
|
24
|
+
each time it fires, the gateway runs that selection against the booking and POSTs
|
|
25
|
+
the result to your receiver. So two things must agree — the registered selection
|
|
26
|
+
and the code that parses the payload.
|
|
27
|
+
|
|
28
|
+
That registration is done **once, in an external system** (the Peek App Store
|
|
29
|
+
`broadcast_to_url` config / your app's `app.json`), not from your application
|
|
30
|
+
code. So the two halves split cleanly:
|
|
31
|
+
|
|
32
|
+
| Half | Where it lives | This package provides |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| Register the query | external config, set once | the canonical query string to paste (below) |
|
|
35
|
+
| Parse the delivery | your receiver's code | `parseBookingWebhook(body)` → a clean `Booking` |
|
|
36
|
+
|
|
37
|
+
The query string below and the parser are derived from the **same** field
|
|
38
|
+
selection this package uses for booking reads, and a test pins the string, so the
|
|
39
|
+
two can't silently drift.
|
|
40
|
+
|
|
41
|
+
## Step 1 — register this query (external, one-time)
|
|
42
|
+
|
|
43
|
+
Paste the following into the webhook's `output_fields_gql_query` field (with
|
|
44
|
+
`output_format: "gql"`). It is the **maximal** selection — it includes guests and
|
|
45
|
+
the full price breakdown, so one registration captures everything; the parser
|
|
46
|
+
auto-detects whatever a given payload carries.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
{ displayId id primaryGuest { name email phone optinMarketing optinSms isGdpr postalCode } activitySnapshot { type name id } ticketQuantities { quantity resourceOptionSnapshot { name id } } reservationStatus checkinStatus returnStatus fulfillmentStatusOverride { status } timeSnapshot { id legacyId } purchasedAt purchasedAtUtc startsAt startsAtUtc endsAt endsAtUtc availabilityTimeId bookingPortalUrl operatorNotes value { convenienceFee { amount formatted } deposit { amount formatted } discount { amount formatted } discountedPrice { amount formatted } fees { amount formatted } flatPartnerFee { amount formatted } price { amount formatted } retailPrice { amount formatted } taxes { amount formatted } tips { amount formatted } total { formatted amount } } balance { total { amount formatted } } tips { price { amount formatted } } order { displayId id promoCodes { code } channelSnapshot { id name agent { name } } initialQuote { source { actor { app } } } } questionAnswers { answer questionText questionLocationSnapshot { latitude longitude } } tickets { questionAnswers { answer questionText } } resourcePoolAssignments { quantity resourcePool { name shortName resources { name } } resourceAssignments { resource { id name } } } bookingGuests { id name country dateOfBirth email isGdpr isParticipant optinSms optinMarketing phone postalCode fieldResponses { id text fieldLocation { field { name } } } } primaryGuest { id name country dateOfBirth email isGdpr isParticipant optinSms optinMarketing phone postalCode fieldResponses { id text fieldLocation { field { name } } } } }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> This package owns this string internally and snapshots it in
|
|
53
|
+
> `test/bookings/booking-webhook.test.ts`. If the booking fields ever change, the
|
|
54
|
+
> test fails here first — update the test snapshot, this doc, and the external
|
|
55
|
+
> config together.
|
|
56
|
+
|
|
57
|
+
## Step 2 — parse the delivered webhook into a `Booking`
|
|
58
|
+
|
|
59
|
+
This is the only part that lives in your code. Hand the raw request body to
|
|
60
|
+
`parseBookingWebhook`; it returns the same clean
|
|
61
|
+
[`Booking`](../README.md#resources) model the read services return.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { parseBookingWebhook, type Booking } from "@peektravel/app-utilities";
|
|
65
|
+
|
|
66
|
+
app.post("/booking-webhook", (req, res) => {
|
|
67
|
+
const booking: Booking = parseBookingWebhook(req.body);
|
|
68
|
+
// booking.bookingId, booking.customerName, booking.startsAt, booking.isCanceled, …
|
|
69
|
+
res.sendStatus(200);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`parseBookingWebhook`:
|
|
74
|
+
|
|
75
|
+
- **Needs no auth, network, or `PeekAccessService`** — it is a pure transform.
|
|
76
|
+
Construct nothing; just call it. (This is why it's a standalone function, not a
|
|
77
|
+
method on the access service, and why the receiver needs no gateway
|
|
78
|
+
credentials to parse.)
|
|
79
|
+
- **Tolerates the delivery envelope.** It accepts the `{ booking: … }` wrapper
|
|
80
|
+
the webhook sends, a bare booking node, or a JSON string body.
|
|
81
|
+
- **Auto-detects** guests and the price breakdown from the payload, so there is
|
|
82
|
+
nothing to keep in sync with the registered query — `booking.guests` and
|
|
83
|
+
`booking.taxes`/`booking.fees`/… populate when present.
|
|
84
|
+
- **Never throws on malformed input** — a missing/garbled body yields a `Booking`
|
|
85
|
+
with empty fields rather than an exception, so a bad delivery can't crash your
|
|
86
|
+
handler.
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- The webhook fires on booking **create** and **update**; both deliver the same
|
|
91
|
+
booking payload, and `parseBookingWebhook` handles them identically. It does
|
|
92
|
+
not currently surface which event fired.
|
|
93
|
+
- Authenticating the delivery (verifying it really came from Peek) is the
|
|
94
|
+
receiver's responsibility and out of scope for this parser.
|
|
95
|
+
|
|
96
|
+
# Waiver webhooks
|
|
97
|
+
|
|
98
|
+
A waiver webhook fires when a participant signs a liability agreement. Unlike the
|
|
99
|
+
booking webhook, its payload is **fixed** — Peek delivers a predefined shape (the
|
|
100
|
+
`waiver_webhook_data` output format), so there is **no GraphQL query to
|
|
101
|
+
register**.
|
|
102
|
+
|
|
103
|
+
## Step 1 — subscribe to the event (external, one-time)
|
|
104
|
+
|
|
105
|
+
Register the webhook for the `agreement_signature_created` event in the external
|
|
106
|
+
config. Leave `output_fields_gql_query` null; the `output_format` is
|
|
107
|
+
`waiver_webhook_data`. There is nothing query-shaped to paste.
|
|
108
|
+
|
|
109
|
+
## Step 2 — parse the delivered webhook into a `Waiver`
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { parseWaiverWebhook, type Waiver } from "@peektravel/app-utilities";
|
|
113
|
+
|
|
114
|
+
app.post("/waiver-webhook", (req, res) => {
|
|
115
|
+
const waiver: Waiver = parseWaiverWebhook(req.body);
|
|
116
|
+
// waiver.bookingId, waiver.templateId, waiver.fileUrl, waiver.signedAt,
|
|
117
|
+
// waiver.guestName, waiver.isSignedByGuardian, …
|
|
118
|
+
res.sendStatus(200);
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`parseWaiverWebhook` mirrors the booking parser: a pure transform (no
|
|
123
|
+
auth/network/`PeekAccessService`), it tolerates the `{ waiver: … }` envelope / a
|
|
124
|
+
bare node / a JSON string, maps the raw `snake_case` payload to the clean
|
|
125
|
+
camelCase [`Waiver`](#waiver-webhooks) model, and never throws on malformed input
|
|
126
|
+
(missing fields become `""` / `null` / `false`).
|
|
127
|
+
|
|
128
|
+
The resulting `Waiver` is flat:
|
|
129
|
+
|
|
130
|
+
| Field | Type | From |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `templateId` | `string` | `agreement_template_id` |
|
|
133
|
+
| `bookingId` | `string` | `booking_id` |
|
|
134
|
+
| `fileUrl` | `string` | `file_url` |
|
|
135
|
+
| `signedAt` | `string` | `signed_at` (ISO) |
|
|
136
|
+
| `isSignedByGuardian` | `boolean` | `signed_by_guardian` |
|
|
137
|
+
| `guestName` | `string \| null` | `waiver_data.participant_name` |
|
|
138
|
+
| `isOptinMarketing` | `boolean` | `waiver_data.participant_optin_marketing` |
|
|
139
|
+
| `isOptinSms` | `boolean` | `waiver_data.participant_optin_sms` |
|
|
140
|
+
|
|
141
|
+
Authenticating the delivery is the receiver's responsibility, as with bookings.
|
package/llms.txt
CHANGED
|
@@ -68,6 +68,24 @@ const peek = new PeekAccessService({
|
|
|
68
68
|
importable; branch with `instanceof`.
|
|
69
69
|
- Plain `Error` for validation/precondition failures thrown before any request.
|
|
70
70
|
|
|
71
|
+
## Webhooks (booking + waiver)
|
|
72
|
+
|
|
73
|
+
Two webhook parsers, each a pure transform — needs no auth/network/
|
|
74
|
+
`PeekAccessService`; accepts the `{<key>:…}` envelope, a bare node, or a JSON
|
|
75
|
+
string; never throws on malformed input. Registration is done **once in an
|
|
76
|
+
external system** (App Store config), not from code.
|
|
77
|
+
|
|
78
|
+
- `parseBookingWebhook(body): Booking` — auto-detects guests/breakdown. Booking
|
|
79
|
+
payload shape is defined by a GraphQL selection registered externally; paste
|
|
80
|
+
the canonical maximal selection into `output_fields_gql_query`. That string is
|
|
81
|
+
documented in `docs/webhooks.md` and pinned by a drift-guard test — it is NOT a
|
|
82
|
+
runtime export.
|
|
83
|
+
- `parseWaiverWebhook(body): Waiver` — maps the fixed `snake_case`
|
|
84
|
+
`waiver_webhook_data` payload to the clean `Waiver` model. **No query to
|
|
85
|
+
register** — just subscribe to the `agreement_signature_created` event.
|
|
86
|
+
|
|
87
|
+
**Full guide + booking query-to-register: `docs/webhooks.md` (shipped).**
|
|
88
|
+
|
|
71
89
|
## UI components (`@peektravel/app-utilities/ui`)
|
|
72
90
|
|
|
73
91
|
Browser-only, framework-agnostic Web Components (Custom Elements, light DOM,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peektravel/app-utilities",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "GraphQL JS mapping utilities extracted from the Peek Pro Autopilot connector.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
41
|
"llms.txt",
|
|
42
|
-
"docs/ui.md"
|
|
42
|
+
"docs/ui.md",
|
|
43
|
+
"docs/webhooks.md"
|
|
43
44
|
],
|
|
44
45
|
"sideEffects": [
|
|
45
46
|
"**/ui/**",
|