@medalsocial/sdk 1.8.0 → 1.10.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
@@ -268,10 +268,56 @@ if (summary.can_reschedule) {
268
268
 
269
269
  `can_cancel` / `can_reschedule` already apply the policy windows — honour them rather than re-deriving from `cancel_window_hours`.
270
270
 
271
- **Booking writes are idempotent by default.** The SDK retries 429/5xx automatically, so every booking `POST` (`create`, `cancel`, `reschedule`, `markNoShow`, and both `manage` writes) carries a generated `Idempotency-Key` — a retry after a gateway failure replays the original result instead of booking the slot twice. Supply your own `options.idempotencyKey` to extend that guarantee across *your* retries too: the server remembers a key for 24 hours, keyed by `(key, workspace, method + path)`.
271
+ **Booking writes are idempotent by default.** The SDK retries 429/5xx automatically, so every booking `POST` (`create`, `cancel`, `reschedule`, `markNoShow`, `payment.start`, and the `manage` writes) carries a generated `Idempotency-Key` — a retry after a gateway failure replays the original result instead of booking the slot twice. Supply your own `options.idempotencyKey` to extend that guarantee across *your* retries too: the server remembers a key for 24 hours, keyed by `(key, workspace, method + path)`.
272
272
 
273
273
  `update(id, input)` requires at least one of `notes` / `internal_notes`; `update(id, {})` is a compile error, matching the API's own 400.
274
274
 
275
+ #### Persons, relations and events
276
+
277
+ `medal.bookings.persons` are the children, pets, or employees a contact books for — no login of their own. `medal.bookings.relations` links two contacts directionally (guardian, employer, partner, and so on). `medal.bookings.events` are arrangementer — scheduled group sessions bookings register against; registering a booking *to* an event ships in a later release, so this is a read/create surface today.
278
+
279
+ ```ts
280
+ // Persons a contact books for (active-only unless include_inactive)
281
+ const { data: persons } = await medal.bookings.persons.list(contact.id);
282
+
283
+ // Add a person under a contact
284
+ const { data: person } = await medal.bookings.persons.create({
285
+ contact_id: contact.id,
286
+ name: 'Ola',
287
+ birth_year: 2018,
288
+ relation_type: 'guardian',
289
+ });
290
+
291
+ // Events in a date range (yyyy-mm-dd, inclusive) — one month
292
+ const { data: events } = await medal.bookings.events.list({
293
+ from: '2026-09-01',
294
+ to: '2026-09-30',
295
+ });
296
+ ```
297
+
298
+ #### Payments
299
+
300
+ `medal.bookings.payment` takes a Vipps payment on a booking as the business; `medal.bookings.manage.payment` does the same on the customer's behalf, keyed by the manage token. `Booking.payment_mode` (and `ManageSummary.payment_mode`) says what the booking requires — `payment_status: 'none'` cannot tell "owes nothing" from "has not paid yet".
301
+
302
+ ```ts
303
+ const { data: started } = await medal.bookings.payment.start(booking.id, {
304
+ return_url: 'https://example.no/retur',
305
+ terms_accepted: true, // must be literally true — 400 otherwise
306
+ terms_version: '2026-09',
307
+ });
308
+ // Hand started.redirect_url to the Vipps Widget SDK UNCHANGED. It is SHOW-ONCE:
309
+ // payment.get() never returns it, and the payment behind it expires in 10 minutes.
310
+
311
+ const { data: payment } = await medal.bookings.payment.get(booking.id);
312
+ payment.state; // created | authorized | captured | cancelled | refunded | failed | expired
313
+ payment.captured_ore; // integer øre — a Vipps payment stays AUTHORIZED after a capture,
314
+ // so the aggregates, not `state`, say what actually moved
315
+ ```
316
+
317
+ **Never trust the return redirect.** The customer can close the tab, hit back, or edit the URL — the outcome reaches you through Medal. Poll `payment.get(...)` on your return page, or read the booking's `payment_status`.
318
+
319
+ The customer must accept your terms **before** a payment is initiated: `terms_accepted: true` is required by the type *and* by the API, and a request without it leaves no payment (and no consent record) behind. A `return_url` your workspace's own sites do not vouch for answers **422**, not 400 — the URL parses, it is just not yours. Starting a second payment while one is live answers **409**, and `payment.get(...)` on a booking with no payment yet answers **404**, exactly as an unknown booking does.
320
+
275
321
  ### Customer portal
276
322
 
277
323
  Self-service for the workspace's own customers: they sign in with an e-mailed one-time code, then see and change their profile, list their bookings, export their data, or erase their account. The API key needs `read:portal` and `write:portal`.
@@ -291,6 +337,8 @@ const { data: session } = await medal.portal.login.verify({ email: 'ida@example.
291
337
 
292
338
  // 3. Session-bound calls, from your server, with the token read back from the cookie
293
339
  const { data: me } = await medal.portal.me(session.session_token);
340
+ // me.persons (the contact's bookings.persons) and me.labels (the workspace's own
341
+ // words for the person concept, e.g. { person: 'Barn', persons: 'Barn' }) are new
294
342
  const { data: bookings } = await medal.portal.myBookings(session.session_token);
295
343
  // bookings.upcoming[i].manage_token is set while the booking is still manageable —
296
344
  // it opens your site's manage page (medal.bookings.manage.*); past bookings carry null