diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/controller/SiteController.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/controller/SiteController.kt index 9e299c540..4a05591db 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/controller/SiteController.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/controller/SiteController.kt @@ -28,6 +28,7 @@ import org.eclipse.tractusx.bpdm.gate.api.model.response.PageOutputResponse import org.eclipse.tractusx.bpdm.gate.api.model.response.PageStartAfterResponse import org.eclipse.tractusx.bpdm.gate.api.model.response.ValidationResponse import org.eclipse.tractusx.bpdm.gate.config.ApiConfigProperties +import org.eclipse.tractusx.bpdm.gate.containsDuplicates import org.eclipse.tractusx.bpdm.gate.service.SiteService import org.eclipse.tractusx.bpdm.gate.service.ValidationService import org.springframework.http.HttpStatus @@ -42,9 +43,9 @@ class SiteController( ) : GateSiteApi { override fun upsertSites(sites: Collection): ResponseEntity { -// if (sites.size > apiConfigProperties.upsertLimit || sites.map { it.externalId }.containsDuplicates()) { -// return ResponseEntity(HttpStatus.BAD_REQUEST) -// } + if (sites.size > apiConfigProperties.upsertLimit || sites.map { it.externalId }.containsDuplicates()) { + return ResponseEntity(HttpStatus.BAD_REQUEST) + } siteService.upsertSites(sites) return ResponseEntity(HttpStatus.OK) } diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/repository/LegalEntityRepository.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/repository/LegalEntityRepository.kt index 3af25a4f8..3443d58fc 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/repository/LegalEntityRepository.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/repository/LegalEntityRepository.kt @@ -25,7 +25,7 @@ import org.springframework.data.repository.CrudRepository interface LegalEntityRepository : JpaRepository, CrudRepository { - fun findDistinctByBpnIn(externalId: Collection): Set + fun findDistinctByExternalIdIn(externalId: Collection): Set fun findByExternalId(externalId: String): LegalEntity? } \ No newline at end of file diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/AddressPersistenceService.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/AddressPersistenceService.kt index c03f9f4e0..48e3b1540 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/AddressPersistenceService.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/AddressPersistenceService.kt @@ -20,9 +20,10 @@ package org.eclipse.tractusx.bpdm.gate.service import jakarta.transaction.Transactional -import org.eclipse.tractusx.bpdm.common.exception.BpdmNotFoundException import org.eclipse.tractusx.bpdm.common.util.replace import org.eclipse.tractusx.bpdm.gate.api.model.AddressGateInputRequest +import org.eclipse.tractusx.bpdm.gate.entity.AddressIdentifier +import org.eclipse.tractusx.bpdm.gate.entity.AddressState import org.eclipse.tractusx.bpdm.gate.entity.LogisticAddress import org.eclipse.tractusx.bpdm.gate.repository.GateAddressRepository import org.eclipse.tractusx.bpdm.gate.repository.LegalEntityRepository @@ -46,8 +47,12 @@ class AddressPersistenceService( addresses.forEach { address -> val legalEntityRecord = - address.legalEntityExternalId?.let { legalEntityRepository.findByExternalId(it) ?: throw BpdmNotFoundException("Business Partner", it) } - val siteRecord = address.siteExternalId?.let { siteEntityRepository.findByExternalId(it) ?: throw BpdmNotFoundException("Business Partner", it) } + address.legalEntityExternalId?.let { legalEntityRepository.findByExternalId(it) } + val siteRecord = address.siteExternalId?.let { siteEntityRepository.findByExternalId(it) } + +// if(legalEntityRecord == null && siteRecord == null) { +// throw BpdmNotFoundException("Business Partner", "Error") +// } val fullAddress = address.toAddressGate(legalEntityRecord, siteRecord) addressRecord.find { it.externalId == address.externalId }?.let { existingAddress -> @@ -64,14 +69,22 @@ class AddressPersistenceService( address.name = changeAddress.name address.bpn = changeAddress.bpn address.externalId = changeAddress.externalId - address.legalEntity= changeAddress.legalEntity + address.legalEntity = changeAddress.legalEntity address.siteExternalId = changeAddress.siteExternalId address.physicalPostalAddress = changeAddress.physicalPostalAddress address.alternativePostalAddress = changeAddress.alternativePostalAddress - address.identifiers.replace(changeAddress.identifiers) - address.states.replace(changeAddress.states) + address.identifiers.replace(changeAddress.identifiers.map { toEntityIdentifier(it, address) }) + address.states.replace(changeAddress.states.map { toEntityAddress(it, address) }) + + } + + fun toEntityAddress(dto: AddressState, address: LogisticAddress): AddressState { + return AddressState(dto.description, dto.validFrom, dto.validTo, dto.type, address) + } + fun toEntityIdentifier(dto: AddressIdentifier, address: LogisticAddress): AddressIdentifier { + return AddressIdentifier(dto.value, dto.type, address) } } \ No newline at end of file diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/LegalEntityPersistenceService.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/LegalEntityPersistenceService.kt index b4e80334a..23dae4f86 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/LegalEntityPersistenceService.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/LegalEntityPersistenceService.kt @@ -20,31 +20,40 @@ package org.eclipse.tractusx.bpdm.gate.service +import org.eclipse.tractusx.bpdm.common.exception.BpdmNotFoundException import org.eclipse.tractusx.bpdm.common.util.replace import org.eclipse.tractusx.bpdm.gate.api.model.LegalEntityGateInputRequest -import org.eclipse.tractusx.bpdm.gate.entity.LegalEntity +import org.eclipse.tractusx.bpdm.gate.entity.* +import org.eclipse.tractusx.bpdm.gate.repository.GateAddressRepository import org.eclipse.tractusx.bpdm.gate.repository.LegalEntityRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class LegalEntityPersistenceService( - private val gateLegalEntityRepository: LegalEntityRepository + private val gateLegalEntityRepository: LegalEntityRepository, + private val gateAddressRepository: GateAddressRepository ) { @Transactional fun persistLegalEntitiesBP(legalEntities: Collection) { //finds Legal Entity by External ID - val legalEntityRecord = gateLegalEntityRepository.findDistinctByBpnIn(legalEntities.map { it.externalId }) + val legalEntityRecord = gateLegalEntityRepository.findDistinctByExternalIdIn(legalEntities.map { it.externalId }) //Business Partner persist legalEntities.forEach { legalEntity -> val fullLegalEntity = legalEntity.toLegalEntity() legalEntityRecord.find { it.externalId == legalEntity.externalId }?.let { existingLegalEntity -> - updateLegalEntity(existingLegalEntity, fullLegalEntity) + val logisticAddressRecord = gateAddressRepository.findByExternalId(existingLegalEntity.externalId + "_legalAddress") + ?: throw BpdmNotFoundException("Business Partner", "Error") + + updateAddress(logisticAddressRecord, fullLegalEntity.legalAddress) + + updateLegalEntity(existingLegalEntity, fullLegalEntity, logisticAddressRecord) gateLegalEntityRepository.save(existingLegalEntity) + } ?: run { gateLegalEntityRepository.save(fullLegalEntity) @@ -53,18 +62,53 @@ class LegalEntityPersistenceService( } } - - private fun updateLegalEntity(legalEntity: LegalEntity, legalEntityRequest:LegalEntity): LegalEntity { + private fun updateLegalEntity(legalEntity: LegalEntity, legalEntityRequest: LegalEntity, logisticAddressRecord: LogisticAddress): LegalEntity { legalEntity.bpn = legalEntityRequest.bpn legalEntity.externalId = legalEntityRequest.externalId legalEntity.legalForm = legalEntityRequest.legalForm - legalEntity.legalName= legalEntityRequest.legalName - legalEntity.identifiers.replace(legalEntityRequest.identifiers) - legalEntity.states.replace(legalEntityRequest.states) - legalEntity.classifications.replace(legalEntityRequest.classifications) - legalEntity.legalAddress = legalEntityRequest.legalAddress - legalEntity.legalAddress.legalEntity= legalEntity + legalEntity.legalName = legalEntityRequest.legalName + legalEntity.identifiers.replace(legalEntityRequest.identifiers.map { toEntityIdentifier(it, legalEntity) }) + legalEntity.states.replace(legalEntityRequest.states.map { toEntityState(it, legalEntity) }) + legalEntity.classifications.replace(legalEntityRequest.classifications.map { toEntityClassification(it, legalEntity) }) + legalEntity.legalAddress = logisticAddressRecord + legalEntity.legalAddress.legalEntity = legalEntity + return legalEntity } + fun toEntityIdentifier(dto: LegalEntityIdentifier, legalEntity: LegalEntity): LegalEntityIdentifier { + return LegalEntityIdentifier(dto.value, dto.type, dto.issuingBody, legalEntity) + } + + fun toEntityClassification(dto: Classification, legalEntity: LegalEntity): Classification { + return Classification(dto.value, dto.code, dto.type, legalEntity) + } + + fun toEntityState(dto: LegalEntityState, legalEntity: LegalEntity): LegalEntityState { + return LegalEntityState(dto.officialDenotation, dto.validFrom, dto.validTo, dto.type, legalEntity) + } + + private fun updateAddress(address: LogisticAddress, changeAddress: LogisticAddress) { + + address.name = changeAddress.name + address.bpn = changeAddress.bpn + address.externalId = changeAddress.externalId + address.legalEntity = changeAddress.legalEntity + address.siteExternalId = changeAddress.siteExternalId + address.physicalPostalAddress = changeAddress.physicalPostalAddress + address.alternativePostalAddress = changeAddress.alternativePostalAddress + + address.identifiers.replace(changeAddress.identifiers.map { toEntityIdentifier(it, address) }) + address.states.replace(changeAddress.states.map { toEntityAddress(it, address) }) + + } + + fun toEntityAddress(dto: AddressState, address: LogisticAddress): AddressState { + return AddressState(dto.description, dto.validFrom, dto.validTo, dto.type, address) + } + + fun toEntityIdentifier(dto: AddressIdentifier, address: LogisticAddress): AddressIdentifier { + return AddressIdentifier(dto.value, dto.type, address) + } + } \ No newline at end of file diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/ResponseMappings.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/ResponseMappings.kt index 95302ad09..08f096642 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/ResponseMappings.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/ResponseMappings.kt @@ -122,7 +122,7 @@ fun SiteGateInputRequest.toSiteGate(legalEntity: LegalEntity): Site { val addressInputRequest = AddressGateInputRequest( address = site.mainAddress, - externalId = externalId + "_mainAddress", + externalId = externalId + "_site", legalEntityExternalId = externalId ) diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/SitePersistenceService.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/SitePersistenceService.kt index 9dcae4b1f..1ed07573c 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/SitePersistenceService.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/SitePersistenceService.kt @@ -22,7 +22,8 @@ package org.eclipse.tractusx.bpdm.gate.service import org.eclipse.tractusx.bpdm.common.exception.BpdmNotFoundException import org.eclipse.tractusx.bpdm.common.util.replace import org.eclipse.tractusx.bpdm.gate.api.model.SiteGateInputRequest -import org.eclipse.tractusx.bpdm.gate.entity.Site +import org.eclipse.tractusx.bpdm.gate.entity.* +import org.eclipse.tractusx.bpdm.gate.repository.GateAddressRepository import org.eclipse.tractusx.bpdm.gate.repository.LegalEntityRepository import org.eclipse.tractusx.bpdm.gate.repository.SiteRepository import org.springframework.stereotype.Service @@ -31,21 +32,18 @@ import org.springframework.transaction.annotation.Transactional @Service class SitePersistenceService( private val siteRepository: SiteRepository, - private val legalEntityRepository: LegalEntityRepository + private val legalEntityRepository: LegalEntityRepository, + private val addressRepository: GateAddressRepository ) { @Transactional fun persistSitesBP(sites: Collection) { - //finds Legal Entity by External ID - //val legalEntities = gateLegalEntityRepository.findDistinctByBpnIn(sites.map { it.legalEntityExternalId }) - //Finds Site in DB val externalIdColl: MutableCollection = mutableListOf() sites.forEach { externalIdColl.add(it.externalId) } val siteRecord = siteRepository.findByExternalIdIn(externalIdColl) - // for (legalEntity in legalEntities) { sites.forEach { site -> val legalEntityRecord = @@ -53,16 +51,21 @@ class SitePersistenceService( legalEntityRepository.findByExternalId(site.legalEntityExternalId) ?: throw BpdmNotFoundException("Business Partner", it) } - val fullSite = site.toSiteGate(legalEntityRecord) //TODO (Needs to receive an Legal Entity) + val fullSite = site.toSiteGate(legalEntityRecord) + siteRecord.find { it.externalId == site.externalId }?.let { existingSite -> + + val logisticAddressRecord = + addressRepository.findByExternalId(site.externalId + "_site") ?: throw BpdmNotFoundException("Business Partner", "Error") + + updateAddress(logisticAddressRecord, fullSite.mainAddress) + updateSite(existingSite, fullSite) siteRepository.save(existingSite) } ?: run { siteRepository.save(fullSite) } - //} } - //} } @@ -72,8 +75,35 @@ class SitePersistenceService( site.name = updatedSite.name site.externalId = updatedSite.externalId site.legalEntity = updatedSite.legalEntity - site.states.replace(updatedSite.states) + site.states.replace(updatedSite.states.map { toEntityAddress(it, site) }) + + } + + fun toEntityAddress(dto: SiteState, site: Site): SiteState { + return SiteState(dto.description, dto.validFrom, dto.validTo, dto.type, site) + } + + private fun updateAddress(address: LogisticAddress, changeAddress: LogisticAddress) { + + address.name = changeAddress.name + address.bpn = changeAddress.bpn + address.externalId = changeAddress.externalId + address.legalEntity = changeAddress.legalEntity + address.siteExternalId = changeAddress.siteExternalId + address.physicalPostalAddress = changeAddress.physicalPostalAddress + address.alternativePostalAddress = changeAddress.alternativePostalAddress + + address.identifiers.replace(changeAddress.identifiers.map { toEntityIdentifier(it, address) }) + address.states.replace(changeAddress.states.map { toEntityAddress(it, address) }) + + } + + fun toEntityAddress(dto: AddressState, address: LogisticAddress): AddressState { + return AddressState(dto.description, dto.validFrom, dto.validTo, dto.type, address) + } + fun toEntityIdentifier(dto: AddressIdentifier, address: LogisticAddress): AddressIdentifier { + return AddressIdentifier(dto.value, dto.type, address) } } \ No newline at end of file diff --git a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/AddressControllerInputIT.kt b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/AddressControllerInputIT.kt index 3c3b15424..c3aaafae7 100644 --- a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/AddressControllerInputIT.kt +++ b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/AddressControllerInputIT.kt @@ -547,6 +547,14 @@ internal class AddressControllerInputIT @Autowired constructor( RequestValues.addressGateInputRequest2 ) + val legalEntity = listOf( + RequestValues.legalEntityGateInputRequest1, + ) + + val sites = listOf( + RequestValues.siteGateInputRequest1, + ) + val parentLegalEntitiesSaas = listOf( SaasValues.legalEntityResponse1 ) diff --git a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/LegalEntityControllerInputIT.kt b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/LegalEntityControllerInputIT.kt index 030953157..ef012953a 100644 --- a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/LegalEntityControllerInputIT.kt +++ b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/LegalEntityControllerInputIT.kt @@ -34,12 +34,12 @@ import org.eclipse.tractusx.bpdm.gate.api.model.request.PaginationStartAfterRequ import org.eclipse.tractusx.bpdm.gate.api.model.response.PageStartAfterResponse import org.eclipse.tractusx.bpdm.gate.api.model.response.ValidationResponse import org.eclipse.tractusx.bpdm.gate.api.model.response.ValidationStatus +import org.eclipse.tractusx.bpdm.gate.repository.LegalEntityRepository import org.eclipse.tractusx.bpdm.gate.util.* import org.eclipse.tractusx.bpdm.gate.util.EndpointValues.GATE_API_INPUT_LEGAL_ENTITIES_PATH import org.eclipse.tractusx.bpdm.gate.util.EndpointValues.SAAS_MOCK_BUSINESS_PARTNER_PATH import org.eclipse.tractusx.bpdm.gate.util.EndpointValues.SAAS_MOCK_FETCH_BUSINESS_PARTNER_PATH -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import org.springframework.beans.factory.annotation.Autowired @@ -60,7 +60,8 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti internal class LegalEntityControllerInputIT @Autowired constructor( private val webTestClient: WebTestClient, private val objectMapper: ObjectMapper, - val gateClient: GateClient + val gateClient: GateClient, + private val legalEntityRepository: LegalEntityRepository ) { companion object { @RegisterExtension @@ -595,4 +596,52 @@ internal class LegalEntityControllerInputIT @Autowired constructor( assertThat(actualResponse).isEqualTo(expectedResponse) } + + /** + * When upserting legal entities + * Then SaaS upsert api should be called with the legal entity data mapped to the SaaS data model + */ + @Test + fun `upsert and persist legal entities`() { + val legalEntities = listOf( + RequestValues.legalEntityGateInputRequest1, + RequestValues.legalEntityGateInputRequest2, + ) + + val expectedLegalEntities = listOf( + SaasValues.legalEntityRequest1, + SaasValues.legalEntityRequest2, + ) + + wireMockServer.stubFor( + put(urlPathMatching(SAAS_MOCK_BUSINESS_PARTNER_PATH)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody( + objectMapper.writeValueAsString( + UpsertResponse( + emptyList(), + emptyList(), + 2, + 0 + ) + ) + ) + ) + ) + + try { + gateClient.legalEntities().upsertLegalEntities(legalEntities) + } catch (e: WebClientResponseException) { + assertEquals(HttpStatus.OK, e.statusCode) + } + + val legalEntityRecordExternal1 = legalEntityRepository.findByExternalId("external-1") + assertNotEquals(legalEntityRecordExternal1, null) + + val legalEntityRecordExternal2 = legalEntityRepository.findByExternalId("external-2") + assertNotEquals(legalEntityRecordExternal2, null) + + } } \ No newline at end of file diff --git a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/SiteControllerInputIT.kt b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/SiteControllerInputIT.kt index 18423611f..f6b439224 100644 --- a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/SiteControllerInputIT.kt +++ b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/SiteControllerInputIT.kt @@ -31,10 +31,12 @@ import org.eclipse.tractusx.bpdm.gate.api.model.response.PageStartAfterResponse import org.eclipse.tractusx.bpdm.gate.api.model.response.ValidationResponse import org.eclipse.tractusx.bpdm.gate.api.model.response.ValidationStatus import org.eclipse.tractusx.bpdm.gate.config.SaasConfigProperties +import org.eclipse.tractusx.bpdm.gate.repository.SiteRepository import org.eclipse.tractusx.bpdm.gate.util.* import org.eclipse.tractusx.bpdm.gate.util.EndpointValues.SAAS_MOCK_BUSINESS_PARTNER_PATH import org.eclipse.tractusx.bpdm.gate.util.EndpointValues.SAAS_MOCK_DELETE_RELATIONS_PATH import org.eclipse.tractusx.bpdm.gate.util.EndpointValues.SAAS_MOCK_RELATIONS_PATH +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -55,7 +57,8 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti internal class SiteControllerInputIT @Autowired constructor( private val objectMapper: ObjectMapper, private val saasConfigProperties: SaasConfigProperties, - val gateClient: GateClient + val gateClient: GateClient, + private val siteRepository: SiteRepository ) { companion object { @RegisterExtension @@ -408,6 +411,11 @@ internal class SiteControllerInputIT @Autowired constructor( RequestValues.siteGateInputRequest2 ) + val legalEntities = listOf( + RequestValues.legalEntityGateInputRequest1, + RequestValues.legalEntityGateInputRequest2 + ) + val parentLegalEntitiesSaas = listOf( SaasValues.legalEntityResponse1, SaasValues.legalEntityResponse2 @@ -540,11 +548,19 @@ internal class SiteControllerInputIT @Autowired constructor( ) try { + gateClient.legalEntities().upsertLegalEntities(legalEntities) gateClient.sites().upsertSites(sites) } catch (e: WebClientResponseException) { assertEquals(HttpStatus.OK, e.statusCode) } + //Check if persisted site data + val siteExternal1 = siteRepository.findByExternalId("site-external-1") + Assertions.assertNotEquals(siteExternal1, null) + + val siteExternal2 = siteRepository.findByExternalId("site-external-2") + Assertions.assertNotEquals(siteExternal2, null) + // TODO: check that "upsert sites" was called in SaaS as expected // val upsertSitesRequest = wireMockServer.deserializeMatchedRequests(stubMappingUpsertSites, objectMapper).single() // assertThat(upsertSitesRequest.businessPartners).containsExactlyInAnyOrderElementsOf(expectedSites)