# bpn-connect Query Reference > **Scope: endpoint design and the XML→SQL migration plan for the `bpn-connect` Go proxy.** > For schema semantics, conventions and confidence marks see > [`docs/buspro-database-reference.md`](buspro-database-reference.md), which is authoritative > and wins on any conflict. The table descriptions below are kept for endpoint context. This document summarizes the BusProNet schema exports in `docs/buspronet-schema` for future `bpn-connect` endpoint/query development. Generated from: - `docs/buspronet-schema/BusPronet_20260601-1517.csv`: table columns - `docs/buspronet-schema/BusPronet_20260601-1518.csv`: foreign keys Current limitation: the visible exports do not include primary-key or index catalog data, even though those were expected. Identity flags are not a reliable primary-key substitute in this database because many `ID...` columns are not marked as identity. Add PK/index exports before finalizing performance-sensitive queries. ## Naming Conventions - Primary identifiers usually follow `ID`, for example `Buchung.IDBuchung`, `Adresse.IDAdresse`, `AdressePerson.IDAdressePerson`. - Foreign key columns usually include `_FS`, for example `BuchungTN.IDBuchung_FS`. - Address data is split into: - `Adresse`: address/customer account root. - `AdressePerson`: person attached to an address. - `AdresseAnschrift`: postal address. - `AdresseKommunikation`: communication values such as email/phone. - Booking data is split into: - `Buchung`: booking header. - `BuchungTN`: booking participants. - `BuchungTNFerienziel`, `BuchungTNLeistung`: participant travel/service rows. - Public booking number appears to be `Buchung.Vorgang`. - Internal booking id is `Buchung.IDBuchung`. - Many tables have `Status` fields. Based on observed data, `Status = 'L'` appears to be a soft-delete marker and should be excluded from portal lookup queries. Other status values still need table-specific business validation. ## Core Tables ### `Buchung` Booking header. Important columns: | Column | Type | Nullable | Notes | | --- | --- | --- | --- | | `IDBuchung` | `bigint` | no | Internal booking id. | | `Vorgang` | `nvarchar(100)` | no | Public booking number/reference. | | `IDReise_FS` | `bigint` | yes | Links to `Reise.IDReise`; no FK was present in the export. | | `IDAdresse_FS_Anmelder` | `bigint` | yes | Applicant address id; no FK was present in the export. | | `IDAdresseAnschrift_FS_Anmelder` | `bigint` | yes | Applicant postal address id; no FK was present in the export. | | `IdAdressePerson_FS_Anmelder` | `bigint` | yes | Applicant person id; FK exists. | | `IDAdresse_FS_Agentur` | `bigint` | no | Agency address; FK exists. | | `IDAdresse_FS_Mandant` | `bigint` | no | Tenant/client address; FK exists. | | `LoginWert1` | `nvarchar(100)` | no | Possible login-related value; semantics unknown. | | `LoginWert2` | `nvarchar(100)` | no | Possible login-related value; semantics unknown. | | `LoginWert3` | `nvarchar(100)` | no | Possible login-related value; semantics unknown. | Known exported foreign keys: - `Buchung.IdAdressePerson_FS_Anmelder -> AdressePerson.IDAdressePerson` - `Buchung.IDAdresse_FS_Agentur -> Adresse.IDAdresse` - `Buchung.IDAdresse_FS_ErsterSachb -> Adresse.IDAdresse` - `Buchung.IDAdresse_FS_LetzterSachb -> Adresse.IDAdresse` - `Buchung.IDAdresse_FS_Mandant -> Adresse.IDAdresse` Notable missing FKs in the export: - `Buchung.IDAdresse_FS_Anmelder` - `Buchung.IDAdresseAnschrift_FS_Anmelder` - `Buchung.IDReise_FS` Treat those as likely relationships but verify against real data. ### `BuchungTN` Booking participant. | Column | Type | Nullable | Notes | | --- | --- | --- | --- | | `IDBuchungTN` | `bigint` | no | Participant row id. | | `IDBuchung_FS` | `bigint` | no | Booking header id. | | `IDAdressePerson_FS` | `bigint` | no | Participant person id. | | `IDAdresseAnschrift_FS` | `bigint` | yes | Participant postal address id. | | `Status` | `nvarchar(2)` | no | Participant status; values need business validation. | | `BuchungsDatum` | `datetime` | no | Booking date. | | `StornoStatusKV` | `nvarchar(2)` | yes | Cancellation-related status. | | `StornoDatum` | `datetime` | yes | Cancellation date. | | `Position` | `smallint` | no | Participant position within booking. | Known exported foreign keys: - `BuchungTN.IDBuchung_FS -> Buchung.IDBuchung` - `BuchungTN.IDAdressePerson_FS -> AdressePerson.IDAdressePerson` ### `Adresse` Customer/address root. | Column | Type | Nullable | Notes | | --- | --- | --- | --- | | `IDAdresse` | `bigint` | no | Address/customer id needed by the local user record. | | `SuchName` | `nvarchar(402)` | no | Search/display name. | | `Status` | `nvarchar(2)` | no | Address status; active values need validation. | | `Art` | `tinyint` | no | Address type; values need validation. | | `IDAdresseAnschrift_FS_Versandanschrift` | `bigint` | no | Default shipping/postal address. | | `IDAdressePerson_FS_ReferenzM` | `bigint` | no | Reference person. | | `DatumLöschungDSGVO` | `datetime` | yes | GDPR deletion marker. Exclude rows where this is not null for portal access. | ### `AdressePerson` Person attached to an address. | Column | Type | Nullable | Notes | | --- | --- | --- | --- | | `IDAdressePerson` | `bigint` | no | Person id needed by the local user record. | | `IDAdresse_FS` | `bigint` | no | Parent address id. | | `Name` | `nvarchar(200)` | no | Last name. | | `Vorname` | `nvarchar(200)` | yes | First name. | | `Geschlecht` | `nvarchar(2)` | no | Gender/salutation code. | | `Status` | `nvarchar(2)` | no | Person status; active values need validation. | | `Passwort` | `nvarchar(200)` | no | Legacy BusProNet password field. Do not use from `bpn-connect`; password migration remains on the BusProNet XML API until the algorithm is known. | | `Registrierung` | `bit` | no | Registration flag; semantics need validation. | | `IDFremd` | `nvarchar(100)` | no | External id field. | Known exported foreign key: - `AdressePerson.IDAdresse_FS -> Adresse.IDAdresse` ### `AdresseKommunikation` Communication values for an address/person. | Column | Type | Nullable | Notes | | --- | --- | --- | --- | | `IDAdresseKommunikation` | `bigint` | no | Communication row id. | | `IDAdresse_FS` | `bigint` | no | Address id. | | `IDAdressePerson_FS` | `bigint` | no | Person id. No FK was present in the export, but the name strongly implies `AdressePerson.IDAdressePerson`. | | `Wert` | `nvarchar(512)` | no | Communication value, likely email/phone depending on type. | | `WertTelefonie` | `nvarchar(100)` | yes | Phone-specific normalized value. | | `IDArtKommunikation_FS` | `bigint` | no | Communication type. | | `Status` | `nvarchar(2)` | no | Communication status; active values need validation. | | `Newsletter` | `bit` | no | Newsletter flag. | | `DoubleOptIn` | `bit` | no | Double opt-in flag. | Known exported foreign keys: - `AdresseKommunikation.IDAdresse_FS -> Adresse.IDAdresse` - `AdresseKommunikation.IDArtKommunikation_FS -> ArtKommunikation.IDArtKommunikation` Important missing FK: - `AdresseKommunikation.IDAdressePerson_FS` is not exported as a FK. Join it to `AdressePerson.IDAdressePerson` only after validating with real data. ### `ArtKommunikation` Communication type lookup. | Column | Type | Nullable | Notes | | --- | --- | --- | --- | | `IDArtKommunikation` | `bigint` | no | Type id. | | `Langtext` | `nvarchar(200)` | yes | Long label. | | `Kurztext` | `nvarchar(40)` | yes | Short label. | | `Code` | `nvarchar(20)` | yes | Type code. Find the email code/id here. | | `Status` | `nvarchar(2)` | yes | Type status. | Before relying on email lookup queries, run: ```sql SELECT IDArtKommunikation, Langtext, Kurztext, Code, Status FROM dbo.ArtKommunikation ORDER BY IDArtKommunikation; ``` Then hard-code or configure the email type ids/codes in `bpn-connect`. ### `AdresseAnschrift` Postal address. | Column | Type | Nullable | Notes | | --- | --- | --- | --- | | `IDAdresseAnschrift` | `bigint` | no | Postal address id. | | `IDAdresse_FS` | `bigint` | no | Address id. | | `IDAdressePerson_FS` | `bigint` | yes | Person id. | | `Anrede` | `nvarchar(100)` | yes | Salutation. | | `Name` | `nvarchar(400)` | yes | Postal name. | | `Strasse` | `nvarchar(200)` | no | Street. | | `PLZ` | `nvarchar(20)` | no | Postal code. | | `Ort` | `nvarchar(200)` | no | City. | | `Status` | `nvarchar(2)` | no | Postal address status. | No direct FK from `AdresseAnschrift` to `Adresse` or `AdressePerson` was present in the export subset shown by the current FK file. Treat these as likely relationships and validate them with data. ### `Reise` and `Produkt` Travel/product metadata. `Reise` important columns: - `IDReise` - `IDProdukt_FS` - `Bezeichnung` - `Code` - `TerminVon` - `TerminBis` - `Status` - `StornoDatum` `Produkt` important columns: - `IDProdukt` - `Bezeichnung` - `Code` - `Suchfeld` - `Status` Known exported foreign key: - `Reise.IDProdukt_FS -> Produkt.IDProdukt` ## Relationship Map Most relevant joins for portal access: ```text Buchung.IDBuchung -> BuchungTN.IDBuchung_FS -> BuchungTN.IDAdressePerson_FS -> AdressePerson.IDAdressePerson -> AdressePerson.IDAdresse_FS -> Adresse.IDAdresse ``` Applicant joins: ```text Buchung.IdAdressePerson_FS_Anmelder -> AdressePerson.IDAdressePerson -> AdressePerson.IDAdresse_FS -> Adresse.IDAdresse ``` Email joins to validate: ```text AdresseKommunikation.IDAdressePerson_FS -> AdressePerson.IDAdressePerson AdresseKommunikation.IDAdresse_FS -> Adresse.IDAdresse AdresseKommunikation.IDArtKommunikation_FS -> ArtKommunikation.IDArtKommunikation ``` ## Required bpn-connect Endpoints All endpoints are private and authenticated by API key: ```http X-API-Key: {apiKey} Accept: application/json ``` Keep responses narrow. `bpn-connect` should return only fields needed by the Symfony portal and should never return BusProNet passwords, bank data, credit card data, or broad customer records by default. Authentication rule: Symfony owns portal passwords after a user has been bootstrapped locally. The BusProNet legacy password algorithm is currently unknown, so `bpn-connect` must not verify passwords from the database. Keep the existing BusProNet XML password check only for first-time legacy migration. Once the local Symfony password hash exists, all subsequent login/profile refresh logic should use local authentication plus `bpn-connect` read endpoints. ### `GET /api/bookings/applicant-lookup` Purpose: resolve an applicant by email and public booking number. This is the core endpoint for local user creation after a booking and for the portal access request flow. Request: ```http GET /api/bookings/applicant-lookup?email={email}&bookingNumber={vorgang} ``` Response `200`: ```json { "bookingNumber": "111386", "bookingId": 123456, "addressId": 98765, "personId": 54321, "email": "customer@example.com", "firstName": "Example", "lastName": "Customer" } ``` Response rules: - Return `200` when `email` belongs to `Buchung.IdAdressePerson_FS_Anmelder` for the given `Buchung.Vorgang`. - Return `404` when there is no matching non-deleted applicant. - Return `409` if the query returns more than one applicant row after deduplication. This should be rare and indicates inconsistent BusProNet data. - Normalize input email by trimming and lowercasing before querying. SQL: ```sql DECLARE @Email nvarchar(512) = LOWER(LTRIM(RTRIM(@email))); DECLARE @Vorgang nvarchar(100) = LTRIM(RTRIM(@bookingNumber)); SELECT TOP (2) b.Vorgang AS bookingNumber, b.IDBuchung AS bookingId, a.IDAdresse AS addressId, ap.IDAdressePerson AS personId, @Email AS email, ap.Vorname AS firstName, ap.Name AS lastName FROM dbo.Buchung b INNER JOIN dbo.AdressePerson ap ON ap.IDAdressePerson = b.IdAdressePerson_FS_Anmelder INNER JOIN dbo.Adresse a ON a.IDAdresse = ap.IDAdresse_FS WHERE b.Vorgang = @Vorgang AND a.DatumLöschungDSGVO IS NULL AND a.Status <> 'L' AND ap.Status <> 'L' AND EXISTS ( SELECT 1 FROM dbo.AdresseKommunikation ak WHERE ak.IDAdresse_FS = ap.IDAdresse_FS AND ak.IDAdressePerson_FS = ap.IDAdressePerson AND LOWER(LTRIM(RTRIM(ak.Wert))) = @Email AND ak.Status <> 'L' -- TODO: restrict to validated email communication type. -- AND ak.IDArtKommunikation_FS IN (...) ) ORDER BY ap.IDAdressePerson; ``` ### `GET /api/customers/{addressId}/persons/{personId}` Purpose: fetch the personal data for a known BusProNet person. Use this after a local user exists and already stores `addressId` and `personId`. Do not use email as the identifier because multiple `Adresse` rows can share an email address. Request: ```http GET /api/customers/{addressId}/persons/{personId} ``` Response `200`: ```json { "addressId": 98765, "personId": 54321, "firstName": "Example", "lastName": "Customer", "email": "customer@example.com", "genderCode": "F", "title": null, "birthDate": "1980-01-01T00:00:00Z", "nationality": "deutsch", "nationalityCountryName": "Deutschland", "street": "Example Street 1", "postalCode": "12345", "city": "Example City", "addressCountryName": "Deutschland" } ``` Response rules: - Return `200` for a non-deleted address/person pair. - Return `404` if the address/person pair does not exist or has `Status = 'L'`. - Return only portal-editable or portal-visible fields. SQL: ```sql DECLARE @AddressId bigint = @addressId; DECLARE @PersonId bigint = @personId; SELECT a.IDAdresse AS addressId, ap.IDAdressePerson AS personId, ap.Name AS lastName, ap.Vorname AS firstName, ap.Geschlecht AS genderCode, ap.Titel AS title, ap.Geburtsdatum AS birthDate, ap.Briefanrede AS letterSalutation, ap.Anrede AS salutation, ap.Newsletter AS newsletter, email.Wert AS email, manifest.IDArtNationalitaet_FS AS nationalityId, person_nat.Langtext AS nationalityCountryName, person_nat.Kurztext AS nationality, person_nat.Code AS nationalityCode, aa.IDAdresseAnschrift AS postalAddressId, aa.Anrede AS postalSalutation, aa.Name AS postalName, aa.Strasse AS street, aa.PLZ AS postalCode, aa.Ort AS city, aa.Ortsteil AS district, aa.IDArtNationalitaet_FS_Land AS addressCountryId, address_country.Langtext AS addressCountryName, address_country.Kurztext AS addressCountryNationality, address_country.Code AS addressCountryCode, ad.TelefonOK AS phoneAllowed, ad.MailOK AS mailAllowed, ad.PostOK AS postAllowed FROM dbo.AdressePerson ap INNER JOIN dbo.Adresse a ON a.IDAdresse = ap.IDAdresse_FS OUTER APPLY ( SELECT TOP (1) ak.Wert FROM dbo.AdresseKommunikation ak WHERE ak.IDAdresse_FS = ap.IDAdresse_FS AND ak.IDAdressePerson_FS = ap.IDAdressePerson AND ak.Status <> 'L' -- TODO: restrict to validated email communication type. -- AND ak.IDArtKommunikation_FS IN (...) ORDER BY ak.IDAdresseKommunikation DESC ) email LEFT JOIN dbo.AdresseAnschrift aa ON aa.IDAdresseAnschrift = ap.IDAdresseAnschrift_FS_Versandanschrift AND aa.Status <> 'L' LEFT JOIN dbo.ArtNationalitaet address_country ON address_country.IDArtNationalitaet = aa.IDArtNationalitaet_FS_Land AND COALESCE(address_country.Status, '') <> 'L' OUTER APPLY ( SELECT TOP (1) m.IDArtNationalitaet_FS FROM dbo.AdressePersonManifest m WHERE m.IDAdressePerson_FS = ap.IDAdressePerson AND m.IDAdresse_FS = ap.IDAdresse_FS AND COALESCE(m.Status, '') <> 'L' ORDER BY m.IDAdressePersonManifest DESC ) manifest LEFT JOIN dbo.ArtNationalitaet person_nat ON person_nat.IDArtNationalitaet = manifest.IDArtNationalitaet_FS AND COALESCE(person_nat.Status, '') <> 'L' LEFT JOIN dbo.AdresseDatenschutz ad ON ad.IdAdresse_FS = a.IDAdresse WHERE a.IDAdresse = @AddressId AND ap.IDAdressePerson = @PersonId AND a.DatumLöschungDSGVO IS NULL AND a.Status <> 'L' AND ap.Status <> 'L'; ``` ### Optional Debug Endpoint: `GET /api/customers/by-email` Purpose: manual investigation only. Email is not unique across `Adresse`, so this endpoint should return a list and should not be used as a local user identifier. Request: ```http GET /api/customers/by-email?email={email} ``` Use the same joins as `GET /api/customers/{addressId}/persons/{personId}`, but start from `AdresseKommunikation` and return all non-deleted matching persons. Do not use this for account creation unless the user also provides a booking number or another disambiguating key. ## Symfony Login And Bootstrap Replacement Plan Current Symfony login behavior: - Existing local users authenticate against the local Symfony password hash. - Existing legacy local users may still have an encrypted legacy password in the local `User.password` column. Symfony validates it locally once, then replaces it with a Symfony password hash. - If no local user exists, `BpnAuthenticator::bootstrapUser()` currently calls the BusProNet XML API with email and password: - `getPersonalDataWithPassword(email, password)` - `getCrmAttributesWithPassword(email, password)` - After any successful local login, `BpnAuthenticator::refreshBpnSnapshot()` calls the BusProNet XML API by local `addressId` and `personId`: - `getPersonalData(email, addressId, personId)` - `getCrmAttributes(email, addressId, personId)` Replacement rule: - Keep login password validation local to Symfony. - Keep the current XML password-based bootstrap for users that exist in BusProNet but do not yet have a local Symfony user. - On successful XML legacy verification, Symfony creates a local user with a Symfony password hash from the submitted password. - Future logins use only the local hash and must not call XML for password verification. - Replace post-login snapshot refresh with `bpn-connect` id-based read endpoints. ### Login Endpoint Usage No `bpn-connect` endpoint is needed to authenticate a password. Existing local user login should do: 1. Find local `User` by normalized email. 2. Validate local Symfony password hash. 3. If the local row still has a legacy encrypted local password, migrate it to a Symfony hash without contacting BusProNet. 4. Call `GET /api/customers/{addressId}/persons/{personId}` to refresh profile completeness. 5. Call `GET /api/customers/{addressId}/crm-attributes` to refresh roles and hotel codes. Missing local user login should do: 1. Call the existing BusProNet XML password-based bootstrap. 2. If valid, create a local `User` with a Symfony hash of the submitted password. 3. Store returned `addressId`, `personId`, `roles`, `hotelCodes`, and `profileComplete`. 4. Persist and continue login. 5. If invalid, fail with the generic invalid-login message. Do not add a `bpn-connect` password verification endpoint until the BusProNet password algorithm is known and validated against real examples. The observed database values do not currently match simple MD5/SHA/Base64 derivations. ### `GET /api/customers/{addressId}/crm-attributes` Purpose: replace XML `SelektionCRM` reads used during login refresh and by the OAuth CRM attributes endpoint. Request: ```http GET /api/customers/{addressId}/crm-attributes ``` Minimal response: ```json { "roles": ["ROLE_CUSTOMER"], "hotelCodes": [] } ``` Detailed response may also include selection groups and CRM actions if the current `/api/crm-attributes` response must remain compatible. Known role mapping from the XML parser: - `Selektionsstamm.IDSelektionsstamm = 1292` selected: `ROLE_ADMIN` and `ROLE_HOUSE_MANAGER` - `Selektionsstamm.IDSelektionsstamm = 1293` selected: `ROLE_MANAGER` - `Selektionsstamm.IDSelektionsstamm = 1070` selected: `ROLE_TEAMER` - selected `Selektionsstamm.Selektion` matching `Hausleitung {CODE}`: `ROLE_HOUSE_MANAGER` and hotel code `{CODE}` - admin users also receive default hotel code `SSL` - no matched role means `ROLE_CUSTOMER` Candidate SQL for selected customer selections: ```sql DECLARE @AddressId bigint = @addressId; SELECT sg.IDSelektionsgruppenstamm AS groupId, sg.Selektionsgruppe AS groupLabel, s.IDSelektionsstamm AS selectionId, s.Selektion AS selectionLabel, CAST(1 AS bit) AS selected FROM dbo.Selektionen sel INNER JOIN dbo.Selektionsstamm s ON s.IDSelektionsstamm = sel.IDSelektionsstamm_FS LEFT JOIN dbo.Selektionsgruppe sg_map ON sg_map.IDSelektionsstamm_FS = s.IDSelektionsstamm LEFT JOIN dbo.Selektionsgruppenstamm sg ON sg.IDSelektionsgruppenstamm = sg_map.IDSelektionsgruppenstamm_FS WHERE sel.IDAdresse_FS = @AddressId AND COALESCE(s.Status, '') <> 'L' AND COALESCE(sg.Status, '') <> 'L' ORDER BY sg.Position, sg.Selektionsgruppe, sg_map.Position, s.Selektion; ``` The Go service can derive `roles` and `hotelCodes` from this result with the mapping above. Before shipping, verify whether XML `SelektionCRM` includes unselected mutable options; if the portal only needs roles/hotel codes, selected rows are enough. Candidate SQL for CRM actions if compatibility is required: ```sql DECLARE @AddressId bigint = @addressId; SELECT ca.IDCRMAktion AS id, ca.Code AS code, ca.Bezeichnung AS label, ca.Aktiv AS selected FROM dbo.CRMZuweisung cz INNER JOIN dbo.CRMAktion ca ON ca.IDCRMAktion = cz.IDCRMAktion_FS WHERE cz.IDAdresse_FS = @AddressId AND ca.Status <> 'L' ORDER BY ca.Bezeichnung; ``` This needs data validation against the XML response before compatibility is guaranteed. ## XML Calls To Replace For Local User Processes | Current Symfony call | Current XML purpose | Replacement | | --- | --- | --- | | `getPersonalData(email, addressId, personId)` | Load profile by known user ids. | `GET /api/customers/{addressId}/persons/{personId}` | | `updatePersonalData(email, personalData)` | Persist profile edits. | Future `PATCH /api/customers/{addressId}/persons/{personId}` with transaction-safe updates. | | `getCrmAttributes(email, addressId, personId)` | Refresh roles/hotel codes and expose CRM attributes. | `GET /api/customers/{addressId}/crm-attributes` | | `getPersonalDataWithPassword(email, password)` | Missing-local-user bootstrap by BPN password. | Keep XML until the local user has been created/migrated. | | `getCrmAttributesWithPassword(email, password)` | Roles for missing-local-user bootstrap by BPN password. | Keep XML until the local user has been created/migrated. | | `register(registrationData)` | Create BusProNet address/person and local user. | Future `POST /api/customers` if direct DB writes are allowed; otherwise keep XML until write rules are known. | | `createAddress(personalData)` | Create BusProNet address/person without mail. | Future `POST /api/customers` if direct DB writes are allowed; otherwise keep XML until write rules are known. | Recommended migration sequence: 1. Keep XML for legacy password bootstrap only. 2. Replace booking applicant lookup for guest provisioning and portal access. 3. Replace post-login profile/CRM refresh reads. 4. Replace personal-data page reads. 5. Replace OAuth/userinfo and CRM API reads. 6. Replace booking form prefill reads. 7. Defer personal-data writes until update rules are validated. 8. Defer registration/create-address until the insert graph is known. ### Phase 1: Keep XML For Legacy Password Bootstrap Keep these XML calls temporarily: - `getPersonalDataWithPassword(email, password)` - `getCrmAttributesWithPassword(email, password)` Scope: - Only used when no local Symfony user exists yet. - On success, create the local user with a Symfony password hash from the submitted password. - Store `addressId`, `personId`, `roles`, `hotelCodes`, and `profileComplete`. - Future logins for that user use only the local Symfony password hash. Do not implement a `bpn-connect` password verification endpoint until the BusProNet password algorithm is known and verified. ### Phase 2: Booking Applicant Lookup Replace XML/fragile booking lookup plumbing with: ```http GET /api/bookings/applicant-lookup?email={email}&bookingNumber={vorgang} ``` Symfony integration points: - `BookingGuestAccountManager::provisionGuestAccountForBooking()` - `BookingGuestAccountManager::requestPortalAccess()` - booking-created welcome account provisioning This endpoint returns only the fields needed for local user creation: - `bookingNumber` - `bookingId` - `addressId` - `personId` - `email` - `firstName` - `lastName` ### Phase 3: Post-login Snapshot Refresh Replace XML reads in `BpnAuthenticator::refreshBpnSnapshot()` with: ```http GET /api/customers/{addressId}/persons/{personId} GET /api/customers/{addressId}/crm-attributes ``` Symfony keeps local password validation. `bpn-connect` only refreshes ids, profile completeness, roles, and hotel codes after the password has already been validated locally. ### Phase 4: Personal-data Page Reads Replace XML read in `PersonalDataController::loadPersonalData()` with: ```http GET /api/customers/{addressId}/persons/{personId} ``` Keep `updatePersonalData()` on XML for now. Reads are low-risk; writes require validated BusProNet update semantics. ### Phase 5: OAuth/Userinfo And CRM Reads Replace XML reads in: - `UserinfoController` - `CrmAttributeController` with: ```http GET /api/customers/{addressId}/persons/{personId} GET /api/customers/{addressId}/crm-attributes ``` ### Phase 6: Booking Form Prefill Reads Replace XML read in `ParticipantDataPrefiller::prefillApplicantFromUser()` with: ```http GET /api/customers/{addressId}/persons/{personId} ``` ### Phase 7: Defer Personal-data Writes Keep XML call: - `updatePersonalData(email, personalData)` Future replacement: ```http PATCH /api/customers/{addressId}/persons/{personId} ``` Do this only after write rules are validated for: - `AdressePerson` - `AdresseAnschrift` - `AdresseKommunikation` - `AdressePersonManifest` - `AdresseZusatzfeld` or other tables holding height, weight, shoe size, and remarks if those are not in the core person/address tables. ### Phase 8: Defer Registration And Create-address Keep XML calls: - `register(registrationData)` - `createAddress(personalData)` Future replacement: ```http POST /api/customers ``` This is higher risk than read replacement because it requires the full BusProNet insert graph, required defaults, and side effects. ## Candidate SQL: Find Email Communication Type Run once during `bpn-connect` development: ```sql SELECT IDArtKommunikation, Langtext, Kurztext, Code, Status FROM dbo.ArtKommunikation WHERE LOWER(COALESCE(Langtext, '')) LIKE '%mail%' OR LOWER(COALESCE(Kurztext, '')) LIKE '%mail%' OR LOWER(COALESCE(Code, '')) LIKE '%mail%' ORDER BY IDArtKommunikation; ``` If the data uses German labels, also inspect all rows manually. The schema alone cannot prove the canonical email type. ## Candidate SQL: Validate Missing Relationships Use these checks before shipping lookup logic: ```sql -- Check whether AdresseKommunikation.IDAdressePerson_FS is a real person link. SELECT TOP (20) ak.IDAdresseKommunikation, ak.IDAdresse_FS, ak.IDAdressePerson_FS, ap.IDAdressePerson, ap.IDAdresse_FS AS PersonAddressId, ak.Wert FROM dbo.AdresseKommunikation ak LEFT JOIN dbo.AdressePerson ap ON ap.IDAdressePerson = ak.IDAdressePerson_FS WHERE ak.IDAdressePerson_FS <> 0 AND ap.IDAdressePerson IS NULL; ``` ```sql -- Check whether Buchung.IDAdresse_FS_Anmelder matches the applicant person's address. SELECT TOP (100) b.IDBuchung, b.Vorgang, b.IDAdresse_FS_Anmelder, b.IdAdressePerson_FS_Anmelder, ap.IDAdresse_FS AS ApplicantPersonAddressId FROM dbo.Buchung b LEFT JOIN dbo.AdressePerson ap ON ap.IDAdressePerson = b.IdAdressePerson_FS_Anmelder WHERE b.IDAdresse_FS_Anmelder IS NOT NULL AND ap.IDAdresse_FS <> b.IDAdresse_FS_Anmelder; ``` ```sql -- Inspect status values before adding active-only predicates. SELECT 'Adresse' AS table_name, Status, COUNT(*) AS rows FROM dbo.Adresse GROUP BY Status UNION ALL SELECT 'AdressePerson', Status, COUNT(*) FROM dbo.AdressePerson GROUP BY Status UNION ALL SELECT 'AdresseKommunikation', Status, COUNT(*) FROM dbo.AdresseKommunikation GROUP BY Status UNION ALL SELECT 'BuchungTN', Status, COUNT(*) FROM dbo.BuchungTN GROUP BY Status ORDER BY table_name, Status; ``` ## Indexes To Confirm Or Add The current CSV files do not include index definitions. For the lookup endpoint, confirm whether these access paths already exist: - `Buchung(Vorgang)` including `IDBuchung`, `IdAdressePerson_FS_Anmelder` - `BuchungTN(IDBuchung_FS)` including `IDAdressePerson_FS`, `Status` - `AdressePerson(IDAdressePerson)` including `IDAdresse_FS`, `Vorname`, `Name`, `Status` - `AdresseKommunikation(IDAdressePerson_FS, Wert)` including `IDAdresse_FS`, `IDArtKommunikation_FS`, `Status` - `AdresseKommunikation(IDAdresse_FS, Wert)` including `IDAdressePerson_FS`, `IDArtKommunikation_FS`, `Status` If the database collation is case-sensitive and email lookup must be case-insensitive, avoid `LOWER(Wert)` on large tables unless a computed normalized-email column or suitable index exists. ## Future Endpoint Candidates The XML plumbing can be replaced incrementally by small read endpoints backed by targeted SQL: | Endpoint | Purpose | Core tables | | --- | --- | --- | | `GET /api/bookings/lookup` | Resolve email + public booking number to booking/person/address ids. | `Buchung`, `BuchungTN`, `AdressePerson`, `AdresseKommunikation`, `Adresse` | | `GET /api/bookings/{bookingId}` | Fetch booking header/detail by internal id. | `Buchung`, `Reise`, `Produkt`, `BuchungTN` | | `GET /api/bookings/{bookingId}/participants` | Fetch participants for a booking. | `BuchungTN`, `AdressePerson`, `AdresseAnschrift` | | `GET /api/customers/{addressId}/persons/{personId}` | Fetch a minimal customer/person profile. | `Adresse`, `AdressePerson`, `AdresseKommunikation`, `AdresseAnschrift` | Keep endpoint responses narrow. Return only ids and fields needed by the Symfony portal so the Go service does not become a broad BusProNet data mirror. ## Regeneration Notes When new catalog exports are added, regenerate this document with: 1. Columns export from `sys.tables`, `sys.schemas`, `sys.columns`, `sys.types`. 2. Foreign key export from `sys.foreign_keys` and related FK columns. 3. Primary-key export from `sys.key_constraints`. 4. Index export from `sys.indexes`, `sys.index_columns`, and `sys.columns`. Primary keys and indexes should be added to the table sections and the lookup query should be revisited once the real access paths are known.