From a1ab2e3b5a16ee2734fcaa043a6af2cf94df994a Mon Sep 17 00:00:00 2001 From: Nico Koprowski Date: Mon, 2 Oct 2023 17:52:52 +0200 Subject: [PATCH] refactor(gate): business partner service - employ early mapping from DTO to entity in order to make the service logic reusable for different DTOs (L/S/A DTOs for example) - merge more methods containing the same business logic --- .../tractusx/bpdm/common/dto/AddressType.kt | 10 +- .../bpdm/common/util/CollectionUtils.kt | 31 +++ .../bpdm/gate/entity/PhysicalPostalAddress.kt | 28 +-- .../gate/entity/generic/BusinessPartner.kt | 26 +-- .../gate/entity/generic/Classification.kt | 2 +- .../bpdm/gate/entity/generic/PostalAddress.kt | 6 +- .../bpdm/gate/entity/generic/State.kt | 2 +- .../exception/BpdmMissingStageException.kt | 30 +++ .../gate/service/BusinessPartnerMappings.kt | 62 +---- .../gate/service/BusinessPartnerService.kt | 221 +++++++++++------- 10 files changed, 249 insertions(+), 169 deletions(-) create mode 100644 bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/BpdmMissingStageException.kt diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressType.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressType.kt index aa507d0d7..6d6868e51 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressType.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/dto/AddressType.kt @@ -19,10 +19,10 @@ package org.eclipse.tractusx.bpdm.common.dto -enum class AddressType { +enum class AddressType(val businessPartnerTypes: Collection) { - LegalAndSiteMainAddress, - LegalAddress, - SiteMainAddress, - AdditionalAddress + LegalAndSiteMainAddress(listOf(BusinessPartnerType.LEGAL_ENTITY, BusinessPartnerType.SITE)), + LegalAddress(listOf(BusinessPartnerType.LEGAL_ENTITY)), + SiteMainAddress(listOf(BusinessPartnerType.SITE)), + AdditionalAddress(listOf(BusinessPartnerType.ADDRESS)) } \ No newline at end of file diff --git a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/util/CollectionUtils.kt b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/util/CollectionUtils.kt index 6aa57562e..10f3da37a 100644 --- a/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/util/CollectionUtils.kt +++ b/bpdm-common/src/main/kotlin/org/eclipse/tractusx/bpdm/common/util/CollectionUtils.kt @@ -24,6 +24,37 @@ fun MutableCollection.replace(elements: Collection) { addAll(elements) } + +/** + * Copy overlapping elements by index from [elements] to [this] collection by applying the [copyFunction]. + * Remove remaining elements in the original collection and add additional [elements] from given collection + */ +fun MutableCollection.copyAndSync(elements: Collection, copyFunction: (T, T) -> T) { + // copy the overlap of the two collections + zip(elements).forEach { (fromState, toState) -> copyFunction(fromState, toState) } + + val sizeDifference = size - elements.size + if (sizeDifference > 0) { + //Remove the remaining elements from the original collection + drop(elements.size).forEach { remove(it) } + } else { + //Add the additional elements to the original collection + addAll(elements.drop(size)) + } +} + +/** + * Partitions the elements in the collection according to the [predicate] function and subsequently applies [transform] to the partitioned elements + */ +fun Collection.partitionAndMap( + predicate: (T) -> Boolean, + transform: (T) -> R +): Pair, List> { + + val (first, second) = partition { predicate(it) } + return Pair(first.map { transform(it) }, second.map { transform(it) }) +} + fun Collection.findDuplicates(): Set = this.groupBy { it } .filter { it.value.size > 1 } diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/PhysicalPostalAddress.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/PhysicalPostalAddress.kt index 12ae6a9b5..f61af9749 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/PhysicalPostalAddress.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/PhysicalPostalAddress.kt @@ -29,47 +29,47 @@ class PhysicalPostalAddress( @AttributeOverride(name = "latitude", column = Column(name = "phy_latitude")) @AttributeOverride(name = "longitude", column = Column(name = "phy_longitude")) @AttributeOverride(name = "altitude", column = Column(name = "phy_altitude")) - val geographicCoordinates: GeographicCoordinate?, + var geographicCoordinates: GeographicCoordinate?, @Column(name = "phy_country") @Enumerated(EnumType.STRING) - val country: CountryCode?, + var country: CountryCode?, /** * Region within the country */ @Column(name = "phy_admin_area_l1_region") - val administrativeAreaLevel1: String?, + var administrativeAreaLevel1: String?, /** * Further possibility to describe the region/address(e.g. County) */ @Column(name = "phy_admin_area_l2") - val administrativeAreaLevel2: String?, + var administrativeAreaLevel2: String?, /** * Further possibility to describe the region/address(e.g. Township) */ @Column(name = "phy_admin_area_l3") - val administrativeAreaLevel3: String?, + var administrativeAreaLevel3: String?, /** * A postal code, also known as postcode, PIN or ZIP Code */ @Column(name = "phy_postcode") - val postalCode: String?, + var postalCode: String?, /** * The city of the address (Synonym: Town, village, municipality) */ @Column(name = "phy_city") - val city: String?, + var city: String?, /** * Divides the city in several smaller areas */ @Column(name = "phy_district_l1") - val district: String?, + var district: String?, @Embedded @AttributeOverride(name = "name", column = Column(name = "phy_street_name")) @@ -80,7 +80,7 @@ class PhysicalPostalAddress( @AttributeOverride(name = "additionalNamePrefix", column = Column(name = "phy_additional_name_prefix")) @AttributeOverride(name = "nameSuffix", column = Column(name = "phy_name_suffix")) @AttributeOverride(name = "additionalNameSuffix", column = Column(name = "phy_additional_name_suffix")) - val street: Street?, + var street: Street?, // specific for PhysicalPostalAddress @@ -88,29 +88,29 @@ class PhysicalPostalAddress( * A separate postal code for a company, also known as postcode, PIN or ZIP Code */ @Column(name = "phy_company_postcode") - val companyPostalCode: String?, + var companyPostalCode: String?, /** * The practice of designating an area for industrial development */ @Column(name = "phy_industrial_zone") - val industrialZone: String?, + var industrialZone: String?, /** * Describes a specific building within the address */ @Column(name = "phy_building") - val building: String?, + var building: String?, /** * Describes the floor/level the delivery shall take place */ @Column(name = "phy_floor") - val floor: String?, + var floor: String?, /** * Describes the door/room/suite on the respective floor the delivery shall take place */ @Column(name = "phy_door") - val door: String? + var door: String? ) diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/BusinessPartner.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/BusinessPartner.kt index 2853d3667..1dc3bf63b 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/BusinessPartner.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/BusinessPartner.kt @@ -36,43 +36,43 @@ class BusinessPartner( @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "business_partners_name_parts", joinColumns = [JoinColumn(name = "business_partner_id")]) @OrderColumn(name = "name_parts_order") - var nameParts: MutableList = mutableListOf(), + val nameParts: MutableList = mutableListOf(), @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "business_partners_roles", joinColumns = [JoinColumn(name = "business_partner_id")]) @Enumerated(EnumType.STRING) @Column(name = "role_name") - var roles: SortedSet = sortedSetOf(), + val roles: SortedSet = sortedSetOf(), @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "business_partners_identifiers", joinColumns = [JoinColumn(name = "business_partner_id")]) - var identifiers: SortedSet = sortedSetOf(), + val identifiers: SortedSet = sortedSetOf(), @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "business_partners_states", joinColumns = [JoinColumn(name = "business_partner_id")]) - var states: SortedSet = sortedSetOf(), + val states: SortedSet = sortedSetOf(), @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "business_partners_classifications", joinColumns = [JoinColumn(name = "business_partner_id")]) - var classifications: SortedSet = sortedSetOf(), + val classifications: SortedSet = sortedSetOf(), @Column(name = "short_name") - var shortName: String?, + var shortName: String? = null, @Column(name = "legal_form") - var legalForm: String?, + var legalForm: String? = null, @Column(name = "is_owner") - var isOwner: Boolean, + var isOwner: Boolean = false, @Column(name = "bpnl") - var bpnL: String?, + var bpnL: String? = null, @Column(name = "bpns") - var bpnS: String?, + var bpnS: String? = null, @Column(name = "bpna") - var bpnA: String?, + var bpnA: String? = null, @OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true) @JoinColumn(name = "postal_address_id", unique = true) @@ -83,10 +83,10 @@ class BusinessPartner( var stage: StageType, @Column(name = "parent_id") - var parentId: String?, + var parentId: String? = null, @Column(name = "parent_type") @Enumerated(EnumType.STRING) - var parentType: BusinessPartnerType? + var parentType: BusinessPartnerType? = null ) : BaseEntity() diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/Classification.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/Classification.kt index 12fda1b07..b13344343 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/Classification.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/Classification.kt @@ -31,7 +31,7 @@ data class Classification( @Column(name = "type") @Enumerated(EnumType.STRING) - val type: ClassificationType, + var type: ClassificationType, @Column(name = "code") var code: String?, diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/PostalAddress.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/PostalAddress.kt index 891d1982d..28342724a 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/PostalAddress.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/PostalAddress.kt @@ -31,12 +31,12 @@ class PostalAddress( @Column(name = "address_type") @Enumerated(EnumType.STRING) - var addressType: AddressType?, + var addressType: AddressType? = null, @Embedded - var physicalPostalAddress: PhysicalPostalAddress?, + var physicalPostalAddress: PhysicalPostalAddress? = null, @Embedded - var alternativePostalAddress: AlternativePostalAddress? + var alternativePostalAddress: AlternativePostalAddress? = null ) : BaseEntity() diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/State.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/State.kt index 10c0230fd..1e195f36a 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/State.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/State.kt @@ -37,7 +37,7 @@ data class State( @Column(name = "type", nullable = false) @Enumerated(EnumType.STRING) - val type: BusinessStateType, + var type: BusinessStateType, @Column(name = "description") var description: String? diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/BpdmMissingStageException.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/BpdmMissingStageException.kt new file mode 100644 index 000000000..81d95990c --- /dev/null +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/BpdmMissingStageException.kt @@ -0,0 +1,30 @@ +/******************************************************************************* + * 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.gate.exception + +import org.eclipse.tractusx.bpdm.common.model.StageType +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + +@ResponseStatus(HttpStatus.BAD_REQUEST) +class BpdmMissingStageException( + externalIds: Collection, + stageType: StageType +) : RuntimeException("Business Partner Stage $stageType does not exist for business partner with external identifiers: ${externalIds.joinToString()}.") \ No newline at end of file diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/BusinessPartnerMappings.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/BusinessPartnerMappings.kt index 0a82aa2aa..3f0eb5514 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/BusinessPartnerMappings.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/BusinessPartnerMappings.kt @@ -19,10 +19,12 @@ package org.eclipse.tractusx.bpdm.gate.service -import org.eclipse.tractusx.bpdm.common.dto.* +import org.eclipse.tractusx.bpdm.common.dto.BusinessPartnerIdentifierDto +import org.eclipse.tractusx.bpdm.common.dto.BusinessPartnerStateDto +import org.eclipse.tractusx.bpdm.common.dto.ClassificationDto +import org.eclipse.tractusx.bpdm.common.dto.GeoCoordinateDto import org.eclipse.tractusx.bpdm.common.exception.BpdmNullMappingException import org.eclipse.tractusx.bpdm.common.model.StageType -import org.eclipse.tractusx.bpdm.common.util.replace import org.eclipse.tractusx.bpdm.gate.api.model.AlternativePostalAddressGateDto import org.eclipse.tractusx.bpdm.gate.api.model.BusinessPartnerPostalAddressDto import org.eclipse.tractusx.bpdm.gate.api.model.PhysicalPostalAddressGateDto @@ -83,9 +85,9 @@ class BusinessPartnerMappings { ) } - fun toBusinessPartner(dto: BusinessPartnerInputRequest, stage: StageType, parentId: String?, parentType: BusinessPartnerType?): BusinessPartner { + fun toBusinessPartnerInput(dto: BusinessPartnerInputRequest): BusinessPartner { return BusinessPartner( - stage = stage, + stage = StageType.Input, externalId = dto.externalId, nameParts = dto.nameParts.toMutableList(), roles = dto.roles.toSortedSet(), @@ -99,32 +101,15 @@ class BusinessPartnerMappings { bpnS = dto.bpnS, bpnA = dto.bpnA, postalAddress = toPostalAddress(dto.postalAddress), - parentId = parentId, - parentType = parentType, + parentId = null, + parentType = null, ) } - fun updateBusinessPartner(entity: BusinessPartner, dto: BusinessPartnerInputRequest, parentId: String?, parentType: BusinessPartnerType?) { - entity.nameParts.replace(dto.nameParts) - entity.roles.replace(dto.roles) - entity.identifiers.replace(dto.identifiers.map(::toIdentifier)) - entity.states.replace(dto.states.map(::toState)) - entity.classifications.replace(dto.classifications.map(::toClassification)) - entity.shortName = dto.shortName - entity.legalForm = dto.legalForm - entity.isOwner = dto.isOwner - entity.bpnL = dto.bpnL - entity.bpnS = dto.bpnS - entity.bpnA = dto.bpnA - entity.parentId = parentId - entity.parentType = parentType - updatePostalAddress(entity.postalAddress, dto.postalAddress) - } - //Output - fun toBusinessPartnerOutput(dto: BusinessPartnerOutputRequest, stage: StageType, parentId: String?, parentType: BusinessPartnerType?): BusinessPartner { + fun toBusinessPartnerOutput(dto: BusinessPartnerOutputRequest): BusinessPartner { return BusinessPartner( - stage = stage, + stage = StageType.Output, externalId = dto.externalId, nameParts = dto.nameParts.toMutableList(), roles = dto.roles.toSortedSet(), @@ -137,29 +122,12 @@ class BusinessPartnerMappings { bpnL = dto.bpnL, bpnS = dto.bpnS, bpnA = dto.bpnA, - parentId = parentId, - parentType = parentType, + parentId = null, + parentType = null, postalAddress = toPostalAddress(dto.postalAddress) ) } - fun updateBusinessPartnerOutput(entity: BusinessPartner, dto: BusinessPartnerOutputRequest, parentId: String?, parentType: BusinessPartnerType?) { - entity.nameParts.replace(dto.nameParts) - entity.roles.replace(dto.roles) - entity.identifiers.replace(dto.identifiers.map(::toIdentifier)) - entity.states.replace(dto.states.map(::toState)) - entity.classifications.replace(dto.classifications.map(::toClassification)) - entity.shortName = dto.shortName - entity.legalForm = dto.legalForm - entity.isOwner = dto.isOwner - entity.bpnL = dto.bpnL - entity.bpnS = dto.bpnS - entity.bpnA = dto.bpnA - entity.parentId = parentId - entity.parentType = parentType - updatePostalAddress(entity.postalAddress, dto.postalAddress) - } - private fun toPostalAddressDto(entity: PostalAddress) = BusinessPartnerPostalAddressDto( addressType = entity.addressType, @@ -174,12 +142,6 @@ class BusinessPartnerMappings { alternativePostalAddress = normalize(dto.alternativePostalAddress)?.let(::toAlternativePostalAddress) ) - private fun updatePostalAddress(entity: PostalAddress, dto: BusinessPartnerPostalAddressDto) { - entity.addressType = dto.addressType - entity.physicalPostalAddress = normalize(dto.physicalPostalAddress)?.let(::toPhysicalPostalAddress) - entity.alternativePostalAddress = normalize(dto.alternativePostalAddress)?.let(::toAlternativePostalAddress) - } - private fun toPhysicalPostalAddressDto(entity: PhysicalPostalAddress) = PhysicalPostalAddressGateDto( geographicCoordinates = entity.geographicCoordinates?.let(::toGeoCoordinateDto), diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/BusinessPartnerService.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/BusinessPartnerService.kt index 0dd13182c..f75cea979 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/BusinessPartnerService.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/BusinessPartnerService.kt @@ -19,22 +19,25 @@ package org.eclipse.tractusx.bpdm.gate.service -import org.eclipse.tractusx.bpdm.common.dto.AddressType import org.eclipse.tractusx.bpdm.common.dto.BusinessPartnerType import org.eclipse.tractusx.bpdm.common.dto.response.PageDto import org.eclipse.tractusx.bpdm.common.model.StageType import org.eclipse.tractusx.bpdm.common.service.toPageDto +import org.eclipse.tractusx.bpdm.common.util.copyAndSync +import org.eclipse.tractusx.bpdm.common.util.partitionAndMap +import org.eclipse.tractusx.bpdm.common.util.replace import org.eclipse.tractusx.bpdm.gate.api.model.ChangelogType -import org.eclipse.tractusx.bpdm.gate.api.model.IBaseBusinessPartnerGateDto import org.eclipse.tractusx.bpdm.gate.api.model.request.BusinessPartnerInputRequest import org.eclipse.tractusx.bpdm.gate.api.model.request.BusinessPartnerOutputRequest import org.eclipse.tractusx.bpdm.gate.api.model.response.BusinessPartnerInputDto import org.eclipse.tractusx.bpdm.gate.api.model.response.BusinessPartnerOutputDto import org.eclipse.tractusx.bpdm.gate.api.model.response.SharingStateDto import org.eclipse.tractusx.bpdm.gate.entity.ChangelogEntry -import org.eclipse.tractusx.bpdm.gate.entity.generic.BusinessPartner +import org.eclipse.tractusx.bpdm.gate.entity.generic.* +import org.eclipse.tractusx.bpdm.gate.exception.BpdmMissingStageException import org.eclipse.tractusx.bpdm.gate.repository.ChangelogRepository import org.eclipse.tractusx.bpdm.gate.repository.generic.BusinessPartnerRepository +import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -49,110 +52,164 @@ class BusinessPartnerService( @Transactional fun upsertBusinessPartnersInput(dtos: Collection): Collection { - val existingEntitiesByExternalId = getExistingEntityByExternalId(dtos, StageType.Input) - - val savedEntities = dtos.map { dto -> - existingEntitiesByExternalId[dto.externalId] - ?.let { existingEntity -> updateBusinessPartnerInput(existingEntity, dto) } - ?: run { insertBusinessPartnerInput(dto) } - } - - return savedEntities.map(businessPartnerMappings::toBusinessPartnerInputDto) + val entities = dtos.map { dto -> businessPartnerMappings.toBusinessPartnerInput(dto) } + return upsertBusinessPartnersInput(entities).map(businessPartnerMappings::toBusinessPartnerInputDto) } - private fun insertBusinessPartnerInput(dto: BusinessPartnerInputRequest): BusinessPartner { - val newEntity = businessPartnerMappings.toBusinessPartner(dto, StageType.Input, null, null) - return businessPartnerRepository.save(newEntity) - .also { - saveChangelog(dto.externalId, ChangelogType.CREATE, StageType.Input, checkBusinessPartnerType(dto.postalAddress.addressType)) - initSharingState(dto) - } + @Transactional + fun upsertBusinessPartnersOutput(dtos: Collection): Collection { + val entities = dtos.map { dto -> businessPartnerMappings.toBusinessPartnerOutput(dto) } + return upsertBusinessPartnersOutput(entities).map(businessPartnerMappings::toBusinessPartnerOutputDto) } - private fun updateBusinessPartnerInput(existingEntity: BusinessPartner, dto: BusinessPartnerInputRequest): BusinessPartner { - businessPartnerMappings.updateBusinessPartner(existingEntity, dto, null, null) - return businessPartnerRepository.save(existingEntity) - .also { - saveChangelog(dto.externalId, ChangelogType.UPDATE, StageType.Input, checkBusinessPartnerType(dto.postalAddress.addressType)) - } + fun getBusinessPartnersInput(pageRequest: PageRequest, externalIds: Collection?): PageDto { + val stage = StageType.Input + return getBusinessPartners(pageRequest, externalIds, stage) + .toPageDto(businessPartnerMappings::toBusinessPartnerInputDto) } - private fun checkBusinessPartnerType(type: AddressType?): List? { - return when (type) { - AddressType.LegalAndSiteMainAddress -> listOf(BusinessPartnerType.LEGAL_ENTITY, BusinessPartnerType.SITE) - AddressType.AdditionalAddress -> listOf(BusinessPartnerType.ADDRESS) - AddressType.LegalAddress -> listOf(BusinessPartnerType.LEGAL_ENTITY) - AddressType.SiteMainAddress -> listOf(BusinessPartnerType.SITE) - else -> null - } + fun getBusinessPartnersOutput(pageRequest: PageRequest, externalIds: Collection?): PageDto { + val stage = StageType.Output + return getBusinessPartners(pageRequest, externalIds, stage) + .toPageDto(businessPartnerMappings::toBusinessPartnerOutputDto) } - //Output Logic - @Transactional - fun upsertBusinessPartnersOutput(dtos: Collection): Collection { - val existingEntitiesByExternalId = getExistingEntityByExternalId(dtos, StageType.Output) + private fun upsertBusinessPartnersInput(entityCandidates: List): List { + val resolvedCandidates = resolveCandidatesForStage(entityCandidates, StageType.Input) - val savedEntities = dtos.map { dto -> - existingEntitiesByExternalId[dto.externalId] - ?.let { existingEntity -> updateBusinessPartnerOutput(existingEntity, dto) } - ?: run { insertBusinessPartnerOutput(dto) } - } + saveChangelog(resolvedCandidates) - return savedEntities.map(businessPartnerMappings::toBusinessPartnerInputDto) - } + val partners = resolvedCandidates.map { it.businessPartner } + partners.forEach { entity -> initSharingState(entity) } - private fun insertBusinessPartnerOutput(dto: BusinessPartnerOutputRequest): BusinessPartner { - val newEntity = businessPartnerMappings.toBusinessPartnerOutput(dto, StageType.Output, null, null) - return businessPartnerRepository.save(newEntity) - .also { - saveChangelog(dto.externalId, ChangelogType.CREATE, StageType.Output, checkBusinessPartnerType(dto.postalAddress.addressType)) - initSharingState(dto) - } + return businessPartnerRepository.saveAll(partners) } - private fun updateBusinessPartnerOutput(existingEntity: BusinessPartner, dto: BusinessPartnerOutputRequest): BusinessPartner { - businessPartnerMappings.updateBusinessPartnerOutput(existingEntity, dto, null, null) - return businessPartnerRepository.save(existingEntity) - .also { - saveChangelog(dto.externalId, ChangelogType.UPDATE, StageType.Output, checkBusinessPartnerType(dto.postalAddress.addressType)) - } + private fun upsertBusinessPartnersOutput(entityCandidates: List): List { + val externalIds = entityCandidates.map { it.externalId } + assertInputStageExists(externalIds) + + val resolvedCandidates = resolveCandidatesForStage(entityCandidates, StageType.Output) + + saveChangelog(resolvedCandidates) + + return businessPartnerRepository.saveAll(resolvedCandidates.map { it.businessPartner }) } - fun getBusinessPartnersInput(pageRequest: PageRequest, externalIds: Collection?): PageDto { - val stage = StageType.Input - val page = when { + private fun getBusinessPartners(pageRequest: PageRequest, externalIds: Collection?, stage: StageType): Page { + return when { externalIds.isNullOrEmpty() -> businessPartnerRepository.findByStage(stage, pageRequest) else -> businessPartnerRepository.findByStageAndExternalIdIn(stage, externalIds, pageRequest) } - return page.toPageDto(businessPartnerMappings::toBusinessPartnerInputDto) } - fun getBusinessPartnersOutput(pageRequest: PageRequest, externalIds: Collection?): PageDto { - val stage = StageType.Output - val page = when { - externalIds.isNullOrEmpty() -> businessPartnerRepository.findByStage(stage, pageRequest) - else -> businessPartnerRepository.findByStageAndExternalIdIn(stage, externalIds, pageRequest) - } - return page.toPageDto(businessPartnerMappings::toBusinessPartnerOutputDto) + private fun initSharingState(entity: BusinessPartner) { + // TODO make businessPartnerType optional + sharingStateService.upsertSharingState(SharingStateDto(BusinessPartnerType.ADDRESS, entity.externalId)) } - private fun getExistingEntityByExternalId( - dtos: Collection, - stage: StageType - ): Map { - val externalIds = dtos.map { it.externalId }.toSet() - return businessPartnerRepository.findByStageAndExternalIdIn(stage, externalIds) - .associateBy { it.externalId } + private fun saveChangelog(resolvedCandidates: Collection) { + + + val (partnersToUpdate, partnersToInsert) = resolvedCandidates.partitionAndMap({ it.wasResolved }, { it.businessPartner }) + + partnersToUpdate.forEach { saveChangelog(it, ChangelogType.UPDATE) } + partnersToInsert.forEach { saveChangelog(it, ChangelogType.CREATE) } } - private fun initSharingState(dto: IBaseBusinessPartnerGateDto) { - // TODO make businessPartnerType optional - sharingStateService.upsertSharingState(SharingStateDto(BusinessPartnerType.ADDRESS, dto.externalId)) + private fun saveChangelog(partner: BusinessPartner, changelogType: ChangelogType) { + val businessPartnerTypes = partner.postalAddress.addressType?.businessPartnerTypes ?: listOf(null) + + businessPartnerTypes.forEach { type -> changelogRepository.save(ChangelogEntry(partner.externalId, type, changelogType, partner.stage)) } + } + + private fun assertInputStageExists(externalIds: Collection) { + val existingExternalIds = businessPartnerRepository.findByStageAndExternalIdIn(StageType.Input, externalIds) + .map { it.externalId } + .toSet() + + externalIds.minus(existingExternalIds) + .takeIf { it.isNotEmpty() } + ?.let { throw BpdmMissingStageException(it, StageType.Input) } } - private fun saveChangelog(externalId: String, changelogType: ChangelogType, stage: StageType, businessPartnerType: List?) { - businessPartnerType?.forEach { type -> - changelogRepository.save(ChangelogEntry(externalId, type, changelogType, stage)) - } ?: changelogRepository.save(ChangelogEntry(externalId, null, changelogType, stage)) + /** + * Resolve all [entityCandidates] by looking for existing business partner data in the given [stage] + * + * Resolving a candidate means to exchange the candidate entity with the existing entity and copy from the candidate to that existing entity. + * + */ + private fun resolveCandidatesForStage(entityCandidates: List, stage: StageType): List { + val existingPartnersByExternalId = businessPartnerRepository.findByStageAndExternalIdIn(stage, entityCandidates.map { it.externalId }) + .associateBy { it.externalId } + + return entityCandidates.map { candidate -> + val existingEntity = existingPartnersByExternalId[candidate.externalId] + if (existingEntity != null) + ResolutionResult(copyValues(candidate, existingEntity), true) + else + ResolutionResult(candidate, false) + } + } + + data class ResolutionResult( + val businessPartner: BusinessPartner, + val wasResolved: Boolean + ) + + private fun copyValues(fromPartner: BusinessPartner, toPartner: BusinessPartner): BusinessPartner { + return toPartner.apply { + stage = fromPartner.stage + shortName = fromPartner.shortName + legalForm = fromPartner.legalForm + isOwner = fromPartner.isOwner + bpnL = fromPartner.bpnL + bpnS = fromPartner.bpnS + bpnA = fromPartner.bpnA + parentId = fromPartner.parentId + parentType = fromPartner.parentType + + nameParts.replace(fromPartner.nameParts) + roles.replace(fromPartner.roles) + + states.copyAndSync(fromPartner.states, ::copyValues) + classifications.copyAndSync(fromPartner.classifications, ::copyValues) + identifiers.copyAndSync(fromPartner.identifiers, ::copyValues) + + copyValues(fromPartner.postalAddress, postalAddress) + } } + + private fun copyValues(fromState: State, toState: State) = + toState.apply { + validFrom = fromState.validFrom + validTo = fromState.validTo + description = fromState.description + type = fromState.type + } + + private fun copyValues(fromClassification: Classification, toClassification: Classification) = + toClassification.apply { + value = fromClassification.value + type = fromClassification.type + code = fromClassification.code + } + + private fun copyValues(fromIdentifier: Identifier, toIdentifier: Identifier) = + toIdentifier.apply { + type = fromIdentifier.type + value = fromIdentifier.value + issuingBody = fromIdentifier.issuingBody + } + + private fun copyValues(fromPostalAddress: PostalAddress, toPostalAddress: PostalAddress) = + toPostalAddress.apply { + addressType = fromPostalAddress.addressType + physicalPostalAddress = fromPostalAddress.physicalPostalAddress + alternativePostalAddress = fromPostalAddress.alternativePostalAddress + } + + + + }