@@ -84,10 +84,123 @@ add-on pages for you.
8484
8585### Errors
8686
87- - ` AdminAccountRequiredError ` — gateway returned HTTP 418.
88- - ` RateLimitError ` — HTTP 429 after retries were exhausted.
89- - ` PeekGraphQLError ` — the response contained a GraphQL ` errors ` array
90- (preserved on ` .graphqlErrors ` ).
87+ Two kinds of failures surface as exceptions:
88+
89+ ** Typed gateway errors** (importable, branch on the class):
90+
91+ - ` AdminAccountRequiredError ` — gateway returned HTTP 418 (install lacks admin
92+ rights). Carries ` .statusCode === 418 ` .
93+ - ` RateLimitError ` — HTTP 429 after the configured ` retryDelaysMs ` backoff was
94+ exhausted. Carries ` .statusCode === 429 ` .
95+ - ` PeekGraphQLError ` — the response contained a GraphQL ` errors ` array, preserved
96+ on ` .graphqlErrors ` .
97+
98+ ** Plain ` Error ` validation/precondition failures** thrown by the service layer
99+ * before* any network call — e.g. an empty config field, a ` bookingId ` that
100+ doesn't resolve to a ` b_… ` id, a non-positive-integer ` quantity ` , a malformed
101+ currency, or a "booking not found". Branch on ` .message ` only as a last resort;
102+ prefer guarding inputs to the documented formats below.
103+
104+ ``` ts
105+ import {
106+ PeekAccessService ,
107+ RateLimitError ,
108+ AdminAccountRequiredError ,
109+ PeekGraphQLError ,
110+ } from ' @peek-travel/app-utilities' ;
111+
112+ try {
113+ await peek .getBookingService ().makePayment ({ /* … */ });
114+ } catch (err ) {
115+ if (err instanceof RateLimitError ) {
116+ // back off and retry later
117+ } else if (err instanceof AdminAccountRequiredError ) {
118+ // this install can't perform admin-only operations
119+ } else if (err instanceof PeekGraphQLError ) {
120+ console .error (err .graphqlErrors ); // raw gateway errors
121+ } else {
122+ throw err ; // validation / precondition failure
123+ }
124+ }
125+ ```
126+
127+ ## Conventions & input formats
128+
129+ These rules are enforced in the service layer (a violation throws a plain
130+ ` Error ` before any request):
131+
132+ - ** Booking ids** are normalized internally — lowercased with ` - ` → ` _ ` — so
133+ ` B-ABC123 ` and ` b_abc123 ` are equivalent. Payment/refund operations require an
134+ id that resolves to the ` b_… ` form.
135+ - ** Quantities** (add-ons, etc.) are ** positive-integer strings** : ` "1" ` , ` "2" ` .
136+ - ** Currency** is a 3-letter uppercase ISO code: ` "USD" ` , ` "EUR" ` .
137+ - ** Amounts** are numeric strings: ` "25.00" ` .
138+ - ** Payment source ids** are ` ps_… ` , or one of ` cash/cash ` , ` custom/other ` ,
139+ ` custom/voucher ` . ** Payment ids** (refunds) are ` pmt_… ` .
140+ - ** Idempotency keys** are required on ` makePayment ` , ` refund ` , and any
141+ ` create({ markAsPaid: true }) ` ; pass a stable UUID (` crypto.randomUUID() ` ).
142+ - ** ` create() ` takes pre-resolved ids only** — no free-text matching. Resolve
143+ ` activityId ` + ticket ` resourceOptionId ` s from ` getProductService() ` and
144+ ` availabilityTimeId ` from ` getAvailabilityService() ` .
145+ - ** Add-on option ids** are ticket ids on products whose ` type ` is
146+ ` ADD_ON_PRODUCT_TYPE ` .
147+
148+ ## Recipes
149+
150+ ** Find an activity and its add-ons**
151+
152+ ``` ts
153+ import { ADD_ON_PRODUCT_TYPE , type Product } from ' @peek-travel/app-utilities' ;
154+
155+ const products: Product [] = await peek .getProductService ().getAllProducts ();
156+ const activities = products .filter ((p ) => p .type !== ADD_ON_PRODUCT_TYPE );
157+ const addons = products .filter ((p ) => p .type === ADD_ON_PRODUCT_TYPE );
158+ ```
159+
160+ ** Create a paid booking end-to-end**
161+
162+ ``` ts
163+ import { randomUUID } from ' node:crypto' ;
164+
165+ const products = await peek .getProductService ().getAllProducts ();
166+ const activity = products .find ((p ) => p .name === ' Sunset Kayak Tour' )! ;
167+
168+ const [slot] = await peek .getAvailabilityService ().getAvailabilityTimes ({
169+ activityId: activity .productId ,
170+ date: ' 2026-06-20' ,
171+ resourceOptionQuantities: [{ resourceOptionId: activity .tickets [0 ]! .id , quantity: 2 }],
172+ });
173+
174+ const created = await peek .getBookingService ().create ({
175+ activityId: activity .productId ,
176+ availabilityTimeId: slot .availabilityTimeId ,
177+ tickets: [{ resourceOptionId: activity .tickets [0 ]! .id , quantity: 2 }],
178+ guest: { name: ' Sam Rivera' , email: ' sam@example.com' },
179+ markAsPaid: true ,
180+ idempotencyKey: randomUUID (),
181+ });
182+ console .log (created .bookingId , created .balanceFormatted );
183+ ```
184+
185+ ** Add an add-on to an existing booking**
186+
187+ ``` ts
188+ const { updatedBookingAddons } = await peek
189+ .getBookingService ()
190+ .addAddon (' b_abc123' , { addonOptionId: ' io_helmet' , quantity: ' 2' });
191+ ```
192+
193+ ** Look up a booking with guests and balance**
194+
195+ ``` ts
196+ const booking = await peek .getBookingService ().getById (' b_abc123' , {
197+ includeGuests: true ,
198+ includePriceBreakdown: true ,
199+ });
200+ if (booking ) {
201+ console .log (booking .displayId , booking .outstandingBalanceDisplay );
202+ }
203+ ```
91204
92205The package ships dual ESM + CommonJS builds with bundled type declarations, so
93206both ` import ` and ` require ` consumers (including the Node 22 / CommonJS Firebase
@@ -141,7 +254,9 @@ systems. The publish workflow runs these automatically — see
141254## Project layout
142255
143256```
144- src/ source (public API barrel: src/index.ts)
145- test/ vitest unit tests
146- dist/ build output (generated, git-ignored)
257+ src/ source (public API barrel: src/index.ts)
258+ test/ vitest unit tests
259+ dist/ build output (generated, git-ignored)
260+ docs/internal/ maintainer docs (ARCHITECTURE.md — not shipped)
261+ llms.txt AI-agent quickstart (shipped in the package)
147262```
0 commit comments