# fb_connectx (v2)

Supplier orchestrator between **bw_front** (booking_window_hotel_front_node) and
the supplier services:

```
bw_front ──► connectX ─┬─ TBO  merged tbo-hotels code   (in-process, TBO_MODE=local)
 markup        fetch all ├─ BW   merged bw_hotel_node code (in-process, BW_MODE=local)
 conversion    normalize └─ GRN  merged grnc code         (in-process, GRN_MODE=local)
 sorting       merge
```

`local` needs `.env.tbo` / `.env.bw` / `.env.grn` (copy of that service's own `.env`, see the
`.example` files). Without the file, or with `<CODE>_MODE=http`, connectX calls
the separate service at `<CODE>_URL` as before - that is the rollback.

connectX never marks up or converts. Prices it returns are NET, in the
supplier's currency. v1 (partner API, MCP) is in `_backup_v1/`.

## Endpoints (all `/internal` calls require `connectx-api-key: <client key>`)

| Method | Path | Purpose |
|---|---|---|
| GET | `/health` | liveness + configured suppliers (no key needed) |
| POST | `/internal/hotels/search` | `{ request, sources? }` → parallel search, merged hotels with `offers[]` |
| POST | `/internal/suppliers/:source/:action` | body forwarded to the supplier; its status + body returned verbatim |

`action`: `search | details | prebook | book | cancel | voucher | status | cancellation_amount`.
`status` for GRN is a GET upstream; pass the booking reference as `?path_param=`.

Response header `x-connectx-origin`: `supplier` (relayed reply) or `connectx`
(connectX's own failure: `SUPPLIER_TIMEOUT` 504, `SUPPLIER_UNREACHABLE` 502,
unsupported 501, disabled 503).

The end user's `Authorization` header is forwarded to the suppliers
(tbo-hotels / grnc authenticate the user from it). BW calls also carry
`x-internal-key: $BW_SUPPLIER_KEY`.

### Search response

```json
{
  "status": true,
  "search_id": "CX_…",
  "suppliers": { "BW": { "status": "success", "count": 12 }, "TBO": { "status": "timeout", "count": 0, "error": {…} } },
  "total_offers": 40,
  "total_hotels": 31,
  "hotels": [{
    "merge_key": "grand palace|bangkok",
    "hotel_name": "Grand Palace", "city": "Bangkok", "country": "Thailand", "star_rating": 4, "…": "…",
    "sources": ["BW", "TBO"],
    "offers": [{ "source": "BW", "seq": 0, "net_price": 3000, "currency": "THB", "raw": { "…supplier's own hotel object…" } }]
  }]
}
```

Merge key = `lower(name)|lower(city)` (what bw_front's normalizer used),
falling back to the supplier hotel id.

## Databases

All MySQL settings are in the main `.env`:
`MYSQL_HOST/PORT/USER/PASSWORD` (shared login), `DB_PREFIX`, and one database
name per part: `TBO_DB_NAME`, `GRN_DB_NAME`, `BW_DB_NAME`, `CONNECTX_DB_NAME`,
`HOTELS_MASTER_TABLE`. A part on another server or login overrides only itself
with `<PART>_DB_HOST/PORT/USER/PASSWORD` (PART = TBO, GRN, BW, CONNECTX,
HOTELS_MASTER; hotels_master follows TBO's server by default). Values missing
from the main `.env` still fall back to the old `.env.tbo` / `.env.grn` /
`.env.bw`; those files are no longer needed.

Supplier code settings, also in the main `.env`: `JWT_SECRET` (must be
bw_front's value - the suppliers verify the end user's Bearer token with it),
`TBO_RESPONSE_TIME`, `TBO_SEARCH_BUDGET_MS`, `TBO_CHUNK_TIMEOUT_MS`. Supplier
accounts (TBO user/password/URLs, GRN api key/base URL) are never read from env:
they are per client in the database. The startup log names
anything missing (`<SUPPLIER>: missing settings: ...`).

## Clients and their supplier accounts

connectX serves many clients (products, projects, hotel resellers). Each client
has its own `connectx-api-key` and its own supplier accounts; a supplier the
client has not switched on and configured is never called for it.

Setup (once per environment):

1. Run `sql/connectx_clients.sql` (creates schema `connectx`).
2. Set `CONNECTX_CREDENTIALS_KEY` (32 bytes, hex) and `CONNECTX_ADMIN_KEY` in `.env`.
3. Set `CONNECTX_ADMIN_JWT_SECRET`, restart, and create the first admin on the server:
   `node scripts/create-admin.js "Your Name" you@example.com super_admin` (asks for the password).
4. Log in: `POST /admin/login {"email","password"}` -> `token` (8 h). Send
   `Authorization: Bearer <token>` on every other `/admin` call. The optional
   `connectx-admin-key` header (CONNECTX_ADMIN_KEY) still works as a super-admin fallback for scripts.

Admin accounts (super admin only): `GET/POST /admin/admins`, `GET/PUT/DELETE /admin/admins/:id`,
`POST /admin/admins/:id/password`. Own account: `GET /admin/me`, `POST /admin/me/password`.
Five wrong passwords pause an account for 15 minutes; disabling an admin or changing a
password ends that admin's sessions at once.

5. Manage clients and their suppliers:

| Method | Path | Body |
| --- | --- | --- |
| POST | `/admin/clients` | `{ "name": "Booking Window" }` -> returns `api_key` (also shown later by GET) |
| GET | `/admin/clients`, `/admin/clients/:id` | includes each client's `api_key`; supplier credentials masked |
| PUT | `/admin/clients/:id` | `{ "name"?, "status": 0/1 }` |
| DELETE | `/admin/clients/:id` | removes the client and all its supplier rows |
| POST | `/admin/clients/:id/rotate-key` | new `api_key`, old one stops working |
| PUT | `/admin/clients/:id/suppliers/:code` | `{ "is_active": true, "credentials": {...}, "settings": {...} }` or `{ "import_from_env": true }` |
| DELETE | `/admin/clients/:id/suppliers/:code` | |

Supplier fields:
- **TBO** credentials `username`, `password`; settings `search_url`, `prebook_url`, `book_url`, `cancel_url`, `voucher_url` (required), `booking_detail_url`, `end_user_ip`.
- **GRN** credentials `api_key`; settings `base_url`.
- **BW** nothing: `{ "is_active": true }`.

Credentials are AES-256-GCM encrypted in the DB and never logged or returned.
Per-client accounts need `local` supplier mode; `http` mode (old separate
services) cannot use a client's account.

bw_front is a client like any other: create it, set its suppliers, and put its
key in bw_front's `CONNECTX_API_KEY`. There is no other way in.

## Code structure (same layout as bw_front)

```
connectx_server.js            express + all routes (callback style)
config.js                     env -> config object
app/
  controllers/hotelController.js      searchHotels(req, callback), supplierCall(req, callback)
  services/hotelListService.js        parallel supplier search -> normalize -> merge
  services/supplierCallService.js     the only HTTP call to a supplier
  services/hotelNormalizerService.js  one NET shape per supplier hotel
  services/internalAuthMiddleware.js  x-internal-key check
  suppliers/tboService.js             TBO paths + search payload
  suppliers/grnService.js             GRN paths + search payload
  suppliers/bwService.js              BW paths + search payload
  suppliers/tbo/                      tbo-hotels code (routes, repo, GlobalService, config) - near verbatim
  suppliers/bw/                       bw_hotel_node code (controller, occupancy helper, db)
  suppliers/grn/                      grnc code (routes/hotel.routes.js, grn.service, GlobalService, auth, db)
  functions/localRoutes.js            router.post(...) collector + in-process invoke (replaces the HTTP hop)
  core/hotelMergeEngine.js            cross-supplier merge
  functions/commonFunctions.js        withTimeout, decodeBearer, redact, appError
```

Adding a supplier: new file in `app/suppliers/`, add it to `SUPPLIERS` in
`supplierCallService.js` and to `SUPPLIER_ORDER` in `hotelListService.js`.

## Run

```bash
# create .env with the settings listed under "Databases" and "Clients" above
npm install
npm start
```
