@medalsocial/sdk 1.6.0 → 1.7.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
@@ -204,6 +204,74 @@ const { data: removed } = await medal.deals.remove(deal.id);
204
204
  console.log(updated.success, unlinked.success, removed.success);
205
205
  ```
206
206
 
207
+ ### Bookings
208
+
209
+ Money is always **integer øre** (`amount_ore`, `price_ore`) — never a float, never kroner. Timestamps come back as ISO 8601 strings; on the way in, either Unix milliseconds or an ISO string is accepted.
210
+
211
+ ```ts
212
+ // Catalogue + free slots
213
+ const { data: services } = await medal.bookings.listServices();
214
+ const { data: resources } = await medal.bookings.listResources();
215
+ const { data: slots } = await medal.bookings.availability({
216
+ service_id: services[0].id,
217
+ from_ts: Date.now(),
218
+ to_ts: Date.now() + 7 * 86_400_000,
219
+ resource_id: resources[0].id, // optional — defaults to every capable resource
220
+ });
221
+
222
+ // Book a party — all items succeed or none do (max 50)
223
+ const { data: created } = await medal.bookings.create(
224
+ {
225
+ items: [
226
+ { service_id: services[0].id, start_ts: slots[0].start_ts! },
227
+ { service_id: services[0].id, start_ts: slots[1].start_ts!, booked_for_name: 'Ida', booked_for_birth_year: 2018 },
228
+ ],
229
+ contact: { phone: '+4790000000', email: 'ida@example.com', name: 'Ida Hansen' },
230
+ notes: 'Bursdag',
231
+ },
232
+ { idempotencyKey: crypto.randomUUID() }, // optional — see the note below
233
+ );
234
+ // created.bookings[i].manage_token is returned EXACTLY ONCE (only its hash is
235
+ // stored, and an idempotent replay omits it) — persist it for the manage link.
236
+ // It is UNRECOVERABLE if lost: `Booking` has no token field, so re-reading the
237
+ // booking gives you nothing. Reschedule to mint a fresh one, or act by id.
238
+
239
+ // Staff actions — policy windows are bypassed, cancels attributed to staff
240
+ const { data: booking } = await medal.bookings.get(created.bookings[0].id);
241
+ await medal.bookings.update(booking.id, { internal_notes: 'Allergisk mot parfyme' });
242
+ await medal.bookings.cancel(booking.id, { reason: 'Sykdom' });
243
+ const { data: moved } = await medal.bookings.reschedule(booking.id, {
244
+ new_start_ts: '2026-09-02T09:00:00.000Z',
245
+ new_resource_id: resources[0].id,
246
+ });
247
+ // moved.booking_id is a NEW id with a NEW manage_token — the old booking is cancelled
248
+ await medal.bookings.markNoShow(moved.booking_id);
249
+
250
+ // Listing — check `truncated`: when true, matching bookings exist that no
251
+ // cursor reaches, so narrow the from_ts/to_ts window
252
+ const page = await medal.bookings.list({ status: 'confirmed', from_ts: Date.now(), limit: 50 });
253
+ console.log(page.pagination.has_more, page.pagination.next_cursor, page.pagination.truncated);
254
+ ```
255
+
256
+ **Customer actions go through `medal.bookings.manage`**, keyed by the manage token instead of the booking id. This is not the same route with a different lookup key: the workspace's cancel/reschedule windows are **enforced**, and the cancel is attributed to the customer. Use it to relay a customer's own click on the link in their confirmation email.
257
+
258
+ ```ts
259
+ const { data: summary } = await medal.bookings.manage.get(manageToken);
260
+ if (summary.can_cancel) await medal.bookings.manage.cancel(manageToken, { reason: 'Endret plan' });
261
+ if (summary.can_reschedule) {
262
+ const { data } = await medal.bookings.manage.reschedule(manageToken, {
263
+ new_start_ts: '2026-09-02T09:00:00.000Z',
264
+ });
265
+ console.log(data.booking_id, data.manage_token); // old token stops working
266
+ }
267
+ ```
268
+
269
+ `can_cancel` / `can_reschedule` already apply the policy windows — honour them rather than re-deriving from `cancel_window_hours`.
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)`.
272
+
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
+
207
275
  ### GDPR
208
276
 
209
277
  ```ts