Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bpdm-gate): Adding a currentness attribute to the business partner input re… #986

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ object BusinessPartnerNonVerboseValues {

val bpInputRequestMinimal = BusinessPartnerInputRequest(
externalId = BusinessPartnerVerboseValues.externalId2,
address = bpPostalAddressInputDtoMinimal
address = bpPostalAddressInputDtoMinimal,
currentness = null
)

val bpInputRequestFull = BusinessPartnerVerboseValues.bpInputRequestFull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ object BusinessPartnerVerboseValues {
const val businessStatusDescription1 = "Active"
const val businessStatusDescription2 = "Insolvent"

val currentness1 = "2024-06-28 11:59:00"
val currentness2 = "2024-06-28 12:00:00"
val currentness3 = "2024-06-28 12:01:00"

val businessStatusValidFrom1 = LocalDateTime.of(2020, 1, 1, 0, 0)
val businessStatusValidFrom2 = LocalDateTime.of(2019, 1, 1, 0, 0)

Expand Down Expand Up @@ -331,7 +335,8 @@ object BusinessPartnerVerboseValues {
addressType = AddressType.LegalAddress,
physicalPostalAddress = postalAddress2,
alternativePostalAddress = alternativeAddressFull
)
),
currentness = null

)

Expand Down Expand Up @@ -631,7 +636,8 @@ object BusinessPartnerVerboseValues {
addressType = AddressType.LegalAndSiteMainAddress,
physicalPostalAddress = physicalAddressChina,
alternativePostalAddress = AlternativePostalAddressDto()
)
),
currentness = null
)

val bpInputRequestCleaned = BusinessPartnerInputRequest(
Expand Down Expand Up @@ -659,7 +665,8 @@ object BusinessPartnerVerboseValues {
addressType = AddressType.LegalAddress,
physicalPostalAddress = postalAddress2,
alternativePostalAddress = alternativeAddressFull
)
),
currentness = null
)

val bpInputRequestError = BusinessPartnerInputRequest(
Expand Down Expand Up @@ -687,7 +694,8 @@ object BusinessPartnerVerboseValues {
addressType = AddressType.LegalAddress,
physicalPostalAddress = postalAddress2,
alternativePostalAddress = alternativeAddressFull
)
),
currentness = null
)

val bpOutputDtoCleaned = BusinessPartnerOutputDto(
Expand Down Expand Up @@ -802,6 +810,60 @@ object BusinessPartnerVerboseValues {
updatedAt = Instant.now()
)

val bpInputRequestWithCurrentness1 = 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
),
currentness = currentness1

)

val bpInputRequestWithCurrentness2 = 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
),
currentness = currentness2

)

val bpInputRequestWithCurrentness3 = 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
),
currentness = currentness3

)

val now = Instant.now()


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 currentness: String?
}

Original file line number Diff line number Diff line change
Expand Up @@ -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 currentness: String?

) : IBaseBusinessPartnerGateDto
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ data class BusinessPartnerInputDto(
override val legalEntity: LegalEntityRepresentationInputDto = LegalEntityRepresentationInputDto(),
override val site: SiteRepresentationInputDto = SiteRepresentationInputDto(),
override val address: AddressRepresentationInputDto = AddressRepresentationInputDto(),
override val currentness:String? = null,

@get:Schema(description = CommonDescription.createdAt)
val createdAt: Instant,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ data class BusinessPartnerOutputDto(
override val legalEntity: LegalEntityRepresentationOutputDto,
override val site: SiteRepresentationOutputDto?,
override val address: AddressComponentOutputDto,
override val currentness: String? = null,

@get:Schema(description = CommonDescription.createdAt)
val createdAt: Instant,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -104,6 +105,9 @@ class BusinessPartnerDb(
@JoinColumn(name = "address_confidence_id", unique = true)
var addressConfidence: ConfidenceCriteriaDb?,

@Column(name = "currentness")
var currentness: Instant? = null,

) : BaseEntity() {

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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
Expand Down Expand Up @@ -88,6 +89,11 @@ class BusinessPartnerMappings {
bpnS = dto.site.siteBpn,
bpnA = dto.address.addressBpn,
postalAddress = toPostalAddress(dto.address),
currentness = try {
getTimestampToInstant(dto.currentness)
} catch (e: Exception) {
null
},
legalEntityConfidence = null,
siteConfidence = null,
addressConfidence = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,17 @@ class BusinessPartnerService(
val partnerToUpsert = existingPartner ?: BusinessPartnerDb.createEmpty(upsertData.sharingState, upsertData.stage)

val hasChanges = compareUtil.hasChanges(upsertData, partnerToUpsert)
val shouldUpdate = when {
Copy link

@Sebastian-Wurm Sebastian-Wurm Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not mix the technical problem of messages / versions of the same business partner arriving in a wrong order with the currentness of the business partner. Currentness information has business value to it and must be added anyway sooner or later to the Gate as it is already available in the Pool. It may well be, that the currentness of a business partner needs to be changed, because currentness information was wrong. If the corrected currentness lies before the wrong currentness, this change would not be possible with this code. Let's remove this code here and rather introduce an integer version number at a business partner or a sequence number at the message level in another pull request. This version / sequence number could be optional for those source systems, that handle sequence problems on their own. If it is set, it must be incremented on each change of the business partner / sending of a message. Source systems not having an integer version / sequence number could rely on converting the timestamp to a unix time. This is best practice in other MDM system for decades.

upsertData.currentness == null -> true
existingPartner?.currentness == null -> true
else -> upsertData.currentness!!.isAfter(existingPartner.currentness)
}

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class BusinessPartnerComparisonUtil {
entity.identifiers != persistedBP.identifiers ||
entity.states != persistedBP.states ||
entity.classifications != persistedBP.classifications ||
entity.currentness != persistedBP.currentness ||
postalAddressHasChanges(entity.postalAddress, persistedBP.postalAddress)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class BusinessPartnerCopyUtil {
legalEntityConfidence = fromPartner.legalEntityConfidence
siteConfidence = fromPartner.siteConfidence
addressConfidence = fromPartner.addressConfidence
currentness = fromPartner.currentness

nameParts.replace(fromPartner.nameParts)
roles.replace(fromPartner.roles)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> List<T>.containsDuplicates(): Boolean = size != distinct().size

Expand All @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ object PartnerFileUtil {
// 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),
address = row.toAddressRepresentationInputDto(formatter, errors, index)
address = row.toAddressRepresentationInputDto(formatter, errors, index),
currentness = null

)
} catch (e: Exception) {
errors.add("Row ${index + 1} has error: ${e.message}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE business_partners
ADD COLUMN currentness TIMESTAMP WITHOUT TIME ZONE NULL
Original file line number Diff line number Diff line change
Expand Up @@ -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", currentness = null)
)) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,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.bpInputRequestWithCurrentness2) //12:00
gateClient.businessParters.upsertBusinessPartnersInput(firstUpsertRequest).body!!

val beforeFirstUpsertRequest = listOf(BusinessPartnerVerboseValues.bpInputRequestWithCurrentness1) // 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 currentness timestamp and the record updated`() {

val firstUpsertRequest = listOf(BusinessPartnerVerboseValues.bpInputRequestWithCurrentness2) //12:00
gateClient.businessParters.upsertBusinessPartnersInput(firstUpsertRequest).body!!

val laterUpsertRequest = listOf(BusinessPartnerVerboseValues.bpInputRequestWithCurrentness3) // 12:01
gateClient.businessParters.upsertBusinessPartnersInput(laterUpsertRequest).body!!

val searchResponsePage = gateClient.businessParters.getBusinessPartnersInput(
listOf(BusinessPartnerVerboseValues.externalId1))

this.mockAndAssertUtils.assertUpsertResponsesMatchRequests(searchResponsePage.content, laterUpsertRequest)
}



Expand Down