diff --git a/bpdm-common-test/src/main/kotlin/org/eclipse/tractusx/bpdm/test/testdata/gate/BusinessPartnerNonVerboseValues.kt b/bpdm-common-test/src/main/kotlin/org/eclipse/tractusx/bpdm/test/testdata/gate/BusinessPartnerNonVerboseValues.kt index ecc4f77d1..2c210c9a2 100644 --- a/bpdm-common-test/src/main/kotlin/org/eclipse/tractusx/bpdm/test/testdata/gate/BusinessPartnerNonVerboseValues.kt +++ b/bpdm-common-test/src/main/kotlin/org/eclipse/tractusx/bpdm/test/testdata/gate/BusinessPartnerNonVerboseValues.kt @@ -36,7 +36,8 @@ object BusinessPartnerNonVerboseValues { val bpInputRequestMinimal = BusinessPartnerInputRequest( externalId = BusinessPartnerVerboseValues.externalId2, - address = bpPostalAddressInputDtoMinimal + address = bpPostalAddressInputDtoMinimal, + externalSequenceTimestamp = null ) val bpInputRequestFull = BusinessPartnerVerboseValues.bpInputRequestFull diff --git a/bpdm-common-test/src/main/kotlin/org/eclipse/tractusx/bpdm/test/testdata/gate/BusinessPartnerVerboseValues.kt b/bpdm-common-test/src/main/kotlin/org/eclipse/tractusx/bpdm/test/testdata/gate/BusinessPartnerVerboseValues.kt index c91d12667..59a52f232 100644 --- a/bpdm-common-test/src/main/kotlin/org/eclipse/tractusx/bpdm/test/testdata/gate/BusinessPartnerVerboseValues.kt +++ b/bpdm-common-test/src/main/kotlin/org/eclipse/tractusx/bpdm/test/testdata/gate/BusinessPartnerVerboseValues.kt @@ -72,6 +72,10 @@ object BusinessPartnerVerboseValues { const val businessStatusDescription1 = "Active" const val businessStatusDescription2 = "Insolvent" + val externalSequenceTimestamp1 = "2024-06-28 11:59:00" + val externalSequenceTimestamp2 = "2024-06-28 12:00:00" + val externalSequenceTimestamp3 = "2024-06-28 12:01:00" + val businessStatusValidFrom1 = LocalDateTime.of(2020, 1, 1, 0, 0) val businessStatusValidFrom2 = LocalDateTime.of(2019, 1, 1, 0, 0) @@ -331,7 +335,8 @@ object BusinessPartnerVerboseValues { addressType = AddressType.LegalAddress, physicalPostalAddress = postalAddress2, alternativePostalAddress = alternativeAddressFull - ) + ), + externalSequenceTimestamp = null ) @@ -358,7 +363,8 @@ object BusinessPartnerVerboseValues { addressType = AddressType.SiteMainAddress, physicalPostalAddress = postalAddress2, alternativePostalAddress = alternativeAddressFull - ) + ), + externalSequenceTimestamp = null ) //New Values for Logistic Addresses Tests @@ -376,7 +382,7 @@ object BusinessPartnerVerboseValues { building = "Bauteil A", floor = "Etage 1", door = "Door One", - street = StreetDto(name = "Mercedesstraße", houseNumber = "", direction = "direction1", houseNumberSupplement = "A"), + street = StreetDto(name = "Mercedesstraße", houseNumber = "", direction = "direction1", houseNumberSupplement = "A") ) val postalAddressLogisticAddress2 = PhysicalPostalAddressDto( @@ -393,7 +399,7 @@ object BusinessPartnerVerboseValues { building = "Building Two", floor = "Floor Two", door = "Door Two", - street = StreetDto(name = "TODO", houseNumber = "", direction = "direction1", houseNumberSupplement = "B"), + street = StreetDto(name = "TODO", houseNumber = "", direction = "direction1", houseNumberSupplement = "B") ) //New Values for Logistic Address Tests @@ -657,7 +663,8 @@ object BusinessPartnerVerboseValues { addressType = AddressType.LegalAndSiteMainAddress, physicalPostalAddress = physicalAddressChina, alternativePostalAddress = AlternativePostalAddressDto() - ) + ), + externalSequenceTimestamp = null ) val bpInputRequestCleaned = BusinessPartnerInputRequest( @@ -685,7 +692,8 @@ object BusinessPartnerVerboseValues { addressType = AddressType.LegalAddress, physicalPostalAddress = postalAddress2, alternativePostalAddress = alternativeAddressFull - ) + ), + externalSequenceTimestamp = null ) val bpInputRequestError = BusinessPartnerInputRequest( @@ -713,7 +721,8 @@ object BusinessPartnerVerboseValues { addressType = AddressType.LegalAddress, physicalPostalAddress = postalAddress2, alternativePostalAddress = alternativeAddressFull - ) + ), + externalSequenceTimestamp = null ) val bpOutputDtoCleaned = BusinessPartnerOutputDto( @@ -829,6 +838,60 @@ object BusinessPartnerVerboseValues { updatedAt = Instant.now() ) + val bpInputRequestWithExternalSequenceTimestamp1 = BusinessPartnerInputRequest( + externalId = externalId1, + legalEntity = LegalEntityRepresentationInputDto( + legalEntityBpn = "BPNL0000000000XY", + shortName = "short", + legalName = "Limited Liability Company Name", + legalForm = "Limited Liability Company" + ), + address = AddressRepresentationInputDto( + addressBpn = "BPNA0000000001XY", + name = "Address Name", + addressType = null, + physicalPostalAddress = physicalAddressMinimal + ), + externalSequenceTimestamp = externalSequenceTimestamp1 + + ) + + val bpInputRequestWithExternalSequenceTimestamp2 = BusinessPartnerInputRequest( + externalId = externalId1, + legalEntity = LegalEntityRepresentationInputDto( + legalEntityBpn = "BPNL0000000000XY", + shortName = "short1", + legalName = "Limited Liability Company Name", + legalForm = "Limited Liability Company" + ), + address = AddressRepresentationInputDto( + addressBpn = "BPNA0000000001XY", + name = "Address Name", + addressType = null, + physicalPostalAddress = physicalAddressMinimal + ), + externalSequenceTimestamp = externalSequenceTimestamp2 + + ) + + val bpInputRequestWithExternalSequenceTimestamp3 = BusinessPartnerInputRequest( + externalId = externalId1, + legalEntity = LegalEntityRepresentationInputDto( + legalEntityBpn = "BPNL0000000000XY", + shortName = "short2", + legalName = "Limited Liability Company Name", + legalForm = "Limited Liability Company" + ), + address = AddressRepresentationInputDto( + addressBpn = "BPNA0000000001XY", + name = "Another address Name", + addressType = null, + physicalPostalAddress = physicalAddressMinimal + ), + externalSequenceTimestamp = externalSequenceTimestamp3 + + ) + val now = Instant.now() diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/IBaseBusinessPartnerGateDto.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/IBaseBusinessPartnerGateDto.kt index c3f129bb6..5b34aa902 100644 --- a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/IBaseBusinessPartnerGateDto.kt +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/IBaseBusinessPartnerGateDto.kt @@ -30,5 +30,8 @@ interface IBaseBusinessPartnerGateDto : IBaseBusinessPartnerDto { @get:Schema(description = "Indicates whether the sharing member claims (in the initial upload) the business partner to belong to the company data of the sharing member.") val isOwnCompanyData: Boolean + + @get:Schema(description = "The timestamp indicates the last time point of change from the user side") + val externalSequenceTimestamp: String? } diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/request/BusinessPartnerInputRequest.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/request/BusinessPartnerInputRequest.kt index 538ef9f08..66a229cf2 100644 --- a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/request/BusinessPartnerInputRequest.kt +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/request/BusinessPartnerInputRequest.kt @@ -42,6 +42,7 @@ data class BusinessPartnerInputRequest( override val isOwnCompanyData: Boolean = false, override val legalEntity: LegalEntityRepresentationInputDto = LegalEntityRepresentationInputDto(), override val site: SiteRepresentationInputDto = SiteRepresentationInputDto(), - override val address: AddressRepresentationInputDto = AddressRepresentationInputDto() + override val address: AddressRepresentationInputDto = AddressRepresentationInputDto(), + override val externalSequenceTimestamp: String? ) : IBaseBusinessPartnerGateDto diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/BusinessPartnerDto.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/BusinessPartnerDto.kt index 497ade6a8..de4147f4b 100644 --- a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/BusinessPartnerDto.kt +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/BusinessPartnerDto.kt @@ -41,6 +41,7 @@ data class BusinessPartnerInputDto( override val legalEntity: LegalEntityRepresentationInputDto = LegalEntityRepresentationInputDto(), override val site: SiteRepresentationInputDto = SiteRepresentationInputDto(), override val address: AddressRepresentationInputDto = AddressRepresentationInputDto(), + override val externalSequenceTimestamp:String? = null, @get:Schema(description = CommonDescription.createdAt) val createdAt: Instant, diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/BusinessPartnerOutputDto.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/BusinessPartnerOutputDto.kt index c6c6f4cd1..610f10a27 100644 --- a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/BusinessPartnerOutputDto.kt +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/BusinessPartnerOutputDto.kt @@ -40,6 +40,7 @@ data class BusinessPartnerOutputDto( override val legalEntity: LegalEntityRepresentationOutputDto, override val site: SiteRepresentationOutputDto?, override val address: AddressComponentOutputDto, + override val externalSequenceTimestamp: String? = null, @get:Schema(description = CommonDescription.createdAt) val createdAt: Instant, diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/BusinessPartnerDb.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/BusinessPartnerDb.kt index fb2301349..2df700e89 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/BusinessPartnerDb.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/entity/generic/BusinessPartnerDb.kt @@ -24,6 +24,7 @@ import org.eclipse.tractusx.bpdm.common.dto.BusinessPartnerRole import org.eclipse.tractusx.bpdm.common.model.BaseEntity import org.eclipse.tractusx.bpdm.common.model.StageType import org.eclipse.tractusx.bpdm.gate.entity.SharingStateDb +import java.time.Instant import java.util.* @Entity @@ -104,6 +105,9 @@ class BusinessPartnerDb( @JoinColumn(name = "address_confidence_id", unique = true) var addressConfidence: ConfidenceCriteriaDb?, + @Column(name = "external_sequence_timestamp") + var externalSequenceTimestamp: Instant? = null, + ) : BaseEntity() { companion object { 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 c25d649a7..1183d7840 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 @@ -31,6 +31,7 @@ import org.eclipse.tractusx.bpdm.gate.api.model.response.* import org.eclipse.tractusx.bpdm.gate.entity.* import org.eclipse.tractusx.bpdm.gate.entity.generic.* import org.eclipse.tractusx.bpdm.gate.exception.BpdmInvalidPartnerException +import org.eclipse.tractusx.bpdm.gate.util.getTimestampToInstant import org.springframework.stereotype.Service @Service @@ -105,6 +106,11 @@ class BusinessPartnerMappings { bpnS = dto.site.siteBpn, bpnA = dto.address.addressBpn, postalAddress = toPostalAddress(dto.address), + externalSequenceTimestamp = try { + getTimestampToInstant(dto.externalSequenceTimestamp) + } catch (e: Exception) { + null + }, legalEntityConfidence = null, siteConfidence = null, addressConfidence = null, 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 ac5ac5a61..71ce49147 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 @@ -127,13 +127,19 @@ class BusinessPartnerService( val changeType = if (existingPartner == null) ChangelogType.CREATE else ChangelogType.UPDATE val partnerToUpsert = existingPartner ?: BusinessPartnerDb.createEmpty(upsertData.sharingState, upsertData.stage) + val hasChanges = changeType == ChangelogType.CREATE || compareUtil.hasChanges(upsertData, partnerToUpsert) + val shouldUpdate = when { + upsertData.externalSequenceTimestamp == null -> true + existingPartner?.externalSequenceTimestamp == null -> true + else -> upsertData.externalSequenceTimestamp!!.isAfter(existingPartner.externalSequenceTimestamp) + } - if (hasChanges) { - changelogRepository.save(ChangelogEntryDb(sharingState.externalId, sharingState.tenantBpnl, changeType, stage)) + if (hasChanges && shouldUpdate) { + changelogRepository.save(ChangelogEntryDb(sharingState.externalId, sharingState.tenantBpnl, changeType, stage)) - copyUtil.copyValues(upsertData, partnerToUpsert) - businessPartnerRepository.save(partnerToUpsert) + copyUtil.copyValues(upsertData, partnerToUpsert) + businessPartnerRepository.save(partnerToUpsert) } return UpsertResult(hasChanges, changeType, partnerToUpsert) diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/BusinessPartnerComparisonUtil.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/BusinessPartnerComparisonUtil.kt index f6d17db62..0955c6a1c 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/BusinessPartnerComparisonUtil.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/BusinessPartnerComparisonUtil.kt @@ -43,6 +43,7 @@ class BusinessPartnerComparisonUtil { entity.identifiers != persistedBP.identifiers || entity.states != persistedBP.states || entity.classifications != persistedBP.classifications || + entity.externalSequenceTimestamp != persistedBP.externalSequenceTimestamp || postalAddressHasChanges(entity.postalAddress, persistedBP.postalAddress) } diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/BusinessPartnerCopyUtil.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/BusinessPartnerCopyUtil.kt index 62ea7eae6..d0f32b7c0 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/BusinessPartnerCopyUtil.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/BusinessPartnerCopyUtil.kt @@ -42,6 +42,7 @@ class BusinessPartnerCopyUtil { legalEntityConfidence = fromPartner.legalEntityConfidence siteConfidence = fromPartner.siteConfidence addressConfidence = fromPartner.addressConfidence + externalSequenceTimestamp = fromPartner.externalSequenceTimestamp nameParts.replace(fromPartner.nameParts) roles.replace(fromPartner.roles) diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/Extensions.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/Extensions.kt index 401e7e1c8..810ce1e4a 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/Extensions.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/Extensions.kt @@ -21,6 +21,10 @@ package org.eclipse.tractusx.bpdm.gate.util import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter fun List.containsDuplicates(): Boolean = size != distinct().size @@ -35,4 +39,10 @@ fun getCurrentUserBpn(): String? { return authentication.tokenAttributes["bpn"] as String? ?: null } return null; +} + +fun getTimestampToInstant(dateTimeString: String?): Instant { + val inputFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + val ldt = LocalDateTime.parse(dateTimeString, inputFormat) + return ldt.atZone(ZoneId.systemDefault()).toInstant() } \ No newline at end of file diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/PartnerFileUtil.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/PartnerFileUtil.kt index c03b46dbf..4c683a71d 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/PartnerFileUtil.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/PartnerFileUtil.kt @@ -87,8 +87,10 @@ object PartnerFileUtil { isOwnCompanyData = true, // Legal entity's business partner number is nothing but tenant's partner number who is performing business partner upload action legalEntity = LegalEntityRepresentationInputDto(legalEntityBpn = tenantBpnl?.takeIf { it.isNotEmpty() }), + site = row.toSiteRepresentationInputDto(formatter, errors, index, row.externalId.orEmpty()), - address = row.toAddressRepresentationInputDto(formatter, errors, index, row.externalId.orEmpty()) + address = row.toAddressRepresentationInputDto(formatter, errors, index, row.externalId.orEmpty()), + externalSequenceTimestamp = null ) } catch (e: Exception) { errors.add("Row - ${index + 2}, External ID - ${row.externalId.orEmpty()} has error: ${e.message}") diff --git a/bpdm-gate/src/main/resources/db/migration/V6_1_0_5__add_externalSequenceTimestamp_column.sql b/bpdm-gate/src/main/resources/db/migration/V6_1_0_5__add_externalSequenceTimestamp_column.sql new file mode 100644 index 000000000..929925a03 --- /dev/null +++ b/bpdm-gate/src/main/resources/db/migration/V6_1_0_5__add_externalSequenceTimestamp_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE business_partners +ADD COLUMN external_sequence_timestamp TIMESTAMP WITHOUT TIME ZONE NULL \ No newline at end of file diff --git a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/auth/AuthTestBase.kt b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/auth/AuthTestBase.kt index daea091c6..3f6c2cdd4 100644 --- a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/auth/AuthTestBase.kt +++ b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/auth/AuthTestBase.kt @@ -50,7 +50,7 @@ abstract class AuthTestBase( @Test fun `PUT Partner Input`() { authAssertions.assert(authExpectations.businessPartner.putInput) { gateClient.businessParters.upsertBusinessPartnersInput(listOf( - BusinessPartnerInputRequest("externalId") + BusinessPartnerInputRequest("externalId", externalSequenceTimestamp = null) )) } } diff --git a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/BusinessPartnerControllerIT.kt b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/BusinessPartnerControllerIT.kt index d4caf81a8..5747ceefb 100644 --- a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/BusinessPartnerControllerIT.kt +++ b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/BusinessPartnerControllerIT.kt @@ -271,6 +271,35 @@ class BusinessPartnerControllerIT @Autowired constructor( assertEquals(0, searchResponsePage2.content.size) } + @Test + fun `insert a late arrival request with minimal business partner and the record won't be updated`() { + + val firstUpsertRequest = listOf(BusinessPartnerVerboseValues.bpInputRequestWithExternalSequenceTimestamp2) //12:00 + gateClient.businessParters.upsertBusinessPartnersInput(firstUpsertRequest).body!! + + val beforeFirstUpsertRequest = listOf(BusinessPartnerVerboseValues.bpInputRequestWithExternalSequenceTimestamp1) // 11:59 + gateClient.businessParters.upsertBusinessPartnersInput(beforeFirstUpsertRequest).body!! + + val searchResponsePage = gateClient.businessParters.getBusinessPartnersInput( + listOf(BusinessPartnerVerboseValues.externalId1)) + + this.mockAndAssertUtils.assertUpsertResponsesMatchRequests(searchResponsePage.content, firstUpsertRequest) + } + + @Test + fun `upsert a new request with later externalSequenceTimestamp timestamp and the record updated`() { + + val firstUpsertRequest = listOf(BusinessPartnerVerboseValues.bpInputRequestWithExternalSequenceTimestamp2) //12:00 + gateClient.businessParters.upsertBusinessPartnersInput(firstUpsertRequest).body!! + + val laterUpsertRequest = listOf(BusinessPartnerVerboseValues.bpInputRequestWithExternalSequenceTimestamp3) // 12:01 + gateClient.businessParters.upsertBusinessPartnersInput(laterUpsertRequest).body!! + + val searchResponsePage = gateClient.businessParters.getBusinessPartnersInput( + listOf(BusinessPartnerVerboseValues.externalId1)) + + this.mockAndAssertUtils.assertUpsertResponsesMatchRequests(searchResponsePage.content, laterUpsertRequest) + }