# 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.

---

## Round trip (mobile) — overnight rules apply per leg

Round trip = **two independent legs**. Apply the full one-way overnight flow **separately** to outbound and return. Backend does **not** copy outbound dates/pickstand to the return leg.

**When only one leg is overnight:** overnight UI + `pickstand` on that leg only; the other leg stays normal same-day flow.

**When both legs are regular same-day:** no extra work — `pickstand` optional, search date = boarding date.

### Round-trip checkout state

```dart
class RoundTripOvernightCheckout {
  // Outbound (from outbound search screen)
  final int outboundTripId;
  final int outboundSubtripId;
  final String outboundSearchDate;   // immutable
  final int outboundPickstandId;
  final int? outboundDropstandId;
  final String outboundSeats;        // e.g. "3,1"
  final int outboundTotalSeat;

  // Return (from return search screen — separate date!)
  final int returnTripId;
  final int returnSubtripId;
  final String returnSearchDate;     // immutable — NOT outbound search date
  final int returnPickstandId;
  final int? returnDropstandId;
  final String returnSeats;          // e.g. "5"
  final int returnTotalSeat;

  final String ticketToken;          // ONE token for both holds + payment
}
```

Generate `ticketToken` once for the whole round-trip checkout.

### Round-trip user flow

```
OUTBOUND (same as one-way sections above)
1. Search outbound date → triplist → boarding picker if overnight
2. seatnew/{outboundSubtrip}/{outboundSearchDate}?pickstand=
3. Select seats → checkseats/client (outbound searchDate + pickstand)

RETURN (repeat with return search date — do not reuse outbound searchDate)
4. Search return date → triplist → boarding picker if overnight
5. seatnew/{returnSubtrip}/{returnSearchDate}?pickstand=
6. Select seats → checkseats/client (return searchDate + return pickstand)

PAYMENT
7. POST round-trip-payment-process/ — both legs in one payload
8. Geidea callback → backend creates 2 tickets (same sessionId)
```

**Hold both legs** (outbound + return) with the **same `ticket_token`** before calling payment.

### Step 5c — Round-trip Geidea payment

`POST /tickets/round-trip-payment-process/`

After holds on **both** legs:

```json
{
  "amount": 2500,
  "user_id": 6064,
  "email": "user@example.com",
  "mobile": "09999222333",

  "trip_id": 117,
  "subtrip_id": 400,
  "pick_location_id": "97",
  "drop_location_id": "101",
  "pickstand": 8689,
  "dropstand": 8691,
  "journeyDate": "2026-06-25",
  "seatnumbers": "6,7",
  "totalseat": 2,
  "fare": 1200,

  "return_trip_id": 117,
  "return_subtrip_id": 400,
  "return_pick_location_id": "101",
  "return_drop_location_id": "97",
  "return_pickstand": 8689,
  "return_dropstand": 8691,
  "return_journeyDate": "2026-06-29",
  "return_seatnumbers": "5",
  "return_totalseat": 1,
  "return_fare": 700,

  "discount": 0,
  "roundTripDiscount": 0,
  "tax": 0
}
```

| Field | Rule |
|-------|------|
| `journeyDate` | Outbound **search** date (NOT boarding, NOT busStartDate) |
| `return_journeyDate` | Return **search** date (separate calendar day from user’s return search) |
| `pickstand` | Outbound `pickup_points[].id` — **required** if outbound is overnight |
| `return_pickstand` | Return `pickup_points[].id` — **required** if return is overnight |
| `seatnumbers` / `return_seatnumbers` | Must match seats held via `checkseats/client` |
| Holds | Same `ticket_token` used for outbound + return holds |

**Optional edge-case hints (rare):**
- `boarding_date` — outbound Scenario A only  
- `return_boarding_date` / `return_boardingDate` — return leg Scenario A only  

**422 on round-trip payment** if overnight leg missing pickstand (same message as one-way).

### Round-trip example (trip 117)

| Leg | User search | Pickup | Send date param | Ticket shows |
|-----|-------------|--------|-----------------|--------------|
| Outbound | 25 Jun | four 01:00 AM | `journeyDate`: `2026-06-25` | **26 Jun** |
| Return | 29 Jun | one 09:30 PM | `return_journeyDate`: `2026-06-29` | **29 Jun** |

### BUG FIX — return From/To must be reversed (not same as outbound)

Round-trip booking history must **not** show the same route on both tickets (e.g. Cairo → El Tor twice).

**Wrong (TBQJSBN2Z5 + TB7U9EJGVL):** both legs sent `pick_location_id: 32`, `drop_location_id: 43`.

**Correct:**

| Leg | `pick_location_id` | `drop_location_id` | Example |
|-----|-------------------|-------------------|---------|
| Outbound | user **From** (e.g. 32 Cairo) | user **To** (e.g. 43 El Tor) | `32`, `43` |
| Return | user return **From** (= outbound **To**) | user return **To** (= outbound **From**) | `43`, `32` |

On `round-trip-payment-process/`:

```dart
pick_location_id: outboundPickId,      // e.g. 32
drop_location_id: outboundDropId,      // e.g. 43
return_pick_location_id: outboundDropId, // 43 — NOT outboundPickId
return_drop_location_id: outboundPickId, // 32 — NOT outboundDropId
```

Use **return search** From/To from the return trip result — do not copy outbound location IDs.  
`return_trip_id` / `return_subtrip_id` can differ (e.g. trip 74 subtrip 235 vs trip 75 subtrip 238); locations must still match the **return** segment.

### Round-trip common mistakes

| Mistake | Result |
|---------|--------|
| `return_pick_location_id` / `return_drop_location_id` same as outbound | Both tickets show same From→To in booking history |
| `return_journeyDate` = outbound search date | Wrong return ticket date |
| `pickstand` on outbound only, omit `return_pickstand` | **422** when return is overnight |
| Reuse outbound `pickstandId` without parsing return `pickup_points` | Wrong stand if return subtrip differs |
| Hold outbound only, skip return hold | **409** seat conflict at payment |
| Send boarding date as `journeyDate` / `return_journeyDate` | Off-by-one on one or both tickets |
| One `OvernightCheckout` shared for both legs | Return uses wrong search date |

### Round-trip test matrix

| # | Outbound | Return | Expected |
|---|----------|--------|----------|
| R1 | 25 Jun + 01:00 AM, 2 seats | 29 Jun evening, 1 seat | Out ticket **26 Jun**; return **29 Jun**; 2 booking IDs |
| R2 | Both overnight 01:00 AM | Different search dates each | Each leg: own searchDate + pickstand |
| R3 | Overnight outbound | Regular return same-day | `pickstand` outbound only; return optional |
| R4 | 3 seats outbound | 2 seats return | Journey list shows all seats per ticket (backend) |
| R5 | Change return pickstand after seats | — | Return seats cleared; map reloads |
| R6 | Cairo→El Tor out + El Tor→Cairo return | Booking history | Out: Cairo→El Tor; return: **El Tor→Cairo** (not both Cairo→El Tor) |

---

## 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 |
| round-trip-payment-process | `journeyDate` + `return_journeyDate` | **required** per overnight leg | same token; both holds |
| round-trip checkseats (×2) | `journey_date` per leg | **required** per overnight leg | same token |

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 |
| Round trip: one search date for both legs | Wrong return ticket date |
| Round trip: missing `return_pickstand` on overnight return | **422** at payment |

---

## 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`
- [ ] `RoundTripOvernightCheckout`: separate outbound + return search dates and pickstands; one `ticketToken`

### 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
- [ ] Round trip: boarding picker + deferred seat 200 on **each** leg independently

### API layer — audit every call to:
- [ ] `triplist`
- [ ] `seatnew` / `seat` / `seatnewlayout`
- [ ] `checkseats` / `checkseats/client`
- [ ] `bookingcheck`
- [ ] `booking` / `unpaid/booking`
- [ ] `payment-process`
- [ ] `round-trip-payment-process` (outbound + return date/pickstand fields)
- [ ] `checkseats/client` — **twice** for round trip (one hold per leg)

Each must use **`searchDate`** for date params and **`pickstandId`** after user selects pickup.
Round trip: **`returnSearchDate`** + **`returnPickstandId`** on return leg only — never reuse outbound search date.

### 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) |
| R1 | Round trip: out 25 Jun 01:00 + return 29 Jun evening | Out **26 Jun**, return **29 Jun** |
| R2 | Round trip overnight both legs | Separate pickstand per leg |
| R3 | Round trip 3 seats out, 2 return | Both tickets full seat count in journey list |

---

## 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  
8. Geidea journey list: one row per seat for mobile multi-seat bookings  
9. Round trip: two `paystatusgeidea` rows → two tickets per payment session  

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`, **`round-trip-payment-process`**.  
> Add immutable `searchDate` + `pickstandId` checkout state (`OvernightCheckout` one-way; `RoundTripOvernightCheckout` round trip with separate return dates/pickstands).  
> Fix 422 on hold. Fix wrong ticket dates. Handle deferred seat 200. Hold both legs before round-trip payment.  
> **Fix round-trip return From/To:** `return_pick_location_id` / `return_drop_location_id` must be reversed (return From = outbound To).  
> Pass all test matrix scenarios (including R1–R3, R6). Do not change backend.
