From 4057840c21835f7328233f3a55fc67bf217bf967 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:15:38 +0200 Subject: [PATCH] feat(pool): #432 Upsert Business Partners from Cleaning Result - create initial poll logic - refactor legal entity creation validation so that it could be used by orchestrator DTOs - create upsert L/S/A business partner logic - create orchestration client configuration --- .../bpdm/common/dto/AddressIdentifierDto.kt | 6 +- .../bpdm/common/dto/AddressStateDto.kt | 10 +- .../bpdm/common/dto/ClassificationDto.kt | 8 +- .../bpdm/common/dto/IBaseLegalEntityDto.kt | 3 - .../bpdm/common/dto/LegalEntityDto.kt | 12 +- .../common/dto/LegalEntityIdentifierDto.kt | 15 +- .../bpdm/common/dto/LegalEntityStateDto.kt | 10 +- .../bpdm/common/dto/LogisticAddressDto.kt | 14 +- .../tractusx/bpdm/common/dto/SiteStateDto.kt | 14 +- .../orchestrator/api/model/LegalEntityDto.kt | 2 +- bpdm-pool/pom.xml | 4 + .../service/OpenSearchSyncStarterService.kt | 5 +- .../pool/config/OrchestratorClientConfig.kt | 46 ++ .../OrchestratorClientConfigProperties.kt | 28 ++ .../pool/exception/BpdmValidationException.kt | 23 + .../service/BusinessPartnerBuildService.kt | 204 ++++---- .../bpdm/pool/service/MetadataService.kt | 25 +- .../pool/service/RequestValidationService.kt | 106 ++-- .../bpdm/pool/service/TaskStepBuildService.kt | 453 ++++++++++++++++++ .../service/TaskStepFetchAndReserveService.kt | 139 ++++++ .../src/main/resources/application.properties | 4 + .../TaskStepFetchAndReserveServiceTest.kt | 127 +++++ 22 files changed, 1056 insertions(+), 202 deletions(-) create mode 100644 bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/config/OrchestratorClientConfig.kt create mode 100644 bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/config/OrchestratorClientConfigProperties.kt create mode 100644 bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/exception/BpdmValidationException.kt create mode 100644 bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepBuildService.kt create mode 100644 bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepFetchAndReserveService.kt create mode 100644 bpdm-pool/src/test/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepFetchAndReserveServiceTest.kt diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressIdentifierDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressIdentifierDto.kt index f6a99b762..2cbde3579 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressIdentifierDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressIdentifierDto.kt @@ -26,8 +26,8 @@ import org.eclipse.tractusx.bpdm.common.dto.openapidescription.AddressIdentifier data class AddressIdentifierDto( @get:Schema(description = AddressIdentifierDescription.value) - val value: String, + override val value: String, @get:Schema(description = AddressIdentifierDescription.type) - val type: String, -) + override val type: String, +) : IBaseAddressIdentifierDto diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressStateDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressStateDto.kt index 35a0af2f4..33ea8c684 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressStateDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressStateDto.kt @@ -28,14 +28,14 @@ import java.time.LocalDateTime data class AddressStateDto( @get:Schema(description = AddressStateDescription.description) - val description: String?, + override val description: String?, @get:Schema(description = AddressStateDescription.validFrom) - val validFrom: LocalDateTime?, + override val validFrom: LocalDateTime?, @get:Schema(description = AddressStateDescription.validTo) - val validTo: LocalDateTime?, + override val validTo: LocalDateTime?, @get:Schema(description = AddressStateDescription.type) - val type: BusinessStateType -) + override val type: BusinessStateType +): IBaseAddressStateDto diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/ClassificationDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/ClassificationDto.kt index 047afddfc..54414abc8 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/ClassificationDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/ClassificationDto.kt @@ -27,11 +27,11 @@ import org.eclipse.tractusx.bpdm.common.model.ClassificationType data class ClassificationDto( @get:Schema(description = ClassificationDescription.type) - val type: ClassificationType, + override val type: ClassificationType, @get:Schema(description = ClassificationDescription.code) - val code: String?, + override val code: String?, @get:Schema(description = ClassificationDescription.value) - val value: String? -) + override val value: String? +) : IBaseClassificationDto diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/IBaseLegalEntityDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/IBaseLegalEntityDto.kt index 9a9548b54..672638b7a 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/IBaseLegalEntityDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/IBaseLegalEntityDto.kt @@ -40,7 +40,4 @@ interface IBaseLegalEntityDto { @get:ArraySchema(arraySchema = Schema(description = LegalEntityDescription.classifications, required = false)) val classifications: Collection - // TODO OpenAPI description for complex field does not work!! - @get:Schema(description = LegalEntityDescription.legalAddress) - val legalAddress: IBaseLogisticAddressDto? } \ No newline at end of file diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityDto.kt index 3b604fb7f..79af635ed 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityDto.kt @@ -26,17 +26,17 @@ import org.eclipse.tractusx.bpdm.common.dto.openapidescription.LegalEntityDescri @Schema(description = LegalEntityDescription.header) data class LegalEntityDto( @get:ArraySchema(arraySchema = Schema(description = LegalEntityDescription.identifiers, required = false)) - val identifiers: Collection = emptyList(), + override val identifiers: Collection = emptyList(), @get:Schema(description = LegalEntityDescription.legalShortName) - val legalShortName: String?, + override val legalShortName: String?, @get:Schema(description = LegalEntityDescription.legalForm) - val legalForm: String? = null, + override val legalForm: String? = null, @get:ArraySchema(arraySchema = Schema(description = LegalEntityDescription.states)) - val states: Collection = emptyList(), + override val states: Collection = emptyList(), @get:ArraySchema(arraySchema = Schema(description = LegalEntityDescription.classifications, required = false)) - val classifications: Collection = emptyList(), -) + override val classifications: Collection = emptyList(), +) : IBaseLegalEntityDto diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityIdentifierDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityIdentifierDto.kt index 2545a5146..a646af8ae 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityIdentifierDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityIdentifierDto.kt @@ -22,15 +22,10 @@ package org.eclipse.tractusx.bpdm.common.dto import io.swagger.v3.oas.annotations.media.Schema import org.eclipse.tractusx.bpdm.common.dto.openapidescription.LegalEntityIdentifierDescription -@Schema(description = LegalEntityIdentifierDescription.header) -data class LegalEntityIdentifierDto( - - @get:Schema(description = LegalEntityIdentifierDescription.value) - val value: String, - @get:Schema(description = LegalEntityIdentifierDescription.type) - val type: String, +data class LegalEntityIdentifierDto( - @get:Schema(description = LegalEntityIdentifierDescription.issuingBody) - val issuingBody: String? -) + override val value: String, + override val type: String, + override val issuingBody: String? +) : IBaseLegalEntityIdentifierDto diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityStateDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityStateDto.kt index d46916089..e68a1768b 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityStateDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LegalEntityStateDto.kt @@ -28,14 +28,14 @@ import java.time.LocalDateTime data class LegalEntityStateDto( @get:Schema(description = LegalEntityStateDescription.description) - val description: String?, + override val description: String?, @get:Schema(description = LegalEntityStateDescription.validFrom) - val validFrom: LocalDateTime?, + override val validFrom: LocalDateTime?, @get:Schema(description = LegalEntityStateDescription.validTo) - val validTo: LocalDateTime?, + override val validTo: LocalDateTime?, @get:Schema(description = LegalEntityStateDescription.type) - val type: BusinessStateType -) + override val type: BusinessStateType +) : IBaseLegalEntityStateDto diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LogisticAddressDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LogisticAddressDto.kt index b7ae27f8a..c6502e8d7 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LogisticAddressDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/LogisticAddressDto.kt @@ -30,16 +30,12 @@ data class LogisticAddressDto( val name: String? = null, @get:ArraySchema(arraySchema = Schema(description = LogisticAddressDescription.states)) - val states: Collection = emptyList(), + override val states: Collection = emptyList(), @get:ArraySchema(arraySchema = Schema(description = LogisticAddressDescription.identifiers)) - val identifiers: Collection = emptyList(), + override val identifiers: Collection = emptyList(), - // TODO OpenAPI description for complex field does not work!! - @get:Schema(description = LogisticAddressDescription.physicalPostalAddress) - val physicalPostalAddress: PhysicalPostalAddressDto, + override val physicalPostalAddress: PhysicalPostalAddressDto, - // TODO OpenAPI description for complex field does not work!! - @get:Schema(description = LogisticAddressDescription.alternativePostalAddress) - val alternativePostalAddress: AlternativePostalAddressDto? = null -) \ No newline at end of file + override val alternativePostalAddress: AlternativePostalAddressDto? = null +) : IBaseLogisticAddressDto \ No newline at end of file diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/SiteStateDto.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/SiteStateDto.kt index f308549ae..3be145ddd 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/SiteStateDto.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/SiteStateDto.kt @@ -27,15 +27,11 @@ import java.time.LocalDateTime @Schema(description = SiteStateDescription.header) data class SiteStateDto( - @get:Schema(description = SiteStateDescription.description) - val description: String?, + override val description: String?, - @get:Schema(description = SiteStateDescription.validFrom) - val validFrom: LocalDateTime?, + override val validFrom: LocalDateTime?, - @get:Schema(description = SiteStateDescription.validTo) - val validTo: LocalDateTime?, + override val validTo: LocalDateTime?, - @get:Schema(description = SiteStateDescription.type) - val type: BusinessStateType -) \ No newline at end of file + override val type: BusinessStateType +) : IBaseSiteStateDto \ No newline at end of file diff --git a/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/LegalEntityDto.kt b/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/LegalEntityDto.kt index 2f844959f..8a625ca95 100644 --- a/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/LegalEntityDto.kt +++ b/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/LegalEntityDto.kt @@ -44,6 +44,6 @@ data class LegalEntityDto( override val classifications: Collection = emptyList(), - override val legalAddress: LogisticAddressDto? = null + val legalAddress: LogisticAddressDto? = null ) : IBaseLegalEntityDto diff --git a/bpdm-pool/pom.xml b/bpdm-pool/pom.xml index 459e1dee8..cb5dc92af 100644 --- a/bpdm-pool/pom.xml +++ b/bpdm-pool/pom.xml @@ -43,6 +43,10 @@ ${project.groupId} bpdm-pool-api + + ${project.groupId} + bpdm-orchestrator-api + org.jetbrains.kotlin kotlin-stdlib diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/component/opensearch/impl/service/OpenSearchSyncStarterService.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/component/opensearch/impl/service/OpenSearchSyncStarterService.kt index f641c6796..c1819d6b5 100644 --- a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/component/opensearch/impl/service/OpenSearchSyncStarterService.kt +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/component/opensearch/impl/service/OpenSearchSyncStarterService.kt @@ -122,7 +122,7 @@ class OpenSearchSyncStarterService( * @return true if index mapping changed, false otherwise */ private fun updateOnInit(indexDefinition: IndexDefinition): Boolean { - val indexAlreadyExists = openSearchClient.indices().exists { it.index(indexDefinition.indexName) }.value() +/* val indexAlreadyExists = openSearchClient.indices().exists { it.index(indexDefinition.indexName) }.value() return if (!indexAlreadyExists) { true @@ -137,7 +137,8 @@ class OpenSearchSyncStarterService( deleteIndexIfExists(tempIndexName) requiredMappingMetadata != existingMappingMetadata - } + }*/ + return true } private fun getIndexMappings(indexName: String): MappingMetadata { diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/config/OrchestratorClientConfig.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/config/OrchestratorClientConfig.kt new file mode 100644 index 000000000..135258863 --- /dev/null +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/config/OrchestratorClientConfig.kt @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.pool.config + +import org.eclipse.tractusx.orchestrator.api.client.OrchestrationApiClient +import org.eclipse.tractusx.orchestrator.api.client.OrchestrationApiClientImpl +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.client.WebClient + + +@Configuration +class OrchestratorClientConfig { + + @Bean + fun orchestratorClientNoAuth(configProperties: OrchestratorClientConfigProperties): OrchestrationApiClient { + val url = configProperties.baseUrl + return OrchestrationApiClientImpl { webClientBuilder(url).build() } + } + + + private fun webClientBuilder(url: String) = + WebClient.builder() + .baseUrl(url) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + +} \ No newline at end of file diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/config/OrchestratorClientConfigProperties.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/config/OrchestratorClientConfigProperties.kt new file mode 100644 index 000000000..2450eb03c --- /dev/null +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/config/OrchestratorClientConfigProperties.kt @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.pool.config + +import org.springframework.boot.context.properties.ConfigurationProperties + + +@ConfigurationProperties(prefix = "bpdm.pool-orchestrator") +data class OrchestratorClientConfigProperties( + val baseUrl: String = "http://localhost:8085/" +) diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/exception/BpdmValidationException.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/exception/BpdmValidationException.kt new file mode 100644 index 000000000..a2075d62d --- /dev/null +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/exception/BpdmValidationException.kt @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.pool.exception + + +class BpdmValidationException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/BusinessPartnerBuildService.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/BusinessPartnerBuildService.kt index 10138dbef..6b294c3a6 100644 --- a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/BusinessPartnerBuildService.kt +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/BusinessPartnerBuildService.kt @@ -61,9 +61,13 @@ class BusinessPartnerBuildService( fun createLegalEntities(requests: Collection): LegalEntityPartnerCreateResponseWrapper { logger.info { "Create ${requests.size} new legal entities" } - val errorsByRequest = requestValidationService.validateLegalEntityCreates(requests) - val errors = errorsByRequest.flatMap { it.value } - val validRequests = requests.filterNot { errorsByRequest.containsKey(it) } + + val errorsByRequest = requestValidationService.validateLegalEntityCreates(requests.associateWith { it.legalEntity }) { theRequest -> theRequest.index } + val errorsByRequestAddress = + requestValidationService.validateLegalEntityCreatesAddresses(requests.associateWith { it.legalAddress }) { theRequest -> theRequest.index } + + val errors = errorsByRequest.flatMap { it.value } + errorsByRequestAddress.flatMap { it.value } + val validRequests = requests.filterNot { errorsByRequest.containsKey(it) || errorsByRequestAddress.containsKey(it) } val legalEntityMetadataMap = metadataService.getMetadata(requests.map { it.legalEntity }).toMapping() val addressMetadataMap = metadataService.getMetadata(requests.map { it.legalAddress }).toMapping() @@ -347,7 +351,7 @@ class BusinessPartnerBuildService( legalEntity = partner, ) - site.states.addAll(request.states.map { toEntity(it, site) }) + site.states.addAll(request.states.map { toSiteState(it, site) }) return site } @@ -373,16 +377,16 @@ class BusinessPartnerBuildService( partner.states.clear() partner.classifications.clear() - partner.states.addAll(request.states.map { toEntity(it, partner) }) - partner.identifiers.addAll(request.identifiers.map { toEntity(it, metadataMap, partner) }) - partner.classifications.addAll(request.classifications.map { toEntity(it, partner) }.toSet()) + partner.states.addAll(request.states.map { toLegalEntityState(it, partner) }) + partner.identifiers.addAll(request.identifiers.map { toLegalEntityIdentifier(it, metadataMap.idTypes, partner) }) + partner.classifications.addAll(request.classifications.map { toLegalEntityClassification(it, partner) }.toSet()) } private fun updateSite(site: Site, request: SiteDto) { site.name = request.name site.states.clear() - site.states.addAll(request.states.map { toEntity(it, site) }) + site.states.addAll(request.states.map { toSiteState(it, site) }) } private fun createLogisticAddress( @@ -410,8 +414,8 @@ class BusinessPartnerBuildService( bpn = bpn, legalEntity = null, site = null, - physicalPostalAddress = createPhysicalAddress(dto.physicalPostalAddress, metadataMap), - alternativePostalAddress = dto.alternativePostalAddress?.let { createAlternativeAddress(it, metadataMap) }, + physicalPostalAddress = createPhysicalAddress(dto.physicalPostalAddress, metadataMap.regions), + alternativePostalAddress = dto.alternativePostalAddress?.let { createAlternativeAddress(it, metadataMap.regions) }, name = dto.name ) @@ -422,24 +426,24 @@ class BusinessPartnerBuildService( private fun updateLogisticAddress(address: LogisticAddress, dto: LogisticAddressDto, metadataMap: AddressMetadataMapping) { address.name = dto.name - address.physicalPostalAddress = createPhysicalAddress(dto.physicalPostalAddress, metadataMap) - address.alternativePostalAddress = dto.alternativePostalAddress?.let { createAlternativeAddress(it, metadataMap) } + address.physicalPostalAddress = createPhysicalAddress(dto.physicalPostalAddress, metadataMap.regions) + address.alternativePostalAddress = dto.alternativePostalAddress?.let { createAlternativeAddress(it, metadataMap.regions) } address.identifiers.apply { clear() - addAll(dto.identifiers.map { toEntity(it, metadataMap, address) }) + addAll(dto.identifiers.map { toAddressIdentifier(it, metadataMap.idTypes, address) }) } address.states.apply { clear() - addAll(dto.states.map { toEntity(it, address) }) + addAll(dto.states.map { toAddressState(it, address) }) } } - private fun createPhysicalAddress(physicalAddress: PhysicalPostalAddressDto, metadataMap: AddressMetadataMapping): PhysicalPostalAddress { + private fun createPhysicalAddress(physicalAddress: PhysicalPostalAddressDto, regions: Map): PhysicalPostalAddress { return PhysicalPostalAddress( geographicCoordinates = physicalAddress.geographicCoordinates?.let { toEntity(it) }, country = physicalAddress.country, - administrativeAreaLevel1 = metadataMap.regions[physicalAddress.administrativeAreaLevel1], + administrativeAreaLevel1 = regions[physicalAddress.administrativeAreaLevel1], administrativeAreaLevel2 = physicalAddress.administrativeAreaLevel2, administrativeAreaLevel3 = physicalAddress.administrativeAreaLevel3, postCode = physicalAddress.postalCode, @@ -454,11 +458,11 @@ class BusinessPartnerBuildService( ) } - private fun createAlternativeAddress(alternativeAddress: AlternativePostalAddressDto, metadataMap: AddressMetadataMapping): AlternativePostalAddress { + private fun createAlternativeAddress(alternativeAddress: AlternativePostalAddressDto, regions: Map): AlternativePostalAddress { return AlternativePostalAddress( geographicCoordinates = alternativeAddress.geographicCoordinates?.let { toEntity(it) }, country = alternativeAddress.country, - administrativeAreaLevel1 = metadataMap.regions[alternativeAddress.administrativeAreaLevel1], + administrativeAreaLevel1 = regions[alternativeAddress.administrativeAreaLevel1], postCode = alternativeAddress.postalCode, city = alternativeAddress.city, deliveryServiceType = alternativeAddress.deliveryServiceType, @@ -467,86 +471,10 @@ class BusinessPartnerBuildService( ) } - private fun createStreet(dto: StreetDto): Street { - return Street( - name = dto.name, - houseNumber = dto.houseNumber, - milestone = dto.milestone, - direction = dto.direction - ) - } - - private fun toEntity(dto: LegalEntityStateDto, legalEntity: LegalEntity): LegalEntityState { - return LegalEntityState( - description = dto.description, - validFrom = dto.validFrom, - validTo = dto.validTo, - type = dto.type, - legalEntity = legalEntity - ) - } - - private fun toEntity(dto: SiteStateDto, site: Site): SiteState { - return SiteState( - description = dto.description, - validFrom = dto.validFrom, - validTo = dto.validTo, - type = dto.type, - site = site - ) - } - - private fun toEntity(dto: AddressStateDto, address: LogisticAddress): AddressState { - return AddressState( - description = dto.description, - validFrom = dto.validFrom, - validTo = dto.validTo, - type = dto.type, - address = address - ) - } - - private fun toEntity(dto: ClassificationDto, partner: LegalEntity): LegalEntityClassification { - return LegalEntityClassification( - value = dto.value, - code = dto.code, - type = dto.type, - legalEntity = partner - ) - } - - private fun toEntity( - dto: LegalEntityIdentifierDto, - metadataMap: LegalEntityMetadataMapping, - partner: LegalEntity - ): LegalEntityIdentifier { - return LegalEntityIdentifier( - value = dto.value, - type = metadataMap.idTypes[dto.type]!!, - issuingBody = dto.issuingBody, - legalEntity = partner - ) - } - - private fun toEntity( - dto: AddressIdentifierDto, - metadataMap: AddressMetadataMapping, - partner: LogisticAddress - ): AddressIdentifier { - return AddressIdentifier( - value = dto.value, - type = metadataMap.idTypes[dto.type]!!, - address = partner - ) - } - - private fun toEntity(dto: GeoCoordinateDto): GeographicCoordinate { + fun toEntity(dto: GeoCoordinateDto): GeographicCoordinate { return GeographicCoordinate(dto.latitude, dto.longitude, dto.altitude) } - private fun createCurrentnessTimestamp(): Instant { - return Instant.now().truncatedTo(ChronoUnit.MICROS) - } private fun LegalEntityMetadataDto.toMapping() = LegalEntityMetadataMapping( @@ -561,13 +489,95 @@ class BusinessPartnerBuildService( ) - private data class LegalEntityMetadataMapping( + data class LegalEntityMetadataMapping( val idTypes: Map, val legalForms: Map ) - private data class AddressMetadataMapping( + data class AddressMetadataMapping( val idTypes: Map, val regions: Map ) + + companion object { + + fun createCurrentnessTimestamp(): Instant { + return Instant.now().truncatedTo(ChronoUnit.MICROS) + } + + fun createStreet(dto: IBaseStreetDto): Street { + return Street( + name = dto.name, + houseNumber = dto.houseNumber, + milestone = dto.milestone, + direction = dto.direction + ) + } + + fun toLegalEntityState(dto: IBaseLegalEntityStateDto, legalEntity: LegalEntity): LegalEntityState { + return LegalEntityState( + description = dto.description, + validFrom = dto.validFrom, + validTo = dto.validTo, + type = dto.type, + legalEntity = legalEntity + ) + } + + fun toSiteState(dto: IBaseSiteStateDto, site: Site): SiteState { + return SiteState( + description = dto.description, + validFrom = dto.validFrom, + validTo = dto.validTo, + type = dto.type, + site = site + ) + } + + fun toAddressState(dto: IBaseAddressStateDto, address: LogisticAddress): AddressState { + return AddressState( + description = dto.description, + validFrom = dto.validFrom, + validTo = dto.validTo, + type = dto.type, + address = address + ) + } + + fun toLegalEntityClassification(dto: IBaseClassificationDto, partner: LegalEntity): LegalEntityClassification { + return LegalEntityClassification( + value = dto.value, + code = dto.code, + type = dto.type, + legalEntity = partner + ) + } + + fun toLegalEntityIdentifier( + dto: IBaseLegalEntityIdentifierDto, + idTypes: Map, + partner: LegalEntity + ): LegalEntityIdentifier { + return LegalEntityIdentifier( + value = dto.value, + type = idTypes[dto.type]!!, + issuingBody = dto.issuingBody, + legalEntity = partner + ) + } + + fun toAddressIdentifier( + dto: IBaseAddressIdentifierDto, + idTypes: Map, + partner: LogisticAddress + ): AddressIdentifier { + return AddressIdentifier( + value = dto.value, + type = idTypes[dto.type]!!, + address = partner + ) + } + + } + } \ No newline at end of file diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/MetadataService.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/MetadataService.kt index bbd625808..79ef0c647 100644 --- a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/MetadataService.kt +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/MetadataService.kt @@ -142,7 +142,7 @@ class MetadataService( return resultList } - fun getMetadata(requests: Collection): LegalEntityMetadataDto { + fun getMetadata(requests: Collection): LegalEntityMetadataDto { val idTypeKeys = requests.flatMap { it.identifiers }.map { it.type }.toSet() val idTypes = identifierTypeRepository.findByBusinessPartnerTypeAndTechnicalKeyIn(IdentifierBusinessPartnerType.LEGAL_ENTITY, idTypeKeys) @@ -152,18 +152,35 @@ class MetadataService( return LegalEntityMetadataDto(idTypes, legalForms) } - fun getMetadata(requests: Collection): AddressMetadataDto { + fun getMetadata(requests: Collection): AddressMetadataDto { val idTypeKeys = requests.flatMap { it.identifiers }.map { it.type }.toSet() val idTypes = identifierTypeRepository.findByBusinessPartnerTypeAndTechnicalKeyIn(IdentifierBusinessPartnerType.ADDRESS, idTypeKeys) - val regionKeys = requests.mapNotNull { it.physicalPostalAddress.administrativeAreaLevel1 } - .plus(requests.mapNotNull { it.alternativePostalAddress?.administrativeAreaLevel1 }) + val regionKeys = requests.mapNotNull { administrativeAreaLevel1ToString(it.physicalPostalAddress?.administrativeAreaLevel1) } + .plus(requests.mapNotNull { administrativeAreaLevel1ToString(it.alternativePostalAddress?.administrativeAreaLevel1) }) .toSet() val regions = regionRepository.findByRegionCodeIn(regionKeys) return AddressMetadataDto(idTypes, regions) } + private fun administrativeAreaLevel1ToString(administrativeAreaLevel1: Any?): String? { + + return when (administrativeAreaLevel1) { + is RegionDto -> { + administrativeAreaLevel1.regionCode + } + + is String -> { + administrativeAreaLevel1 + } + + else -> { + null; + } + } + } + /** * If no country rule exists use default rules */ diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/RequestValidationService.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/RequestValidationService.kt index 47ab80c00..1bc4cbc3e 100644 --- a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/RequestValidationService.kt +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/RequestValidationService.kt @@ -19,9 +19,7 @@ package org.eclipse.tractusx.bpdm.pool.service -import org.eclipse.tractusx.bpdm.common.dto.BusinessPartnerType -import org.eclipse.tractusx.bpdm.common.dto.LegalEntityDto -import org.eclipse.tractusx.bpdm.common.dto.LogisticAddressDto +import org.eclipse.tractusx.bpdm.common.dto.* import org.eclipse.tractusx.bpdm.common.util.findDuplicates import org.eclipse.tractusx.bpdm.pool.api.model.request.* import org.eclipse.tractusx.bpdm.pool.api.model.response.* @@ -40,56 +38,76 @@ class RequestValidationService( private val addressRepository: LogisticAddressRepository, private val metadataService: MetadataService ) { - fun validateLegalEntityCreates( - requests: Collection - ): Map>> { - val legalEntityRequests = requests.map { it.legalEntity } - val legalAddressRequests = requests.map { it.legalAddress } + fun validateLegalEntityCreates( + legalEntitiesByRequest: Map, entityKeyFunc : (IRequest) -> String? + ): Map>> { + val legalEntityRequests = legalEntitiesByRequest.values val legalEntityMetadata = metadataService.getMetadata(legalEntityRequests).toKeys() - val addressMetadata = metadataService.getMetadata(legalAddressRequests).toKeys() - val legalEntityDuplicateIdentifierCandidates = getLegalEntityDuplicateIdentifierCandidates(legalEntityRequests) - val addressDuplicateIdentifierCandidates = getAddressDuplicateIdentifierCandidates(legalAddressRequests) - return requests.flatMap { request -> - val legalEntity = request.legalEntity - val legalAddress = request.legalAddress + return legalEntitiesByRequest.map { + val legalEntity = it.value + val request = it.key val validationErrors = - validateLegalFormExists(legalEntity, legalEntityMetadata.legalForms, LegalEntityCreateError.LegalFormNotFound, request.index) + - validateIdentifierTypesExists( - legalEntity, - legalEntityMetadata.idTypes, - LegalEntityCreateError.LegalEntityIdentifierNotFound, - request.index - ) + - validateRegionExists(legalAddress, addressMetadata.regions, LegalEntityCreateError.LegalAddressRegionNotFound, request.index) + + validateLegalFormExists(legalEntity + , legalEntityMetadata.legalForms + , LegalEntityCreateError.LegalFormNotFound + , entityKeyFunc(request) + ) + + validateIdentifierTypesExists( + legalEntity, + legalEntityMetadata.idTypes, + LegalEntityCreateError.LegalEntityIdentifierNotFound, + entityKeyFunc(request) + ) + + validateLegalEntityIdentifiersDuplicated( + legalEntity = legalEntity, + existingIdentifiers = legalEntityDuplicateIdentifierCandidates, + bpn = null, + error = LegalEntityCreateError.LegalEntityDuplicateIdentifier, + entityKey = entityKeyFunc(request) + ) + request to validationErrors + }.toMap() + .filterValues { it.isNotEmpty() } + + } + + fun validateLegalEntityCreatesAddresses( + addressByRequest: Map, entityKeyFunc : (IRequest) -> String? + ): Map>> { + + val legalAddressRequests = addressByRequest.values + val addressDuplicateIdentifierCandidates = getAddressDuplicateIdentifierCandidates(legalAddressRequests) + val addressMetadata = metadataService.getMetadata(legalAddressRequests).toKeys() + + return addressByRequest.map{ + val legalAddress = it.value + val request = it.key + val validationErrors = + validateRegionExists(legalAddress, + addressMetadata.regions, + LegalEntityCreateError.LegalAddressRegionNotFound, + entityKeyFunc(request)) + validateIdentifierTypesExists( legalAddress, addressMetadata.idTypes, LegalEntityCreateError.LegalAddressIdentifierNotFound, - request.index - ) + - validateLegalEntityIdentifiersDuplicated( - legalEntity = legalEntity, - existingIdentifiers = legalEntityDuplicateIdentifierCandidates, - bpn = null, - error = LegalEntityCreateError.LegalEntityDuplicateIdentifier, - entityKey = request.index + entityKeyFunc(request) ) + validateAddressIdentifiersDuplicated( address = legalAddress, existingIdentifiers = addressDuplicateIdentifierCandidates, bpn = null, error = LegalEntityCreateError.LegalAddressDuplicateIdentifier, - entityKey = request.index + entityKey = entityKeyFunc(request) ) - - validationErrors.map { Pair(request, it) } - - }.groupBy({ it.first }, { it.second }) + request to validationErrors + }.toMap() + .filterValues { it.isNotEmpty() } } fun validateLegalEntityUpdates( @@ -278,7 +296,7 @@ class RequestValidationService( } - private fun validateIdentifierTypesExists(request: LegalEntityDto, existingTypes: Set, error: ERROR, entityKey: String?) + private fun validateIdentifierTypesExists(request: IBaseLegalEntityDto, existingTypes: Set, error: ERROR, entityKey: String?) : Collection> { val requestedTypes = request.identifiers.map { it.type } val missingTypes = requestedTypes - existingTypes @@ -292,7 +310,7 @@ class RequestValidationService( } } - private fun validateLegalFormExists(request: LegalEntityDto, existingLegalForms: Set, error: ERROR, entityKey: String?) + private fun validateLegalFormExists(request: IBaseLegalEntityDto, existingLegalForms: Set, error: ERROR, entityKey: String?) : Collection> { if (request.legalForm != null) { @@ -304,7 +322,7 @@ class RequestValidationService( return emptyList() } - private fun validateIdentifierTypesExists(request: LogisticAddressDto, existingTypes: Set, error: ERROR, entityKey: String?) + private fun validateIdentifierTypesExists(request: IBaseLogisticAddressDto, existingTypes: Set, error: ERROR, entityKey: String?) : Collection> { val requestedTypes = request.identifiers.map { it.type } val missingTypes = requestedTypes - existingTypes @@ -318,10 +336,10 @@ class RequestValidationService( } } - private fun validateRegionExists(request: LogisticAddressDto, existingRegions: Set, error: ERROR, entityKey: String?) + private fun validateRegionExists(request: IBaseLogisticAddressDto, existingRegions: Set, error: ERROR, entityKey: String?) : Collection> { val requestedTypes = listOfNotNull( - request.physicalPostalAddress.administrativeAreaLevel1, + request.physicalPostalAddress?.administrativeAreaLevel1, request.alternativePostalAddress?.administrativeAreaLevel1 ) @@ -352,7 +370,7 @@ class RequestValidationService( emptyList() } - private fun getLegalEntityDuplicateIdentifierCandidates(requests: Collection) + private fun getLegalEntityDuplicateIdentifierCandidates(requests: Collection) : Map { val identifiers = requests.flatMap { it.identifiers } val idValues = identifiers.map { it.value } @@ -366,7 +384,7 @@ class RequestValidationService( return duplicatesFromRequest.plus(duplicatesFromDb) } - private fun getAddressDuplicateIdentifierCandidates(requests: Collection) + private fun getAddressDuplicateIdentifierCandidates(requests: Collection) : Map { val identifiers = requests.flatMap { it.identifiers } val idValues = identifiers.map { it.value } @@ -381,7 +399,7 @@ class RequestValidationService( } private fun validateLegalEntityIdentifiersDuplicated( - legalEntity: LegalEntityDto, + legalEntity: IBaseLegalEntityDto, existingIdentifiers: Map, bpn: String?, error: ERROR, @@ -399,7 +417,7 @@ class RequestValidationService( } private fun validateAddressIdentifiersDuplicated( - address: LogisticAddressDto, + address: IBaseLogisticAddressDto, existingIdentifiers: Map, bpn: String?, error: ERROR, diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepBuildService.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepBuildService.kt new file mode 100644 index 000000000..9f4f7fc8d --- /dev/null +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepBuildService.kt @@ -0,0 +1,453 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.pool.service + +import jakarta.transaction.Transactional +import org.eclipse.tractusx.bpdm.common.dto.BusinessPartnerType +import org.eclipse.tractusx.bpdm.common.util.replace +import org.eclipse.tractusx.bpdm.pool.api.model.ChangelogType +import org.eclipse.tractusx.bpdm.pool.dto.AddressMetadataDto +import org.eclipse.tractusx.bpdm.pool.dto.ChangelogEntryCreateRequest +import org.eclipse.tractusx.bpdm.pool.dto.LegalEntityMetadataDto +import org.eclipse.tractusx.bpdm.pool.entity.* +import org.eclipse.tractusx.bpdm.pool.exception.BpdmValidationException +import org.eclipse.tractusx.bpdm.pool.repository.LegalEntityRepository +import org.eclipse.tractusx.bpdm.pool.repository.LogisticAddressRepository +import org.eclipse.tractusx.bpdm.pool.repository.SiteRepository +import org.eclipse.tractusx.orchestrator.api.model.* +import org.springframework.stereotype.Service +import java.time.Instant +import java.time.temporal.ChronoUnit + +@Service +class TaskStepBuildService( + private val metadataService: MetadataService, + private val bpnIssuingService: BpnIssuingService, + private val changelogService: PartnerChangelogService, + private val legalEntityRepository: LegalEntityRepository, + private val logisticAddressRepository: LogisticAddressRepository, + private val siteRepository: SiteRepository, +) { + + enum class CleaningError(val message: String) { + LEGAL_NAME_IS_NULL("Legal name is null"), + COUNTRY_CITY_IS_NULL("Country or city in physicalAddress is null"), + LEGAL_ENTITY_IS_NULL("Legal entity or BpnL Reference is null"), + LEGAL_ADDRESS_IS_NULL("Legal Address is null"), + LOGISTIC_ADDRESS_IS_NULL("Logistic Address or Physical Address is null"), + PHYSICAL_ADDRESS_IS_NULL("Physical Address is null"), + ALTERNATIVE_ADDRESS_DATA_IS_NULL("Country or city or deliveryServiceType or deliveryServiceNumber in alternativeAddress is null"), + MAINE_ADDRESS_IS_NULL("Main address is null"), + BPNS_IS_NULL("BpnS Reference is null"), + BPNA_IS_NULL("BpnA Reference is null"), + SITE_NAME_IS_NULL("Site name is null"), + INVALID_LOGISTIC_ADDRESS_BPN("Invalid Logistic Address BPN"), + INVALID_LEGAL_ENTITY_BPN("Invalid legal entity BPN"), + INVALID_SITE_BPN("Invalid site BPN") + + } + + @Transactional + fun upsertBusinessPartner(taskEntry: TaskStepReservationEntryDto): TaskStepResultEntryDto { + + // TODO associate generated BPN with BPN request identifier + val businessPartnerDto = taskEntry.businessPartner + var siteResult: SiteDto? = null + var addressResult: LogisticAddressDto? = null + + val legalEntity = upsertLegalEntity(businessPartnerDto.legalEntity) + var siteEntity: Site? = null + if (businessPartnerDto.site != null) { + siteEntity = upsertSite(businessPartnerDto.site, legalEntity) + siteResult = businessPartnerDto.site!!.copy( + bpnSReference = BpnReferenceDto(referenceValue = siteEntity.bpn, referenceType = BpnReferenceType.Bpn) + ) + } + if (businessPartnerDto.address != null) { + val addressEntity = upsertLogisticAddress(businessPartnerDto.address, legalEntity, siteEntity) + addressResult = businessPartnerDto.address!!.copy( + bpnAReference = BpnReferenceDto(referenceValue = addressEntity.bpn, referenceType = BpnReferenceType.Bpn) + ) + } + + return TaskStepResultEntryDto( + taskId = taskEntry.taskId, + businessPartner = BusinessPartnerFullDto( + generic = businessPartnerDto.generic, + legalEntity = businessPartnerDto.legalEntity!!.copy( + bpnLReference = BpnReferenceDto(referenceValue = legalEntity.bpn, referenceType = BpnReferenceType.Bpn) + ), + site = siteResult, + address = addressResult + ) + ) + } + + private fun upsertLogisticAddress( + addressDto: LogisticAddressDto?, + legalEntity: LegalEntity, + siteEntity: Site? + ): LogisticAddress { + + val bpnAReference = addressDto?.bpnAReference ?: throw BpdmValidationException(CleaningError.BPNA_IS_NULL.message) + + val isCreate = bpnAReference.referenceType == BpnReferenceType.BpnRequestIdentifier + val changelogType = + if (isCreate) ChangelogType.CREATE else ChangelogType.UPDATE + + val upsertAddress = if (isCreate) { + val bpnLA = bpnIssuingService.issueAddressBpns(1).single() + createLogisticAddressInternal(addressDto, bpnLA) + } else { + val addressMetadataMap = metadataService.getMetadata(listOf(addressDto)).toMapping() + + val updateAddress = logisticAddressRepository.findByBpn(bpnAReference.referenceValue) + if (updateAddress != null) { + updateLogisticAddress(updateAddress, addressDto, addressMetadataMap) + } else { + throw BpdmValidationException(CleaningError.INVALID_LOGISTIC_ADDRESS_BPN.message) + } + updateAddress + } + if (siteEntity != null) { + upsertAddress.site = siteEntity + } else { + upsertAddress.legalEntity = legalEntity + } + logisticAddressRepository.save(upsertAddress) + changelogService.createChangelogEntries( + listOf( + ChangelogEntryCreateRequest(upsertAddress.bpn, changelogType, BusinessPartnerType.ADDRESS) + ) + ) + + return upsertAddress + } + + private fun createLogisticAddress( + addressDto: LogisticAddressDto? + ): LogisticAddress { + + val bpnLA = bpnIssuingService.issueAddressBpns(1) + val newAddress = createLogisticAddressInternal(addressDto, bpnLA[0]) + changelogService.createChangelogEntries( + listOf( + ChangelogEntryCreateRequest(newAddress.bpn, ChangelogType.CREATE, BusinessPartnerType.ADDRESS) + ) + ) + + return newAddress + } + + private fun createLogisticAddressInternal( + dto: LogisticAddressDto?, + bpn: String + ): LogisticAddress { + + if (dto?.physicalPostalAddress == null) { + throw BpdmValidationException(CleaningError.LOGISTIC_ADDRESS_IS_NULL.message) + } + + val addressMetadataMap = metadataService.getMetadata(listOf(dto)).toMapping() + val address = LogisticAddress( + bpn = bpn, + legalEntity = null, + site = null, + physicalPostalAddress = createPhysicalAddress(dto.physicalPostalAddress!!, addressMetadataMap.regions), + alternativePostalAddress = dto.alternativePostalAddress?.let { createAlternativeAddress(it, addressMetadataMap.regions) }, + name = dto.name + ) + updateAddressIdentifiersAndStates(address, dto, addressMetadataMap.idTypes) + + return address + } + + private fun updateLogisticAddress(address: LogisticAddress, dto: LogisticAddressDto, metadataMap: BusinessPartnerBuildService.AddressMetadataMapping) { + + if (dto.physicalPostalAddress == null) { + + throw BpdmValidationException(CleaningError.PHYSICAL_ADDRESS_IS_NULL.message) + } + + address.name = dto.name + address.physicalPostalAddress = createPhysicalAddress(dto.physicalPostalAddress!!, metadataMap.regions) + address.alternativePostalAddress = dto.alternativePostalAddress?.let { createAlternativeAddress(it, metadataMap.regions) } + + updateAddressIdentifiersAndStates(address, dto, metadataMap.idTypes) + } + + private fun updateAddressIdentifiersAndStates( + address: LogisticAddress, + dto: LogisticAddressDto, + idTypes: Map + ) { + address.identifiers.apply { + clear() + addAll(dto.identifiers.map { + AddressIdentifier( + value = it.value, + type = idTypes[it.type]!!, + address = address + ) + }) + } + address.states.apply { + clear() + addAll(dto.states.map { + AddressState( + description = it.description, + validFrom = it.validFrom, + validTo = it.validTo, + type = it.type, + address = address + ) + }) + } + } + + private fun createAlternativeAddress(alternativeAddress: AlternativePostalAddressDto, regions: Map): AlternativePostalAddress { + + if (alternativeAddress.country == null || alternativeAddress.city == null || + alternativeAddress.deliveryServiceType == null || alternativeAddress.deliveryServiceNumber == null + ) { + + throw BpdmValidationException(CleaningError.ALTERNATIVE_ADDRESS_DATA_IS_NULL.message) + } + + return AlternativePostalAddress( + geographicCoordinates = alternativeAddress.geographicCoordinates?.let { GeographicCoordinate(it.latitude, it.longitude, it.altitude) }, + country = alternativeAddress.country!!, + administrativeAreaLevel1 = regions[alternativeAddress.administrativeAreaLevel1], + postCode = alternativeAddress.postalCode, + city = alternativeAddress.city!!, + deliveryServiceType = alternativeAddress.deliveryServiceType!!, + deliveryServiceNumber = alternativeAddress.deliveryServiceNumber!!, + deliveryServiceQualifier = alternativeAddress.deliveryServiceQualifier + ) + } + + private fun createPhysicalAddress(physicalAddress: PhysicalPostalAddressDto, regions: Map): PhysicalPostalAddress { + + if (physicalAddress.country == null || physicalAddress.city == null) { + throw BpdmValidationException(CleaningError.COUNTRY_CITY_IS_NULL.message) + } + + return PhysicalPostalAddress( + geographicCoordinates = physicalAddress.geographicCoordinates?.let { GeographicCoordinate(it.latitude, it.longitude, it.altitude) }, + country = physicalAddress.country!!, + administrativeAreaLevel1 = regions[physicalAddress.administrativeAreaLevel1], + administrativeAreaLevel2 = physicalAddress.administrativeAreaLevel2, + administrativeAreaLevel3 = physicalAddress.administrativeAreaLevel3, + postCode = physicalAddress.postalCode, + city = physicalAddress.city!!, + districtLevel1 = physicalAddress.district, + street = physicalAddress.street?.let { + Street( + name = it.name, + houseNumber = it.houseNumber, + milestone = it.milestone, + direction = it.direction + ) + }, + companyPostCode = physicalAddress.companyPostalCode, + industrialZone = physicalAddress.industrialZone, + building = physicalAddress.building, + floor = physicalAddress.floor, + door = physicalAddress.door + ) + } + + private fun createLegalEntity( + legalEntityDto: LegalEntityDto, + bpnL: String, + metadataMap: BusinessPartnerBuildService.LegalEntityMetadataMapping + ): LegalEntity { + + if (legalEntityDto.legalName == null) { + throw BpdmValidationException(CleaningError.LEGAL_NAME_IS_NULL.message) + } + + // it has to be validated that the legalForm exits + val legalForm = legalEntityDto.legalForm?.let { metadataMap.legalForms[it]!! } + val legalName = Name(value = legalEntityDto.legalName!!, shortName = legalEntityDto.legalShortName) + val newLegalEntity = LegalEntity( + bpn = bpnL, + legalName = legalName, + legalForm = legalForm, + currentness = Instant.now().truncatedTo(ChronoUnit.MICROS), + ) + updateLegalEntity(newLegalEntity, legalEntityDto, + legalEntityDto.identifiers.map { BusinessPartnerBuildService.toLegalEntityIdentifier(it, metadataMap.idTypes, newLegalEntity) }) + + return newLegalEntity + } + + private fun updateLegalEntity( + legalEntity: LegalEntity, + legalEntityDto: LegalEntityDto, + identifiers: List + ) { + val legalName = legalEntityDto.legalName ?: throw BpdmValidationException(CleaningError.LEGAL_NAME_IS_NULL.message) + + legalEntity.currentness = BusinessPartnerBuildService.createCurrentnessTimestamp() + + legalEntity.legalName = Name(value = legalName, shortName = legalEntityDto.legalShortName) + + legalEntity.identifiers.replace(identifiers) + + legalEntity.states.replace(legalEntityDto.states + .map { BusinessPartnerBuildService.toLegalEntityState(it, legalEntity) }) + + legalEntity.classifications.replace( + legalEntityDto.classifications + .map { BusinessPartnerBuildService.toLegalEntityClassification(it, legalEntity) }.toSet() + ) + } + + + fun upsertLegalEntity( + legalEntityDto: LegalEntityDto? + ): LegalEntity { + + val bpnLReference = legalEntityDto?.bpnLReference ?: throw BpdmValidationException(CleaningError.LEGAL_ENTITY_IS_NULL.message) + val legalAddress = legalEntityDto.legalAddress ?: throw BpdmValidationException(CleaningError.LEGAL_ADDRESS_IS_NULL.message) + + val isCreate = bpnLReference.referenceType == BpnReferenceType.BpnRequestIdentifier + val changelogType = + if (isCreate) ChangelogType.CREATE else ChangelogType.UPDATE + + val legalEntityMetadataMap = metadataService.getMetadata(listOf(legalEntityDto)).toMapping() + + val upsertLe = if (isCreate) { + val bpnL = bpnIssuingService.issueLegalEntityBpns(1).single() + val createdLe = createLegalEntity(legalEntityDto, bpnL, legalEntityMetadataMap) + val address = createLogisticAddress(legalAddress) + createdLe.legalAddress = address + address.legalEntity = createdLe + createdLe + } else { + + val updateLe = legalEntityRepository.findByBpn(bpnLReference.referenceValue) + if (updateLe != null) { + if (legalEntityDto.hasChanged == false) { + updateLegalEntity(updateLe, legalEntityDto, + legalEntityDto.identifiers.map { BusinessPartnerBuildService.toLegalEntityIdentifier(it, legalEntityMetadataMap.idTypes, updateLe) }) + val addressMetadataMap = metadataService.getMetadata(listOf(legalAddress)).toMapping() + updateLogisticAddress(updateLe.legalAddress, legalAddress, addressMetadataMap) + } + } else { + throw BpdmValidationException(CleaningError.INVALID_LEGAL_ENTITY_BPN.message) + } + updateLe + } + legalEntityRepository.save(upsertLe) + changelogService.createChangelogEntries( + listOf( + ChangelogEntryCreateRequest(upsertLe.bpn, changelogType, BusinessPartnerType.LEGAL_ENTITY) + ) + ) + + return upsertLe + } + + private fun upsertSite( + siteDto: SiteDto?, + legalEntity: LegalEntity + ): Site { + val bpnSReference = siteDto?.bpnSReference ?: throw BpdmValidationException(CleaningError.BPNS_IS_NULL.message) + val mainAddress = siteDto.mainAddress ?: throw BpdmValidationException(CleaningError.MAINE_ADDRESS_IS_NULL.message) + + val isCreate = bpnSReference.referenceType == BpnReferenceType.BpnRequestIdentifier + val changelogType = if (isCreate) ChangelogType.CREATE else ChangelogType.UPDATE + + val upsertSite = if (isCreate) { + val bpnS = bpnIssuingService.issueSiteBpns(1).single() + val createSite = createSite(siteDto, bpnS, legalEntity) + val address = createLogisticAddress(mainAddress) + createSite.mainAddress = address + address.site = createSite + createSite + } else { + + val updateSite = siteRepository.findByBpn(siteDto.bpnSReference?.referenceValue!!) + if (updateSite != null) { + if (siteDto.hasChanged == false) { + updateSite(updateSite, siteDto) + val addressMetadataMap = metadataService.getMetadata(listOf(mainAddress)).toMapping() + updateLogisticAddress(updateSite.mainAddress, mainAddress, addressMetadataMap) + } + } else { + throw BpdmValidationException(CleaningError.INVALID_SITE_BPN.message) + } + updateSite + } + siteRepository.save(upsertSite) + changelogService.createChangelogEntries( + listOf( + ChangelogEntryCreateRequest(upsertSite.bpn, changelogType, BusinessPartnerType.SITE) + ) + ) + + return upsertSite + } + + private fun createSite( + siteDto: SiteDto, + bpnS: String, + partner: LegalEntity + ): Site { + + val name = siteDto.name ?: throw BpdmValidationException(CleaningError.SITE_NAME_IS_NULL.message) + + val site = Site( + bpn = bpnS, + name = name, + legalEntity = partner, + ) + + site.states.addAll(siteDto.states + .map { BusinessPartnerBuildService.toSiteState(it, site) }) + + return site + } + + private fun updateSite(site: Site, siteDto: SiteDto) { + + val name = siteDto.name ?: throw BpdmValidationException(CleaningError.SITE_NAME_IS_NULL.message) + + site.name = name + + site.states.clear() + site.states.addAll(siteDto.states + .map { BusinessPartnerBuildService.toSiteState(it, site) }) + } + + private fun AddressMetadataDto.toMapping() = + BusinessPartnerBuildService.AddressMetadataMapping( + idTypes = idTypes.associateBy { it.technicalKey }, + regions = regions.associateBy { it.regionCode } + ) + + private fun LegalEntityMetadataDto.toMapping() = + BusinessPartnerBuildService.LegalEntityMetadataMapping( + idTypes = idTypes.associateBy { it.technicalKey }, + legalForms = legalForms.associateBy { it.technicalKey } + ) +} \ No newline at end of file diff --git a/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepFetchAndReserveService.kt b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepFetchAndReserveService.kt new file mode 100644 index 000000000..37373c65d --- /dev/null +++ b/bpdm-pool/src/main/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepFetchAndReserveService.kt @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.pool.service + +import mu.KotlinLogging +import org.eclipse.tractusx.bpdm.common.dto.IBaseLegalEntityDto +import org.eclipse.tractusx.bpdm.common.dto.IBaseLogisticAddressDto +import org.eclipse.tractusx.bpdm.pool.api.model.response.ErrorInfo +import org.eclipse.tractusx.bpdm.pool.api.model.response.LegalEntityCreateError +import org.eclipse.tractusx.bpdm.pool.exception.BpdmValidationException +import org.eclipse.tractusx.orchestrator.api.client.OrchestrationApiClient +import org.eclipse.tractusx.orchestrator.api.model.* +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service + +@Service +class TaskStepFetchAndReserveService( + private val orchestrationClient: OrchestrationApiClient, + private val taskStepBuildService: TaskStepBuildService, + private val requestValidationService: RequestValidationService, +) { + private val logger = KotlinLogging.logger { } + + @Scheduled(cron = "\${bpdm.pool-orchestrator.golden-record-scheduler-cron-expr:-}", zone = "UTC") + fun fetchAndReserve() { + try { + logger.info { "Starting polling for cleaning tasks from Orchestrator..." } + val reservationRequest = TaskStepReservationRequest(step = TaskStep.PoolSync, amount = 10) + val taskStepReservation = orchestrationClient.goldenRecordTasks.reserveTasksForStep(reservationRequest = reservationRequest) + + logger.info { "${taskStepReservation.reservedTasks.size} tasks found for cleaning. Proceeding with cleaning..." } + + if (taskStepReservation.reservedTasks.isNotEmpty()) { + val taskResults = upsertGoldenRecordIntoPool(taskStepReservation.reservedTasks) + orchestrationClient.goldenRecordTasks.resolveStepResults(TaskStepResultRequest(step = TaskStep.PoolSync, results = taskResults)) + } + logger.info { "Cleaning tasks processing completed for this iteration." } + } catch (ex: Throwable) { + logger.error(ex) { "Error while processing cleaning task" } + } + + } + + fun upsertGoldenRecordIntoPool(taskEntries: List): List { + + //TODO Implement validation for sites, ... + val validationStepErrorsByEntry = validateLegalEntityCreateTasks(taskEntries) + + return taskEntries.map { + + val existingEntryError = validationStepErrorsByEntry.get(it) + existingEntryError ?: businessPartnerTaskResult(it) + } + } + + fun businessPartnerTaskResult(taskStep: TaskStepReservationEntryDto): TaskStepResultEntryDto { + + return try { + taskStepBuildService.upsertBusinessPartner(taskStep) + } catch (ex: BpdmValidationException) { + TaskStepResultEntryDto( + taskId = taskStep.taskId, + errors = listOf( + TaskErrorDto( + type = TaskErrorType.Unspecified, + description = ex.message ?: "" + ) + ) + ) + } + } + + private fun validateLegalEntityCreateTasks( + tasks: List + ): Map { + + val isTaskCreateLegalEntity = + { task: TaskStepReservationEntryDto -> task.businessPartner.legalEntity?.bpnLReference?.referenceType == BpnReferenceType.BpnRequestIdentifier } + + val legalEntitiesToCreateSteps = tasks + .filter { isTaskCreateLegalEntity(it) } + + val legalEntityByTask = legalEntitiesToCreateSteps + .associateWith { it.businessPartner.legalEntity as IBaseLegalEntityDto } + .toMap() + val addressByTask = tasks + .filter { it.businessPartner.legalEntity?.legalAddress != null } + .associateWith { it.businessPartner.legalEntity?.legalAddress as IBaseLogisticAddressDto } + .toMap() + + val errorsByRequest = + requestValidationService.validateLegalEntityCreates(legalEntityByTask) { task -> task.businessPartner.legalEntity?.bpnLReference?.referenceValue } + val errorsByRequestAddress = + requestValidationService.validateLegalEntityCreatesAddresses(addressByTask) { task -> task.businessPartner.legalEntity?.bpnLReference?.referenceValue } + + val legalEntityCreateTaskResults = legalEntitiesToCreateSteps + .map { taskStep -> + taskStep to taskStepResultEntryDto(taskStep, errorsByRequest, errorsByRequestAddress) + }.toMap() + .filterValues { it != null } + return legalEntityCreateTaskResults + } + + private fun taskStepResultEntryDto( + taskStep: TaskStepReservationEntryDto, + errorsByRequest: Map>>, + errorsByRequestAddress: Map>> + ) = if (errorsByRequest.containsKey(taskStep) || errorsByRequestAddress.containsKey(taskStep)) { + taskResultsForErrors( + taskStep.taskId, + errorsByRequest.getOrDefault(taskStep, mutableListOf()) + errorsByRequestAddress.getOrDefault(taskStep, mutableListOf()) + ) + } else { + null + } + + private fun taskResultsForErrors(taskId: String, errors: Collection>): TaskStepResultEntryDto { + + return TaskStepResultEntryDto(taskId = taskId, errors = errors.map { TaskErrorDto(type = TaskErrorType.Unspecified, description = it.message) }) + } + +} \ No newline at end of file diff --git a/bpdm-pool/src/main/resources/application.properties b/bpdm-pool/src/main/resources/application.properties index 65def6447..e83b2f154 100644 --- a/bpdm-pool/src/main/resources/application.properties +++ b/bpdm-pool/src/main/resources/application.properties @@ -57,6 +57,10 @@ bpdm.opensearch.max-page=20 # Special value "-" disables scheduling. See javadoc of org.springframework.scheduling.support.CronExpression.parse for format. bpdm.opensearch.export-scheduler-cron-expr=- bpdm.opensearch.refresh-on-write=false +# Orchestrator Client +bpdm.pool-orchestrator.golden-record-scheduler-cron-expr=- +bpdm.pool-orchestrator.base-url=http://localhost:8085/ + #Datasource configuration bpdm.datasource.host=localhost spring.datasource.url=jdbc:postgresql://${bpdm.datasource.host}:5432/bpdm diff --git a/bpdm-pool/src/test/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepFetchAndReserveServiceTest.kt b/bpdm-pool/src/test/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepFetchAndReserveServiceTest.kt new file mode 100644 index 000000000..b0b8c134f --- /dev/null +++ b/bpdm-pool/src/test/kotlin/org/eclipse/tractusx/bpdm/pool/service/TaskStepFetchAndReserveServiceTest.kt @@ -0,0 +1,127 @@ +package org.eclipse.tractusx.bpdm.pool.service + +import com.neovisionaries.i18n.CountryCode +import org.assertj.core.api.Assertions.assertThat +import org.eclipse.tractusx.bpdm.pool.Application +import org.eclipse.tractusx.bpdm.pool.api.client.PoolClientImpl +import org.eclipse.tractusx.bpdm.pool.service.TaskStepBuildService.CleaningError +import org.eclipse.tractusx.bpdm.pool.util.PostgreSQLContextInitializer +import org.eclipse.tractusx.bpdm.pool.util.TestHelpers +import org.eclipse.tractusx.orchestrator.api.model.* +import org.eclipse.tractusx.orchestrator.api.model.BpnReferenceType.BpnRequestIdentifier +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = [Application::class] +) +@ActiveProfiles("test") +@ContextConfiguration(initializers = [PostgreSQLContextInitializer::class]) +class TaskStepFetchAndReserveServiceTest @Autowired constructor( + val cleaningStepService: TaskStepFetchAndReserveService, + val testHelpers: TestHelpers, + val poolClient: PoolClientImpl +) { + + + @BeforeEach + fun beforeEach() { + testHelpers.truncateDbTables() + testHelpers.createTestMetadata() + } + + @Test + fun `upsert Golden Record into pool with empty legal entity`() { + + val fullBpWithLegalEntity = minFullBusinessPartner().copy( + legalEntity = emptyLegalEntity() + ) + + val result = cleanStep(taskId = "TASK_1", businessPartner = fullBpWithLegalEntity) + assertTaskError(result[0], "TASK_1", CleaningError.LEGAL_ENTITY_IS_NULL) + } + + @Test + fun `upsert Golden Record into pool with legal entity without legal name to create`() { + + val fullBpWithLegalEntity = minFullBusinessPartner().copy( + legalEntity = emptyLegalEntity().copy( + bpnLReference = BpnReferenceDto(referenceValue = "123", referenceType = BpnRequestIdentifier), + legalAddress = LogisticAddressDto() + ) + ) + + val result = cleanStep(taskId = "TASK_1", businessPartner = fullBpWithLegalEntity) + assertTaskError(result[0], "TASK_1", CleaningError.LEGAL_NAME_IS_NULL) + } + + @Test + fun `upsert Golden Record into pool with legal entity to create`() { + + val fullBpWithLegalEntity = minFullBusinessPartner().copy( + legalEntity = minValidLegalEntity( + BpnReferenceDto(referenceValue = "123", referenceType = BpnRequestIdentifier), + BpnReferenceDto(referenceValue = "222", referenceType = BpnRequestIdentifier) + ) + ) + val resultSteps = cleanStep(taskId = "TASK_1", businessPartner = fullBpWithLegalEntity) + assertThat(resultSteps[0].taskId).isEqualTo("TASK_1") + assertThat(resultSteps[0].errors.size).isEqualTo(0) + + val createdLegalEntity = poolClient.legalEntities.getLegalEntity(resultSteps[0].businessPartner?.legalEntity?.bpnLReference?.referenceValue!!) + assertThat(createdLegalEntity.legalAddress.bpnLegalEntity).isNotNull() + } + + fun cleanStep(taskId: String, businessPartner: BusinessPartnerFullDto): List { + + val steps = singleTaskStep(taskId = "TASK_1", businessPartner = businessPartner) + return cleaningStepService.upsertGoldenRecordIntoPool(steps) + } + + fun singleTaskStep(taskId: String, businessPartner: BusinessPartnerFullDto): List { + + return listOf( + TaskStepReservationEntryDto( + taskId = taskId, + businessPartner = businessPartner + ) + ) + } + + fun minFullBusinessPartner(): BusinessPartnerFullDto { + + return BusinessPartnerFullDto(generic = BusinessPartnerGenericDto()) + } + + fun emptyLegalEntity(): LegalEntityDto { + + return LegalEntityDto() + } + + fun minValidLegalEntity(bpnLReference: BpnReferenceDto, bpnAReference: BpnReferenceDto): LegalEntityDto { + + return LegalEntityDto( + bpnLReference = bpnLReference, + legalName = "legalName_" + bpnLReference.referenceValue, + legalAddress = LogisticAddressDto( + bpnAReference = bpnAReference, + physicalPostalAddress = PhysicalPostalAddressDto( + country = CountryCode.DE, + city = "City" + bpnLReference.referenceValue + ) + ) + ) + } + + fun assertTaskError(step: TaskStepResultEntryDto, taskId: String, error: CleaningError) { + + assertThat(step.taskId).isEqualTo(taskId) + assertThat(step.errors.size).isEqualTo(1) + assertThat(step.errors[0].description).isEqualTo(error.message) + + } +} \ No newline at end of file