@ophelio/sdk 0.3.2 → 0.5.0

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
@@ -21,7 +21,10 @@ import { Ophelio } from '@ophelio/sdk'
21
21
  const ophelio = new Ophelio({ apiKey: process.env.OPHELIO_API_KEY })
22
22
 
23
23
  // Gate admission check
24
- const result = await ophelio.admit.check({ card: 'M-123456' })
24
+ const result = await ophelio.admit.check({
25
+ card: 'M-123456',
26
+ targets: ['venue_a'], // where this gate is; optional
27
+ })
25
28
  if (result.admitted) {
26
29
  console.log(result.pricing_group) // e.g. 'member_adult'
27
30
  }
@@ -136,11 +139,11 @@ for await (const change of paginate((page) =>
136
139
  // …
137
140
  }
138
141
 
139
- // or drain everything into an array (loads the whole collection into memory):
142
+ // or load everything into one array (holds the whole collection in memory):
140
143
  const customers = await collect((page) => ophelio.customers.list(page))
141
144
  ```
142
145
 
143
- To page manually — e.g. to surface `total_size` or drive numbered pages — feed
146
+ To page manually — for example to show `total_size` or build numbered pages — feed
144
147
  `next_page_token` back in as `page_token` until it comes back empty.
145
148
 
146
149
  `page_size` defaults to 50 and is capped at 250. Because pagination is
@@ -156,8 +159,8 @@ Everything reachable with an API key is covered:
156
159
 
157
160
  | Namespace | Methods |
158
161
  | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
- | `admit` | `check({ card })` |
160
- | `members` | `sync({ since? })`, `get(id)`, `listEntitlements(id, { type? })`, `update(id, ...)`, `remove(id)`, `setPrimary(id)`, `reissueCard(id, { note? })` |
162
+ | `admit` | `check({ card, targets? })` |
163
+ | `members` | `sync({ since? })`, `get(id)`, `listEntitlements(id, { type?, targets? })`, `update(id, ...)`, `remove(id)`, `setPrimary(id)`, `reissueCard(id, { note? })` |
161
164
  | `memberships` | `list(params?)`, `get(id)`, `create(...)`, `update(id, ...)`, `cancel(id, ...)`, `uncancel(id)`, `renew(id, ...)`, `upgrade(id, ...)`, `downgrade(id, ...)`, `listMembers(id)`, `addMember(id, ...)`, `listScheduledChanges(id, params?)`, `cancelScheduledChange(id, changeId)`, `listTransactions(id, params?)`, `listTermTransactions(id, termId, params?)`, `createTransaction(id, ...)` |
162
165
  | `customers` | `list(params?)`, `get(id, params?)`, `create(...)`, `update(id, ...)`, `delete(id)`, `listMemberships(id, params?)` — `list`/`get` accept `view: 'full'` to include `membership_summary` |
163
166
  | `entitlements` | `redeem(...)`, `recordUsage(...)`, `list()`, `get(id)`, `create(...)`, `update(id, ...)`, `delete(id)` |
@@ -166,8 +169,8 @@ Everything reachable with an API key is covered:
166
169
  | `memberRoles` | `list()`, `get(id)`, `create(...)`, `update(id, ...)`, `delete(id)` |
167
170
  | `pricingGroups` | `list()`, `get(id)`, `create(...)`, `update(id, ...)`, `delete(id)` |
168
171
  | `salesChannels` | `list()`, `get(id)`, `create(...)`, `update(id, ...)`, `delete(id)` |
169
- | `targetCodes` | `list(params?)`, `get(id)`, `create(...)`, `update(id, ...)`, `delete(id)` — `update` takes `display_name` / `description` only; `code` is immutable |
170
- | `calendars` | `list(params?)`, `get(id)`, `create(...)`, `update(id, ...)`, `delete(id)`, `listRules(id, params?)`, `createRule(id, ...)` |
172
+ | `targetCodes` | `list(params?)`, `get(id, params?)`, `create(...)`, `update(id, ...)`, `delete(id)` — `list`/`get` accept `view: 'full'` to include `plan_entitlement_count`; `update` takes `display_name` / `description` only, as `code` is immutable |
173
+ | `calendars` | `list(params?)`, `get(id, params?)`, `create(...)`, `update(id, ...)`, `delete(id)`, `listRules(id, params?)`, `createRule(id, ...)` — `list`/`get` accept `view: 'full'` to include `plan_entitlement_count` |
171
174
  | `calendarRules` | `get(id)`, `update(id, ...)`, `delete(id)` |
172
175
  | `planEntitlements` | `get(id)`, `update(id, ...)`, `delete(id)`, `listAvailabilities(id, params?)`, `createAvailability(id, ...)` |
173
176
  | `planEntitlementAvailabilities` | `get(id)`, `update(id, ...)`, `delete(id)` |
@@ -183,6 +186,70 @@ Every method accepts a trailing `options` argument: `{ signal?, headers? }`
183
186
  Endpoints requiring a user session (API key management, organisation and
184
187
  project admin) are intentionally not in the SDK — API keys cannot call them.
185
188
 
189
+ ### Admission
190
+
191
+ `admit.check` admits a member who has free entry they can use **here and
192
+ now**. `admitted` comes back `true` only when the member holds a `free_entry`
193
+ entitlement that passes all three checks: the membership's status still allows
194
+ it; if the membership is in its grace period, the entitlement has
195
+ `active_during_grace` set; and no availability row rules it out for the date
196
+ and place of the request — an entitlement with no rows applies everywhere and
197
+ always, and one that has rows needs just one of them to match. An active
198
+ membership on its own is not enough.
199
+
200
+ ```ts
201
+ const result = await ophelio.admit.check({ card: 'M-123456' })
202
+
203
+ if (result.admitted) {
204
+ console.log(result.pricing_group) // 'member_adult' — price the ticket
205
+ } else if (
206
+ result.deny_reason === 'free_entry_not_granted' ||
207
+ result.deny_reason === 'free_entry_unavailable'
208
+ ) {
209
+ result.entitlements // still populated — discounts, guest passes, early access
210
+ result.pricing_group // still resolved — prices the ticket at the member rate
211
+ }
212
+ ```
213
+
214
+ Two results of that rule are deliberate, and you will meet both on a default
215
+ configuration:
216
+
217
+ - A membership in its **grace period** is not admitted unless its free entry
218
+ has `active_during_grace` set. A grace period is time to renew, not
219
+ permission to enter. The denial reads `free_entry_unavailable`.
220
+ - A plan with **no free-entry entitlement** never admits anyone. It still
221
+ works for discounts, guest passes and early access — a benefits-only plan.
222
+
223
+ The two cases have separate deny reasons, because staff act on them
224
+ differently:
225
+
226
+ | `deny_reason` | Meaning |
227
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
228
+ | `free_entry_not_granted` | The plan grants no free entry at all. Permanent — no operator action changes it, so do not alert on it. |
229
+ | `free_entry_unavailable` | The plan grants free entry, but it is not usable for this request: no availability row covers this date or this target, or the membership is in its grace period and its free entry does not have `active_during_grace` set. May stop happening on its own. |
230
+
231
+ `free_entry_unavailable` is the one worth monitoring: a calendar nobody rolled
232
+ over to the new year turns away a whole plan at once and shows up as this
233
+ reason. It also covers the everyday grace-period case above, so alert on a rise
234
+ in how often it happens rather than on the reason itself.
235
+
236
+ Only these two free-entry denials return the member's remaining `entitlements`
237
+ and their `pricing_group`, because a visitor who has benefits but not free
238
+ entry is usually about to buy a ticket at the desk. Every other denial —
239
+ `card_not_found`, `member_removed`, `membership_expired`,
240
+ `membership_cancelled`, `membership_incomplete` and `payment_overdue` —
241
+ returns an empty `entitlements` list and a `null` `pricing_group`. If you build
242
+ a desk screen that offers the member their benefits, drive it from the two
243
+ free-entry denials only. The `renew_at_desk` action is separate again: it
244
+ depends on the membership's status, not on whether the member was admitted, so
245
+ it can appear on an admitted response too.
246
+
247
+ `members.sync` returns **more** members than `admit.check` would admit, and
248
+ that is deliberate: a benefits-only member is turned away at the gate but still
249
+ has discounts and guest passes an offline till needs. An offline gate reaches
250
+ the same answer by reading each cached member's `entitlements` — if free entry
251
+ is in the list, admit — and applies no rules of its own.
252
+
186
253
  ### Calendar rules
187
254
 
188
255
  `date_from` and `date_until` are **timezone-free calendar dates** (`YYYY-MM-DD`),
@@ -194,8 +261,8 @@ open-ended on that side.
194
261
  JavaScript's `Date.prototype.getDay()` calls Sunday 0). An empty array means
195
262
  every day.
196
263
 
197
- Conditions AND within a rule and rules OR across a calendar, so "weekends in
198
- August" is a single rule:
264
+ A date has to satisfy every condition in a rule, and a calendar matches a date
265
+ if any one of its rules does. So "weekends in August" is a single rule:
199
266
 
200
267
  ```ts
201
268
  await ophelio.calendars.createRule(calendar.id, {
@@ -205,18 +272,33 @@ await ophelio.calendars.createRule(calendar.id, {
205
272
  })
206
273
  ```
207
274
 
208
- A calendar containing no rules is valid for **all** dates, not none.
275
+ A calendar with no rules matches **every** date, not none: a half-finished
276
+ calendar lets everyone through rather than quietly turning members away. You
277
+ cannot reach that state through the API, because three requests are refused:
278
+
279
+ - A rule has to restrict something. One that sets neither `date_from` nor
280
+ `date_until` nor a weekday is refused with a **400**.
281
+ - A calendar with no rules cannot be attached to a plan entitlement, whether an
282
+ availability row is being created with it or an existing row repointed to it.
283
+ Attaching one is refused with a **409**, because a row that restricts no
284
+ dates would make the entitlement valid on every date, no matter what the
285
+ other rows say.
286
+ - The last rule of a calendar that is in use cannot be deleted. Deleting it is
287
+ refused with a **409** that says how many plan entitlements use the calendar.
288
+ Add the replacement rule first, then delete.
209
289
 
210
290
  ### Target codes
211
291
 
212
- The registered vocabulary of locations a gate reports (`venue_a`,
213
- `coffee_shop`). A `code` is lowercase letters and digits with single
292
+ A registered code for each venue, zone, shop or gate that accepts
293
+ memberships (`venue_a`, `coffee_shop`). Register each one once, then name it
294
+ wherever a benefit is restricted, and wherever a request says where it is
295
+ coming from. A `code` is lowercase letters and digits with single
214
296
  underscores between segments, at least one letter, 2–64 characters — and it is
215
297
  **matched exactly**. A miscased code is an unregistered code, so send values
216
298
  back verbatim from `list()` rather than constructing or case-folding them.
217
299
 
218
- A code outside the grammar is **rejected, never transformed**: `Venue A` comes
219
- back as a 400, not as a stored `venue_a`.
300
+ A code that does not match that format is **rejected, never corrected for
301
+ you**: `Venue A` comes back as a 400, rather than being stored as `venue_a`.
220
302
 
221
303
  `code` is **immutable from creation**; `display_name` and `description` stay
222
304
  editable forever. `UpdateTargetCodeParams` therefore has no `code` field, so a
@@ -247,10 +329,61 @@ await ophelio.planEntitlements.createAvailability(planEntitlement.id, {
247
329
  })
248
330
  ```
249
331
 
250
- Rows **OR**, and the absence of rows is judged **per plan entitlement**: a plan
251
- entitlement with no rows at all is unrestricted, never restricted to nothing.
252
- Every code in `targets` must already exist in the project's target code
253
- vocabulary — an unregistered one is rejected, never registered on the fly.
332
+ A plan entitlement applies if **any one** of its rows matches. Having no rows
333
+ is decided for each plan entitlement on its own: an entitlement with no rows
334
+ applies on every date and in every place, rather than nowhere. Every code in
335
+ `targets` must already be registered as a target code — an unregistered one is
336
+ rejected, not registered for you.
337
+
338
+ ### Target context
339
+
340
+ A read can say where it is being made, and the entitlements it gets back are
341
+ checked against the availability rows for that place. `admit.check` and
342
+ `members.listEntitlements` take `targets` — a set of registered target codes,
343
+ sent comma-separated on the wire — and `entitlements.recordUsage` takes a
344
+ singular `target`, because one usage event happens at one place.
345
+
346
+ ```ts
347
+ const result = await ophelio.admit.check({
348
+ card: 'M-123456',
349
+ targets: ['venue_a', 'coffee_shop'],
350
+ })
351
+
352
+ result.target_context_applied // true — the entitlements were target-filtered
353
+
354
+ for (const entitlement of result.entitlements) {
355
+ entitlement.valid_at // ['coffee_shop'] — where it is valid, when restricted
356
+ entitlement.valid_on // the date windows it matched, when restricted
357
+ }
358
+ ```
254
359
 
255
- Availability rows can be authored today but nothing reads them yet, so they do
256
- not restrict admission.
360
+ **Three cases, answered differently.** Omitting `targets` says nothing about
361
+ where the request is: every target restriction is then treated as met, and
362
+ `target_context_applied` comes back `false`. Passing `[]` says the caller names
363
+ no targets, so only benefits valid everywhere come back. Passing codes means a
364
+ benefit has to name one of them. Dates are checked in all three cases.
365
+
366
+ **Codes are matched exactly.** Send them back verbatim from
367
+ `targetCodes.list()`; an unregistered or miscased code is refused with a 400
368
+ rather than dropped, so a typo cannot pass for a benefit that does not apply.
369
+
370
+ **The parameter does a different job on each endpoint.** At the gate it is part
371
+ of the admission decision; on `members.listEntitlements` it filters the list you
372
+ get back. `members.sync` takes no targets at all — each member's entitlements
373
+ are a snapshot taken at the moment of the sync, and the gate that caches them
374
+ applies no rules of its own.
375
+
376
+ `valid_on` and `valid_at` list only what an entitlement **matched**. `valid_on`
377
+ is omitted when no date restricts it, and `valid_at` when no place does. What
378
+ it failed to match is never returned: the response says where and when a
379
+ benefit is valid, never which restrictions the member missed. Read the two
380
+ lists separately — because an entitlement applies if any one row matches, a
381
+ date in `valid_on` and a place in `valid_at` need not come from the same row.
382
+
383
+ A project can require every read to say where it is. Once all of its gates send
384
+ their own `targets`, an operator turns the setting on, and from then on a read
385
+ that omits `targets` altogether is refused with a 400. Passing `[]` is not the
386
+ same as omitting it: that read has said where it is and named no targets, so it
387
+ is still answered, exactly as the three cases above describe. It is a
388
+ per-project setting rather than anything the client sends, so start sending
389
+ `targets` before expecting it to be required.
package/dist/index.cjs CHANGED
@@ -248,12 +248,30 @@ function pageQuery(params) {
248
248
  //#endregion
249
249
  //#region src/resources/admit.ts
250
250
  var AdmitResource = class extends APIResource {
251
- /** Gate admission check: `GET /admit?card=<cardId>`. */
251
+ /**
252
+ * Gate admission check: `GET /admit?card=<cardId>`.
253
+ *
254
+ * `targets` names the venue, zone, shop or gate the request is being made
255
+ * at, and is the context the entitlements are judged in — a benefit
256
+ * restricted to somewhere else drops out of `entitlements`. Three states,
257
+ * and they are answered differently: omit it and every target restriction
258
+ * is treated as satisfied; pass `[]` and only benefits valid anywhere
259
+ * survive; pass codes and a benefit needs one of them. Read
260
+ * `target_context_applied` on the result to tell the first apart from the
261
+ * other two.
262
+ *
263
+ * Codes are matched exactly, so send them back verbatim from
264
+ * `targetCodes.list()` — an unregistered or miscased code is refused with
265
+ * a 400 rather than quietly ignored.
266
+ */
252
267
  check(params, options) {
253
268
  return this.transport.request({
254
269
  method: "GET",
255
270
  path: "/api/v1/admit",
256
- query: { card: params.card },
271
+ query: {
272
+ card: params.card,
273
+ targets: params.targets
274
+ },
257
275
  retryable: true,
258
276
  options
259
277
  });
@@ -329,7 +347,7 @@ var BillingOptionsResource = class extends APIResource {
329
347
  //#endregion
330
348
  //#region src/resources/calendar-rules.ts
331
349
  /**
332
- * A calendar rule is one row of a calendar's date vocabulary. Conditions AND
350
+ * A calendar rule is one row of a calendar's set of dates. Conditions AND
333
351
  * within a rule and rules OR across a calendar, so "weekends in August" is a
334
352
  * single rule rather than two.
335
353
  *
@@ -367,17 +385,24 @@ var CalendarRulesResource = class extends APIResource {
367
385
  //#endregion
368
386
  //#region src/resources/calendars.ts
369
387
  /**
370
- * Calendars are named, reusable date vocabularies ("Peak 2026", "Weekends").
388
+ * Calendars are named, reusable sets of dates ("Peak 2026", "Weekends").
371
389
  * The dates themselves live in the rules attached to them.
372
390
  *
373
391
  * A calendar containing no rules is valid for **all** dates, not none.
374
392
  */
375
393
  var CalendarsResource = class extends APIResource {
394
+ /**
395
+ * Pass `view: 'full'` to include `plan_entitlement_count` on every
396
+ * calendar.
397
+ */
376
398
  async list(params, options) {
377
399
  const response = await this.transport.request({
378
400
  method: "GET",
379
401
  path: "/api/v1/calendars",
380
- query: pageQuery(params),
402
+ query: {
403
+ ...pageQuery(params),
404
+ ...params?.view ? { view: params.view } : {}
405
+ },
381
406
  retryable: true,
382
407
  options
383
408
  });
@@ -387,10 +412,14 @@ var CalendarsResource = class extends APIResource {
387
412
  total_size: response.total_size
388
413
  };
389
414
  }
390
- get(id, options) {
415
+ /**
416
+ * Pass `view: 'full'` to include `plan_entitlement_count`.
417
+ */
418
+ get(id, params = {}, options) {
391
419
  return this.transport.request({
392
420
  method: "GET",
393
421
  path: `/api/v1/calendars/${encodeURIComponent(id)}`,
422
+ query: params.view ? { view: params.view } : void 0,
394
423
  retryable: true,
395
424
  options
396
425
  });
@@ -543,6 +572,14 @@ var EntitlementsResource = class extends APIResource {
543
572
  /**
544
573
  * Fire-and-forget batch usage reporting (one row per member). Idempotent
545
574
  * per (key, member). Returns the recorded usage rows.
575
+ *
576
+ * `target` is the single registered code for the venue, zone, shop or gate
577
+ * the usage happened at — one event, one place, which is why it is
578
+ * singular here where the reads take `targets`. It is both the context the
579
+ * entitlement is judged in and a column on the row that comes back, so it
580
+ * is queryable in a way the free-form `context` is not. It is part of the
581
+ * idempotency fingerprint: a replay of the same key at a different target
582
+ * is refused rather than silently returning the original row.
546
583
  */
547
584
  async recordUsage(params, options = {}) {
548
585
  const { idempotencyKey, ...requestOptions } = options;
@@ -668,11 +705,29 @@ var MembersResource = class extends APIResource {
668
705
  options
669
706
  });
670
707
  }
708
+ /**
709
+ * The entitlements a member can use right now.
710
+ *
711
+ * `targets` filters the result down to the benefits valid at the venue,
712
+ * zone, shop or gate it names — the same codes `admit.check` takes, but
713
+ * read as a filter here rather than as the context a gate decision is made
714
+ * in. Three states: omit it and nothing is filtered by target; pass `[]`
715
+ * and only benefits valid anywhere come back; pass codes and a benefit
716
+ * needs one of them. Codes are matched exactly, so send them back verbatim
717
+ * from `targetCodes.list()` — an unregistered one is refused with a 400.
718
+ *
719
+ * Each entitlement carries `valid_on` and `valid_at` when something
720
+ * restricts it by date or by target; both list only what it matched, never
721
+ * what it failed.
722
+ */
671
723
  async listEntitlements(id, params = {}, options) {
672
724
  return (await this.transport.request({
673
725
  method: "GET",
674
726
  path: `/api/v1/members/${encodeURIComponent(id)}/entitlements`,
675
- query: { type: params.type },
727
+ query: {
728
+ type: params.type,
729
+ targets: params.targets
730
+ },
676
731
  retryable: true,
677
732
  options
678
733
  })).entitlements;
@@ -1257,16 +1312,23 @@ var SalesChannelsResource = class extends APIResource {
1257
1312
  //#endregion
1258
1313
  //#region src/resources/target-codes.ts
1259
1314
  /**
1260
- * The registered vocabulary of locations a gate can report (`venue_a`,
1261
- * `coffee_shop`). Codes are matched exactly — list them and send values back
1262
- * verbatim rather than constructing or case-folding them.
1315
+ * The registered code for each venue, zone, shop or gate a request can name
1316
+ * (`venue_a`, `coffee_shop`). Codes are matched exactly — list them and send
1317
+ * values back verbatim rather than constructing or case-folding them.
1263
1318
  */
1264
1319
  var TargetCodesResource = class extends APIResource {
1320
+ /**
1321
+ * Pass `view: 'full'` to include `plan_entitlement_count` on every target
1322
+ * code.
1323
+ */
1265
1324
  async list(params, options) {
1266
1325
  const response = await this.transport.request({
1267
1326
  method: "GET",
1268
1327
  path: "/api/v1/target-codes",
1269
- query: pageQuery(params),
1328
+ query: {
1329
+ ...pageQuery(params),
1330
+ ...params?.view ? { view: params.view } : {}
1331
+ },
1270
1332
  retryable: true,
1271
1333
  options
1272
1334
  });
@@ -1276,10 +1338,14 @@ var TargetCodesResource = class extends APIResource {
1276
1338
  total_size: response.total_size
1277
1339
  };
1278
1340
  }
1279
- get(id, options) {
1341
+ /**
1342
+ * Pass `view: 'full'` to include `plan_entitlement_count`.
1343
+ */
1344
+ get(id, params = {}, options) {
1280
1345
  return this.transport.request({
1281
1346
  method: "GET",
1282
1347
  path: `/api/v1/target-codes/${encodeURIComponent(id)}`,
1348
+ query: params.view ? { view: params.view } : void 0,
1283
1349
  retryable: true,
1284
1350
  options
1285
1351
  });