# Flutter App — Overnight / Midnight Booking (Single Source of Truth)

Copy **this entire file** into Cursor when updating the BusValley **Flutter app**.

**Backend:** Deploy latest API fixes before QA (`data.busvalley.net/busvalley_new`).  
**Scope:** Flutter only — do not change PHP. Do not recompute service/manifest dates on the client.

**Deprecated docs (merged here):** `CURSOR_PROMPT_flutter-overnight-booking.md`, `CURSOR_PROMPT_mobile-web-overnight-booking.md` (Flutter sections).

---

## What you are fixing

1. **422 on seat hold** — missing `pickstand` on overnight trips  
2. **Wrong ticket date** — off-by-one day on Geidea / online booking  
3. **Empty seat map** — must select boarding point first (deferred 200, not error)  
4. **Missing trips** — main routes (trip 117, 75) now appear when backend is deployed  

---

## Golden rule: three dates, one sent to API

| Concept | Meaning | Flutter sends to API? | Example |
|---------|---------|----------------------|---------|
| **Search date** | Calendar day user picked on search screen | **Yes** → `journeydate` / `journey_date` / `journeyDate` | User searched **29 Jun** → always send `2026-06-29` |
| **Boarding date** | Day passenger boards at **selected** pickup | **No** (display only) — from `pickup_points[].boarding_date` or ticket API | 01:00 AM “four” → show **30 Jun** |
| **Service date** | Bus leaves origin (inventory/manifest) | **Never** — read `busStartDate` from search for labels only | **29 Jun** |

```
User searches 29 Jun → picks "four (01:00 AM)" → every API call uses journeydate=2026-06-29 + pickstand=8581
Backend stores tickets.journeydata = 2026-06-30 (boarding day)
```

**Do NOT:**
- Send `boarding_date` or `busStartDate` as `journeydate`
- Add +1 day in Flutter for after-midnight pickups
- Change `journeydate` after user picks a boarding point (breaks hold token)

**Optional (edge case only):** If product explicitly books “1 AM on the search calendar morning” (Scenario A below), you may send `boarding_date` equal to search date on book/payment — backend accepts it. **Default flow: send search date only.**

---

## Must-pass QA scenarios

### Scenario A — 1 AM on search morning (boarding day = search day)

| Step | Value |
|------|--------|
| Search / `journeydate` | **15 June 2026** |
| Pickup | **four** (01:00 AM), `pickstand` **8581** |
| Ticket must show | **15 June 2026** (NOT 16) |
| Service day (display only) | 14 June 2026 |

Ref: TBRTWZAC5V (was wrong at 16 Jun before fix).

### Scenario B — Evening search, 1 AM boards next calendar day

| Step | Value |
|------|--------|
| Search / `journeydate` | **29 June 2026** |
| Pickup | **four** (01:00 AM), `pickstand` **8581** |
| Ticket must show | **30 June 2026** |
| Service day (display only) | 29 June 2026 |

Ref: admin TBLKI1M6UJ ✓; online TBCV9N3HY8 was **2026-07-01** ✗ before Geidea fix.

### Scenario C — Same as B (17 Jun search → 18 Jun boarding)

| Step | Value |
|------|--------|
| Search / `journeydate` | **17 June 2026** |
| Pickup | **four** (01:00 AM), `pickstand` **8581** |
| Ticket must show | **18 June 2026** (NOT 17) |
| Service day (display only) | 17 June 2026 |

Ref: TB8Z0EPD94.

### Scenario D — Evening pickup, same calendar day

| Step | Value |
|------|--------|
| Search / `journeydate` | **29 June 2026** |
| Pickup | **one** (09:30 PM), `pickstand` **8578** |
| Ticket must show | **29 June 2026** |

---

## Checkout state (persist entire flow)

```dart
class OvernightCheckout {
  final int tripId;           // e.g. 117
  final int subtripId;        // e.g. 400
  final String searchDate;    // FROM SEARCH SCREEN — NEVER CHANGES
  final int pickstandId;      // pickup_points[].id (pickdrop row id)
  final int? dropstandId;
  final String ticketToken;   // same UUID across hold → book → payment
}
```

Generate `ticketToken` once per checkout; reuse until complete or cancel.

---

## Required user flow

```
1. User selects FROM, TO, SEARCH DATE
2. POST triplist → parse requires_pickstand, busStartDate, pickup_points
3. If requires_pickstand OR multiple pickup_points → boarding picker FIRST
4. User selects pickup → save pickstandId = pickup_points[].id
5. GET seatnew/{subtripId}/{searchDate}?pickstand={pickstandId}
   (first call without pickstand → 200 deferred, empty layout — NOT an error)
6. User selects seats
7. POST checkseats/client — searchDate + pickstand + ticket_token + seats
8. POST bookingcheck — same params
9. POST booking OR payment-process — same params
```

**Same `searchDate`, `pickstandId`, `ticket_token` on steps 7–9.**

---

## API base URL

```
https://data.busvalley.net/busvalley_new/modules/api/v1/
```

---

## Step 1 — Trip search

`POST /triplist`

```json
{
  "pick_location_id": "97",
  "drop_location_id": "101",
  "journeydate": "2026-06-29"
}
```

Optional after user picks boarding point: include `"pickstand": 8581` to refresh `available_seat`.

**Parse from each trip:**

```json
{
  "subtripId": "400",
  "tripid": "117",
  "journeydate": "2026-06-29",
  "busStartDate": "2026-06-29",
  "requires_pickstand": true,
  "eligiblePickStandIds": [8578, 8579, 8580, 8581],
  "available_seat": null,
  "pickup_points": [
    { "id": 8578, "pickid": 8578, "place": "one",   "time": "09:30PM", "offset": 0, "boarding_date": "2026-06-29" },
    { "id": 8579, "pickid": 8579, "place": "two",   "time": "10:30PM", "offset": 0, "boarding_date": "2026-06-29" },
    { "id": 8580, "pickid": 8580, "place": "three", "time": "11:00PM", "offset": 0, "boarding_date": "2026-06-29" },
    { "id": 8581, "pickid": 8581, "place": "four",  "time": "01:00AM",  "offset": 1, "boarding_date": "2026-06-30" }
  ]
}
```

**Field meanings:**
- `id` / `pickid` = **pickdrop row id** → use as `pickstand` (NOT location id, NOT stand id)
- `place` = admin label (“one”, “two”, “four”) — show in UI
- **Overnight:** `pickup_points` lists **all** segment pickups (evening + after-midnight), same as admin
- `boarding_date` = calendar day at that stand; use for labels when `boarding_date != searchDate`

**Boarding picker UI:**
- Show all `pickup_points` for overnight trips
- Label: `"${place} (${time})"` e.g. `four (01:00AM)`
- If `boarding_date != searchDate`: append `— boards 30 Jun`
- On select: `pickstandId = pickup.id`; **keep `searchDate` unchanged**
- When `requires_pickstand && available_seat == null`: show “Select boarding point” (not “0 seats”)

---

## Step 2 — Seat map

`GET /tickets/seatnew/{subtripId}/{searchDate}?pickstand={pickstandId}`

Also: `/tickets/seat/{subtripId}/{searchDate}?pickstand=` or `/tickets/seatnewlayout/...`

| Param | Value |
|-------|--------|
| Path date | **searchDate** (NOT `boarding_date`) |
| Query `pickstand` | Required for real seats after user picks boarding point |

**Deferred response (overnight, no pickstand yet) — HTTP 200, NOT 422:**

```json
{
  "status": "success",
  "response": 200,
  "requires_pickstand": true,
  "eligible_pick_stands": [8578, 8579, 8580, 8581],
  "message": "Select a boarding point (pickstand) to load seat availability.",
  "seatlayout": [],
  "totalseat": 0
}
```

→ Show boarding picker; reload when user selects `pickstand`. Do not treat as error.

---

## Step 3 — Hold seats

`POST /tickets/checkseats/client`

```json
{
  "subtrip_id": 400,
  "ticket_token": "<uuid>",
  "seat_names": "3",
  "journey_date": "2026-06-29",
  "pickstand": 8581
}
```

Accepts `journey_date` or `journeydate`. **`pickstand` required** on overnight → **422** if missing.

**422 response:**

```json
{
  "status": "fail",
  "response": 422,
  "message": "pickstand is required for overnight trips before checking seat availability."
}
```

Show: “Please select your boarding point before continuing.” Block this call client-side if `pickstandId <= 0`.

---

## Step 4 — bookingcheck

`POST /tickets/bookingcheck`

```json
{
  "subtrip_id": 400,
  "seat_names": "3",
  "journeydate": "2026-06-29",
  "pickstand": 8581,
  "dropstand": 8582,
  "ticket_token": "<same-token>"
}
```

---

## Step 5a — Direct booking

`POST /tickets/booking` or `POST /tickets/unpaid/booking`

```json
{
  "trip_id": 117,
  "subtripId": 400,
  "journeydate": "2026-06-29",
  "pickstand": 8581,
  "dropstand": 8582,
  "pick_location_id": "97",
  "drop_location_id": "101",
  "seatnumbers": "3",
  "totalseat": 1,
  "ticket_token": "<same-token>",
  "payment_status": "paid"
}
```

---

## Step 5b — Geidea payment

1. `checkseats/client` with same params  
2. `POST /tickets/payment-process/`

```json
{
  "trip_id": 117,
  "subtrip_id": 400,
  "journeyDate": "2026-06-29",
  "pickstand": 8581,
  "dropstand": 8582,
  "seatnumbers": "3",
  "pick_location_id": "97",
  "drop_location_id": "101"
}
```

**Geidea uses `journeyDate` (camelCase)** — same **searchDate** value.

---

## Parameter cheat sheet

| Step | Date param | `pickstand` | `ticket_token` |
|------|------------|-------------|----------------|
| triplist | `journeydate` | optional | — |
| seatnew / seat / seatnewlayout | path = **searchDate**; query `pickstand` | optional first; required for seats | — |
| bookingcheck | `journeydate` | **required** overnight | required |
| checkseats/client | `journey_date` or `journeydate` | **required** overnight | required |
| booking | `journeydate` | **required** overnight | required |
| payment-process | `journeyDate` | **required** | hold must exist |

Backend also accepts `pickstand_id` as alias.

---

## Ticket list / my bookings (display)

Passenger APIs return normalized boarding day:

| Field | Use |
|-------|-----|
| `journeydata` | Boarding calendar day (show to user) |
| `boarding_date` | Same (when present) |

Do **not** show admin manifest/service day to passengers.

Optional subtitle: “Bus departs {busStartDate}” from original search context only.

---

## Detect overnight in UI

Use (in order):
1. `requires_pickstand == true` from triplist or seat API (**preferred**)
2. Multiple entries in `pickup_points`
3. Any pickup has `offset >= 1` or `boarding_date != searchDate`

**Safest:** Require boarding selection if `requires_pickstand` **or** `pickup_points.length > 1`.

---

## Common Flutter mistakes

| Mistake | Result |
|---------|--------|
| Omit `pickstand` on checkseats | **422** |
| Send `boarding_date` as `journeydate` | Wrong ticket date / off-by-one |
| Send `busStartDate` as `journeydate` | Wrong inventory |
| Add +1 day in app for 01:00 AM | July 1 instead of June 30 |
| Change `journeydate` after boarding pick | Hold/payment mismatch |
| Treat deferred seat 200 as error | User stuck before picker |
| Show all seats available when `seatlayout: []` | Overbooking UX bug |

---

## Before vs after (your failing hold)

**Before (broken):**
```json
{
  "subtrip_id": 400,
  "journey_date": "2026-06-11",
  "seat_names": "3",
  "ticket_token": "..."
}
```

**After (fixed):**
```json
{
  "subtrip_id": 400,
  "journey_date": "2026-06-29",
  "seat_names": "3",
  "pickstand": 8581,
  "ticket_token": "..."
}
```

---

## Implementation checklist

### Models
- [ ] `TripResult`: `requiresPickstand`, `busStartDate`, `eligiblePickStandIds`, `pickupPoints` (`id`, `pickid`, `place`, `time`, `offset`, `boardingDate`)
- [ ] `OvernightCheckout`: immutable `searchDate`, `pickstandId`, `ticketToken`, `subtripId`, `tripId`

### UI
- [ ] Boarding picker from full `pickup_points` (overnight)
- [ ] Hint when `boardingDate != searchDate`
- [ ] Block seats/hold until `pickstandId > 0`
- [ ] Handle deferred seat 200 → show picker, not error
- [ ] On pickup change: clear seats, reload seat map
- [ ] Optional: “Bus departs {busStartDate}” when useful

### API layer — audit every call to:
- [ ] `triplist`
- [ ] `seatnew` / `seat` / `seatnewlayout`
- [ ] `checkseats` / `checkseats/client`
- [ ] `bookingcheck`
- [ ] `booking` / `unpaid/booking`
- [ ] `payment-process`

Each must use **`searchDate`** for date params and **`pickstandId`** after user selects pickup.

### Client validation
```dart
if (requiresPickstand && pickstandId <= 0) {
  showBoardingPicker();
  return;
}
await holdSeats(
  journeyDate: searchDate,  // NOT boardingDate, NOT busStartDate
  pickstand: pickstandId,
  ticketToken: ticketToken,
);
```

---

## Test matrix (must pass before release)

| # | Action | Expected |
|---|--------|----------|
| 1 | Search 29 Jun AA→EE | Trip 400 in results; all 4 `pickup_points` |
| 2 | Select four (01:00 AM) | Label shows boards **30 Jun**; `pickstand` 8581 |
| 3 | Seat URL | `/seatnew/400/2026-06-29?pickstand=8581` |
| 4 | Seats without pickstand first | **200** deferred, show picker |
| 5 | checkseats with pickstand | **200** success |
| 6 | checkseats without pickstand | **422** (or blocked client-side) |
| 7 | Geidea book Scenario B | `journeydata` = **2026-06-30** |
| 8 | Direct book Scenario B | Same **2026-06-30** |
| 9 | Evening pickup same search | `journeydata` = **2026-06-29** |
| 10 | Scenario A (15 Jun + 01:00) | `journeydata` = **2026-06-15** |
| 11 | Scenario C (17 Jun + 01:00) | `journeydata` = **2026-06-18** |
| 12 | Change pickstand after seat select | Seats cleared; map reloads |
| 13 | Trip 117 / 75 main routes | Appear in search (backend deployed) |

---

## Trip examples (trip 117)

| User search | Pickup | Send `journeydate` | Show ticket date | `busStartDate` (label) |
|-------------|--------|--------------------|------------------|-------------------------|
| 29 Jun | four 01:00 AM | `2026-06-29` | **30 Jun** | 29 Jun |
| 17 Jun | four 01:00 AM | `2026-06-17` | **18 Jun** | 17 Jun |
| 15 Jun | four 01:00 AM | `2026-06-15` | **15 Jun** | 14 Jun |
| 29 Jun | one 09:30 PM | `2026-06-29` | **29 Jun** | 29 Jun |
| 11 Jun | four 01:00 AM | `2026-06-11` | **11 Jun** | 10 Jun |

Always use actual `pickup_points[].id` from API — do not hardcode except in tests.

---

## Backend fixes (already in API — no Flutter workaround)

1. Geidea double date conversion removed  
2. `resolveBookingStorageDates` on all online book/payment paths  
3. Service-day vs boarding-day resolution for 01:00 AM + evening same bus (Scenarios B & C)  
4. `triplist`: all overnight `pickup_points`, correct `place`, per-pickup `boarding_date`  
5. `pickstand` required on overnight hold/book/payment  
6. Seat map deferred **200** until `pickstand`  
7. Ticket list: `journeydata` / `boarding_date` = boarding day  

Deploy backend before final app QA.

---

## Out of scope (Flutter)

- Do not compute manifest/service dates for storage  
- Do not send `busStartDate` or `bus_service_date` as journey param  
- Do not modify PHP backend  

---

## Cursor instruction (paste with this doc)

> Update the BusValley Flutter app for overnight/midnight booking per this document.  
> Find all API calls to `triplist`, `seatnew`, `checkseats`, `bookingcheck`, `booking`, `payment-process`.  
> Add immutable `searchDate` + `pickstandId` checkout state.  
> Fix 422 on hold. Fix wrong ticket dates. Handle deferred seat 200.  
> Pass all test matrix scenarios. Do not change backend.
