# Website payment flow (Geidea drop-in)

This document describes the **end-to-end payment flow** for the BusValley **website** (React checkout), aligned with this backend repo.

**Live API base (production):**

```
https://data.busvalley.com/busvalley_new/modules/api/v1/tickets/
```

---

## Important: session is NOT created on the frontend

The website **does not** call Geidea’s Create Session API directly.

| Layer | Responsibility |
|-------|----------------|
| **Website** | Validate form, refresh seat hold, POST booking data to backend, load Geidea JS SDK, embed UI in `#checkout-tag` |
| **Backend** | Create Geidea session (server-to-server), store `paystatusgeidea`, create ticket on payment callback |
| **Geidea** | Card / wallet / 3DS UI inside the embedded container |

---

## High-level architecture

```
User (checkout page)
    → POST payment-process (backend)
        → Geidea Create Session API
        ← sessionId
    → GeideaCheckout.startPayment(sessionId, null, "checkout-tag")
        → User pays (card / wallet / 3DS)
        → Geidea POST payment-sucess (server callback)
            → bookticketGeidea() creates ticket
    → onSuccess → GET get-booking-id/{sessionId} (retry)
    → GET bookingid/{bookingId}
    → Redirect /ticket-traking
```

---

## Phase 1 — Before payment (booking → checkout)

| Step | What happens | Frontend (typical) |
|------|----------------|-------------------|
| 1 | User selects seats | Booking / seat map |
| 2 | `checkseats` holds seats; returns `ticket_token`, `temporary_book_id` | `seatHold.js` |
| 3 | Hold timer starts; user opens checkout | `bookingHoldTimer.js` |
| 4 | Checkout verifies `bookingInfo`, active hold, timer not expired | `Checkout/index.jsx` |
| 5 | User fills traveler info (name, email, mobile) | `TravellerInformation` |
| 6 | User accepts booking policy | `PassengersCheckList` |
| 7 | Payment section unlocks when validation passes | `getValidationStatus()` |

### Seat hold API

| Method | Endpoint |
|--------|----------|
| POST | `/tickets/checkseats/{book_from}` |
| POST | `/tickets/deleteHoldSeat` (cancel / back navigation) |

**Hold fields to keep consistent** across `checkseats`, `payment-process`, and `deleteHoldSeat`:

- `ticket_token`
- `journey_date` / `journeyDate`
- `temporary_book_id` (especially for release)
- `subtrip_id`, `seat_names`, `pickstand`

---

## Phase 2 — Session request (website → backend → Geidea)

| Step | What happens |
|------|----------------|
| 8 | User clicks Pay / Select Payment Method |
| 9 | Re-validate traveler fields |
| 10 | Re-validate seat hold via `checkseats` |
| 11 | Build payload from localStorage + form |
| 12 | Add `language`: `"ar"` or `"en"` |
| 13 | Add hold fields: `ticket_token`, `journey_date`, `temporary_book_id` |
| 14 | POST to backend (single or round trip) |
| 15 | Backend creates Geidea session, inserts `paystatusgeidea`, returns `sessionId` |
| 16 | Frontend sets `sessionId`, shows drop-in container |

### Endpoints

| Trip type | Method | Endpoint |
|-----------|--------|----------|
| One-way | POST | `/tickets/payment-process/` |
| Round trip | POST | `/tickets/round-trip-payment-process/` |

### Example request body (website → backend)

```json
{
  "amount": 522,
  "language": "ar",
  "user_id": 123,
  "trip_id": 74,
  "subtrip_id": 235,
  "pick_location_id": 1,
  "drop_location_id": 2,
  "pickstand": 10,
  "dropstand": 20,
  "seatnumbers": "11,12",
  "totalseat": 2,
  "journeyDate": "2026-06-15",
  "totalprice": 532,
  "discount": 10,
  "grandtotal": 522,
  "offerer": "DRI20",
  "tax": 0,
  "email": "guest@example.com",
  "mobile": "01012345678",
  "first_name": "Ahmed",
  "last_name": "Ali",
  "UiMode": "dropin",
  "ticket_token": "abc123",
  "journey_date": "2026-06-15",
  "temporary_book_id": 456
}
```

**Field notes:**

| Field | Backend usage |
|-------|----------------|
| `amount` | Charged total sent to Geidea; must match server fare when a known offer is applied |
| `language` | Forwarded to Geidea session (`en` / `ar`) for HPP language |
| `email`, `mobile`, `first_name`, `last_name` | Passenger resolution / `user_details` |
| `offerer` | Offer code; server validates fare for active offers |
| `ticket_token` | Matches seat hold during payment (required for reliable hold) |
| `temporary_book_id` | Used mainly for `deleteHoldSeat`; payment can infer hold from token + seats |
| `UiMode` | **Frontend only** — drop-in vs redirect; backend ignores |

### Success response (backend → website)

```json
{
  "message": "Session created successfully.",
  "status": "success",
  "response": 200,
  "data": {
    "sessionId": "8f370e23-b22a-40ed-833e-08dc85577cac"
  }
}
```

### Backend checks before Geidea session

1. Resolve passenger (`email` / `mobile` / `user_id`) — blocks staff/agent phone as passenger
2. Overnight / pick-stand validation
3. Seat conflict check (respects `ticket_token` hold)
4. **Offer fare validation** (active offers must match server-computed total)
5. Geidea session create + `paystatusgeidea` insert

### Fare validation errors (422)

When an active offer is applied and amounts do not match server calculation:

```json
{
  "message": "Payment amount does not match the fare for this trip and seats.",
  "status": "fail",
  "response": 422,
  "data": { "list_price": 532, "offer_discount": 10, "grand_total": 522 }
}
```

Website should display this and **not** open Geidea UI.

---

## Phase 3 — Geidea drop-in UI (frontend only)

| Step | What happens |
|------|----------------|
| 17 | Load `geideaCheckout.min.js` from `merchant.geidea.net` |
| 18 | Render `<div id="checkout-tag" />` |
| 19 | When `sessionId` + script ready, `new GeideaCheckout(onSuccess, onError, onCancel)` |
| 20 | `payment.startPayment(sessionId, null, "checkout-tag")` |
| 21 | Geidea renders card / wallet / 3DS inside the div |

**Website provides:** empty container + loading state.  
**Geidea provides:** all payment screens.

### Checkout vs `/payment` page

| | `/checkout` | `/payment` |
|---|-------------|--------------|
| Trigger | User opens payment accordion after form valid | Auto-start if booking data exists |
| Container | Inline `#checkout-tag` | `GeideaDropInCheckout` component |
| Session | `createPaymentSession()` | Same logic |

Both use the same SDK call: `startPayment(sessionId, null, "checkout-tag")`.

---

## Phase 4 — After payment

### Server path (creates the ticket)

Geidea calls the backend (not the browser):

| Method | Endpoint | Handler |
|--------|----------|---------|
| POST | `/tickets/payment-sucess` | `paymentSucess()` → `bookticketGeidea()` |

**Note:** Route spelling is `payment-sucess` (not `success`). Geidea merchant callback URL must match:

```
https://data.busvalley.com/busvalley_new/modules/api/v1/tickets/payment-sucess
```

### Frontend path (after `onSuccess`)

| Step | What happens |
|------|----------------|
| 23 | Geidea SDK `onSuccess(data)` |
| 24 | Stop hold timer; show “Finalizing booking…” |
| 25 | `GET /tickets/get-booking-id/{sessionId}` **with retries** |
| 26 | `GET /tickets/bookingid/{bookingId}` **with retries** |
| 27 | Save to Redux + localStorage |
| 28 | Redirect to `/ticket-traking` |

**Why retries?** Ticket creation runs in the Geidea **server callback**. `get-booking-id` returns a booking only after `bookticketGeidea()` sets `paystatusgeidea.ticket_id`.

### `get-booking-id` response

```json
{
  "status": "success",
  "booking_id": "TBXXXXXXXX",
  "is_exist": 0
}
```

`is_exist`: `0` = user account created today; `1` = existing user.

### `bookingid` response

Full ticket payload via `singelBooking` (seats, trip, passenger name, company, etc.).

### Error / cancel

| Callback | Behavior |
|----------|----------|
| `onError` | Toast with Geidea `detailResponseMessage` |
| `onCancel` | Toast “Payment cancelled”; release hold via `deleteHoldSeat` if applicable |

---

## Round trip

| Step | Endpoint |
|------|----------|
| Create session | `POST /tickets/round-trip-payment-process/` |
| Geidea callback | Same `payment-sucess` |
| Resolve booking | Same `get-booking-id` / `bookingid` |

Backend stores **two** `paystatusgeidea` rows (outbound + return) with the **same** `sessionId` and splits amounts proportionally. One Geidea charge for the combined total.

---

## API endpoint reference

| Purpose | Method | Path |
|---------|--------|------|
| Hold seats | POST | `checkseats/{book_from}` |
| Release hold | POST | `deleteHoldSeat` |
| Create session (one-way) | POST | `payment-process/` |
| Create session (round trip) | POST | `round-trip-payment-process/` |
| Geidea callback | POST | `payment-sucess` |
| Booking ID from session | GET | `get-booking-id/{sessionId}` |
| Ticket details | GET | `bookingid/{bookingId}` |
| Payment status check | POST | `check-payment` |

Routes defined in: `modules/Ticket/Config/Routes.php`  
Handlers in: `modules/Ticket/Controllers/Api/Ticket.php`

---

## Legacy / alternate paths (not main website checkout)

| Path | Notes |
|------|-------|
| `/geidea` route | Older iframe HPP |
| `checkPayment.jsx` | URL param callback `?session=` |
| `TravellerInformation` multi-gateway | PayPal / Razorpay / etc. — checkout now uses Geidea drop-in |
| `POST book-ticket` | Alternate `bookticketGeidea` entry; main flow uses `payment-sucess` callback |

---

## Deployment checklist (payment)

- [ ] `app.baseURL` = `https://data.busvalley.com/busvalley_new`
- [ ] Geidea callback URL registered as `.../modules/api/v1/tickets/payment-sucess`
- [ ] Geidea credentials in admin (`geidea_wave` table / payment settings)
- [ ] CORS allows website origin (`app/Config/ApiCors.php`)
- [ ] Website axios base URL includes `/busvalley_new/modules/api/v1`
- [ ] Frontend sends `language`, `ticket_token`, correct `amount` when offer applied

---

## One-line summary

**Website:** validate → hold seats → ask backend for `sessionId` → Geidea SDK drop-in → on success, poll for booking → ticket tracking.  
**Backend:** create Geidea session, validate fare, finalize ticket on Geidea server callback.  
**Geidea:** payment UI + POST callback to backend.
