bookbaltic

com.bookbaltic.services.staff_choice

Status: Draft. Version 2026-11-03. Part of BookBaltic Scheduling Extensions for USP / UCP Local Services.

Extends: dev.usp-protocol.services.availability, dev.usp-protocol.services.bookings

Schema: https://bookbaltic.com/schemas/usp/staff_choice.json

What it adds: "Any staff" as a first-class choice, pinned vs preferred staff, what happens when the staff member drops out


1. Staff selection

Base USP already does: list staff (resources[].options), filter by resource_id, book by slot. Gaps: (i) with N therapists free at 15:00, USP requires N slots; an agent that does not care must deduplicate, and the merchant cannot keep assignment flexible; (ii) no way to say whether the guest insists on that person; (iii) no defined behaviour when the assigned staff member becomes unavailable.

Related: discussion #1022 lists what a small salon sells: a service, a time slot and "often a specific professional (or anyone available)"; staff_mode: "any" is our concrete shape for it and is an extension of USP 4.1 (one slot per resource combination). The goods side already has the same pattern: #896 lets the buyer agree ahead of time what happens on a surprise after the order (otherwise: cancel | contact_buyer), and #944 defines how a business asks for a fresh buyer decision after the order, with a decision_deadline. A general post-order consultation protocol stays with #944; awaiting_buyer is one domain case of it. staff_preference and staff_change are the services counterpart; if #896 and #944 land, staff_preference can be expressed as a services contingency and awaiting_buyer as a #944 consultation with the same deadline. The fields stay on the booking so free bookings without checkout (#922 Phase 2) work too. Against #922: staff_mode is input to Phase 1 availability, staff_preference and staff_change to Phase 2 post-booking.

Not live: HEADSPA does not publish staff, so the extension is specified but not declared in its profile.

Schema

{
  "$id": "https://bookbaltic.com/schemas/usp/staff_choice.json",
  "requires": { "protocol": { "min": "2026-08-20" },
    "capabilities": { "dev.usp-protocol.services.availability": { "min": "2026-08-20" },
                      "dev.usp-protocol.services.bookings": { "min": "2026-08-20" } } },
  "$defs": {
    "AvailabilityQueryExt": { "type": "object", "properties": {
      "staff_mode": { "enum": ["any", "per_staff"], "default": "per_staff",
        "description": "any: one slot per start, staff assigned by the merchant. per_staff: base USP behaviour." } } },
    "SlotExt": { "type": "object", "properties": {
      "staff_assignment": { "enum": ["assigned", "any"] },
      "eligible_staff_count": { "type": "integer", "minimum": 1,
        "description": "Optional. How many staff could take this start; never their identities." } } },
    "BookingRequestExt": { "type": "object", "properties": {
      "staff_preference": { "enum": ["required", "preferred", "none"], "default": "preferred" } } },
    "BookingExt": { "type": "object", "properties": {
      "staff_preference": { "enum": ["required", "preferred", "none"] },
      "staff_change": { "type": "object", "required": ["changed_at", "outcome"], "properties": {
        "changed_at": { "type": "string", "format": "date-time" },
        "outcome": { "enum": ["reassigned", "awaiting_buyer", "canceled_by_business"] },
        "previous": { "$ref": "#/$defs/StaffRef" },
        "current": { "$ref": "#/$defs/StaffRef" },
        "decision_deadline": { "type": "string", "format": "date-time",
          "description": "As UCP #944: the last instant a buyer decision is admitted. Required when outcome is awaiting_buyer." } },
        "if": { "properties": { "outcome": { "const": "awaiting_buyer" } } },
        "then": { "required": ["decision_deadline"] } } } },
    "StaffRef": { "type": "object", "required": ["id", "name"], "additionalProperties": false,
      "properties": { "id": { "type": "string" }, "name": { "type": "string", "description": "Display name only" } } }
  }
}

Rules

Example (HEADSPA, illustrative staff; not declared in its profile)

Request:

{ "service_id": "svc-aqua-head-spa", "start_date": "2026-11-07", "end_date": "2026-11-07",
  "party_size": 1, "com.bookbaltic.services.staff_choice": { "staff_mode": "any" } }

Response slot:

{ "id": "slot_20261107_1400_any", "service_id": "svc-aqua-head-spa",
  "start": "2026-11-07T14:00:00+02:00", "end": "2026-11-07T15:30:00+02:00", "duration": "PT90M",
  "state": "available", "resources": [{ "id": "room-1", "type": "room", "name": "Ritual room" }],
  "com.bookbaltic.services.staff_choice": { "staff_assignment": "any", "eligible_staff_count": 2 } }

Booking after the pinned therapist dropped out:

{ "com.bookbaltic.services.staff_choice": {
    "staff_preference": "required",
    "staff_change": { "changed_at": "2026-11-06T09:12:00+02:00", "outcome": "awaiting_buyer",
      "previous": { "id": "staff-anna", "name": "Anna" },
      "current": { "id": "staff-liga", "name": "Liga" },
      "decision_deadline": "2026-11-06T20:00:00+02:00" } } }

Without the extension: the agent gets base per-staff slots and books one; it cannot express "any" or "required", and a dropout arrives as an unexplained booking.rescheduled.

Open questions: should staff_change become a new USP webhook booking.resource_changed rather than ride on booking.rescheduled? Should per-staff price differences use slot.pricing only (we assume yes)?