166 lines
8.3 KiB
Markdown
166 lines
8.3 KiB
Markdown
# Live socket-API endpoints — direct-MSSQL feasibility & queries
|
||
|
||
> **Authority: [`docs/buspro-database-reference.md`](../buspro-database-reference.md).**
|
||
> Schema semantics live there and win on any conflict. This file is retained for the
|
||
> per-endpoint migration verdicts and the `frei` discovery SQL.
|
||
|
||
This complements `travel-queries.md`. That doc replaced the **XML export** (static
|
||
catalog). This one covers the **live socket API** methods on
|
||
`src/BusProNet/ApiClient.php` and classifies how replicable each is by direct MSSQL
|
||
query in the Go proxy.
|
||
|
||
**Verdict in one line:** the *structural* parts replicate cleanly; the *live "free"
|
||
counts* are computed by BusPro's allocation logic (not a flat column) and should either
|
||
stay on the socket API or be reproduced with an explicitly-approximated query;
|
||
`getMutableData()` is pure business-rule logic — keep it on the socket API.
|
||
|
||
| Endpoint (type) | Tier | Replicable in SQL? |
|
||
|---|---|---|
|
||
| `getProducts()` (`PRODUKTE`) | 1 | ✅ trivial |
|
||
| `getAgencies()` (`AGENTUREN`) | 1 | ✅ structural (confirm agency filter + address joins) |
|
||
| `getTravelData()` (`PRODUKTDATEN`) | 1 | ✅ = `travel-queries.md` Query B |
|
||
| `getAvailabilities()` (`VERFUEGBARKEIT`) | 2 | ⚠ structure yes; `frei` count is computed |
|
||
| `getAvailabilitiesExtended()` (`VERFUEGBARKEIT2`) | 2 | ⚠ structure yes (we have the extra fields); `frei` computed |
|
||
| `getHotelAvailability()` (`VERFUEGBARKEITHOTEL`) | 2 | ⚠ `frei` = contingent − active bookings |
|
||
| `getMutableData()` (`MOEGLICHEAENDERUNGEN`) | 3 | ✋ business rules — keep on socket API |
|
||
|
||
---
|
||
|
||
## Tier 1 — structural, replicate as-is
|
||
|
||
### `getProducts()` → `Product{id, code, name}`
|
||
Parser keeps only rows with a non-empty `code` (`ProductsParser`).
|
||
```sql
|
||
SELECT IDProdukt AS id, Code AS code, Bezeichnung AS name
|
||
FROM dbo.Produkt
|
||
WHERE Code IS NOT NULL AND Code <> ''
|
||
ORDER BY Bezeichnung;
|
||
```
|
||
|
||
### `getTravelData(idprodukt[, hotelId])` → `Travel`
|
||
This is the same payload as `travel-queries.md` **Query B** (header + services +
|
||
pickups + hotels/rooms + tags). The *static* structure is fully covered there. Its
|
||
embedded live `preis`/`frei` values overlap Tier 2 — source those the same way you
|
||
decide to handle availability below. No new SQL needed; reuse Query B.
|
||
|
||
### `getAgencies()` → `Agency{id, name, code, street, postCode, city, phone}`
|
||
`name/code` map to `Adresse.SuchName` + a code column; `street/postCode/city` live in
|
||
`AdresseAnschrift`, `phone` in `AdresseKommunikation` (1-to-N → pick the primary row).
|
||
Two things to pin with the discovery queries below: (1) **which `Adresse` rows are
|
||
agencies**, and (2) the exact `AdresseAnschrift`/`AdresseKommunikation` column + primary-row flag.
|
||
```sql
|
||
-- skeleton (fill the agency filter + verified column names from discovery):
|
||
SELECT a.IDAdresse AS id,
|
||
a.SuchName AS name,
|
||
a.Code AS code, -- ⚠ confirm code column on Adresse
|
||
an.Strasse AS street, -- ⚠ from AdresseAnschrift
|
||
an.Plz AS postCode,
|
||
an.Ort AS city,
|
||
ak.Wert AS phone -- ⚠ from AdresseKommunikation (primary tel)
|
||
FROM dbo.Adresse a
|
||
LEFT JOIN dbo.AdresseAnschrift an ON an.IDAdresse_FS = a.IDAdresse /* AND an.IsPrimary */
|
||
LEFT JOIN dbo.AdresseKommunikation ak ON ak.IDAdresse_FS = a.IDAdresse /* AND ak.Art = 'TEL' */
|
||
WHERE /* a is an agency — see discovery */ 1 = 0;
|
||
```
|
||
Discovery:
|
||
```sql
|
||
-- (1) how agencies are flagged (inspect the org/role tables):
|
||
SELECT TOP 20 * FROM dbo.AdresseAgenturOrganisation;
|
||
SELECT IDArtAgenturOrganisation, * FROM dbo.ArtAgenturOrganisation; -- the agency "types"
|
||
-- (2) the address + comm column names / primary-row flags:
|
||
SELECT TOP 5 * FROM dbo.AdresseAnschrift;
|
||
SELECT TOP 5 * FROM dbo.AdresseKommunikation;
|
||
SELECT TOP 5 * FROM dbo.Adresse; -- find the 'code' column
|
||
```
|
||
|
||
---
|
||
|
||
## Tier 2 — availability: structure is flat, the **`frei` count is computed**
|
||
|
||
The response models (`Availability`, `ExtendedAvailability`, `Room`) carry per-item
|
||
`status`, `preis`, structural attrs (dates/ages/mandatory/board) **and** `frei`. The
|
||
first set is a flat read; **`frei` is not stored**.
|
||
|
||
**Why `frei` is computed (validated):** `Leistung.Kontingent` is a fixed planning
|
||
allotment, not the remaining free count — e.g. service `155234` has `Kontingent=2500`
|
||
but `2945` booking rows (bookings *exceed* the allotment). So:
|
||
```
|
||
frei = Kontingent − (active, non-cancelled, non-expired bookings) [± pooled contingent]
|
||
```
|
||
with three real complications:
|
||
- **Cancellations**: `BuchungTNLeistung` has no own status; join up via `IDBuchungTN_FS`
|
||
→ `BuchungTN` → `Buchung` and exclude Storno (the `BuchungStatus*` flags / Storno date).
|
||
- **Option expiry**: `BuchungTNLeistung.OptionsDatum` + `Leistung.KontingentVerfallTage`
|
||
— expired options free their slots back.
|
||
- **Pooled contingents**: `Leistung.IDLeistung_FS_KontingentAUS` / `KontingentAusPartner`
|
||
— a service may draw from another service's pool (or a `PartnerKontingent`), so its
|
||
`frei` depends on a sibling, not just its own bookings.
|
||
|
||
> **Recommendation:** keep the *authoritative* `frei` on the socket API
|
||
> (`getAvailabilities`/`getHotelAvailability`). If you want it in SQL, treat it as a
|
||
> documented **approximation** and validate row-by-row against the socket API before
|
||
> trusting it for booking decisions. The structural fields below are safe to serve from SQL.
|
||
|
||
### `getAvailabilities()` / `getAvailabilitiesExtended()` — structural part
|
||
```sql
|
||
SELECT
|
||
l.IDLeistung AS serviceId,
|
||
l.Status AS status,
|
||
lp.Preis AS price,
|
||
l.Kontingent AS allotment, -- NOT 'frei' (see calc above)
|
||
-- extended-only fields (VERFUEGBARKEIT2):
|
||
l.Termin_VON AS dateFrom, l.Termin_BIS AS dateTo,
|
||
l.Alter_VON AS ageFrom, l.Alter_BIS AS ageTo,
|
||
l.Pflichtmerkmal AS mandatory
|
||
FROM dbo.ReiseLeistung rl
|
||
JOIN dbo.Leistung l ON l.IDLeistung = rl.IDLeistung_FS
|
||
LEFT JOIN dbo.LeistungPreis lp ON lp.IDLeistung_FS = l.IDLeistung AND lp.IDArtPreisSchema_FS = 1
|
||
WHERE rl.IDReise_FS = @DateId
|
||
ORDER BY l.Art, l.Unterart;
|
||
-- travel-level 'buchungstatusmoeglich' / 'status' come from the Reise + the allowed
|
||
-- ArtBuchungsStatus set; the extended 'uhrzeit_von'/'hinweis' come from the pickup
|
||
-- time plan (ZustiegZeitplan) and a Leistung text — see travel-queries.md B2/B2b.
|
||
```
|
||
The live `frei` per `serviceId` = the calc above (see recommendation).
|
||
|
||
### `getHotelAvailability(idreise, idpartner, terminbis)` → `Room{frei, status, preis, …}`
|
||
Rooms/price/`status` are the `travel-queries.md` **B3** chain;
|
||
`status` = `PartnerKontingent.BuchungsStatus` (`''`/`S`/`A`). `frei` = `PartnerKontingent.Anzahl`
|
||
**minus active hotel bookings** for that `(IDAdresse, IDZimmer, date-span)` — same
|
||
cancellation/expiry caveats as above.
|
||
|
||
### Discovery — quantify the `frei` calc against the socket API
|
||
```sql
|
||
-- active (non-storno) booking rows for a service, with option dates:
|
||
SELECT btl.IDLeistung_FS, COUNT(*) AS rows_total,
|
||
SUM(CASE WHEN btl.OptionsDatum IS NULL OR btl.OptionsDatum >= GETDATE() THEN 1 ELSE 0 END) AS rows_live
|
||
FROM dbo.BuchungTNLeistung btl
|
||
JOIN dbo.BuchungTN bt ON bt.IDBuchungTN = btl.IDBuchungTN_FS
|
||
JOIN dbo.Buchung b ON b.IDBuchung = bt.IDBuchung_FS -- ⚠ confirm FK name
|
||
WHERE btl.IDLeistung_FS = @ServiceId
|
||
/* AND b.<storno flag> = 0 */ -- ⚠ confirm Storno column
|
||
GROUP BY btl.IDLeistung_FS;
|
||
-- then compare Leistung.Kontingent − rows_live vs the socket-API 'frei' for the same service.
|
||
```
|
||
|
||
---
|
||
|
||
## Tier 3 — `getMutableData()` — keep on the socket API
|
||
|
||
Returns per-aspect `möglich` (bool) + `möglichbiszum` (deadline) for: participant
|
||
count, transportation, pickup, accommodation, additional services, participant data.
|
||
These are **derived from booking rules / change-cutoff config / departure timing**
|
||
(not a flat table). Reproducing them in SQL would mean re-implementing BusPro's
|
||
change-deadline logic and risks silent divergence. Recommendation: leave this on the
|
||
socket API; don't migrate it.
|
||
|
||
---
|
||
|
||
## Summary for the Go proxy
|
||
- **Migrate now (SQL):** `getProducts`, `getAgencies` (after the agency-filter discovery),
|
||
`getTravelData` (reuse Query B), and the **structural** fields of the two availability
|
||
endpoints.
|
||
- **Keep computed:** the live `frei` counts — socket API, or an explicitly-validated
|
||
approximation.
|
||
- **Keep on socket:** `getMutableData`.
|