@ophelio/sdk 0.4.0 → 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
@@ -139,11 +139,11 @@ for await (const change of paginate((page) =>
139
139
  // …
140
140
  }
141
141
 
142
- // 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):
143
143
  const customers = await collect((page) => ophelio.customers.list(page))
144
144
  ```
145
145
 
146
- 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
147
147
  `next_page_token` back in as `page_token` until it comes back empty.
148
148
 
149
149
  `page_size` defaults to 50 and is capped at 250. Because pagination is
@@ -186,6 +186,70 @@ Every method accepts a trailing `options` argument: `{ signal?, headers? }`
186
186
  Endpoints requiring a user session (API key management, organisation and
187
187
  project admin) are intentionally not in the SDK — API keys cannot call them.
188
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
+
189
253
  ### Calendar rules
190
254
 
191
255
  `date_from` and `date_until` are **timezone-free calendar dates** (`YYYY-MM-DD`),
@@ -197,8 +261,8 @@ open-ended on that side.
197
261
  JavaScript's `Date.prototype.getDay()` calls Sunday 0). An empty array means
198
262
  every day.
199
263
 
200
- Conditions AND within a rule and rules OR across a calendar, so "weekends in
201
- 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:
202
266
 
203
267
  ```ts
204
268
  await ophelio.calendars.createRule(calendar.id, {
@@ -208,7 +272,20 @@ await ophelio.calendars.createRule(calendar.id, {
208
272
  })
209
273
  ```
210
274
 
211
- 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.
212
289
 
213
290
  ### Target codes
214
291
 
@@ -220,8 +297,8 @@ underscores between segments, at least one letter, 2–64 characters — and it
220
297
  **matched exactly**. A miscased code is an unregistered code, so send values
221
298
  back verbatim from `list()` rather than constructing or case-folding them.
222
299
 
223
- A code outside the grammar is **rejected, never transformed**: `Venue A` comes
224
- 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`.
225
302
 
226
303
  `code` is **immutable from creation**; `display_name` and `description` stay
227
304
  editable forever. `UpdateTargetCodeParams` therefore has no `code` field, so a
@@ -252,15 +329,16 @@ await ophelio.planEntitlements.createAvailability(planEntitlement.id, {
252
329
  })
253
330
  ```
254
331
 
255
- Rows **OR**, and the absence of rows is judged **per plan entitlement**: a plan
256
- entitlement with no rows at all is unrestricted, never restricted to nothing.
257
- Every code in `targets` must already be registered as a target code — an
258
- 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.
259
337
 
260
338
  ### Target context
261
339
 
262
340
  A read can say where it is being made, and the entitlements it gets back are
263
- judged against the availability rows in that light. `admit.check` and
341
+ checked against the availability rows for that place. `admit.check` and
264
342
  `members.listEntitlements` take `targets` — a set of registered target codes,
265
343
  sent comma-separated on the wire — and `entitlements.recordUsage` takes a
266
344
  singular `target`, because one usage event happens at one place.
@@ -279,29 +357,33 @@ for (const entitlement of result.entitlements) {
279
357
  }
280
358
  ```
281
359
 
282
- **Three states, answered differently.** Omitting `targets` is the permissive
283
- default: every target restriction is treated as satisfied, and
284
- `target_context_applied` comes back `false`. Passing `[]` says the caller is
285
- naming no targets, so only benefits valid anywhere survive. Passing codes
286
- means a benefit needs one of them. Dates are enforced in all three.
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.
287
365
 
288
366
  **Codes are matched exactly.** Send them back verbatim from
289
367
  `targetCodes.list()`; an unregistered or miscased code is refused with a 400
290
368
  rather than dropped, so a typo cannot pass for a benefit that does not apply.
291
369
 
292
- **The parameter means something different on each surface.** At the gate it is
293
- the context the decision is made in; on `members.listEntitlements` it is a
294
- filter over the result. `members.sync` takes no target context at all — the
295
- offline roster carries each member's entitlements as at the moment of sync,
296
- and the gate that caches it evaluates no rules of its own.
297
-
298
- `valid_on` and `valid_at` carry only what an entitlement **matched**, and are
299
- omitted entirely when nothing restricts it on that axis. What it failed is
300
- never returned: the response says where and when a benefit is valid, never
301
- which restrictions the member did not satisfy. Because rows OR, the two read
302
- as independent unions rather than as pairs.
303
-
304
- A project can require target context — once every gate reports its own, it is
305
- turned on and a read that says nothing about where it is is refused from then
306
- on. It is a per-project setting rather than anything the client sends, so roll
307
- out sending `targets` before expecting it to be enforced.
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.d.cts CHANGED
@@ -1652,7 +1652,7 @@ interface components {
1652
1652
  reason: string;
1653
1653
  })[];
1654
1654
  admitted: boolean; /** @enum {string|null} */
1655
- deny_reason: "card_not_found" | "member_removed" | "membership_cancelled" | "membership_expired" | "membership_incomplete" | "payment_overdue" | null;
1655
+ deny_reason: "card_not_found" | "free_entry_not_granted" | "free_entry_unavailable" | "member_removed" | "membership_cancelled" | "membership_expired" | "membership_incomplete" | "payment_overdue" | null;
1656
1656
  member: components["schemas"]["AdmitMember"] | null;
1657
1657
  membership_id: string | null; /** @enum {string|null} */
1658
1658
  membership_status: "active" | "cancelled" | "expired" | "grace_period" | "incomplete" | "past_due" | null;
@@ -3329,6 +3329,14 @@ interface operations {
3329
3329
  "application/json": components["schemas"]["ErrorResponse"];
3330
3330
  };
3331
3331
  };
3332
+ 409: {
3333
+ headers: {
3334
+ [name: string]: unknown;
3335
+ };
3336
+ content: {
3337
+ "application/json": components["schemas"]["ErrorResponse"];
3338
+ };
3339
+ };
3332
3340
  };
3333
3341
  };
3334
3342
  PlanEntitlementAvailabilitiesController_get: {
@@ -3422,6 +3430,14 @@ interface operations {
3422
3430
  "application/json": components["schemas"]["ErrorResponse"];
3423
3431
  };
3424
3432
  };
3433
+ 409: {
3434
+ headers: {
3435
+ [name: string]: unknown;
3436
+ };
3437
+ content: {
3438
+ "application/json": components["schemas"]["ErrorResponse"];
3439
+ };
3440
+ };
3425
3441
  };
3426
3442
  };
3427
3443
  PlanPricingGroupMappingsController_delete: {
@@ -3671,6 +3687,7 @@ interface operations {
3671
3687
  MembersController_sync: {
3672
3688
  parameters: {
3673
3689
  query?: {
3690
+ /** @description Registered target codes of the place this roster is for, sent comma-separated (`?targets=venue_a,coffee_shop`). Each member's entitlements are then narrowed to those usable there. Codes are matched exactly; an unregistered code is refused. Omit the parameter to receive the whole roster, with entitlements narrowed by date alone. Sending it with nothing in it (`?targets=`) is an error here, unlike on `GET /admit` and `GET /members/:id/entitlements`, where it means the caller named no place: this roster is used during an outage without being checked again, so an empty value would leave a gate short of entitlements it would not discover until the outage it kept the roster for. */targets?: string[];
3674
3691
  since?: string;
3675
3692
  };
3676
3693
  header?: never;
@@ -3687,6 +3704,14 @@ interface operations {
3687
3704
  "application/json": components["schemas"]["MemberSyncResponse"];
3688
3705
  };
3689
3706
  };
3707
+ 400: {
3708
+ headers: {
3709
+ [name: string]: unknown;
3710
+ };
3711
+ content: {
3712
+ "application/json": components["schemas"]["ErrorResponse"];
3713
+ };
3714
+ };
3690
3715
  };
3691
3716
  };
3692
3717
  MembersController_get: {
@@ -4768,6 +4793,14 @@ interface operations {
4768
4793
  "application/json": components["schemas"]["ErrorResponse"];
4769
4794
  };
4770
4795
  };
4796
+ 409: {
4797
+ headers: {
4798
+ [name: string]: unknown;
4799
+ };
4800
+ content: {
4801
+ "application/json": components["schemas"]["ErrorResponse"];
4802
+ };
4803
+ };
4771
4804
  };
4772
4805
  };
4773
4806
  CalendarRulesController_update: {
package/dist/index.d.mts CHANGED
@@ -1652,7 +1652,7 @@ interface components {
1652
1652
  reason: string;
1653
1653
  })[];
1654
1654
  admitted: boolean; /** @enum {string|null} */
1655
- deny_reason: "card_not_found" | "member_removed" | "membership_cancelled" | "membership_expired" | "membership_incomplete" | "payment_overdue" | null;
1655
+ deny_reason: "card_not_found" | "free_entry_not_granted" | "free_entry_unavailable" | "member_removed" | "membership_cancelled" | "membership_expired" | "membership_incomplete" | "payment_overdue" | null;
1656
1656
  member: components["schemas"]["AdmitMember"] | null;
1657
1657
  membership_id: string | null; /** @enum {string|null} */
1658
1658
  membership_status: "active" | "cancelled" | "expired" | "grace_period" | "incomplete" | "past_due" | null;
@@ -3329,6 +3329,14 @@ interface operations {
3329
3329
  "application/json": components["schemas"]["ErrorResponse"];
3330
3330
  };
3331
3331
  };
3332
+ 409: {
3333
+ headers: {
3334
+ [name: string]: unknown;
3335
+ };
3336
+ content: {
3337
+ "application/json": components["schemas"]["ErrorResponse"];
3338
+ };
3339
+ };
3332
3340
  };
3333
3341
  };
3334
3342
  PlanEntitlementAvailabilitiesController_get: {
@@ -3422,6 +3430,14 @@ interface operations {
3422
3430
  "application/json": components["schemas"]["ErrorResponse"];
3423
3431
  };
3424
3432
  };
3433
+ 409: {
3434
+ headers: {
3435
+ [name: string]: unknown;
3436
+ };
3437
+ content: {
3438
+ "application/json": components["schemas"]["ErrorResponse"];
3439
+ };
3440
+ };
3425
3441
  };
3426
3442
  };
3427
3443
  PlanPricingGroupMappingsController_delete: {
@@ -3671,6 +3687,7 @@ interface operations {
3671
3687
  MembersController_sync: {
3672
3688
  parameters: {
3673
3689
  query?: {
3690
+ /** @description Registered target codes of the place this roster is for, sent comma-separated (`?targets=venue_a,coffee_shop`). Each member's entitlements are then narrowed to those usable there. Codes are matched exactly; an unregistered code is refused. Omit the parameter to receive the whole roster, with entitlements narrowed by date alone. Sending it with nothing in it (`?targets=`) is an error here, unlike on `GET /admit` and `GET /members/:id/entitlements`, where it means the caller named no place: this roster is used during an outage without being checked again, so an empty value would leave a gate short of entitlements it would not discover until the outage it kept the roster for. */targets?: string[];
3674
3691
  since?: string;
3675
3692
  };
3676
3693
  header?: never;
@@ -3687,6 +3704,14 @@ interface operations {
3687
3704
  "application/json": components["schemas"]["MemberSyncResponse"];
3688
3705
  };
3689
3706
  };
3707
+ 400: {
3708
+ headers: {
3709
+ [name: string]: unknown;
3710
+ };
3711
+ content: {
3712
+ "application/json": components["schemas"]["ErrorResponse"];
3713
+ };
3714
+ };
3690
3715
  };
3691
3716
  };
3692
3717
  MembersController_get: {
@@ -4768,6 +4793,14 @@ interface operations {
4768
4793
  "application/json": components["schemas"]["ErrorResponse"];
4769
4794
  };
4770
4795
  };
4796
+ 409: {
4797
+ headers: {
4798
+ [name: string]: unknown;
4799
+ };
4800
+ content: {
4801
+ "application/json": components["schemas"]["ErrorResponse"];
4802
+ };
4803
+ };
4771
4804
  };
4772
4805
  };
4773
4806
  CalendarRulesController_update: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ophelio/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Official JavaScript / TypeScript SDK for the Ophel.io membership & entitlement API.",
5
5
  "license": "MIT",
6
6
  "author": "Ophel.io",