# Parity table — scheduling / availability algebra

Demo (canonical): `/private/tmp/Navagoo_MI_dev/navagoo-app/src/lib/schedule.ts`
Ours: `frontend/components/BookingScheduleService.php` (BSS)

| Demo behavior | Demo ref | Our ref | Status | Note |
|---|---|---|---|---|
| `'HH:mm' → minutes` | schedule.ts:48 | BSS.php:50 | done | ours also handles 12h shop window via `wallToMin` BSS.php:63 |
| Snap to step (round) | schedule.ts:82 | BSS.php:90 | done | step from `slot_time_step ?? 15` |
| Sun-first weekday token | schedule.ts:77 | BSS.php:99 | done | ours maps to `UserProfile::DAYS_MAP` (Sat=1..Fri=7) |
| Half-open overlap test | schedule.ts:88 | BSS.php:263 | done | identical |
| Overnight = `close<=open` (window extends >1440) | schedule.ts:93,124 | BSS.php:141 | done | window only |
| Overnight *shift* tail extends past midnight | schedule.ts:185 | BSS.php:126 | done | |
| **Business-day attribution of bookings/time-off** (`businessDayOf`) | schedule.ts:100 | — | missing | ours matches by literal calendar date; overnight appointments mis-placed |
| **`toBusinessMinutes`/`fromBusinessMinutes`** continuous-minute remap | schedule.ts:109,114 | — | missing | no coordinate shift across midnight |
| **Booking duration = Σ service durations, derived, floor 15** | schedule.ts:161,170 | BSS.php:286,525 | partial | ours reads stored `to_hour`; fallback = 60-min default, no Σ-service path |
| Interval subtraction (working − offs) | schedule.ts:133,150 | BSS.php:194 | done | direct port |
| `availability` = shifts − time-off, sorted | schedule.ts:212 | BSS.php:217 | done | |
| `isWorkingOn` | schedule.ts:226 | BSS.php:135 | done | |
| Time-off scope (shop-wide OR specialist) | schedule.ts:195,207 | BSS.php:157,161 | done | ours adds `all_day` flag → window span |
| Bookings-for-day selector (non-cancelled) | schedule.ts:233 | BSS.php:278,499 | done | active set via `Booking::statusesFilter()` |
| `checkPlacement` rejection order (overlap→time-off→outside→cannot-perform) | schedule.ts:312 | BSS.php:273 | done | |
| Overlap excludes `ignoreBookingId` | schedule.ts:323 | BSS.php:283 | done | |
| Inside-shift containment | schedule.ts:336 | BSS.php:307 | done | |
| **Can-perform** (no-linkage ⇒ anyone) | schedule.ts:289 | BSS.php:250 | partial | ours requires explicit `user_shop_service` row for every service; no "unrestricted" shortcut |
| Placement reason text (4 strings) | schedule.ts:352 | BSS.php:327 | done | ours bilingual via `Yii::t` |
| Lane packing (greedy first-fit, cluster reset) | schedule.ts:374 | BSS.php:438 | done | |
| Calendar columns (working + muted-with-booking) | schedule.ts:269 | BSS.php:384 | done | ours scopes to agents + sorts working-first |
| Muted column not draggable | schedule.ts:261 | BSS.php:552 | done | |
| Day bounds widen to cover overflow | schedule.ts:416 | BSS.php:558,593 | done | ours also snaps to whole hours |
| Placement validated on create/move (not enumerated) | store.ts:1337,1574 | BSS.php:631 (endpoints) | done | different invocation pattern |
| Shaded out-of-shift gaps (complement) | (component) | BSS.php:230 | done | ours pre-computes; additive |
| Free-slot enumeration / slot picker | — | BSS.php:348 | n/a (ours-only) | demo uses drag-to-place; no slot list |
| `dayLayout` view payload (zoom, hours, now-line, URLs) | — (in components) | BSS.php:488 | n/a (ours-only) | glue layer |
| Drag-to-place UX on the grid | store drag actions store.ts:1388,1424 | not in this service | unverified | confirm in `frontend/views/booking/*` day view |

## Summary

The interval algebra, availability subtraction, four-reason placement check (and its rejection
order), lane packing and column composition are faithful, equivalent ports — the algebra core is in
good parity. Two structural gaps pull the score down:

1. **No business-day / overnight coordinate model** (rules 4 in business.md) — overnight *appointments*
   (not just shift shading) are mis-attributed/dropped. This is the demo's headline abstraction.
2. **Booking duration is read from a stored `to_hour`, not derived from Σ-service-duration** — diverges
   from the demo's "end is always derived, finance untouched" contract; falls back to a 60-min block.

Smaller: can-perform semantics are stricter than the demo (no "unrestricted service" shortcut).
Ours adds a free-slot picker and a view payload with no demo analog (additive, not regressions).

**Area score: 78%**

## Verified verdict (adversarial)

Re-read both refs line-by-line against OUR models. The algebra primitives are faithful, but
several "done" rows are only correct for **day shops** and one carries a real, area-wide bug the
first pass missed. Concrete refutations:

### NEW bug the analyst missed — cancelled bookings are NOT excluded
- Demo excludes cancelled everywhere: `bookingsForShopOnBusinessDay` filters `b.status !== 'cancelled'`
  (schedule.ts:243); overlap iterates only those rows.
- Ours uses `activeStatuses()` = `array_keys(Booking::statusesFilter())` (BSS.php:38). But
  `statusesFilter()` (common/models/base/Booking.php:353-363) returns keys
  `[SCHEDULED=2, COMPLETED=3, INPROGRESS=4, CANCELED=5, NO_SHOW=9, CANCELED_BY_SHOP=7]`
  — it **includes CANCELED (5) and CANCELED_BY_SHOP (7)**. So cancelled bookings still occupy
  the calendar, still block overlap in `checkPlacement` (BSS.php:279), still render as columns/blocks
  in `dayLayout` (BSS.php:500), and still pad `dayBounds`. This is a behavioral divergence, not a port.

### Coordinate-grid divergence makes "done" overnight rows wrong
The two `missing` rows (businessDayOf / toBusinessMinutes) are not isolated gaps — they break
downstream "done" rows for overnight shops:
- Bookings/time-off `from_hour` come from `clockToMin` raw (0..1439, BSS.php:286, 169-173); only the
  shift/window/off **end** gets `+= 1440`. A 01:00 booking sits at minute 60 while its overnight shift
  `22:00→02:00` is `1320..1560`. Inside-shift containment (BSS.php:308-313) therefore rejects valid
  post-midnight placements, and overlap (BSS.php:295) compares operands on mismatched grids.
- Day bookings are matched by literal `booking_date LIKE 'date%'` (BSS.php:280, 501) with no
  `businessDayOf` — post-midnight appointments are attributed to the wrong day → dropped/mis-placed.

### Calendar columns — no active-status filter
- Demo working set requires `sp.status === 'active'` (schedule.ts:275).
- Ours queries `User` by `user_type=AGENT, shop_id` only (BSS.php:386-389); no `status = STATUS_ACTIVE`
  (User has `STATUS_ACTIVE=2`). A non-active agent with shift rows renders as a full, **draggable**
  working column. Downgrade.

### Corrected rows
| Demo behavior | Status (was → now) | Evidence |
|---|---|---|
| Overlap excludes ignoreBookingId / **cancelled** | done → **partial** | ignore works; cancelled NOT excluded — `statusesFilter()` includes 5 & 7 (Booking.php:353) |
| Bookings-for-day selector (non-cancelled) | done → **partial** | same cancelled leak (BSS.php:38,280) |
| Half-open overlap test | done → **partial** | fn correct; operands on wrong grid for overnight (BSS.php:286 vs workingBlocks +1440) |
| Inside-shift containment | done → **partial** | math correct; rejects valid post-midnight placements (60 vs 1320..1560) |
| `checkPlacement` rejection order | done → **partial** | order faithful, but overlap step inherits cancelled leak + grid mismatch |
| Calendar columns (working + muted) | done → **partial** | no `status=ACTIVE` filter (BSS.php:386); inactive agents shown live |
| Lane packing / day bounds / muted-not-draggable | done (day-shop) | algebra faithful; overnight correctness blocked by missing business-day model |
| Duration = Σ service durations, floor 15 | partial (confirmed) | no Σ path anywhere; floor 15 only in freeSlots (BSS.php:352), not checkPlacement/dayLayout; 60-min fallback (BSS.php:293,528,556) |
| Can-perform (no-linkage ⇒ anyone) | partial (confirmed) | ours always requires `user_shop_service` rows (BSS.php:250); stricter than demo |
| Time-off scope, interval subtract, availability, snap, HH:mm, reason text | done (confirmed) | faithful ports |

### Adjusted area score: 61%
The cancelled-exclusion bug is a correctness regression touching overlap, columns, bounds and the
day payload simultaneously; the overnight grid divergence demotes four more "done" rows to day-shop-only.
The pure-algebra primitives remain solid, so the floor stays well above failing — but 78% over-credited
overnight parity and missed an active-data correctness bug.
