# Travel catalog — direct MSSQL queries (to replace the XML export) > **Authority: [`docs/buspro-database-reference.md`](../buspro-database-reference.md).** > Schema semantics, conventions and confidence marks live there and win on any conflict. > This file is retained as a **query cookbook** — it holds worked SQL (included-services > A2a/A2b, the full room-pricing chain B3) not reproduced in the reference. Purpose: reproduce, straight from the BusPro MSSQL tables, the data the Symfony app currently parses from the `Ziel_*.xml` exports, so the Go proxy can serve it without the SFTP/XML round-trip. Two read paths: - **Query A — travel list:** every bookable departure + product + dates (+ hotels, + a minimal bookable-services list). - **Query B — detail:** everything for one selected `(date, hotel)` combination. Validated against the live DB using the sample `var/xmlexport/Ziel_1121.xml` (Produkt `1121`, departure `Reise.IDReise=12665`, hotel `Adresse.IDAdresse=224923`, room-type `Zimmerart 1413`, pickup `Zustieg 494`). Columns verified in `docs/buspronet-schema/*.csv`. Header, services, pickups, hotels, room pricing, availability and tags are all confirmed against live data; the "included services" list for Query A (§A2) is defined as price-`0.00` services + the 10 hardcoded "Leistungen" selection groups (ids baked in). Nothing left to resolve. ## The naming trap (read first) The XML export **renames** the core entities — do not map by name: | XML term | Real table | Notes | |---|---|---| | `` | **`Produkt`** | the travel *product*: `IDProdukt`, `Code`, `Bezeichnung` | | `` | **`Reise`** | the **dated departure**: `IDReise=12665`, `IDProdukt_FS=1121`, `Code`, `TerminVon/Bis`, `ReiseArt`, `Status`, `StornoDatum` | | DB `Termin` table | — | **ignore**: generic appointments (odometer/repairs) | | `` (lei_befoerderung / lei_sonstiges) | `ReiseLeistung → Leistung` | `Leistung.Art` = `BEF` / `SON`; price via `LeistungPreis` | | `` + rooms | `ReisePartner` (+ `Partner*` subsystem) | **not** `ReiseLeistung`; keyed by the hotel `Adresse` | | `` | `LeistungBefoerderung.ZustiegIDs` → `Zustieg` (+ `ZustiegZeitplan` for the time) | | | `` / `` | `ReiseSelektionen → Selektionsstamm` (+ `Selektionsgruppe → Selektionsgruppenstamm`) | | `Produkt 1—N Reise`. **`Reise.IDReise` is the "date id"** and the anchor for all per-departure data. **Two distinct status axes** — don't conflate them: 1. **Departure lifecycle** — `Reise.Status` (`nvarchar(2)`), observed `''` ≈ 9767, `S` ≈ 945, `L` ≈ 845. Confirmed against the live DB: - `''` = active/bookable. - `S` = **Storno** (cancelled): every `S` row has `StornoDatum` set. - `L` = **template / future master** (not a real sellable departure): every `L` row has `StornoDatum NULL`, far-future `TerminVon` (2029–2035) and template-style `Code`s (`MRAGRMP`, no date suffix). `Leistung.Status` mirrors this (`''` = 196 504, `L` = 1 672 — the leistungen attached to those template departures). 2. **Availability / booking status** — booking-state codes, **not** `Leistung.Status` (which only holds `''`/`L`). Canonical dictionary = **`dbo.ArtBuchungsStatus`** (`Code`), confirmed values: | Code | Kurztext | meaning | |---|---|---| | `F` | Fest | firm/final booking (available) | | `O` | Option | held option | | `A` | Anfrage | on request / inquiry-only (groups-inquiry path) | | `W` | Warteliste | waitlist | | `S` | Storno | cancelled | | `U` | Umbuchung | rebooking | The **working column** for hotel availability is **`PartnerKontingent.BuchungsStatus`** (`nvarchar(2)`) — confirmed to carry these: observed `''` ≈ 19 664 (free/Fest), `S` ≈ 12 215 (stop), `A` ≈ 2 060 (on request). So per room/date: `''` = bookable, `S` = not bookable, `A` = inquiry-only. (The same codes also appear via `…IDArtBuchungsstatus_FS` on individual bookings such as `ZusatzZimmer`.) Transport ``/`` is not in `Leistung.Status`; treat it as derived from the transport `Kontingent` count. Bookable filter (departure-level): `Reise.Status = '' AND Reise.StornoDatum IS NULL` (excludes both `S` Storno and `L` templates), then apply `PartnerKontingent.BuchungsStatus` per room/date (exclude `S`, treat `A` as inquiry-only). Note the XML repeats `lei_sonstiges` under each hotel; in the DB the services exist **once** on the departure (`ReiseLeistung`). Conventions: ids `bigint`→`int64`; prices `decimal(18,2)`; `_FS` = foreign key (many soft/unenforced — join defensively); bracket non-ASCII columns (`[Reisebüro]`). --- ## Query A — travel list ### A1. Departures + product + dates (header) ```sql SELECT p.IDProdukt AS ProductId, p.Code AS ProductCode, p.Bezeichnung AS ProductLabel, r.IDReise AS DateId, r.Code AS DateCode, r.ReiseArt AS TravelType, -- XML reiseart, e.g. 'F' r.Bezeichnung AS DateLabel, r.TerminVon AS DateFrom, r.TerminBis AS DateTo FROM dbo.Reise AS r JOIN dbo.Produkt AS p ON p.IDProdukt = r.IDProdukt_FS WHERE r.Status = '' AND r.StornoDatum IS NULL -- bookable (confirm status codes) ORDER BY r.TerminVon, p.Code; ``` Customer description (XML ``) = `dbo.ReiseText.ExternText` (`IDReise_FS`) — join if wanted. ### A2. Included-services list per departure "Minimum services" = the **included** services (no surcharge), from two sources that get merged into one plain list per departure. **A2a — included `Leistung`s (sales price = 0.00).** Priced services are optional add-ons; the package-included ones have a `0.00` sales price (schema 1). ```sql SELECT rl.IDReise_FS AS DateId, l.IDLeistung AS ServiceId, l.Art AS Type, -- 'BEF' (transport) | 'SON' (other) l.Unterart AS SubType, -- BUS/PKW/SPA/VER/SON/LVS/... l.Bezeichnung AS Label FROM dbo.ReiseLeistung rl JOIN dbo.Leistung l ON l.IDLeistung = rl.IDLeistung_FS JOIN dbo.LeistungPreis lp ON lp.IDLeistung_FS = l.IDLeistung AND lp.IDArtPreisSchema_FS = 1 -- standard sales price (confirmed) WHERE rl.IDReise_FS IN (@DateIds) AND lp.Preis = 0.00 -- included = no surcharge ORDER BY rl.IDReise_FS, l.Art, l.Unterart; ``` > Note: a `LEFT JOIN` would also surface services with **no** price row (NULL). Those > are *not* the same as priced-at-zero — keep the inner join so "included" means an > explicit `0.00`. (If BusPro models some included items as price-less rows, widen to > `lp.Preis = 0.00 OR lp.IDLeistungPreis IS NULL` — confirm with the discovery query.) **A2b — "Leistungen…" selection groups (plain list).** The catalog also lists included services as selection tags under the "Leistungen" groups. Filter by a **hardcoded list of `Selektionsgruppenstamm` ids** (configured in the proxy) rather than a label match — the labels can be renamed, the ids are stable. Same join as B4; emit `Selektion` as the plain text item. ```sql SELECT rs.IDReise_FS AS DateId, gs.IDSelektionsgruppenstamm AS GroupId, gs.Selektionsgruppe AS GroupLabel, -- e.g. 'Leistungen inklusive' s.Selektion AS Item FROM dbo.ReiseSelektionen rs JOIN dbo.Selektionsstamm s ON s.IDSelektionsstamm = rs.IDSelektionsstamm_FS JOIN dbo.Selektionsgruppe sg ON sg.IDSelektionsstamm_FS = s.IDSelektionsstamm JOIN dbo.Selektionsgruppenstamm gs ON gs.IDSelektionsgruppenstamm = sg.IDSelektionsgruppenstamm_FS WHERE rs.IDReise_FS IN (@DateIds) AND gs.IDSelektionsgruppenstamm IN (70, 89, 87, 88, 86, 7, 8, 90, 85, 71) ORDER BY rs.IDReise_FS, gs.Position, sg.Position; ``` > The "Leistungen" `Selektionsgruppenstamm` ids (captured from the live DB — hardcode in > the proxy; re-check if groups are added/renamed): > > | id | group | id | group | > |---|---|---|---| > | 70 | Leistungen | 86 | Leistungen Programm | > | 89 | Leistungen Events | 7 | Leistungen Skipass | > | 87 | Leistungen Kurse | 8 | Leistungen Sonstige | > | 88 | Leistungen Personal | 90 | Leistungen Transport | > | 85 | Leistungen Übernachtung | 71 | Leistungen Verpflegung | > `LeistungPreis` has one row per `(Leistung, IDArtPreisSchema_FS)`; schema `1` is the > standard sales price and reproduced the XML service prices exactly (13.90, 50.00, …). --- ## Query B — detail for one `(@DateId [, @HotelId])` Run a header query + one query per child collection; assemble in Go. ### B1. Header — A1 with `WHERE r.IDReise = @DateId` (+ `ReiseText.ExternText`). ### B2. Services (transport + other, with direction & pickups) — **confirmed** ```sql SELECT l.IDLeistung AS ServiceId, l.Art AS Type, -- BEF | SON l.Unterart AS SubType, l.Bezeichnung AS Label, l.Pflichtmerkmal AS Mandatory, l.Kontingent AS Available, l.Alter_VON AS AgeFrom, l.Alter_BIS AS AgeTo, l.Termin_VON AS DateFrom, l.Termin_BIS AS DateTo, lb.Richtung_Hin_Rueck AS Direction, -- 'H' | 'R' (transport only) lb.ZustiegIDs AS PickupIds, -- delimited list of dbo.Zustieg ids lb.IDArtZustiegZeitplan_FS AS PickupTimePlanId, lp.Preis AS Price FROM dbo.ReiseLeistung rl JOIN dbo.Leistung l ON l.IDLeistung = rl.IDLeistung_FS LEFT JOIN dbo.LeistungBefoerderung lb ON lb.IDLeistung_FS = l.IDLeistung 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, l.Bezeichnung; ``` `LeistungBefoerderung` present ⇒ transport (`lei_befoerderung`), else `lei_sonstiges`. English texts (``) = `dbo.LeistungText.ExternText` (validate). ### B2b. Pickup detail (resolve the `ZustiegIDs` list + time) — **confirmed** For each id parsed from `lb.ZustiegIDs`: ```sql SELECT z.IDZustieg AS PickupId, z.Zustiegcode AS Code, z.Plz AS PostalCode, z.Ort AS City, z.Strasse AS Street, CONVERT(time, zp.Uhrzeit) AS PickupTime FROM dbo.Zustieg z LEFT JOIN dbo.ZustiegZeitplan zp ON zp.IDZustieg_FS = z.IDZustieg AND zp.IDArtZustiegZeitplan_FS = @PickupTimePlanId -- from B2 (0 ⇒ no time) WHERE z.IDZustieg = @PickupId; ``` Only the **time-of-day** of `Uhrzeit` is meaningful (the date part is a placeholder). Verified: zustieg `494`, plan `2546` → `05:15`. ### B3. Hotels + rooms + board — **Partner subsystem** The XML `` is a hotel **`Adresse.IDAdresse`** (224923 = "NEU Sportclub Waldschlössli – Gruppen 2"), **not** a `ReisePartner`. Hotels are assigned at the **product** level via the join table `PartnerProdukt (IDProdukt_FS, IDAdresse_FS)` — that's why a departure (`IDReise`) has no `ReisePartner` row. (`ReisePartner`/`IDArtVerpflegung_FS` is the booking-side hotel; for the *catalog* use `PartnerProdukt`. `ProduktBeschreibung.IDAdresse_FS_Hotel` = the single featured hotel.) ```sql -- hotels of the product (each IDAdresse_FS = an XML ) SELECT pp.IDAdresse_FS AS HotelAddressId, a.SuchName AS HotelName FROM dbo.PartnerProdukt pp JOIN dbo.Adresse a ON a.IDAdresse = pp.IDAdresse_FS WHERE pp.IDProdukt_FS = @ProductId -- e.g. 1121 -- AND pp.IDAdresse_FS = @HotelAddressId ORDER BY a.SuchName; ``` Board (`IDArtVerpflegung`) and stay dates aren't on `PartnerProdukt`; board comes from the `PartnerPreisKategorie` rows below, and the stay span = the departure dates (`Reise.TerminVon/TerminBis`, here `11.–13.12.2026` ⇒ `Naechte = 2`). Rooms / price / availability for a hotel address. The pricing fans out over `room → price-category → season tafel → nights`; **validated** against the live DB for hotel `224923`, room-type `Zimmerart 1413` (Code `MBZ`) → `IDArtPartnerPreisCode 38` → category `9478`, season `15420` (covers `2026-12-11`), `Naechte=2`, `EK_VK='VK'`, `IDArtPreisSchema_FS=1` → **`269,00`** (single clean row; same schema id `1` as the `LeistungPreis` side). Map a room to its price category via `IDArtPartnerPreisCode_FS`. (The XML headline `abpreis` of `289,00` = this room price + mandatory services, i.e. computed — see note below; the flat per-room price is `269,00`.) ```sql -- 1) rooms the hotel offers -- PartnerZimmer ⋈ PartnerPreisKategorie on (IDAdresse_FS, IDArtPartnerPreisCode_FS) SELECT pz.IDPartnerZimmer AS RoomId, za.Code AS RoomCode, za.Bezeichnung AS RoomLabel, za.PaxMin, za.PaxMax, pz.IDArtPartnerPreisCode_FS AS PriceCode FROM dbo.PartnerZimmer pz JOIN dbo.Zimmerart za ON za.IDZimmerart = pz.IDZimmerart_FS WHERE pz.IDAdresse_FS = @HotelAddressId; -- 2) which season tafel covers the arrival date SELECT IDPartnerSaisonTafel_FS FROM dbo.PartnerSaisonzeit WHERE IDAdresse_FS = @HotelAddressId AND TerminVon <= @Anreise AND TerminBis >= @Anreise AND EK_VK = 'VK'; -- sale side -- 3) price for the room's category × that season × nights SELECT pp.Preis AS RoomPrice, pp.Naechte, pp.Verlaengerung FROM dbo.PartnerPreisKategorie pk JOIN dbo.PartnerPreis pp ON pp.IDPartnerPreisKategorie_FS = pk.IDPartnerPreisKategorie WHERE pk.IDAdresse_FS = @HotelAddressId AND pk.IDArtPartnerPreisCode_FS = @PriceCode -- from step 1 (room → category) AND pk.IDArtVerpflegung_FS = @BoardId -- ReisePartner.IDArtVerpflegung_FS AND pk.EK_VK = 'VK' AND pp.IDPartnerSaisonTafel_FS = @SaisonTafelId -- from step 2 AND pp.Naechte = @Nights; -- nights of the stay ``` > `EK_VK` distinguishes purchase (`EK`) from sale (`VK`) — always filter `VK` for the > customer price. `PartnerZimmer.PreisJeZimmer` flags per-room vs. per-person pricing. ```sql -- availability (XML 'verfuegbar') — VALIDATED: = PartnerKontingent.Anzahl for the -- (hotel, room-type, date-span). IDZimmer_FS = the XML idbuspro_zimmer (a Zimmerart). -- Sample: IDAdresse_FS=224923, IDZimmer_FS=1413, 2026-12-11 → Anzahl=25 = XML verfuegbar. SELECT IDZimmer_FS, Anzahl, BuchungsStatus FROM dbo.PartnerKontingent WHERE IDAdresse_FS = @HotelAddressId AND IDZimmer_FS = @ZimmerartId -- the room type (XML idbuspro_zimmer) AND TerminVon <= @Abreise AND TerminBis >= @Anreise; ``` > `verfuegbar` is a **flat column** (`PartnerKontingent.Anzahl`) — reproducible by SQL. > Gate it with `BuchungsStatus` (`''` bookable, `S` stop, `A` on-request). > `` (the headline "from" price) is still **computed** (cheapest valid room + > mandatory services) — recompute in the proxy from the per-room prices above. ### B4. Selection tags + groups — **confirmed** ```sql SELECT gs.Selektionsgruppe AS GroupLabel, gs.Position AS GroupPos, s.IDSelektionsstamm AS SelectionId, s.Selektion AS SelectionLabel FROM dbo.ReiseSelektionen rs JOIN dbo.Selektionsstamm s ON s.IDSelektionsstamm = rs.IDSelektionsstamm_FS JOIN dbo.Selektionsgruppe sg ON sg.IDSelektionsstamm_FS = s.IDSelektionsstamm JOIN dbo.Selektionsgruppenstamm gs ON gs.IDSelektionsgruppenstamm = sg.IDSelektionsgruppenstamm_FS WHERE rs.IDReise_FS = @DateId ORDER BY gs.Position, sg.Position; ``` --- ## Status / open decisions 1. ~~**Status codes** (A1)~~ — ✅ confirmed: `Reise.Status` `''` = bookable, `S` = Storno (cancelled, `StornoDatum` set), `L` = template/future master (exclude both). 2. ~~**F/A/O/S column**~~ — ✅ codes in `dbo.ArtBuchungsStatus` (F/O/A/W/S/U); the working hotel-availability column is `PartnerKontingent.BuchungsStatus` (`''`/`S`/`A`). 3. ~~**Hotel link**~~ — ✅ XML `` = `Adresse.IDAdresse`; departure→hotel via `PartnerProdukt(IDProdukt_FS, IDAdresse_FS)` at product level (not `ReisePartner`). 4. ~~**Partner room pricing**~~ — ✅ fully validated end-to-end: room `1413` (MBZ) → code `38` → category `9478` → season `15420` × `Naechte=2` × `VK` × schema `1` = **`269,00`**. (XML `abpreis 289,00` = room + mandatory services = computed.) 5. ~~**`verfuegbar`**~~ — ✅ flat: `PartnerKontingent.Anzahl` for `(IDAdresse_FS, IDZimmer_FS, date-span)`; `1413 / 2026-12-11 → 25`. `` remains computed (recompute in proxy). 6. ~~**Minimum-services filter**~~ — ✅ defined: "included services" = (A2a) `Leistung`s with sales price `0.00` (schema 1) **+** (A2b) selection items from the hardcoded "Leistungen" `Selektionsgruppenstamm` ids, merged into one plain list.