Skip to content

Commit 9f5dbc4

Browse files
oskarbrueningclaude
andcommitted
docs: AI-consumable docs (TSDoc examples, README recipes, llms.txt)
Make the package self-documenting for consuming projects and their AI tools, focusing on what actually ships in the tarball: - TSDoc @example/@throws on the public surface (PeekAccessService, Booking getById/addAddon/makePayment/create, getAllProducts, assignGuide) so examples flow into the bundled .d.ts - README: typed-vs-validation errors with try/catch, a Conventions & input-formats section, and end-to-end Recipes - Add llms.txt AI quickstart and ship it via package.json "files" - Move ARCHITECTURE.md to docs/internal/ (maintainer-only, unshipped) and update its references in CLAUDE.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2d6323e commit 9f5dbc4

9 files changed

Lines changed: 326 additions & 15 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ out, and the clean data models — never raw GraphQL.
99
- If anything about a request is unclear or ambiguous, ask for clarification
1010
before starting any work. Don't guess at intent or proceed on assumptions when
1111
the goal, scope, or approach is uncertain.
12-
- Before making any changes, review `ARCHITECTURE.md`.
13-
- Once you've made all the code changes, update `ARCHITECTURE.md` to reflect
12+
- Before making any changes, review `docs/internal/ARCHITECTURE.md`.
13+
- Once you've made all the code changes, update `docs/internal/ARCHITECTURE.md` to reflect
1414
major changes (new resources, new triads, changed public surface).
1515
- Ensure test coverage remains above 95% (the Vitest gate enforces this on
1616
lines/functions/branches/statements).
@@ -19,7 +19,7 @@ out, and the clean data models — never raw GraphQL.
1919

2020
# Architecture conventions
2121

22-
Preserve the structure described in `ARCHITECTURE.md`. The load-bearing rules:
22+
Preserve the structure described in `docs/internal/ARCHITECTURE.md`. The load-bearing rules:
2323

2424
- **Three-file triad per resource** under `src/internal/<resource>/`:
2525
- `*-queries.ts` — raw GraphQL strings, matching response interfaces, and
@@ -96,4 +96,4 @@ install-script spawn — use `npm install --ignore-scripts`. If the
9696
## Once complete
9797
- Review the new code for obvious duplication; simplify with helper functions.
9898
- Run the linter, the type checker, and the unit tests (with coverage).
99-
- Update `ARCHITECTURE.md` if the public surface, resources, or build changed.
99+
- Update `docs/internal/ARCHITECTURE.md` if the public surface, resources, or build changed.

README.md

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

92205
The package ships dual ESM + CommonJS builds with bundled type declarations, so
93206
both `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
```
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ internal.
154154
mapping, pagination, and converters without real network calls.
155155
- **Publish guard:** `prepublishOnly` runs the build then `publint` and
156156
`@arethetypeswrong/cli` (`attw`) to verify the `exports` map / type resolution
157-
for both module systems. `files: ["dist"]` whitelists only the build output;
157+
for both module systems. `files: ["dist", "llms.txt"]` whitelists the build
158+
output plus the AI-agent quickstart (`README.md`, `LICENSE`, and
159+
`package.json` are always included by npm regardless); this maintainer doc
160+
under `docs/internal/` is intentionally **not** shipped.
158161
`publishConfig.access: "restricted"` marks it a private scoped package.
159162
- **Distribution:** published to **GitHub Packages** (private registry), not
160163
public npm. Releases are automated by `.github/workflows/publish.yml`, which

llms.txt

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# @peek-travel/app-utilities
2+
3+
> Dependency-light TypeScript library wrapping the Peek "backoffice" GraphQL
4+
> gateway. Callers only touch `PeekAccessService`, the per-resource services it
5+
> hands out, and clean data-model types — never raw GraphQL. Ships dual ESM+CJS
6+
> with bundled `.d.ts`. Only runtime dependency: `jsonwebtoken`.
7+
8+
The authoritative, always-in-sync contract is the bundled type declarations
9+
(`dist/index.d.ts`) — read them via go-to-definition. This file is a map; the
10+
`.d.ts` and `README.md` (also shipped) have full detail and examples.
11+
12+
## Entry point
13+
14+
Construct one `PeekAccessService` per install, then call `get<Resource>Service()`
15+
accessors (each memoized, bound to a shared authenticated transport that mints
16+
and caches a short-lived JWT).
17+
18+
```ts
19+
import { PeekAccessService } from "@peek-travel/app-utilities";
20+
21+
const peek = new PeekAccessService({
22+
installId, // JWT subject
23+
jwtSecret, // HMAC secret signing the JWT
24+
issuer, // app name (JWT issuer)
25+
appId, // gateway path segment
26+
gatewayKey, // pk-api-key header
27+
// optional: baseUrl, tokenTtlSeconds, tokenRefreshLeewaySeconds,
28+
// retryDelaysMs, logger, fetch, itemOptionsPageSize
29+
});
30+
```
31+
32+
## Resources (accessor → methods)
33+
34+
- `getProductService()` — `getAllProducts()` (flat list of activities + add-ons;
35+
add-ons have `type === ADD_ON_PRODUCT_TYPE`)
36+
- `getAccountUserService()` — `getAll()`, `getById(userId)`
37+
- `getResourcePoolService()` — `getAll(mode?)`
38+
- `getTimeslotService()` — `getForDay(productId, date, filter?)`, `getById(id)`,
39+
`setAvailability(...)`, `setNotes(...)`, `assignGuide({timeslotIds, guideIds, action})`
40+
- `getResellerService()` — `getAllChannels(agentsPerChannel?)`
41+
- `getPromoCodeService()` — `getAll()`, `create(input)`
42+
- `getDailyNoteService()` — `getToday()`, `update(note)`
43+
- `getAvailabilityService()` — `getAvailabilityTimes({activityId, date, resourceOptionQuantities})`
44+
- `getMembershipService()` — `getAll()`, `purchase(input)`
45+
- `getBookingService()` — `getById(id, opts?)`, `searchByTimeRange(input)`,
46+
`searchByTimeslot(id, opts?)`, `getGuests(id)`, `getPaymentsOnFile(id)`,
47+
`appendNote(id, note, mode?)`, `setCheckinStatus(id, checkedIn)`, `cancel(id, notes?)`,
48+
`makePayment(input)`, `refund(input)`, `createInvoiceLink(id)`,
49+
`listAddons(id)`, `addAddon(id, {addonOptionId, quantity})`,
50+
`removeAddon(id, {addonOptionId, quantity})`, `create(input)`
51+
52+
## Input conventions (enforced in the service layer; violations throw plain Error)
53+
54+
- Booking ids normalized (lowercased, `-`→`_`): `B-ABC123` == `b_abc123`.
55+
Payment/refund need an id resolving to `b_…`.
56+
- Quantities: positive-integer strings (`"1"`, `"2"`).
57+
- Currency: 3-letter uppercase ISO (`"USD"`). Amounts: numeric strings (`"25.00"`).
58+
- Payment source ids: `ps_…` or `cash/cash` | `custom/other` | `custom/voucher`.
59+
Payment ids (refunds): `pmt_…`.
60+
- Idempotency key required on `makePayment`, `refund`, `create({markAsPaid:true})`.
61+
- `create()` uses pre-resolved ids only (resolve via product + availability
62+
services); no free-text matching.
63+
64+
## Errors
65+
66+
- `AdminAccountRequiredError` (HTTP 418), `RateLimitError` (HTTP 429 after
67+
retries), `PeekGraphQLError` (`.graphqlErrors` holds the raw array) — all
68+
importable; branch with `instanceof`.
69+
- Plain `Error` for validation/precondition failures thrown before any request.
70+
71+
## Notes for consumers
72+
73+
- Install is from GitHub Packages (private). Add an `.npmrc` mapping the
74+
`@peek-travel` scope to `https://npm.pkg.github.com` with a `read:packages`
75+
token. See README "Install".
76+
- Everything not re-exported from the package root is internal — do not import
77+
from subpaths; rely on the public barrel.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"./package.json": "./package.json"
2626
},
2727
"files": [
28-
"dist"
28+
"dist",
29+
"llms.txt"
2930
],
3031
"sideEffects": false,
3132
"engines": {

src/internal/bookings/booking-service.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,21 @@ export class BookingService {
147147
this.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
148148
}
149149

150-
/** Returns a single booking by id, or null when not found. */
150+
/**
151+
* Returns a single booking by id, or null when not found. The `bookingId` is
152+
* normalized internally (lowercased, `-` → `_`), so `B-ABC123` and `b_abc123`
153+
* resolve to the same booking.
154+
*
155+
* @example
156+
* ```ts
157+
* const bookings = peek.getBookingService();
158+
* const booking = await bookings.getById("b_abc123", {
159+
* includeGuests: true,
160+
* includePriceBreakdown: true,
161+
* });
162+
* if (booking) console.log(booking.displayId, booking.outstandingBalanceAmount);
163+
* ```
164+
*/
151165
async getById(
152166
bookingId: string,
153167
options: BookingReadOptions = {},
@@ -307,6 +321,24 @@ export class BookingService {
307321
* Charges a booking. Validates input, resolves the order + payment source via
308322
* payments-on-file, then applies the payment. The `idempotencyKey` is passed
309323
* through to Peek.
324+
*
325+
* @example
326+
* ```ts
327+
* const result = await peek.getBookingService().makePayment({
328+
* bookingId: "b_abc123",
329+
* paymentSourceId: "custom/other", // or a "ps_…" source on file
330+
* amount: "25.00",
331+
* currency: "USD",
332+
* idempotencyKey: crypto.randomUUID(),
333+
* });
334+
* console.log(result.transactionId);
335+
* ```
336+
*
337+
* @throws {Error} when `paymentSourceId` is missing or not a `ps_…` id / one
338+
* of `cash/cash`, `custom/other`, `custom/voucher`; when `amount` is not a
339+
* valid number; when `currency` is not a 3-letter uppercase code; when
340+
* `idempotencyKey` is empty; when `bookingId` does not resolve to a `b_…` id;
341+
* when the booking or payment source is not found; or when the charge fails.
310342
*/
311343
async makePayment(input: MakePaymentInput): Promise<MakePaymentResult> {
312344
const normalized = normalizeBookingId(input.bookingId);
@@ -453,6 +485,24 @@ export class BookingService {
453485
* amendOrder. Lists the booking's add-ons first to derive the order id and
454486
* reuse an existing add-on refid; resolves the add-on's parent item via the
455487
* product service. Returns the booking's add-ons after the change.
488+
*
489+
* `quantity` is a **positive-integer string** ("1", "2", …); one add-on
490+
* itemOption is created per unit. `addonOptionId` is the add-on's item-option
491+
* id (a ticket id on an `ADD_ON_PRODUCT_TYPE` product from
492+
* {@link ProductService.getAllProducts}).
493+
*
494+
* @example
495+
* ```ts
496+
* const result = await peek.getBookingService().addAddon("b_abc123", {
497+
* addonOptionId: "io_helmet",
498+
* quantity: "2",
499+
* });
500+
* console.log(result.updatedBookingAddons.addons);
501+
* ```
502+
*
503+
* @throws {Error} when `addonOptionId` is missing, `quantity` is not a
504+
* positive-integer string, the add-on is not found on any product, or any of
505+
* the underlying quote/order mutations fail.
456506
*/
457507
async addAddon(
458508
bookingId: string,
@@ -664,7 +714,27 @@ export class BookingService {
664714

665715
/**
666716
* Creates a booking via createQuoteV2 → createOrderFromQuote, optionally
667-
* marking it paid. IDs must be pre-resolved (no free-text matching).
717+
* marking it paid. IDs must be pre-resolved (no free-text matching) — resolve
718+
* `activityId` + ticket `resourceOptionId`s from {@link ProductService} and
719+
* `availabilityTimeId` from {@link AvailabilityService.getAvailabilityTimes}.
720+
*
721+
* @example
722+
* ```ts
723+
* const created = await peek.getBookingService().create({
724+
* activityId: "a_kayak_tour",
725+
* availabilityTimeId: "at_2026_06_20_0900",
726+
* tickets: [{ resourceOptionId: "ro_adult", quantity: 2 }],
727+
* guest: { name: "Sam Rivera", email: "sam@example.com" },
728+
* markAsPaid: true,
729+
* idempotencyKey: crypto.randomUUID(),
730+
* });
731+
* console.log(created.bookingId, created.displayId, created.balanceFormatted);
732+
* ```
733+
*
734+
* @throws {Error} when `activityId`, `availabilityTimeId`, a ticket
735+
* `resourceOptionId`/positive `quantity`, or the guest `name` is missing; when
736+
* `markAsPaid` is set without an `idempotencyKey`; or when the quote/order
737+
* mutations fail.
668738
*/
669739
async create(input: CreateBookingInput): Promise<CreatedBooking> {
670740
validateCreateInput(input);

src/internal/products/product-service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ export class ProductService {
4141
* Returns every product as a single flat list: activities plus add-ons (the
4242
* latter tagged with the add-on type). Add-ons are gathered across all
4343
* cursor-paginated pages.
44+
*
45+
* @example Split activities from add-ons
46+
* ```ts
47+
* import { ADD_ON_PRODUCT_TYPE } from "@peek-travel/app-utilities";
48+
*
49+
* const products = await peek.getProductService().getAllProducts();
50+
* const activities = products.filter((p) => p.type !== ADD_ON_PRODUCT_TYPE);
51+
* const addons = products.filter((p) => p.type === ADD_ON_PRODUCT_TYPE);
52+
* ```
4453
*/
4554
async getAllProducts(): Promise<Product[]> {
4655
const [activities, itemOptionNodes] = await Promise.all([

0 commit comments

Comments
 (0)