# Group Bookings — Business Rules (parity, implementable)

Area: **Shop · Group bookings** (NEW in dev). Demo encodes these; our app implements
**none** of them. Each rule cites the demo ref and is written to be portable to Yii2.

## Data shape
**BR-G01.** A party = N child `Booking` rows sharing one `groupBookingId` (`GRP-…`). Each
child is otherwise a normal booking (same finance/attribution/schedule). Add
`group_booking_id` + `guest_label` columns to `booking`. — `types.ts:351-357`.

**BR-G02.** The **organiser is the customer-of-record**: every child shares the same
`customer_id`. Other guests are not customers — only `guest_label` strings. — `store.ts:1549,1569`.

**BR-G03.** Default `guest_label` is `"Guest N"` (1-based) when none supplied; the create
UI sets the organiser's row to `"{organiserName} (organiser)"` when the organiser is also
getting a service. — `store.ts:1569`, `GroupBookingModal.tsx:182-188`.

## Creation validation
**BR-G04.** A party requires **≥1 guest** (store) but the create UI enforces **≥2**.
— store `store.ts:1463`; UI `GroupBookingModal.tsx:146`.

**BR-G05.** **Every guest must have ≥1 service.** — `store.ts:1464-1465`; test
`groupBooking.test.ts:99-107`.

**BR-G06.** **Distinct specialist per guest** — duplicates rejected because all guests
share one start time and would self-overlap. — `store.ts:1468-1473`; test `:67-77`.

**BR-G07.** Every child is placement-checked (`checkPlacement`: overlap / time-off /
working hours / specialist-can-perform-service) at the shared start. — `store.ts:1574-1586`.

**BR-G08.** **Atomic create**: build + validate every child first; persist only if all
pass. Any failure → `{error}`, **nothing** created. — `store.ts:1526-1592`; tests
`:79-97` (any conflict rolls back the whole party).

**BR-G09.** Children run **in parallel** → party slot length = `max` of per-guest
durations (each = Σ its service durations, floored 15 min). — `selectors.ts:254-263,290`;
test `:182` (`durationMin = max(45,60,75) = 75`).

## Pricing / payment / VAT
**BR-G10.** Per-guest value = Σ service prices − per-guest `discount`; party value = Σ
child `bookingValue`. — `store.ts:1536-1538`; `selectors.ts:287`.

**BR-G11.** **Whole-party payment timing** (one choice for all children): `online` →
collect full child value; `deposit` → collect `bookingValue × depositPct/100`; `on_visit`
→ collect 0. — `store.ts:1541-1545`. Deposit% is the shop's `depositPct`
(`GroupBookingModal.tsx:113-115`).

**BR-G12.** Available timings are gated by shop accept-flags, with **walk-in variants**
(`walkinOnline/walkinDeposit/walkinOnVisit`) vs app/online (`acceptOnline/acceptDeposit/
acceptOnVisit`). — `GroupBookingModal.tsx:55-68`.

**BR-G13.** **Finance invariance** — a group child derives the **same** charges as an
equivalent standalone booking (same `deriveBookingCharges`). VAT/marketing/Navagoo fees
must not fork for groups. — `store.ts:1587`; test `groupBooking.test.ts:185-229`.

**BR-G14.** **Attribution computed once** on the organiser (classify by source: app /
deep_link / shop_walkin + freeze-list), shared by all children. — `store.ts:1489-1519`.

## Collect
**BR-G15.** Collecting a party settles **each guest's outstanding balance** in one action
(organiser pays for all). — `store.ts:1667-1684`.

**BR-G16.** A single card **tip attaches to the FIRST guest with a balance only** — never
spread across guests (avoids multiplying one party tip). — `store.ts:1672-1683`.

## Status transitions / lifecycle
**BR-G17.** **Start all**: every `scheduled` child → `in_progress`. — `GroupBookingsView.tsx:60-63`.

**BR-G18.** **Complete all**: each child → `completed`, but the per-booking
**collect-to-complete gate** still applies — completion is blocked while the party has an
outstanding balance; UI forces "Collect all" first. — `store.ts:1686-1693`;
`GroupBookingsView.tsx:64-72`; test `:156-167`.

**BR-G19.** **Reschedule all**: re-validate every child at the new shared start *before*
moving any; reject atomically on any clash; children whose status can't be rescheduled
are left in place; each guest keeps specialist + services. — `store.ts:1614-1657`; test
`:113-138`.

**BR-G20.** **Cancel all**: cancel every child via the per-booking transition (reuses the
workflow gate + refund calc + ledger). Carries `cancelledBy` + `refundZone`. — `store.ts:1659-1665`; test `:140-144`.

**BR-G21.** Aggregate party status = the common child status, or **`'mixed'`** when
children differ. — `selectors.ts:285`.

**BR-G22.** Per-guest actions (reschedule/cancel/collect a single child) remain available
inside the expanded drawer — a child is still an independent booking. — `GroupBookingsView.tsx:132-141`.

## Scoping / permissions
**BR-G23.** **Shop-scoped**: the create modal's organiser list and party list are scoped
to the current shop; `groupsForShop` filters by `shopId`. Any Yii2 port must enforce the
shop scope server-side (the manager only sees/acts on their own shop's parties). —
`GroupBookingModal.tsx:49-52`; `selectors.ts:295-306`.

**BR-G24.** Sources: `shop_walkin` (manager), `app`, `deep_link` — drives both attribution
and which accept-flags apply. — `store.ts:329-337`, `:1502-1506`.

---
**Status on our side:** none of BR-G01..G24 are implemented. The `booking` table has no
group/guest columns, and no service/controller enforces any of these rules.
