16 KiB
Travel catalog — direct MSSQL queries (to replace the XML export)
Authority:
docs/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:
-
Departure lifecycle —
Reise.Status(nvarchar(2)), observed''≈ 9767,S≈ 945,L≈ 845. Confirmed against the live DB:''= active/bookable.S= Storno (cancelled): everySrow hasStornoDatumset.L= template / future master (not a real sellable departure): everyLrow hasStornoDatum NULL, far-futureTerminVon(2029–2035) and template-styleCodes (MRAGRMP, no date suffix).Leistung.Statusmirrors this (''= 196 504,L= 1 672 — the leistungen attached to those template departures).
-
Availability / booking status — booking-state codes, not
Leistung.Status(which only holds''/L). Canonical dictionary =dbo.ArtBuchungsStatus(Code), confirmed values:Code Kurztext meaning FFest firm/final booking (available) OOption held option AAnfrage on request / inquiry-only (groups-inquiry path) WWarteliste waitlist SStorno cancelled UUmbuchung 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_FSon individual bookings such asZusatzZimmer.) Transport<status_hin>/<status_rueck>is not inLeistung.Status; treat it as derived from the transportKontingentcount.
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)
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 Leistungs (sales price = 0.00). Priced services are optional
add-ons; the package-included ones have a 0.00 sales price (schema 1).
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 JOINwould 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 explicit0.00. (If BusPro models some included items as price-less rows, widen tolp.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.
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"
Selektionsgruppenstammids (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 LeistungPreishas one row per(Leistung, IDArtPreisSchema_FS); schema1is thestandard 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
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:
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.)
-- 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.)
-- 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_VKdistinguishes purchase (EK) from sale (VK) — always filterVKfor the customer price.PartnerZimmer.PreisJeZimmerflags per-room vs. per-person pricing.
-- 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;
verfuegbaris a flat column (PartnerKontingent.Anzahl) — reproducible by SQL. Gate it withBuchungsStatus(''bookable,Sstop,Aon-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
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
Status codes (A1)— ✅ confirmed:Reise.Status''= bookable,S= Storno (cancelled,StornoDatumset),L= template/future master (exclude both).F/A/O/S column— ✅ codes indbo.ArtBuchungsStatus(F/O/A/W/S/U); the working hotel-availability column isPartnerKontingent.BuchungsStatus(''/S/A).Hotel link— ✅ XML<hotel idbuspro>=Adresse.IDAdresse; departure→hotel viaPartnerProdukt(IDProdukt_FS, IDAdresse_FS)at product level (notReisePartner).Partner room pricing— ✅ fully validated end-to-end: room1413(MBZ) → code38→ category9478→ season15420×Naechte=2×VK× schema1=269,00. (XMLabpreis 289,00= room + mandatory services = computed.)— ✅ flat:verfuegbarPartnerKontingent.Anzahlfor(IDAdresse_FS, IDZimmer_FS, date-span);1413 / 2026-12-11 → 25.<abpreis>remains computed (recompute in proxy).Minimum-services filter— ✅ defined: "included services" = (A2a)Leistungs with sales price0.00(schema 1) + (A2b) selection items from the hardcoded "Leistungen"Selektionsgruppenstammids, merged into one plain list.