docs: include previously ignored documentation files
This commit is contained in:
1 parent
1796ec4964
commit
83b8ba7f8d
12 files changed
+15799
No files matched your search
@@ -0,0 +1,898 @@
|
||||
# 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<TableName>`, 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": "[email protected]",
|
||||
"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": "[email protected]",
|
||||
"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.
|
||||
Reference in new issue
Block a user