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

16 KiB
Raw Blame History

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.ZustiegIDsZustieg (+ 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 lifecycleReise.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 Codes (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 bigintint64; 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 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.

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

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 254605: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.2026Naechte = 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=1269,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_VK distinguishes purchase (EK) from sale (VK) — always filter VK for the customer price. PartnerZimmer.PreisJeZimmer flags 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;

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

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) Leistungs with sales price 0.00 (schema 1) + (A2b) selection items from the hardcoded "Leistungen" Selektionsgruppenstamm ids, merged into one plain list.