@peektravel/app-utilities 0.1.5 → 0.1.6

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 CHANGED
@@ -196,6 +196,39 @@ both `import` and `require` consumers (including the Node 22 / CommonJS Firebase
196
196
  Functions runtime) resolve correctly. Its only runtime dependency is
197
197
  `jsonwebtoken`.
198
198
 
199
+ ## Webhooks
200
+
201
+ Receiver apps can consume Peek **booking** and **waiver** webhooks without
202
+ hand-writing a payload parser. Each has a pure parser (construct nothing — no
203
+ auth/network) that returns a clean model:
204
+
205
+ ```ts
206
+ import {
207
+ parseBookingWebhook,
208
+ parseWaiverWebhook,
209
+ type Booking,
210
+ type Waiver,
211
+ } from "@peektravel/app-utilities";
212
+
213
+ app.post("/booking-webhook", (req, res) => {
214
+ const booking: Booking = parseBookingWebhook(req.body);
215
+ res.sendStatus(200);
216
+ });
217
+
218
+ app.post("/waiver-webhook", (req, res) => {
219
+ const waiver: Waiver = parseWaiverWebhook(req.body);
220
+ res.sendStatus(200);
221
+ });
222
+ ```
223
+
224
+ Both tolerate the delivery envelope / a bare node / a JSON string and never throw
225
+ on malformed input. They differ on registration: a **booking** webhook's payload
226
+ shape is set by a GraphQL query configured **once in an external system** (the
227
+ App Store `broadcast_to_url` config) — this package documents and drift-guards
228
+ the exact query to paste there — whereas a **waiver** webhook has a fixed payload,
229
+ so you just subscribe to its event with no query. **The query to register and the
230
+ full guide: [`docs/webhooks.md`](docs/webhooks.md) (shipped).**
231
+
199
232
  ## UI components (`/ui`)
200
233
 
201
234
  The package also ships framework-agnostic **Web Components** ported from the Peek
@@ -326,6 +359,7 @@ src/ui/ Web Components + Odyssey CSS (barrel: src/ui/index.ts
326
359
  test/ vitest unit tests (test/ui/* run under happy-dom)
327
360
  examples/ui-gallery.html component gallery (npm run sample)
328
361
  dist/ build output (generated, git-ignored)
362
+ docs/webhooks.md booking-webhook consumer guide (shipped)
329
363
  docs/internal/ maintainer docs (ARCHITECTURE.md — not shipped)
330
364
  llms.txt AI-agent quickstart (shipped in the package)
331
365
  ```
package/dist/index.cjs CHANGED
@@ -3078,6 +3078,91 @@ function requireNonEmpty(value, name) {
3078
3078
  }
3079
3079
  }
3080
3080
 
3081
+ // src/internal/bookings/booking-webhook.ts
3082
+ var PAYLOAD_BOOKING_KEY = "booking";
3083
+ var VALUE_OPEN_TOKEN = "value {";
3084
+ var PRICE_BREAKDOWN_KEYS = [
3085
+ "convenienceFee",
3086
+ "deposit",
3087
+ "discount",
3088
+ "discountedPrice",
3089
+ "fees",
3090
+ "flatPartnerFee",
3091
+ "price",
3092
+ "retailPrice",
3093
+ "taxes"
3094
+ ];
3095
+ buildMaximalWebhookQuery();
3096
+ function buildMaximalWebhookQuery() {
3097
+ const fields = bookingQueryFields.replace(
3098
+ VALUE_OPEN_TOKEN,
3099
+ `${VALUE_OPEN_TOKEN} ${PRICE_BREAKDOWN_FIELDS}`
3100
+ );
3101
+ return `{ ${fields} ${bookingGuestsFields} }`.replace(/\s+/g, " ").trim();
3102
+ }
3103
+ function parseBookingWebhook(payload) {
3104
+ const node = extractBookingNode(payload);
3105
+ return fromBookingNode(node, hasGuests(node), hasPriceBreakdown(node));
3106
+ }
3107
+ function extractBookingNode(payload) {
3108
+ let body = payload;
3109
+ if (typeof body === "string") {
3110
+ try {
3111
+ body = JSON.parse(body);
3112
+ } catch {
3113
+ return {};
3114
+ }
3115
+ }
3116
+ if (!body || typeof body !== "object") return {};
3117
+ const record = body;
3118
+ const inner = record[PAYLOAD_BOOKING_KEY];
3119
+ if (inner && typeof inner === "object") return inner;
3120
+ return record;
3121
+ }
3122
+ function hasGuests(node) {
3123
+ return Array.isArray(node.bookingGuests);
3124
+ }
3125
+ function hasPriceBreakdown(node) {
3126
+ const value = node.value;
3127
+ if (!value) return false;
3128
+ return PRICE_BREAKDOWN_KEYS.some((key) => key in value);
3129
+ }
3130
+
3131
+ // src/internal/waivers/waiver-webhook.ts
3132
+ var PAYLOAD_WAIVER_KEY = "waiver";
3133
+ function parseWaiverWebhook(payload) {
3134
+ return fromWaiverNode(extractWaiverNode(payload));
3135
+ }
3136
+ function fromWaiverNode(node) {
3137
+ const data = node ?? {};
3138
+ const waiverData = data.waiver_data ?? {};
3139
+ return {
3140
+ templateId: data.agreement_template_id || "",
3141
+ bookingId: data.booking_id || "",
3142
+ fileUrl: data.file_url || "",
3143
+ signedAt: data.signed_at || "",
3144
+ isSignedByGuardian: Boolean(data.signed_by_guardian),
3145
+ guestName: waiverData.participant_name || null,
3146
+ isOptinMarketing: Boolean(waiverData.participant_optin_marketing),
3147
+ isOptinSms: Boolean(waiverData.participant_optin_sms)
3148
+ };
3149
+ }
3150
+ function extractWaiverNode(payload) {
3151
+ let body = payload;
3152
+ if (typeof body === "string") {
3153
+ try {
3154
+ body = JSON.parse(body);
3155
+ } catch {
3156
+ return {};
3157
+ }
3158
+ }
3159
+ if (!body || typeof body !== "object") return {};
3160
+ const record = body;
3161
+ const inner = record[PAYLOAD_WAIVER_KEY];
3162
+ if (inner && typeof inner === "object") return inner;
3163
+ return record;
3164
+ }
3165
+
3081
3166
  exports.ADD_ON_PRODUCT_TYPE = ADD_ON_PRODUCT_TYPE;
3082
3167
  exports.AccountUserService = AccountUserService;
3083
3168
  exports.AdminAccountRequiredError = AdminAccountRequiredError;
@@ -3095,5 +3180,7 @@ exports.ResourcePoolService = ResourcePoolService;
3095
3180
  exports.ReviewService = ReviewService;
3096
3181
  exports.TimeslotService = TimeslotService;
3097
3182
  exports.noopLogger = noopLogger;
3183
+ exports.parseBookingWebhook = parseBookingWebhook;
3184
+ exports.parseWaiverWebhook = parseWaiverWebhook;
3098
3185
  //# sourceMappingURL=index.cjs.map
3099
3186
  //# sourceMappingURL=index.cjs.map