@peektravel/app-utilities 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peek Travel Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # @peektravel/app-utilities
2
+
3
+ GraphQL JS mapping utilities extracted from the Peek Pro Autopilot connector.
4
+ The package owns the GraphQL queries, authentication, transport, and the
5
+ conversion into clean TypeScript data models — callers work only with the
6
+ high-level `PeekAccessService` and the plain data shapes it returns.
7
+
8
+ ## Install
9
+
10
+ This package is published to **GitHub Packages** (a private registry), not the
11
+ public npm registry. Point the `@peek-travel` scope at GitHub Packages once per
12
+ consuming project by adding an `.npmrc` next to its `package.json`:
13
+
14
+ ```ini
15
+ @peek-travel:registry=https://npm.pkg.github.com
16
+ //npm.pkg.github.com/:_authToken=${NPM_TOKEN}
17
+ ```
18
+
19
+ Then install (and later update) it like any other dependency:
20
+
21
+ ```bash
22
+ npm install @peektravel/app-utilities
23
+ npm update @peektravel/app-utilities
24
+ ```
25
+
26
+ `NPM_TOKEN` must be a GitHub token with the `read:packages` scope. Locally that's
27
+ a personal access token in your environment; in cloud builds (Firebase
28
+ Functions / Google Cloud Build, CI) set it as a build secret/env var. See
29
+ [Releasing](#releasing-github-packages) for how new versions are published.
30
+
31
+ ## Usage
32
+
33
+ Configure one access service per install with everything it needs to
34
+ authenticate and reach the gateway. It mints and caches a short-lived JWT on
35
+ demand and hands out per-resource services that own the resource-specific calls.
36
+
37
+ ```ts
38
+ import { PeekAccessService, type Product } from '@peektravel/app-utilities';
39
+
40
+ const peek = new PeekAccessService({
41
+ installId: 'install-123', // JWT subject
42
+ jwtSecret: process.env.PEEK_INTERNAL_SECRET!, // signs the JWT
43
+ issuer: process.env.APP_NAME!, // JWT issuer
44
+ appId: process.env.PEEK_APP_ID!, // gateway path segment
45
+ gatewayKey: process.env.PEEK_GATEWAY_KEY!, // pk-api-key header
46
+ });
47
+
48
+ const products: Product[] = await peek.getProductService().getAllProducts();
49
+ ```
50
+
51
+ The access service is the authenticated root; each `get<Resource>Service()`
52
+ returns a (memoized) service that owns that resource's calls.
53
+
54
+ `getAllProducts()` returns a single flat list of activities **and** add-ons
55
+ (add-ons tagged with `ADD_ON_PRODUCT_TYPE`), gathering all cursor-paginated
56
+ add-on pages for you.
57
+
58
+ ## Resources
59
+
60
+ | Accessor | Methods |
61
+ | --- | --- |
62
+ | `getProductService()` | `getAllProducts()` |
63
+ | `getAccountUserService()` | `getAll()`, `getById(userId)` |
64
+ | `getResourcePoolService()` | `getAll(mode?)` |
65
+ | `getTimeslotService()` | `getForDay()`, `getById()`, `setAvailability()`, `setNotes()`, `assignGuide()` |
66
+ | `getResellerService()` | `getAllChannels(agentsPerChannel?)` |
67
+ | `getPromoCodeService()` | `getAll()`, `create(input)` |
68
+ | `getDailyNoteService()` | `getToday()`, `update(note)` |
69
+ | `getAvailabilityService()` | `getAvailabilityTimes(query)` |
70
+ | `getMembershipService()` | `getAll()`, `purchase(input)` |
71
+ | `getBookingService()` | `getById()`, `searchByTimeRange()`, `searchByTimeslot()`, `getGuests()`, `getPaymentsOnFile()`, `appendNote()`, `setCheckinStatus()`, `cancel()`, `makePayment()`, `refund()`, `createInvoiceLink()`, `addAddon()`, `create()` |
72
+
73
+ ### Optional configuration
74
+
75
+ | Option | Default | Purpose |
76
+ | --- | --- | --- |
77
+ | `baseUrl` | Peek production gateway | Override the GraphQL gateway base URL |
78
+ | `tokenTtlSeconds` | `3600` | JWT lifetime |
79
+ | `tokenRefreshLeewaySeconds` | `60` | Re-mint this long before expiry |
80
+ | `retryDelaysMs` | `[1000, 2000, 4000]` | Backoff for HTTP 429 retries |
81
+ | `logger` | no-op | Inject a `Logger` for diagnostics |
82
+ | `fetch` | global `fetch` | Custom fetch (e.g. for tests) |
83
+ | `itemOptionsPageSize` | `50` | Add-on pagination page size |
84
+
85
+ ### Errors
86
+
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 '@peektravel/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 '@peektravel/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
+ ```
204
+
205
+ The package ships dual ESM + CommonJS builds with bundled type declarations, so
206
+ both `import` and `require` consumers (including the Node 22 / CommonJS Firebase
207
+ Functions runtime) resolve correctly. Its only runtime dependency is
208
+ `jsonwebtoken`.
209
+
210
+ ## Releasing (GitHub Packages)
211
+
212
+ Releases are automated. Pushing a `v*.*.*` git tag triggers
213
+ `.github/workflows/publish.yml`, which typechecks, lints, runs the test suite
214
+ (95% coverage gate), then publishes to GitHub Packages. `npm publish` runs
215
+ `prepublishOnly` first, so the build plus `publint` + `attw` checks gate every
216
+ release.
217
+
218
+ To cut a release:
219
+
220
+ ```bash
221
+ npm version patch # or minor / major — bumps package.json and creates a git tag
222
+ git push --follow-tags # pushes the commit + tag; the workflow publishes
223
+ ```
224
+
225
+ The workflow asserts the tag matches the `package.json` version, so the two
226
+ never drift. The repo's built-in `GITHUB_TOKEN` (with `packages: write`) handles
227
+ publish auth — no personal token needed in CI. Consumers then pick the new
228
+ version up with a normal `npm update` (see [Install](#install)).
229
+
230
+ > One-time setup: the package scope `@peek-travel` must match the GitHub org
231
+ > that owns the package, and the repo needs the GitHub Actions permission to
232
+ > write packages (granted by the `packages: write` permission in the workflow).
233
+
234
+ ## Development
235
+
236
+ ```bash
237
+ npm install # install dependencies
238
+ npm run build # bundle ESM + CJS + .d.ts into dist/ (tsup)
239
+ npm run dev # rebuild on change
240
+ npm test # run unit tests (vitest)
241
+ npm run test:coverage
242
+ npm run typecheck # tsc --noEmit
243
+ npm run lint # eslint (flat config)
244
+ ```
245
+
246
+ ### Release checks
247
+
248
+ `prepublishOnly` builds the package and runs [`publint`](https://publint.dev)
249
+ and [`@arethetypeswrong/cli`](https://github.com/arethetypeswrong/arethetypeswrong.github.io)
250
+ to verify the `exports` map and type resolution are correct for both module
251
+ systems. The publish workflow runs these automatically — see
252
+ [Releasing](#releasing-github-packages).
253
+
254
+ ## Project layout
255
+
256
+ ```
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)
262
+ ```