diff --git a/config/services.yaml b/config/services.yaml index ebf70e2..dab19d3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -4,6 +4,36 @@ parameters: bpn_crm_id_teamer: '%env(int:APP_BPN_CRM_ID_TEAMER)%' 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_to: '%env(APP_DEFAULT_EMAIL_TO)%' @@ -118,6 +148,7 @@ services: bpn_crm_id_admin: '%bpn_crm_id_admin%' bpn_crm_id_manager: '%bpn_crm_id_manager%' 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%' App\Twig\AppRuntime: diff --git a/docs/user-roles.md b/docs/user-roles.md index 9c93b73..eb1c1da 100644 --- a/docs/user-roles.md +++ b/docs/user-roles.md @@ -65,7 +65,11 @@ slice it, and picking the right one matters: | admin | attribute id `%bpn_crm_id_admin%`, selected | `isAdmin` | | Reisemanager | attribute id `%bpn_crm_id_manager%`, selected | `isManager` | | 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 `isHouseManager`, they are never both claimed, though the hotel codes of a Hausleitung are diff --git a/src/BusProNet/ResponseParser.php b/src/BusProNet/ResponseParser.php index 9a3c9b2..c9081cd 100644 --- a/src/BusProNet/ResponseParser.php +++ b/src/BusProNet/ResponseParser.php @@ -185,9 +185,12 @@ class ResponseParser ; $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; - $hotelCodes[] = $matches[1]; + $hotelCodes[] = $houseManagerCode; } if ($this->config['bpn_crm_id_admin'] === $attribute->getId() && true === $attribute->isSelected()) { $isAdmin = true; @@ -294,13 +297,14 @@ class ResponseParser private function resolveOptions(array $options): array { $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([ 'bpn_default_hotel_code' => null, ]); $optionsResolver->setAllowedTypes('bpn_crm_id_admin', 'int'); $optionsResolver->setAllowedTypes('bpn_crm_id_manager', 'int'); $optionsResolver->setAllowedTypes('bpn_crm_id_teamer', 'int'); + $optionsResolver->setAllowedTypes('bpn_crm_house_manager_ids', 'array'); $optionsResolver->setAllowedTypes('bpn_default_hotel_code', ['string', 'null']); return $optionsResolver->resolve($options); diff --git a/tests/BusProNet/ResponseParserTest.php b/tests/BusProNet/ResponseParserTest.php index 4a8d126..cd92a3f 100644 --- a/tests/BusProNet/ResponseParserTest.php +++ b/tests/BusProNet/ResponseParserTest.php @@ -86,6 +86,25 @@ class ResponseParserTest extends TestCase $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 = 'SelektionCRM141747224526'; + + $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 { $content = 'SelektionCRM141747224526'; @@ -170,6 +189,14 @@ class ResponseParserTest extends TestCase 'bpn_crm_id_admin' => 1292, 'bpn_crm_id_manager' => 1293, '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', + ], ]); } }