# BusValley Cross-Midnight Booking Audit

**Date:** 2026-06-03  
**Scope:** Duplicate seat bookings, wrong ticket dates, wrong journey lists for overnight (cross-midnight) trips  
**Mode:** Read-only audit — no code changes

---

## 1. Executive Summary

BusValley uses **three different calendar meanings** for “journey date” across the codebase:

| Concept | Typical storage | Example (bus leaves City A 16 Aug 22:00, pickup at A 17 Aug 01:15) |
|--------|-------------------|----------------------------------------------------------------------|
| **Search / boarding day** | User input, UI `journeydate` | `2026-08-17` |
| **Bus run / service day** | `temporarybooks.journey_date`, `journeylists.journeydate` | `2026-08-16` |
| **Ticket boarding day** | `tickets.journeydata` | `2026-08-17` |

Modern paths (`BookingInventorySupport`, `SegmentSeatInventoryService`, admin `Ticket::create`, API `Ticket::bookticket`) convert correctly when `pickstand` is present. **Legacy and parallel code paths** still treat the user search date as the bus run date, skip segment overlap, or write the wrong date into `journeylists`.

**Confirmed production pattern (TB30JAE7YF / TBJC5SKYOZ):** Two bookings on the same physical bus run and seat 2 were not blocked because `journeylists.journeydate` for the after-midnight pickup stored **boarding day** (`2026-08-16`) instead of **service day** (`2026-08-15`), so manifest/journey-list grouping and legacy inventory queries treated them as different runs.

**Highest-risk gaps:**

1. **Geidea payment** writes `journeylists.journeydate` via `bookjourneyGeidea()` using **boarding date**, not bus run date.
2. **Mobile trip search** (`Api\Trip::getAllTrip`) uses `pickStandId` before it is defined and does not filter subtrips by boarding-eligible pickups.
3. **Temporary hold overlap** (`getDynamicPickupDropCombinationsTemp`) uses `resolveDepartureDates()` with boarding day passed as bus-start — mismatched with how holds are stored (service day).
4. **No DB uniqueness** on (trip run, seat, segment); seat lock is application-level only.
5. **`modules/Ticket/Database/Controllers/*`** retain pre-overlap exact `journeydata` matching — not routed today but dangerous if re-enabled.

---

## 2. Canonical Date Semantics (Target Model)

| Field / parameter | Meaning |
|-------------------|---------|
| UI / API `journeydate`, `journey_date` (search) | **Boarding calendar day** at the selected pickup stand |
| `tickets.journeydata` | **Boarding calendar day** at `pick_stand_id` |
| `temporarybooks.journey_date` | **Bus run / service day** (when bus departs origin) |
| `journeylists.journeydate` | **Bus run / service day** (driver manifest key) |
| `busStartDate`, `bus_start`, `busRunForBooking()` return | **Bus run / service day** |
| `busStartDate` in `Ticket::findtrip` seat-count helper | Service day derived from search date + first eligible pick stand |

**Inventory rule:** One physical bus = one `trip_id` + one **service day**. All main/sub-trip segments on that run share seat inventory via **stoppage segment overlap**, not separate subtrip buckets.

---

## 3. Date Calculation — Trace by File

### 3.1 Core libraries (single source of truth — when used)

| File | Function | Input | Output / behavior |
|------|----------|-------|-------------------|
| `app/Libraries/BookingInventorySupport.php` | `normalizeCalendarDate()` | Any datetime string | `Y-m-d` in Africa/Cairo |
| | `resolveSearchDepartureDates()` | trip, subtrip, **boarding search date**, pickStandId | `{ bus_start, boarding, all }` |
| | `busRunForBooking()` | trip, subtrip, **boarding search date**, pickStandId | Service day for holds / manifest |
| | `ticketBoardingDateForStorage()` | trip, subtrip, **service day**, pickStandId | Value for `tickets.journeydata` |
| | `resolveDepartureDates()` | trip, subtrip, **service day**, optional boarding, pickStandId | Both dates for inventory SQL |
| | `ticketBelongsToBusRun()` | ticket `journeydata`, pick_stand, target service day | Post-filter for overnight tickets |
| | `holdBelongsToBusRun()` | `temporarybooks.journey_date`, target service day | Hold matches bus run |
| | `applyTicketJourneyDateFilter()` | builder, departure dates | `WHERE journeydata IN (service, boarding)` |
| | `applyTemporaryBookJourneyDateFilter()` | builder, departure dates | `WHERE journey_date` on **service** dates |
| | `lockTripRowForUpdate()` | trip id | `SELECT … FOR UPDATE` on `trips` row |
| `app/Libraries/TripJourneyDateHelper.php` | `boardingDayToBusRunDate()` | boarding day + pick offset | Service day |
| | `getBoardingDateFromBusStartDate()` | service day + pick offset | Boarding day |
| | `getTicketJourneyDateForStorage()` | service day + pick stand | Ticket storage date |
| | `getPassengerBoardingDateForDisplay()` | stored ticket/list date + pick stand | Display boarding day |
| | `applyJourneyListBoardingDatePrefetch()` | SQL prefetch for list search | Broad date OR for boarding search |
| | `pickStandIdsBoardingOnSearchDate()` | search date | Eligible pickup stands |
| `app/Libraries/OvernightTripRunSupport.php` | Facade over helper | search / manifest modes | Boarding search, manifest match |
| `app/Libraries/SegmentSeatInventoryService.php` | `collectSeatBuckets()` | subtrip, **boarding param**, pickStandId | Unified booked/hold/block seats |
| `app/Libraries/TemporaryBookHoldService.php` | `saveHold()` | **service day** `journeyDateYmd` | Writes `temporarybooks.journey_date` |

### 3.2 Trip search (listing trips)

| File | Function | Date behavior | Notes |
|------|----------|---------------|-------|
| `modules/Ticket/Controllers/Ticket.php` | `findtrip()` | `filterjourneydate` = boarding search; per subtrip `pickStandsForBoardingSearchDate()` → `resolveSearchDepartureDates()` → `busStartDate` | **Correct** for overnight filtering |
| | `findtrip()` | `$subTrip->journeydate` = user search date; `busStartDate` exposed separately | **Correct** display |
| | `findtrip()` | Lines 777–783: `ticketModel->where('journeydata', $busStartDate)` never executed | Stale model state risk; seat count uses `getDynamicPickupDropCombinations()` instead |
| | `getSingleTrip()` | `resolveSearchDepartureDates()` + `pickstand` query param | **Correct** when pickstand sent |
| `modules/Trip/Controllers/Api/Trip.php` | `getAllTrip()` | User `journeydate` = boarding search | Mobile search |
| | `getAllTrip()` | Lines 768–773: `resolveSearchDepartureDates(..., $pickStandId)` | **BUG:** `$pickStandId` used **before** assignment at line 783 |
| | `getAllTrip()` | No `pickStandsForBoardingSearchDate()` gate | After-midnight trips may appear on wrong search dates |
| `modules/Trip/Controllers/Trip.php` | `findtrip()` / `getAllTrip()` | Same family as above | Admin web trip module |

### 3.3 Seat availability

| File | Function | Date param semantics | Match with ticket creation? |
|------|----------|---------------------|----------------------------|
| `SegmentSeatInventoryService` | `collectUnavailableSeatNumbers()` | **Boarding search date** | **Yes** — same as `isAlreadybookedsubtrip()` |
| `modules/Ticket/Controllers/Ticket.php` | `getDynamicPickupDropCombinations()` | `resolveSearchDepartureDates($date, …)` — `$date` = boarding | **Yes** |
| `modules/Ticket/Controllers/Api/TemporaryBook.php` | `getDynamicPickupDropCombinations()` | Same as admin Ticket | **Yes** (tickets) |
| | `getDynamicPickupDropCombinationsTemp()` | `resolveDepartureDates($date as bus_start, …)` | **No** — `$date` is boarding day from API |
| `modules/Ticket/Database/Controllers/Ticket.php` | `getDynamicPickupDropCombinations()` | `WHERE journeydata = $date` exact | **No** — no overlap, no conversion |

### 3.4 Temporary holds

| File | Function | `temporarybooks.journey_date` stored as | Check uses |
|------|----------|----------------------------------------|------------|
| `Api/TemporaryBook::store()` | `busRunForBooking()` → service day | Service day | — |
| `Api/TemporaryBook::checkSeats()` | Overlap via `getDynamicPickupDropCombinations` (OK) + `getDynamicPickupDropCombinationsTemp` (wrong date semantics) | — | **Partial mismatch** on hold leg |
| `Api/TemporaryBook::bookingcheck()` | `$busStartDate = (string) $journeydate` (boarding); ticket overlap OK; hold segment uses `resolveDepartureDates` with wrong bus_start | — | **Partial mismatch** |
| `TemporaryBookHoldService::saveHold()` | Normalized service day | Service day | No pre-insert seat conflict check |

### 3.5 Final ticket creation

| File | Function | `tickets.journeydata` | `journeylists.journeydate` | Seat check |
|------|----------|----------------------|---------------------------|------------|
| `Ticket\Controllers\Ticket.php` | `create()` | `ticketBoardingDateForStorage()` | `journeylist(..., $busStartDate)` service day | `lockTripRowForUpdate` + `isAlreadybookedsubtrip()` |
| `Api\Ticket.php` | `bookticket()` | Same | `journeylist(..., $manifestDate)` service day | Lock + segment check |
| `Api\Ticket.php` | `laterBookticket()` | Same | Same pattern | Lock + segment check |
| `Api\Ticket.php` | `processPayment()` / Geidea session | Converts to boarding for `paystatusgeidea.journeydata` | — | Pre-payment seat check (no lock) |
| `Api\Ticket.php` | `bookticketGeidea()` | Recomputed `ticketJourneyDate` at insert | **`bookjourneyGeidea($journeyDate)` — boarding date** | Lock at insert only |
| `Ticket\Database\Controllers\Ticket.php` | `create()` | Raw `strtotime(journeydate)` | Raw request date | `isAlreadybooked()` exact match only |
| `Ticket\Database\Controllers\Api\Ticket.php` | `bookticket()` | Raw normalized request date | Raw date in `journeylist()` | No segment overlap |

### 3.6 Journey list & display

| File | Function | Filter mode | Date source |
|------|----------|-------------|-------------|
| `Controllers/Journeylist.php` | `getJourneylistData()` | Default `list_mode=boarding`: passenger boarding day | `applyJourneyListBoardingDatePrefetch()` + `passengerListMatchesSearchDate()` |
| | `matchesJourneyListSearchRow()` | `list_mode=service`: driver manifest | `journeylists.journeydate` as service day |
| `Api\Ticket.php` | `singelBooking()` | Display only | `getPassengerBoardingDateForDisplay()` rewrites `journeydata` in response |
| `Api\Ticket.php` | `bookjourneyGeidea()` | Inserts list row | **Raw `$journeydate` (boarding)** — no `busRunForBooking()` |

### 3.7 Admin hold seats (separate product)

| File | Function | Behavior |
|------|----------|----------|
| `modules/HoldSeat/Controllers/HoldSeat.php` | create / list | `journeydata`–`journeydataend` range; not aligned with bus-run conversion |
| `HoldSeatSupport` | segment filter in inventory | Merged in `SegmentSeatInventoryService::collectAdminBlockedSeatNumbers()` |

---

## 4. Booking Flows

### 4.1 Website (admin backend)

```
User selects boarding date → Ticket::findtrip()
  → pickStandsForBoardingSearchDate() filters subtrips
  → expand row → getSingleTrip(subtripId, date, pickstand)
  → SegmentSeatInventoryService seat map
  → POST Ticket::create()
  → busRunForBooking + ticketBoardingDateForStorage
  → lockTripRowForUpdate + isAlreadybookedsubtrip()
  → insert tickets + journeylist(manifestDate)
```

**Routing:** `modules/Ticket/Config/Routes.php` → `Controllers\Ticket` (not `Database\Controllers`).

### 4.2 Mobile / public API

```
POST modules/api/v1/trips → Api\Trip::getAllTrip()     [search list]
GET  seatnew/{subtrip}/{date} → Api\Ticket::busSeatNew() [seat map]
POST modules/api/v1/tickets/checkseats → TemporaryBook::checkSeats()
POST modules/api/v1/tickets/bookingcheck → TemporaryBook::bookingcheck()
POST modules/api/v1/tickets/booking → Api\Ticket::bookticket()
POST payment-process → Geidea session → bookticketGeidea() webhook/callback
```

### 4.3 Admin booking (office)

Same as website `Ticket::create()`; `booking.js` sends `journeydate` (boarding) + `pickstand` on hold and booking.

### 4.4 Temporary seat hold

```
checkSeats → overlap check → store() → TemporaryBookHoldService::saveHold(service day)
```

Hold key: `(subtrip_id, ticket_token, DATE(journey_date))` — replaces prior hold for same token only.

### 4.5 Final ticket creation

All modern paths should persist:

- `tickets.journeydata` = boarding day  
- `journeylists.journeydate` = service day  
- Inventory queries keyed on service day + `ticketBelongsToBusRun()`

---

## 5. Cross-Flow Consistency Matrix

| Step | Admin web | API bookticket | Temp hold save | Temp hold check (tickets) | Temp hold check (holds) | Geidea finalize |
|------|-----------|----------------|----------------|---------------------------|-------------------------|-----------------|
| User date meaning | Boarding | Boarding | Boarding | Boarding | Boarding (mislabeled bus_start) | Boarding in session |
| Stored ticket date | Boarding | Boarding | — | — | — | Boarding (recomputed) |
| Stored hold date | — | — | **Service** | — | — | — |
| Stored manifest date | **Service** | **Service** | — | — | — | **Boarding (bug)** |
| Segment overlap | Yes | Yes | N/A | Yes | Partially wrong dates | Yes at insert |
| Row lock | Yes | Yes | No | No | No | Yes at insert only |
| pickstand required | Strongly | Strongly | Strongly | Sent in bookingcheck | Often missing in busSeatNew URL | From session |

---

## 6. Can Cross-Midnight Trips Save Different Dates for the Same Bus Run?

**Yes — in these situations:**

| Scenario | Result |
|----------|--------|
| Same run, different pickups (10 PM vs 1 AM) via **modern** `create()` / `bookticket()` | `tickets.journeydata` differs (16 vs 17 Aug); `journeylists.journeydate` same service day (16 Aug) — **intended** |
| Geidea `bookjourneyGeidea()` | `journeylists.journeydate` = boarding day → **same run can get different manifest dates** |
| Legacy `Database\Controllers` booking | `journeydata` = raw search date; no pick-stand conversion — **inconsistent** |
| Old rows in DB (pre-fix) | `journeydata` or `journeylists.journeydate` may both be wrong |

**Duplicate seat risk:** When manifest or inventory paths use **boarding day as service day**, `ticketBelongsToBusRun()` and segment overlap still work for **new** API bookings, but journey list grouping and any exact `journeydata = busStartDate` query **miss** the collision — as with seat 2 on trip 75.

---

## 7. Seat Availability vs Ticket Creation — Alignment

| Path | Aligned? | Detail |
|------|----------|--------|
| Admin `create()` check vs storage | **Yes** | Both use `requestedJourneyDate` + `pickstand` → same `busStartDate` / boarding storage |
| API `bookticket()` | **Yes** | Same |
| `checkSeats()` ticket overlap | **Yes** | `getDynamicPickupDropCombinations()` |
| `checkSeats()` hold overlap | **No** | `getDynamicPickupDropCombinationsTemp()` treats boarding day as service day |
| `bookingcheck()` | **Mostly** | Ticket leg OK; `resolveDepartureDates` at line 506 uses mislabeled `$busStartDate` |
| `busSeatNew()` without `pickstand` query param | **Risk** | Defaults to first pick stand on subtrip — wrong bus run for after-midnight UI |
| `findtrip()` `getDynamicPickupDropCombinations($busStartDate)` | **Risk** | Passes **service** day into function expecting **boarding** day (works only when boarding = service) |
| Legacy Database controllers | **No** | Exact date + no overlap |

---

## 8. Race Conditions — Duplicate Simultaneous Bookings

| # | Location | Window | Mechanism | Severity |
|---|----------|--------|-----------|----------|
| R1 | `TemporaryBook::checkSeats()` → `saveHold()` | Between overlap check and INSERT | Two sessions, different `ticket_token`, no trip lock; `saveHold` does not re-check seats | **High** |
| R2 | `Api\Ticket::processPayment()` (Geidea) | Seat check without `lockTripRowForUpdate` | Two users pass pre-payment check; both get sessions; second may fail at insert or succeed if timing gaps | **Medium** |
| R3 | `bookticket()` outer `transStart()` at line 265 before inner lock at 447 | User check / validation gap | Narrow; inner path does lock before insert | **Low** |
| R4 | No unique DB constraint on seat per run | Commit time | Two transactions both pass `isAlreadybookedsubtrip()` if neither sees uncommitted other | **High** without unique index |
| R5 | Hold expiry cleanup vs payment | `expires_at` boundary | Expired hold deleted; second user grabs seat milliseconds before first pays | **Medium** |
| R6 | `ticketModel` stale wheres in `findtrip()` loop | Subsequent queries in same request | Orphan `where('journeydata', $busStartDate)` on model — unpredictable filters | **Low–Medium** |

**Mitigations present today:** `lockTripRowForUpdate()` on admin/API final insert; segment overlap instead of exact seat string match; hold token ignored on payer’s own token during final check.

**Missing:** Serializable transaction or unique constraint on `(trip_id, bus_run_date, seat, segment)`; atomic “check-and-hold” in one locked transaction.

---

## 9. Detailed Findings Table

| File | Function | Current behavior | Potential bug | Recommended fix |
|------|----------|------------------|---------------|-----------------|
| `Api/Ticket.php` | `bookjourneyGeidea()` | Writes `journeylists.journeydate` from `$journeyDate` (boarding) | Wrong manifest date for after-midnight pickups; breaks `list_mode=service` and causes duplicate-manifest appearance | Call `busRunForBooking()` or reuse `journeylist()` with `$manifestDate` like `bookticket()` |
| `Api/Trip.php` | `getAllTrip()` | Uses `$pickStandId` at L772 before definition at L783 | Undefined/stale pick stand → wrong `bus_start` for seat counts and availability on mobile search | Move pickStand resolution before `resolveSearchDepartureDates()`; add `pickStandsForBoardingSearchDate()` filter |
| `Api/Trip.php` | `getAllTrip()` | Lists all location-matched subtrips | 16 Aug search may show 1 AM pickup; 17 Aug search may miss or duplicate trips | Mirror `Ticket::findtrip()` eligible-pick-stand gating |
| `Api/TemporaryBook.php` | `getDynamicPickupDropCombinationsTemp()` | `resolveDepartureDates($date as bus_start)` when `$date` is boarding day | Under/over-counts temp holds vs stored service `journey_date` | Use `resolveSearchDepartureDates()`; match `holdBelongsToBusRun()` |
| `Api/TemporaryBook.php` | `bookingcheck()` L438 | `$busStartDate = $journeydate` (boarding) | Misleading; breaks `resolveDepartureDates` hold branch for overnight | `busStartDate = busRunForBooking(...)`; pass boarding separately |
| `Api/TemporaryBook.php` | `checkSeats()` | Ticket check OK; temp check via wrong helper | Concurrent holds may not see each other correctly overnight | Fix temp helper; optional trip lock during hold |
| `TemporaryBookHoldService.php` | `saveHold()` | DELETE+INSERT by token only | Two tokens → duplicate holds on same seat | INSERT only after locked segment availability re-check |
| `Ticket.php` | `findtrip()` L777–783 | Sets `ticketModel` wheres, never resets | Model state pollution across loop iterations | Remove dead code or reset model; use `SegmentSeatInventoryService` |
| `Ticket.php` | `findtrip()` L800 | `getDynamicPickupDropCombinations(..., $busStartDate)` | Service day passed where boarding day expected | Pass `$filterjourneydate` + `$pickStandId` |
| `Api/Ticket.php` | `busSeatNew()` | `pickstand` only from query string; defaults if missing | Seat map wrong until user picks boarding point | Require `pickstand` for overnight subtrips or return eligible stands only |
| `Ticket/Database/Controllers/Ticket.php` | `create()`, `isAlreadybooked()` | Exact `journeydata` + no segment overlap | Duplicate seats across subtrips / overnight if route ever used | Delete or align with `Controllers\Ticket`; not routable today |
| `Ticket/Database/Controllers/Api/Ticket.php` | `bookticket()` | Raw dates, no `busRunForBooking` | Same as legacy admin | Confirm dead; remove or redirect |
| `Api/Ticket1.php` | Multiple | Raw `journeydata` from request | Legacy mobile endpoint if still deployed | Audit routes; deprecate |
| `BookingInventorySupport.php` | `ticketBelongsToBusRun()` | Heuristic for legacy rows where `journeydata` = service day | Edge cases if both boarding and service stored inconsistently | Data migration + optional `boarding_at` DATETIME column |
| `Journeylist.php` | `getJourneylistData()` | Boarding search with prefetch | Rows with wrong `journeylists.journeydate` still appear on wrong day until SQL fix | Run `database/scripts/fix_journeylist_manifest_dates.sql`; backfill |
| `HoldSeat.php` | create / filters | Calendar range on `journeydata` | Admin blocks may not align with bus-run semantics overnight | Document as calendar range; optional bus-run conversion |
| `paystatusgeidea` table | insert in `processPayment()` | Stores boarding `journeydata` | Correct for ticket recompute; wrong if copied to manifest | Keep boarding in session; never copy directly to `journeylists` |
| DB schema | `tickets`, `temporarybooks` | No uniqueness on seat per run | R4 race | Partial unique index + transaction isolation |

---

## 10. temporarybooks vs tickets — Journey Date Logic

| Aspect | `temporarybooks` | `tickets` |
|--------|-------------------|-----------|
| Column | `journey_date` (datetime) | `journeydata` (date/datetime) |
| Written by `store()` | **Service day** via `busRunForBooking()` | — |
| Written by booking | — | **Boarding day** via `ticketBoardingDateForStorage()` |
| Read by inventory | `holdBelongsToBusRun()` vs `bus_start` | `ticketBelongsToBusRun()` vs `bus_start` |
| SQL prefetch | `applyTemporaryBookJourneyDateFilter` on service dates | `applyTicketJourneyDateFilter` on service + boarding |
| **Identical logic?** | **No** — intentional different storage semantics; must map through same `bus_start` | Same |

**Bug:** Hold **overlap readers** (`getDynamicPickupDropCombinationsTemp`) do not use the same entry point (`resolveSearchDepartureDates`) as hold **writer** (`busRunForBooking`), so read/write paths can diverge on overnight legs.

---

## 11. Case Study: TB30JAE7YF / TBJC5SKYOZ

| Booking | Pick stand | Time | `tickets.journeydata` | `journeylists.journeydate` (wrong) | Service day should be |
|---------|------------|------|----------------------|-----------------------------------|----------------------|
| TB30JAE7YF | 8178 | 23:30 | 2026-08-16 | 2026-08-16 | 2026-08-16 |
| TBJC5SKYOZ | 8184 | 01:15 | 2026-08-16 | 2026-08-16 | **2026-08-15** |

TBJC5SKYOZ stored **boarding day in manifest** instead of **service day** for a 01:15 pickup (bus left previous calendar evening). Segment inventory on the ticket row may still block rebooking, but **manifest and legacy exact-date queries** treated this as the same day as TB30JAE7YF, hiding the conflict or allowing operator confusion.

**Likely code path:** Geidea or legacy `journeylist()` without `$manifestDate` override, or `bookjourneyGeidea()`.

---

## 12. Recommended Fix Priority

### P0 — Data & production safety
1. Run manifest backfill SQL for `journeylists.journeydate` on overnight bookings.
2. Fix `bookjourneyGeidea()` to store service day (or delegate to `journeylist()`).

### P1 — Duplicate prevention
3. Fix `getDynamicPickupDropCombinationsTemp()` date semantics everywhere.
4. Add locked check-and-hold in `saveHold()` (or single transaction in `checkSeats()`).
5. Add DB-level uniqueness or exclusion constraint for active tickets per `(trip_id, run_date, seat_number, segment)`.

### P2 — Search & UX correctness
6. Fix `Api\Trip::getAllTrip()` pickStand ordering and overnight subtrip filtering.
7. Require/propagate `pickstand` in all seat-map API calls before showing availability.
8. Fix `Ticket::findtrip()` seat-count date argument and remove stale `ticketModel` wheres.

### P3 — Cleanup
9. Remove or hard-disable `Database\Controllers` ticket copies.
10. Add `list_mode=service` toggle in journey list UI for drivers.
11. Consider `tickets.boarding_at` DATETIME for auditability.

---

## 13. Test Scenarios (for verification after fixes)

1. **16 Aug search, trip 75, Cairo→El Tor:** Only evening pickups; seat 2 blocked after one booking.
2. **17 Aug search, same run:** 01:15 pickup visible; seat 2 still blocked; new booking gets `journeydata=2026-08-17`, manifest `journeydate=2026-08-16`.
3. **Concurrent checkSeats:** Two tokens, same seat → only one hold succeeds.
4. **Geidea flow:** After payment, `journeylists.journeydate` = service day for 01:15 pickup.
5. **Journey list:** 16 Aug passenger list shows 23:30 boarding; 17 Aug list shows 01:15 boarding; `list_mode=service` on 16 Aug shows both.

---

## 14. Files Reviewed (primary)

- `app/Libraries/BookingInventorySupport.php`
- `app/Libraries/TripJourneyDateHelper.php`
- `app/Libraries/OvernightTripRunSupport.php`
- `app/Libraries/SegmentSeatInventoryService.php`
- `app/Libraries/TemporaryBookHoldService.php`
- `modules/Ticket/Controllers/Ticket.php`
- `modules/Ticket/Controllers/Api/Ticket.php`
- `modules/Ticket/Controllers/Api/TemporaryBook.php`
- `modules/Ticket/Controllers/Journeylist.php`
- `modules/Trip/Controllers/Api/Trip.php`
- `modules/Ticket/Database/Controllers/Ticket.php`
- `modules/Ticket/Database/Controllers/Api/Ticket.php`
- `modules/HoldSeat/Controllers/HoldSeat.php`
- `public/js/booking.js`
- `modules/Ticket/Config/Routes.php`
- `database/scripts/fix_journeylist_manifest_dates.sql`

---

*End of audit report.*
