Files
myep/docs/buspronet-schema/travel-queries.md
T

306 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
|---|---|---|
| `<reise idbuspro=1121>` | **`Produkt`** | the travel *product*: `IDProdukt`, `Code`, `Bezeichnung` |
| `<termin idprodukt=1121 idbuspro=12665>` | **`Reise`** | the **dated departure**: `IDReise=12665`, `IDProdukt_FS=1121`, `Code`, `TerminVon/Bis`, `ReiseArt`, `Status`, `StornoDatum` |
| DB `Termin` table | — | **ignore**: generic appointments (odometer/repairs) |
| `<leistung>` (lei_befoerderung / lei_sonstiges) | `ReiseLeistung → Leistung` | `Leistung.Art` = `BEF` / `SON`; price via `LeistungPreis` |
| `<hotel idbuspro=224923>` + rooms | `ReisePartner` (+ `Partner*` subsystem) | **not** `ReiseLeistung`; keyed by the hotel `Adresse` |
| `<zustieg>` | `LeistungBefoerderung.ZustiegIDs``Zustieg` (+ `ZustiegZeitplan` for the time) | |
| `<selektion>` / `<selektiongruppe>` | `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` (20292035) 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
`<status_hin>`/`<status_rueck>` 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 `<text>`) = `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 (`<fremdsprache>`) = `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 `<hotel idbuspro=224923>` 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 <hotel idbuspro>)
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).
> `<abpreis>` (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 `<hotel idbuspro>` = `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`. `<abpreis>` 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.