# Group Available Slots — Backend Delivery Note → Mobile Team

**Re:** your `backend_group_booking_requirements.md` · **From:** backend · **Date:** 2026-08-16
**Status: ✅ LIVE** on `tailwind-poc`.
**Companion:** `GROUP_BOOKING_MOBILE_FLOW.md` / `GROUP_BOOKING_DELIVERY_TO_MOBILE.md` (party create/pay).

You asked for one endpoint that returns the **mutually-available** start times across all the
party's specialists so the group picks **one time everyone is free**. It's built — and it
replaces the client-side intersection you were doing on top of `agent-slots`.

---

## Why not just keep intersecting `agent-slots` client-side

Your `C.3` recipe fetched each specialist's **busy** intervals from `POST /booking/agent-slots`
and computed free windows on the device. Two problems that this endpoint fixes:

1. **`agent-slots` reads the wrong table.** It returns rows from `agent_slots`, which the
   **group-booking flow never writes** — group children are plain `booking` rows. So
   `agent-slots` **under-reports** a specialist already committed to another party, and your
   client-side intersection could have offered a time that then 409s at create. The new
   endpoint computes availability from the authoritative source (shifts − time-off −
   **all** active bookings, parties included).
2. **Duration per participant.** Each guest may take a different-length service. The server
   now computes each specialist's grid at that guest's own duration before intersecting.

---

## Endpoint

```
POST /booking/group-slots        (Bearer required — same as agent-slots)
```

We put it on `BookingController` (beside `agent-slots`), not `/shops/{id}/…`, because the whole
group flow is bearer-gated and this reuses the booking availability engine. If you strongly
prefer the `/shops/{id}/group-available-slots` URL, say so — trivial to add as an alias.

### Request — two accepted shapes

**Preferred (per-participant durations, most accurate):**
```json
{
  "shop_id": 16,
  "date": "2026-08-20",
  "participants": [
    { "specialist_id": 40, "service_ids": [65] },
    { "specialist_id": 41, "service_ids": [63, 64] }
  ]
}
```

**Flat (your original proposal — accepted as-is):**
```json
{ "shop_id": 16, "date": "2026-08-20", "specialist_ids": [40, 41] }
```
- Optional shared `service_ids[]` applies to every specialist in the flat shape.
- **Without any services, duration falls back to 15 min** (`MIN_DURATION`) — the grid is
  then "can everyone START here", not "…and finish their service". **Send `service_ids` per
  participant** for correct results (a 60-min service occupies more of the day than 15).

`participants[i].specialist_id` also accepts the key `agent_id`. `service_ids` are
**shop_service ids** (the same ids you send to `group-booking/create`).

### Response

```json
{
  "success": true, "status": 200,
  "data": {
    "date": "2026-08-20",
    "slots": [
      { "value": "2026-08-20 10:00", "from": "10:00", "to": "10:30", "label": "10:00 AM" },
      { "value": "2026-08-20 10:30", "from": "10:30", "to": "11:00", "label": "10:30 AM" }
    ],
    "book_now": { "value": "2026-08-20 10:00", "from": "10:00", "to": "10:30", "label": "10:00 AM" }
  }
}
```
- **Same row shape as `/booking/slots`** — render `from`/`label`; submit `from` (or `value`)
  as the party's `appointment_date` time to `group-booking/create`.
- `slots` are the times where **ALL** requested specialists are simultaneously free,
  chronological. Empty array = no common time that day.
- `to` is the FIRST participant's end — informational only. In a parallel party each guest
  finishes on their own; the shared fact is the **start**.
- `book_now` = `slots[0]` (or null).

---

## Business rules (all server-enforced — mirrors your requirements doc)

1. **Intersection** — a time is returned only if every requested specialist is free then.
2. **Specialist shift** — bounds come from each specialist's `user_shift` for that weekday
   (NOT the shop's open/close; a specialist's shift is the real constraint).
3. **Breaks / time-off** — `agent_time_off` (per-agent and shop-wide closures) is subtracted.
4. **Existing appointments** — any overlapping booking in an active status
   (scheduled/completed/in-progress/no-show/accepted), **including other group parties**,
   removes the time.
5. **Capability** — a specialist that can't perform its participant's `service_ids` yields
   no slots → empties the intersection (so you never offer an impossible pairing).
6. **Step** — the grid step is the shop's `slot_time_step` (e.g. 30 min), same as solo slots.
7. **Past times** — filtered when `date` is today.

## Not a booking — still re-checked at create

This is a **read**: it creates nothing and takes no lock. `group-booking/create` re-validates
every guest under a row lock, so a stale screen can only ever produce a clean structured
error, never a double-book (same guarantee as the solo flow).

---

## QA — verified live today (shop 16, specialists 40 & 41)

- Both free → 27 shared slots 10:00–23:00 (30-min services); 28 at 15-min fallback. ✓
- Booked specialist 40 at 14:00 → **14:00 dropped** from the intersection (26), while 13:30
  and 14:30 stayed, and specialist 41 alone still offered 14:00. ✓
- Specialist asked for a service they don't perform → **empty** intersection. ✓
- No bearer → **401**. ✓
- Flat `specialist_ids` and rich `participants` shapes both work. ✓

## Open questions for you

- **Q1:** Keep `POST /booking/group-slots`, or also expose `GET /shops/{id}/group-available-slots`?
- **Q2:** Do you need per-slot **per-specialist end times** in the response (for a party where
  durations differ and you want to show each person's finish)? Easy to add as
  `slots[i].ends: {"<specialist_id>": "HH:MM"}`.
- **Q3:** "Any available specialist" mode — a party that says "2 people, any barber" rather
  than naming specialists. Not built (you name specialists today); say if the app needs it.

## Deferred (deliberately)

- **Caching** (you suggested it): not added. The compute is a handful of `freeSlots` calls;
  correctness-on-every-booking matters more than shaving it, and cache-invalidation on every
  new booking for any of those specialists is error-prone. We'll add a short-TTL cache if you
  measure it as a real bottleneck.
- **Sequential/staggered parties** (your "Future Considerations"): today the model is
  **parallel** — everyone starts at the same time, each finishes on their own. If you move to
  staggered chairs, we extend the endpoint to validate the full staggered block.
