docs: include previously ignored documentation files

This commit is contained in:
Björn Fromme
2026-08-24 09:20:56 +02:00
parent 1796ec4964
commit 83b8ba7f8d
12 changed files with 15799 additions and 0 deletions
@@ -0,0 +1,469 @@
# `SelektionCRM` — how the XML is assembled, and how to debug a missing selection
> **Authority: [`docs/buspro-database-reference.md`](../buspro-database-reference.md).**
> Schema semantics and the falsified-theory record live there and win on any conflict.
> This file is retained as the **full investigation record** for the missing-selection /
> `aenderbar` problem, including the queries that closed each theory.
Companion to `travel-queries.md` (static catalog) and `live-api-queries.md` (live
socket endpoints). This one covers the `KUNDENKONTO` / `art=SelektionCRM` response —
the document that drives the portal's entire authorization model.
Request side: `ApiClient::getCrmAttributes()` (`src/BusProNet/ApiClient.php`) sends
`satz typ="KUNDENKONTO"` + `art=SelektionCRM` + the customer's email/password.
Response side: `CrmAttributesResponseParser`
(`src/BusProNet/XmlParser/CrmAttributesResponseParser.php`) turns selection ids into
Symfony roles:
| `IDSelektionsstamm` | Role |
| --- | --- |
| `1070` | `ROLE_TEAMER` |
| `1292` | `ROLE_ADMIN` (+ `ROLE_HOUSE_MANAGER`, + hotel code `SSL`) |
| `1293` | `ROLE_MANAGER` |
| `1477` | `ROLE_GROUPS_MANAGER` |
| `1478` | `ROLE_GROUPS_ADMIN` |
| label `Hausleitung {CODE}` | `ROLE_HOUSE_MANAGER` + hotel code `{CODE}` |
| nothing matched | `ROLE_CUSTOMER` |
Only selections with `auswahl="True"` count. Every `Admin/*` controller is gated on
one of these roles.
---
## The four tables
From `BusPronet_20260601-1517.csv` (columns) and `BusPronet_20260601-1518.csv` (FKs):
| Table | Columns | Role |
| --- | --- | --- |
| `dbo.Selektionsstamm` | `IDSelektionsstamm`, `Selektion`, `Status` nvarchar(2) null, `System` tinyint | the selection master — that's *all four* columns |
| `dbo.Selektionsgruppenstamm` | `IDSelektionsgruppenstamm`, `Selektionsgruppe`, `Status`, `System`, `Ausgrenzung`, `Position` | the group master |
| `dbo.Selektionsgruppe` | `IDSelektionsgruppenstamm_FS`, `IDSelektionsstamm_FS`, `Position` | **membership** — which selection sits in which group, and in what order |
| `dbo.Selektionen` | `IDAdresse_FS`, `IDSelektionsstamm_FS` | **assignment** — which address holds which selection |
`<crmaktionen>` comes from a parallel pair: `dbo.CRMAktion` (master: `Code`,
`Bezeichnung`, `Aktiv`, `Status`, `AktivDatumVon/Bis`, …) and `dbo.CRMZuweisung`
(assignment via `IDAdresse_FS` + `IDCRMAktion_FS`).
### XML → column mapping
| XML | Source |
| --- | --- |
| `<idadresse>` | `Adresse.IDAdresse` (the authenticated account) |
| `<idperson>` | `AdressePerson.IDAdressePerson` |
| `<selektionsgruppe id>` / `bezeichnung` | `Selektionsgruppenstamm.IDSelektionsgruppenstamm` / `.Selektionsgruppe` |
| `<selektion id>` / `bezeichnung` | `Selektionsstamm.IDSelektionsstamm` / `.Selektion` |
| `selektion@auswahl` | `EXISTS (Selektionen WHERE IDAdresse_FS = <account> AND IDSelektionsstamm_FS = <id>)` — confirmed |
| `selektion@aenderbar` | **computed, not stored** — no such column exists anywhere (see below) |
| group order | `Selektionsgruppenstamm.Position`, `<ohne Gruppe>` first |
| selection order within a group | `Selektionsgruppe.Position` |
| `<crmaktion …>` | `CRMAktion` + `CRMZuweisung` (`auswahl` = assignment exists) |
### `aenderbar` is not a stored flag
A case-insensitive search for `aenderbar` / `auswahl` across **every column name in the
database** (1023 tables, `ABOBewegungsdaten``ZustiegsPlanungPersonStelle`, so the
export is complete and untruncated) returns no hit on any `Selektion*` table. The only
`*Aenderbar*` column in the entire schema is `AdresseAgenturZusatz.ZahlungsartAenderbar`,
which is unrelated. And the three core tables have no spare bit to hide it in:
`Selektionsstamm` has exactly four columns, `Selektionsgruppe` three, `Selektionen` two.
So `aenderbar` is **derived at response time**. Don't go looking for the column again —
this is what Query D and Query E are for.
The `<crmaktionen>` half of the document hints at the intended semantics: `CRMAktion`
*does* carry real flags (`Aktiv`, `Anzeige`, `AktivDatumVon`/`Bis`), and in the reference
sample the long-expired "Deal Der Woche KW40-2018" is `aenderbar="False"` while the
standing "NOMAIL" opt-out is `"True"`. `aenderbar` reads as *"the customer may still
toggle this"* — a validity/whitelist question, not a static per-row property.
Two orderings worth noting, because they rule out the obvious alternatives:
- Inside `TEAM` the ids run `… 1374, 1375, 1376, 1373, 1377 …` — neither id order nor
label order. That is `Selektionsgruppe.Position`.
- Within `Interessen` the labels *are* alphabetical, which is consistent with
`Position` having been maintained alphabetically there. Don't read it as a
different rule.
### The decisive observation: the catalog is group-driven
Every `Hausleitung *` row is emitted with `auswahl="False"` for an account that holds
none of them. So the endpoint does **not** list "the selections this address has" — it
lists **the whole published catalog** and then flags which entries the address holds.
`<selektionsgruppe id="0" bezeichnung="&lt;ohne Gruppe&gt;">` is the bucket for
selections with no (or a `0`) `Selektionsgruppe` membership row.
The practical consequence: **inserting a row into `dbo.Selektionen` can never make a
selection appear.** It can only flip `auswahl` from `False` to `True` on an entry that
the catalog already emits. A selection that is absent from the XML is absent from the
*catalog*, and the assignment is irrelevant until that is fixed.
---
## Query A — reconstruct the response
Run this for the account in question and diff its rows against the actual XML. A clean
match proves the reconstruction above; any mismatch belongs in the caveats section.
```sql
DECLARE @AddressId bigint = 141747;
SELECT COALESCE(sg.IDSelektionsgruppenstamm, 0) AS groupId,
COALESCE(sg.Selektionsgruppe, '<ohne Gruppe>') AS groupLabel,
s.IDSelektionsstamm AS selectionId,
s.Selektion AS selectionLabel,
CASE WHEN s.System = 0 THEN 'True' ELSE 'False' END AS aenderbar,
CASE WHEN sel.IDAdresse_FS IS NULL THEN 'False' ELSE 'True' END AS auswahl
FROM dbo.Selektionsstamm s
LEFT JOIN dbo.Selektionsgruppe sgm
ON sgm.IDSelektionsstamm_FS = s.IDSelektionsstamm
LEFT JOIN dbo.Selektionsgruppenstamm sg
ON sg.IDSelektionsgruppenstamm = sgm.IDSelektionsgruppenstamm_FS
LEFT JOIN dbo.Selektionen sel
ON sel.IDSelektionsstamm_FS = s.IDSelektionsstamm
AND sel.IDAdresse_FS = @AddressId
WHERE COALESCE(s.Status, '') <> 'L'
AND COALESCE(sg.Status, '') <> 'L'
ORDER BY COALESCE(sg.Position, -1), sgm.Position, s.Selektion;
```
(`Status = 'L'` is this database's soft-delete marker — see
`../bpn-connect-query-reference.md`.)
Note the join fans out: a selection can belong to **several** groups (confirmed —
`1292` is in both `10` and `91`), so this query emits one row per membership. The XML
shows a selection once per group it is published in.
`Selektionsgruppenstamm.Ausgrenzung` is *exclusion* semantics (mutually exclusive
group), not visibility — don't mistake it for a publish flag.
---
## Query B — diagnose a selection that never appears
Compares the suspect row against known-good neighbours: `1292`/`1293` (long-standing,
emitted, `aenderbar=False`) and `1462` (most recent one that *is* emitted,
`aenderbar=True`). Whatever column differs is the answer.
```sql
SELECT s.IDSelektionsstamm,
s.Selektion,
s.Status,
s.System,
sgm.IDSelektionsgruppenstamm_FS AS groupId,
sgm.Position AS positionInGroup,
(SELECT COUNT(*) FROM dbo.Selektionen x
WHERE x.IDSelektionsstamm_FS = s.IDSelektionsstamm
AND x.IDAdresse_FS = 141747) AS assignedToAddress
FROM dbo.Selektionsstamm s
LEFT JOIN dbo.Selektionsgruppe sgm
ON sgm.IDSelektionsstamm_FS = s.IDSelektionsstamm
WHERE s.IDSelektionsstamm IN (1477, 1478, 1292, 1293, 1462)
ORDER BY s.IDSelektionsstamm;
```
### What Query B ruled out (address `141747`, 2026-08-04)
| id | Selektion | Status | System | groupId | posInGroup | assigned |
| --- | --- | --- | --- | --- | --- | --- |
| 1292 | Admin | *(blank)* | 1 | 10 | 33 | 1 |
| 1292 | Admin | *(blank)* | 1 | **91** | 1 | 1 |
| 1293 | Manager | *(blank)* | 1 | **92** | 1 | 1 |
| 1293 | Manager | *(blank)* | 1 | 10 | 34 | 1 |
| 1462 | Hausleitung AGR | *(blank)* | 1 | 10 | 74 | 0 |
| 1477 | Preisrechner | *(blank)* | 1 | 10 | 75 | 0 |
| 1478 | Preisrechner Admin | *(blank)* | 1 | 10 | 76 | 1 |
Three conclusions:
1. **The missing-membership theory is dead.** `1477`/`1478` *are* in group `10` (TEAM),
at positions `75`/`76`, and `1478` *is* assigned to the address. On every column
available in these three tables, `1478` is indistinguishable from `1462` — which the
XML does emit. Whatever excludes them lives **outside**
`Selektionsstamm` / `Selektionsgruppe` / `Selektionen`.
2. **`aenderbar` is not derived from `System`.** `1292` (`aenderbar="False"`) and `1462`
(`aenderbar="True"`) both have `System = 1`. `Selektionsstamm` has no other column,
so `aenderbar` comes from somewhere else entirely.
3. **A selection can belong to several groups, and some groups are not published.**
`1292` is in `10` + `91`, `1293` is in `10` + `92`; groups `91`/`92` appear nowhere in
the XML, which only emits `0`, `10`, `83`. So there is a **group-level publish
filter** — and, suggestively, the two rows with a second (unpublished) group
membership are exactly the two that come back `aenderbar="False"`.
---
## Query D — the agency whitelist (run this first)
`dbo.AgenturSelektionenKunde` is a bare two-column link table,
`(IDAdresse_FS, IDSelektionsstamm_FS)` — literally *"agency's customer selections"*. The
portal authenticates against BusPro as an **agency** account (`bpn_username` in
`ApiClient`), so a per-agency whitelist of which customer selections that agency may see
and/or edit is exactly the right shape. It is the only candidate found so far that could
explain the `1478` omission **and** `aenderbar` through one mechanism.
```sql
SELECT ask.IDAdresse_FS AS agencyAddressId,
a.SuchName AS agencyName,
ask.IDSelektionsstamm_FS AS selectionId,
s.Selektion AS selectionLabel
FROM dbo.AgenturSelektionenKunde ask
JOIN dbo.Selektionsstamm s ON s.IDSelektionsstamm = ask.IDSelektionsstamm_FS
LEFT JOIN dbo.Adresse a ON a.IDAdresse = ask.IDAdresse_FS
ORDER BY ask.IDAdresse_FS, s.Selektion;
-- same shape, same question:
SELECT * FROM dbo.PartnerSelektionenKunde;
```
Compare the row set for the portal's agency address against a freshly captured response:
| Result | Meaning |
| --- | --- |
| set == the 32 emitted entries | **this is the publish gate**`1477`/`1478` just need rows here |
| set == the `aenderbar="True"` subset only | this is the `aenderbar` source |
| set is some third thing | note it in this doc and continue to C1 |
| table empty | both hypotheses dead — fall through to C1/C2/C3 |
**Result (2026-08-04): `dbo.AgenturSelektionenKunde` is empty.** The agency-whitelist
theory is dead. Do not re-run.
---
## Query E — group-count cross-check (the secondary `aenderbar` theory)
Query B hinted that the two `aenderbar="False"` rows were the two with a *second*,
unpublished group membership (`1292`→{10, 91}, `1293`→{10, 92}) while `aenderbar="True"`
`1462` sat in group `10` alone. Two data points is not a rule — this settles it in one run:
```sql
SELECT s.IDSelektionsstamm,
s.Selektion,
COUNT(*) AS groupCount,
STRING_AGG(CAST(sgm.IDSelektionsgruppenstamm_FS AS varchar(10)), ',') AS groupIds
FROM dbo.Selektionsstamm s
JOIN dbo.Selektionsgruppe sgm ON sgm.IDSelektionsstamm_FS = s.IDSelektionsstamm
WHERE s.IDSelektionsstamm IN (1064,1065,1066,1067,1068,1069,1070,1156,1292,1293,
1299,1304,1377,1459,1461,1462,1477,1478)
GROUP BY s.IDSelektionsstamm, s.Selektion
ORDER BY s.IDSelektionsstamm;
```
Read against the known `aenderbar` values from the reference response:
- `aenderbar="False"`: `1070`, `1156`, `1292`, `1293`, `1299`, `1304`, `1377`
- `aenderbar="True"`: `1064``1069`, `1459`, `1461`, `1462`
If every `False` row has `groupCount > 1` and every `True` row has `groupCount = 1`, the
theory holds. Note it still would not explain `1478` (group `10` only, yet omitted), so
Query D remains the priority.
**Result (2026-08-04): the theory is dead.** Group membership does not correlate with
`aenderbar` at all:
| id | Selektion | groupCount | groupIds | `aenderbar` |
| --- | --- | --- | --- | --- |
| 10641069 | *Interessen* | 1 | 83 | **True** |
| 1070 | E&P Teamer - allg. Merkmal | 1 | 10 | **False** |
| 1292 | Admin | 2 | 91, 10 | **False** |
| 1293 | Manager | 2 | 92, 10 | **False** |
| 1299 / 1304 / 1377 | Hausleitung SSL / DKS / KHH | 1 | 10 | **False** |
| 1459 / 1461 / 1462 | Hausleitung SVS / SHM / AGR | 1 | 10 | **True** |
| 1477 / 1478 | Preisrechner, Preisrechner Admin | 1 | 10 | *(not emitted)* |
`1299`/`1304`/`1377` and `1459`/`1461`/`1462` have **identical** group membership,
identical `Status`, identical `System` — and opposite `aenderbar`. The `1292`/`1293`
double membership was a coincidence.
Confirmed as a side effect: `1156` is absent from this result because the inner join
found no `Selektionsgruppe` row for it. `<ohne Gruppe>` really does mean "no membership
row", as modelled at the top of this document.
---
## Query F — exhaust every remaining link to `Selektionsstamm`
After D and E, the schema export lists exactly these tables with an FK into
`Selektionsstamm`. This counts all of them at once for the rows whose behaviour differs,
which is the last thing the database can tell us:
```sql
SELECT s.IDSelektionsstamm, s.Selektion,
(SELECT COUNT(*) FROM dbo.Selektionen x WHERE x.IDSelektionsstamm_FS = s.IDSelektionsstamm) AS addressAssignments,
(SELECT COUNT(*) FROM dbo.PartnerSelektionenKunde x WHERE x.IDSelektionsstamm_FS = s.IDSelektionsstamm) AS partnerLinks,
(SELECT COUNT(*) FROM dbo.LeistungSelektionenKunde x WHERE x.IDSelektionsstamm_FS = s.IDSelektionsstamm) AS serviceLinks,
(SELECT COUNT(*) FROM dbo.ReiseleiterSelektionen x WHERE x.IDSelektionsstamm_FS = s.IDSelektionsstamm) AS guideLinks,
(SELECT COUNT(*) FROM dbo.ReiseSelektionen x WHERE x.IDSelektionsstamm_FS = s.IDSelektionsstamm) AS travelLinks,
(SELECT COUNT(*) FROM dbo.ReisezielSelektionsstamm x WHERE x.IDSelektionsstamm_FS = s.IDSelektionsstamm) AS destinationLinks
FROM dbo.Selektionsstamm s
WHERE s.IDSelektionsstamm IN (1070,1156,1292,1293,1299,1304,1377,1459,1461,1462,1477,1478,1064)
ORDER BY s.IDSelektionsstamm;
```
The decisive comparison is `1377` (`aenderbar="False"`) vs `1462` (`"True"`) vs `1478`
(omitted). If all three rows are identical here too, then **no per-selection property in
this database distinguishes them**, and the rule is not in the schema at all — go to
"Where this leaves the investigation" below.
**Result (2026-08-04): all link counts are zero** for every row queried, except
`travelLinks` on `1070` (11) and `1156` (3) via `ReiseSelektionen` — neither of which
correlates with `aenderbar` or with being emitted. Assignment counts don't explain
anything either:
| id | assignments | emitted? | `aenderbar` |
| --- | --- | --- | --- |
| 1377 Hausleitung KHH | **0** | yes | False |
| 1462 Hausleitung AGR | 1 | yes | True |
| 1478 Preisrechner Admin | **3** | **no** | — |
`1377` is published with zero assignments; `1478` has three and is not published. This
exhausts every FK path into `Selektionsstamm` in the schema. **Stop querying the
database** — the answer is not in it.
---
## Query C — locate the publish gate
Point (1) means some table or config outside the three core tables decides what gets
emitted. Run these in order; the first hit explains everything.
**C1 — what distinguishes a published group from an unpublished one.** Groups `0`, `10`,
`83` are emitted; `91`, `92` are not:
```sql
SELECT IDSelektionsgruppenstamm, Selektionsgruppe, Status, System, Ausgrenzung, Position
FROM dbo.Selektionsgruppenstamm
ORDER BY Position;
```
If exactly the emitted groups share a `Status`/`System`/`Position` value that `91`/`92`
lack, that's the group gate. It still won't explain `1478` (same group as `1462`), but
it pins down half the rule — and tells you whether `aenderbar` tracks membership in one
of these system groups.
**C2 — hunt for a published-selection whitelist in the config stores.** This is the
leading theory for `1478`: a config value enumerating exposed selection ids that was
maintained up to `1462` and never extended. This database does store id lists in
delimited string columns (`CRMAktion.IDsAgenturEingrenzung`,
`ImportAdressen.ZuweisungSelektionIDs`, `AdresseHistorie.SelektionsIDs`), so the pattern
exists. Search for a known-published id:
```sql
SELECT * FROM dbo.Einstellung
WHERE Wert LIKE '%1462%' OR Wert LIKE '%1292%'
OR Bezeichnung LIKE '%elektion%' OR Beschreibung LIKE '%elektion%';
SELECT * FROM dbo.ProgrammEinstellung
WHERE Wert LIKE '%1462%' OR Wert LIKE '%1292%' OR Variable LIKE '%elektion%';
SELECT * FROM dbo.ConnectEinstellung
WHERE WertString LIKE '%1462%' OR WertString LIKE '%1292%';
```
**C3 — brute-force the whole schema** if C1/C2 come up empty. Find every wide text
column that mentions a published selection id:
```sql
DECLARE @needle nvarchar(20) = '1462';
DECLARE @sql nvarchar(max) = '';
SELECT @sql = @sql + 'SELECT ''' + QUOTENAME(t.name) + ''' AS tbl, ''' + QUOTENAME(c.name)
+ ''' AS col, COUNT(*) AS hits FROM dbo.' + QUOTENAME(t.name)
+ ' WHERE CAST(' + QUOTENAME(c.name) + ' AS nvarchar(max)) LIKE ''%'
+ @needle + '%'' HAVING COUNT(*) > 0;' + CHAR(10)
FROM sys.tables t
JOIN sys.columns c ON c.object_id = t.object_id
JOIN sys.types ty ON ty.user_type_id = c.user_type_id
WHERE ty.name IN ('nvarchar', 'varchar', 'nchar', 'char')
AND (c.max_length = -1 OR c.max_length >= 100)
AND t.name NOT LIKE 'Selektion%';
EXEC sp_executesql @sql; -- read-only; slow, run off-peak
```
**C4 — rule out staleness/caching first, it's free.** `1478` is structurally identical
to an emitted row, which is also what a cached catalog looks like. Re-fetch the XML now
and confirm the sample postdates the insert:
```php
// bin/console or a scratch script
$client->getCrmAttributes($email, $password);
```
If BusPro caches the catalog in-process, an application-pool/service restart on the
BusProNet host is the fix, and no schema change is needed.
---
## Portal side: a correct XML is not enough
`BpnAuthenticator::createOrUpdateLocalUser()` snapshots the roles onto the local `User`
entity at login (`setRoles($crmAttributes->roles)` /
`setHotelCodes($crmAttributes->hotelCodes)`). Roles are **not** re-read per request. So
after fixing BusPro the affected user must **log out and log in again** before
`ROLE_GROUPS_ADMIN` takes effect — an active session keeps the stale role set.
---
## Where this leaves the investigation
**Conclusion as of 2026-08-04, after Queries AF: the schema search is exhausted.**
- `1478` and `1462` are identical on every column and every link the schema exposes —
same blank `Status`, same `System = 1`, same single membership in group `10`, adjacent
positions (76 vs 74), zero links in all five side tables. `1462` is emitted, `1478` is
not.
- `1377` (`aenderbar="False"`) and `1462` (`"True"`) are identical by the same measure.
Opposite flag.
- Assignment count is not it either: `1377` is published with zero assignments, `1478`
has three and is not published.
Rows that are indistinguishable in the database behave differently in the response. Every
column of the three core tables, every FK path into `Selektionsstamm`, and a
whole-schema column-name search have now been checked. So **the publish set and the
`aenderbar` flag are not derived from this database** — they live in the BusProNet
service layer: its own configuration, or an in-process cache of the catalog.
Only C2/C3 (a text search of the config stores, then of every wide text column) remain,
and they are long shots. Prefer the escalation path below.
Escalation path, cheapest first:
1. **Re-fetch and confirm staleness.** Our side does not cache this call — the
`xml-sync` cache tag covers the XML *export* sync, and `ApiClient::getCrmAttributes()`
has no cache layer — so a fresh login hits BusPro directly. If `1478` is still absent
from a response captured after the insert, the staleness is on the BusProNet host and
an application-pool/service restart there is the test.
2. **The UI experiment.** Create a throwaway selection in the TEAM group *through the
BusPro UI*, then diff its rows against `1478` across `Selektionsstamm` /
`Selektionsgruppe` / `Selektionen`. If it appears in the XML, the diff names exactly
what a manual `INSERT` misses. If it does not, the gate is provably config/cache and
not per-row data.
3. **Ask the vendor.** "What determines which `Selektionsstamm` entries `SelektionCRM`
publishes, given group membership is not sufficient — and what drives `aenderbar`?"
Both are one-sentence answers for them and may not be discoverable from the schema at
all. Worth bundling into the open BPN thread.
## Caveats
- **`aenderbar` has no known source.** No such column exists in any of the 1023 tables;
`Selektionsstamm.System` (Query B), the agency whitelist (Query D) and group membership
(Query E) are all falsified. Treat it as unmodellable in a `bpn-connect` endpoint until
the vendor explains it — if `bpn-connect` must serve this response, the safe move is to
keep `SelektionCRM` on the socket API rather than guess, exactly as
`live-api-queries.md` recommends for `getMutableData()`.
- Groups `91` and `92` are known to exist and are never emitted; what they are is still
unexplained (Query C1). Record the answer here when you have it.
- **The publish gate is not identified.** Group membership is necessary but not
sufficient: `1478` sits in the published TEAM group with an assignment and is still
omitted. Query C is the open investigation.
- Consequently Query A **over-reports**: it will return `1477`/`1478` (and anything in
groups `91`/`92`) that the real XML omits. It reconstructs the shape of the response,
not yet its exact membership.
- `dbo.Selektionen` has no status column and no exported primary key, so a duplicate
assignment row may well be insertable. Check before inserting.
- The schema exports carry no PK/index catalog (noted in
`../bpn-connect-query-reference.md`); none of the queries here are
performance-sensitive, but a `bpn-connect` endpoint built on them would want indexes
on `Selektionen.IDAdresse_FS` and `Selektionsgruppe.IDSelektionsstamm_FS` confirmed.
- `Adresse` vs. `AdressePerson`: assignments hang off `IDAdresse`, not `IDPerson`.