feat: map house manager roles and houses by crm ids instead of labels

This commit is contained in:
Björn Fromme
2026-08-18 09:08:01 +02:00
parent 25f5840085
commit 1fd0fbc21e
4 changed files with 70 additions and 4 deletions
+31
View File
@@ -4,6 +4,36 @@ parameters:
bpn_crm_id_teamer: '%env(int:APP_BPN_CRM_ID_TEAMER)%' bpn_crm_id_teamer: '%env(int:APP_BPN_CRM_ID_TEAMER)%'
bpn_default_hotel_code: '%env(default::APP_BPN_DEFAULT_HOTEL_CODE)%' bpn_default_hotel_code: '%env(default::APP_BPN_DEFAULT_HOTEL_CODE)%'
# BusPro CRM selection id => hotel code. A person holding a selected attribute from this
# list is a house manager for that house. Matched by id and not by the "Hausleitung XXX"
# label, so renaming a selection in BusPro cannot silently grant or revoke a house.
# The commented entries are BusPro selections whose code is not a house in the "houses"
# parameter below: granting them would let someone in who then sees no assignments and
# no dispositions at all. Uncomment one once its house exists. The reverse gap is fine -
# not every house has a selection.
bpn_crm_house_manager_ids:
1299: 'SSL'
1300: 'SST'
1301: 'MVK'
# 1302: 'ASB'
1303: 'LPJ'
1304: 'DKS'
1305: 'DGS'
1306: 'DPW'
1307: 'DKI'
1308: 'DWW'
1309: 'PMV'
# 1352: 'ASG'
1371: 'ASC'
1373: 'PCJ'
1374: 'SBW'
# 1375: 'UCH'
# 1376: 'SZO'
1377: 'KHH'
1459: 'SVS'
1461: 'SHM'
1462: 'AGR'
default_email_from: '%env(APP_DEFAULT_EMAIL_FROM)%' default_email_from: '%env(APP_DEFAULT_EMAIL_FROM)%'
default_email_to: '%env(APP_DEFAULT_EMAIL_TO)%' default_email_to: '%env(APP_DEFAULT_EMAIL_TO)%'
@@ -118,6 +148,7 @@ services:
bpn_crm_id_admin: '%bpn_crm_id_admin%' bpn_crm_id_admin: '%bpn_crm_id_admin%'
bpn_crm_id_manager: '%bpn_crm_id_manager%' bpn_crm_id_manager: '%bpn_crm_id_manager%'
bpn_crm_id_teamer: '%bpn_crm_id_teamer%' bpn_crm_id_teamer: '%bpn_crm_id_teamer%'
bpn_crm_house_manager_ids: '%bpn_crm_house_manager_ids%'
bpn_default_hotel_code: '%bpn_default_hotel_code%' bpn_default_hotel_code: '%bpn_default_hotel_code%'
App\Twig\AppRuntime: App\Twig\AppRuntime:
+5 -1
View File
@@ -65,7 +65,11 @@ slice it, and picking the right one matters:
| admin | attribute id `%bpn_crm_id_admin%`, selected | `isAdmin` | | admin | attribute id `%bpn_crm_id_admin%`, selected | `isAdmin` |
| Reisemanager | attribute id `%bpn_crm_id_manager%`, selected | `isManager` | | Reisemanager | attribute id `%bpn_crm_id_manager%`, selected | `isManager` |
| teamer | attribute id `%bpn_crm_id_teamer%`, selected | `isTeamer` | | teamer | attribute id `%bpn_crm_id_teamer%`, selected | `isTeamer` |
| Hausleitung | label matching `/^Hausleitung ([A-Z0-9]+)$/`, selected | `isHouseManager` + one hotel code per match | | Hausleitung | attribute id listed in `%bpn_crm_house_manager_ids%`, selected | `isHouseManager` + the hotel code that id maps to |
`bpn_crm_house_manager_ids` (`config/services.yaml`) maps a BusPro selection id to a hotel
code. Entries whose code is not a house in the `houses` parameter are kept commented out:
such a house manager could log in but would see no assignments and no dispositions at all.
`UserDataHandler::collectPendingRoles()` turns those into markers — `isManager` wins over `UserDataHandler::collectPendingRoles()` turns those into markers — `isManager` wins over
`isHouseManager`, they are never both claimed, though the hotel codes of a Hausleitung are `isHouseManager`, they are never both claimed, though the hotel codes of a Hausleitung are
+7 -3
View File
@@ -185,9 +185,12 @@ class ResponseParser
; ;
$attributes[] = $attribute; $attributes[] = $attribute;
if (1 === preg_match('/^Hausleitung ([A-Z0-9]+)$/', $attributeLabel, $matches) && true === $attribute->isSelected()) { // Matched by id, like every other role: the "Hausleitung XXX" label is BusPro
// wording and must not decide who gets access to which house.
$houseManagerCode = $this->config['bpn_crm_house_manager_ids'][$attribute->getId()] ?? null;
if (null !== $houseManagerCode && true === $attribute->isSelected()) {
$isHouseManager = true; $isHouseManager = true;
$hotelCodes[] = $matches[1]; $hotelCodes[] = $houseManagerCode;
} }
if ($this->config['bpn_crm_id_admin'] === $attribute->getId() && true === $attribute->isSelected()) { if ($this->config['bpn_crm_id_admin'] === $attribute->getId() && true === $attribute->isSelected()) {
$isAdmin = true; $isAdmin = true;
@@ -294,13 +297,14 @@ class ResponseParser
private function resolveOptions(array $options): array private function resolveOptions(array $options): array
{ {
$optionsResolver = new OptionsResolver(); $optionsResolver = new OptionsResolver();
$optionsResolver->setRequired(['bpn_crm_id_admin', 'bpn_crm_id_manager', 'bpn_crm_id_teamer']); $optionsResolver->setRequired(['bpn_crm_id_admin', 'bpn_crm_id_manager', 'bpn_crm_id_teamer', 'bpn_crm_house_manager_ids']);
$optionsResolver->setDefaults([ $optionsResolver->setDefaults([
'bpn_default_hotel_code' => null, 'bpn_default_hotel_code' => null,
]); ]);
$optionsResolver->setAllowedTypes('bpn_crm_id_admin', 'int'); $optionsResolver->setAllowedTypes('bpn_crm_id_admin', 'int');
$optionsResolver->setAllowedTypes('bpn_crm_id_manager', 'int'); $optionsResolver->setAllowedTypes('bpn_crm_id_manager', 'int');
$optionsResolver->setAllowedTypes('bpn_crm_id_teamer', 'int'); $optionsResolver->setAllowedTypes('bpn_crm_id_teamer', 'int');
$optionsResolver->setAllowedTypes('bpn_crm_house_manager_ids', 'array');
$optionsResolver->setAllowedTypes('bpn_default_hotel_code', ['string', 'null']); $optionsResolver->setAllowedTypes('bpn_default_hotel_code', ['string', 'null']);
return $optionsResolver->resolve($options); return $optionsResolver->resolve($options);
+27
View File
@@ -86,6 +86,25 @@ class ResponseParserTest extends TestCase
$this->assertCount(3, $response->getAttributeGroups()); $this->assertCount(3, $response->getAttributeGroups());
} }
/**
* The house of a "Hausleitung" selection comes from the configured id map, never from the
* label: an id that is not mapped grants nothing. That is also what keeps the houses
* commented out in bpn_crm_house_manager_ids from letting someone in who would then see
* no assignments and no dispositions at all.
*/
public function testParseCrmAttributesIgnoresAnUnmappedHouseManagerSelection(): void
{
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>SelektionCRM</art><idadresse>141747</idadresse><idperson>224526</idperson><selektionsmerkmale><selektionsgruppe id="10" bezeichnung="TEAM"><selektion id="1070" bezeichnung="E&amp;P Teamer - allg. Merkmal" aenderbar="False" auswahl="True"></selektion><selektion id="9999" bezeichnung="Hausleitung XYZ" aenderbar="False" auswahl="True"></selektion></selektionsgruppe></selektionsmerkmale></ergebnis>';
$parser = $this->getParserInstance();
$response = $parser->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $content);
$this->assertInstanceOf(CrmAttributesResponse::class, $response);
$this->assertTrue($response->isTeamer());
$this->assertFalse($response->isHouseManager());
$this->assertSame([], $response->getHotelCodes());
}
public function testParseCrmAttributesOfAnEmptyResponse(): void public function testParseCrmAttributesOfAnEmptyResponse(): void
{ {
$content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>SelektionCRM</art><idadresse>141747</idadresse><idperson>224526</idperson><selektionsmerkmale></selektionsmerkmale></ergebnis>'; $content = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>SelektionCRM</art><idadresse>141747</idadresse><idperson>224526</idperson><selektionsmerkmale></selektionsmerkmale></ergebnis>';
@@ -170,6 +189,14 @@ class ResponseParserTest extends TestCase
'bpn_crm_id_admin' => 1292, 'bpn_crm_id_admin' => 1292,
'bpn_crm_id_manager' => 1293, 'bpn_crm_id_manager' => 1293,
'bpn_crm_id_teamer' => 1070, 'bpn_crm_id_teamer' => 1070,
// Deliberately a small excerpt of the configured map: the fixtures only carry
// these three houses, and 1301 is here to be a mapped id no fixture selects.
'bpn_crm_house_manager_ids' => [
1299 => 'SSL',
1301 => 'MVK',
1304 => 'DKS',
1305 => 'DGS',
],
]); ]);
} }
} }