@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.
@@ -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.1.5",
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/**",