diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/GatePartnerUploadApi.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/GatePartnerUploadApi.kt new file mode 100644 index 000000000..41d8212d7 --- /dev/null +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/GatePartnerUploadApi.kt @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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.api + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import org.eclipse.tractusx.bpdm.gate.api.GateBusinessPartnerApi.Companion.BUSINESS_PARTNER_PATH +import org.eclipse.tractusx.bpdm.gate.api.model.response.BusinessPartnerInputDto +import org.eclipse.tractusx.bpdm.gate.api.model.response.PartnerUploadErrorResponse +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.multipart.MultipartFile + +@RequestMapping(BUSINESS_PARTNER_PATH, produces = [MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE]) +interface GatePartnerUploadApi { + + companion object{ + const val BUSINESS_PARTNER_PATH = ApiCommons.BASE_PATH + } + + @Operation( + summary = "Create or update business partners from uploaded CSV file", + description = "Create or update generic business partners. " + + "Updates instead of creating a new business partner if an already existing external ID is used. " + + "The same external ID may not occur more than once in a single request. " + + "For file upload request, the maximum number of business partners in file limited to \${bpdm.api.upsert-limit} entries.", + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Business partners were successfully updated or created"), + ApiResponse(responseCode = "400", description = "On malformed Business partner upload request", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = PartnerUploadErrorResponse::class) + )]), + ] + ) + @PostMapping("/input/partner-upload-process/upload-partner-csv", consumes = ["multipart/form-data"]) + fun uploadPartnerCsvFile( + @RequestPart("file") file: MultipartFile + ): ResponseEntity> + +} \ No newline at end of file diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/GateClient.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/GateClient.kt index e1367bc3f..90aee224a 100644 --- a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/GateClient.kt +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/GateClient.kt @@ -30,4 +30,6 @@ interface GateClient { val sharingState: SharingStateApiClient val stats: StatsApiClient + + val partnerUpload: PartnerUploadApiClient } diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/GateClientImpl.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/GateClientImpl.kt index 76c144482..2ebf4aad4 100644 --- a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/GateClientImpl.kt +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/GateClientImpl.kt @@ -49,6 +49,8 @@ class GateClientImpl( override val stats by lazy { createClient() } + override val partnerUpload by lazy { createClient() } + private inline fun createClient() = httpServiceProxyFactory.createClient(T::class.java) } diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/PartnerUploadApiClient.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/PartnerUploadApiClient.kt new file mode 100644 index 000000000..76b3a3bd7 --- /dev/null +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/PartnerUploadApiClient.kt @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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.api.client + +import org.eclipse.tractusx.bpdm.gate.api.GatePartnerUploadApi +import org.eclipse.tractusx.bpdm.gate.api.model.response.BusinessPartnerInputDto +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.multipart.MultipartFile +import org.springframework.web.service.annotation.HttpExchange +import org.springframework.web.service.annotation.PostExchange + +@HttpExchange(GatePartnerUploadApi.BUSINESS_PARTNER_PATH) +interface PartnerUploadApiClient : GatePartnerUploadApi { + + @PostExchange( + url = "/input/partner-upload-process/upload-partner-csv", + contentType = MediaType.MULTIPART_FORM_DATA_VALUE, + accept = ["application/json"] + ) + override fun uploadPartnerCsvFile( + @RequestPart("file") file: MultipartFile + ): ResponseEntity> + +} \ No newline at end of file diff --git a/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/PartnerUploadErrorResponse.kt b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/PartnerUploadErrorResponse.kt new file mode 100644 index 000000000..2e0dbec79 --- /dev/null +++ b/bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/PartnerUploadErrorResponse.kt @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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.api.model.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.springframework.http.HttpStatus +import java.time.Instant + +@Schema(description = "Error response for invalid partner upload") +class PartnerUploadErrorResponse( + @Schema(description = "Timestamp of the error occurrence") + val timestamp: Instant, + @Schema(description = "HTTP status of the error response") + val status: HttpStatus, + @Schema(description = "List of error messages") + val error: List, + @Schema(description = "Request path where the error occurred") + val path: String +) \ No newline at end of file diff --git a/bpdm-gate/pom.xml b/bpdm-gate/pom.xml index db45040ec..7e9294682 100644 --- a/bpdm-gate/pom.xml +++ b/bpdm-gate/pom.xml @@ -161,6 +161,11 @@ org.eclipse.tractusx bpdm-orchestrator-api + + com.opencsv + opencsv + 5.9 + diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/controller/PartnerUploadController.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/controller/PartnerUploadController.kt new file mode 100644 index 000000000..9abb379b4 --- /dev/null +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/controller/PartnerUploadController.kt @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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.controller + +import org.eclipse.tractusx.bpdm.gate.api.GatePartnerUploadApi +import org.eclipse.tractusx.bpdm.gate.api.model.response.BusinessPartnerInputDto +import org.eclipse.tractusx.bpdm.gate.config.ApiConfigProperties +import org.eclipse.tractusx.bpdm.gate.config.PermissionConfigProperties +import org.eclipse.tractusx.bpdm.gate.service.BusinessPartnerService +import org.eclipse.tractusx.bpdm.gate.service.PartnerUploadService +import org.eclipse.tractusx.bpdm.gate.util.getCurrentUserBpn +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +class PartnerUploadController( + val businessPartnerService: BusinessPartnerService, + val apiConfigProperties: ApiConfigProperties, + val partnerUploadService: PartnerUploadService +) : GatePartnerUploadApi { + + @PreAuthorize("hasAuthority(${PermissionConfigProperties.WRITE_INPUT_PARTNER})") + override fun uploadPartnerCsvFile( + file: MultipartFile + ): ResponseEntity> { + return when { + file.isEmpty -> ResponseEntity(HttpStatus.BAD_REQUEST) + !file.contentType.equals("text/csv", ignoreCase = true) -> ResponseEntity(HttpStatus.BAD_REQUEST) + else -> partnerUploadService.processFile(file, getCurrentUserBpn()) + } + } + +} \ No newline at end of file diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/BpdmInvalidPartnerUploadException.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/BpdmInvalidPartnerUploadException.kt new file mode 100644 index 000000000..a19bfce4e --- /dev/null +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/BpdmInvalidPartnerUploadException.kt @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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 + +class BpdmInvalidPartnerUploadException( + val errors: List +) : RuntimeException() \ No newline at end of file diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/GateExceptionHandler.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/GateExceptionHandler.kt index bc0ba8053..36c3dadfc 100644 --- a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/GateExceptionHandler.kt +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/GateExceptionHandler.kt @@ -20,7 +20,25 @@ package org.eclipse.tractusx.bpdm.gate.exception import org.eclipse.tractusx.bpdm.common.exception.BpdmExceptionHandler +import org.eclipse.tractusx.bpdm.gate.api.model.response.PartnerUploadErrorResponse +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.context.request.WebRequest +import java.time.Instant @ControllerAdvice -class GateExceptionHandler : BpdmExceptionHandler() \ No newline at end of file +class GateExceptionHandler : BpdmExceptionHandler() { + + @ExceptionHandler(BpdmInvalidPartnerUploadException::class) + fun handleInvalidPartnerUploadException(ex:BpdmInvalidPartnerUploadException, request: WebRequest): ResponseEntity { + val errorResponse = PartnerUploadErrorResponse( + timestamp = Instant.now(), + status = HttpStatus.BAD_REQUEST, + error = ex.errors, + path = request.getDescription(false) + ) + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse) + } +} diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/model/PartnerUploadFileRow.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/model/PartnerUploadFileRow.kt new file mode 100644 index 000000000..5f5098650 --- /dev/null +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/model/PartnerUploadFileRow.kt @@ -0,0 +1,214 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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.model + +import com.opencsv.bean.CsvBindByName +import jakarta.validation.constraints.NotEmpty + +data class PartnerUploadFileRow( + + @CsvBindByName(column = "externalId") + @field:NotEmpty(message = "Column 'externalId' is missing and can not be empty") + val externalId: String? = null, + + @CsvBindByName(column = "nameParts1") + val nameParts1: String? = null, + + @CsvBindByName(column = "nameParts2") + val nameParts2: String? = null, + + @CsvBindByName(column = "nameParts3") + val nameParts3: String? = null, + + @CsvBindByName(column = "nameParts4") + val nameParts4: String? = null, + + @CsvBindByName(column = "identifiers.type") + val identifiersType: String? = null, + + @CsvBindByName(column = "identifiers.value") + val identifiersValue: String? = null, + + @CsvBindByName(column = "identifiers.issuingBody") + val identifiersIssuingBody: String? = null, + + @CsvBindByName(column = "states.validFrom") + val statesValidFrom: String? = null, + + @CsvBindByName(column = "states.validTo") + val statesValidTo: String? = null, + + @CsvBindByName(column = "states.type") + val statesType: String? = null, + + @CsvBindByName(column = "roles") + val roles: String? = null, + + @CsvBindByName(column = "isOwnCompanyData") + val isOwnCompanyData: String? = null, + + @CsvBindByName(column = "legalEntity.legalEntityBpn") + @field:NotEmpty(message = "Column 'legalEntity.legalEntityBpn' can not be empty") + val legalEntityBpn: String? = null, + + @CsvBindByName(column = "legalEntity.legalName") + val legalEntityName: String? = null, + + @CsvBindByName(column = "legalEntity.shortName") + val legalEntityShortName: String? = null, + + @CsvBindByName(column = "legalEntity.legalForm") + val legalEntityLegalForm: String? = null, + + @CsvBindByName(column = "site.siteBpn") + val siteBpn: String? = null, + + @CsvBindByName(column = "site.name") + val siteName: String? = null, + + @CsvBindByName(column = "site.states.validFrom") + val siteStatesValidFrom: String? = null, + + @CsvBindByName(column = "site.states.validTo") + val siteStatesValidTo: String? = null, + + @CsvBindByName(column = "site.states.type") + val siteStatesType: String? = null, + + @CsvBindByName(column = "address.addressBpn") + val addressBpn: String? = null, + + @CsvBindByName(column = "address.name") + val addressName: String? = null, + + @CsvBindByName(column = "address.addressType") + val addressType: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.geographicCoordinates.longitude") + val physicalPostalAddressLongitude: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.geographicCoordinates.latitude") + val physicalPostalAddressLatitude: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.geographicCoordinates.altitude") + val physicalPostalAddressAltitude: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.country") + val physicalPostalAddressCountry: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.administrativeAreaLevel1") + val physicalPostalAddressAdminArea1: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.administrativeAreaLevel2") + val physicalPostalAddressAdminArea2: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.administrativeAreaLevel3") + val physicalPostalAddressAdminArea3: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.postalCode") + val physicalPostalAddressPostalCode: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.city") + val physicalPostalAddressCity: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.district") + val physicalPostalAddressDistrict: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.namePrefix") + val physicalPostalAddressStreetNamePrefix: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.additionalNamePrefix") + val physicalPostalAddressStreetAdditionalNamePrefix: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.name") + val physicalPostalAddressStreetName: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.nameSuffix") + val physicalPostalAddressStreetNameSuffix: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.additionalNameSuffix") + val physicalPostalAddressStreetAdditionalNameSuffix: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.houseNumber") + val physicalPostalAddressStreetHouseNumber: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.houseNumberSupplement") + val physicalPostalAddressStreetHouseNumberSupplement: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.milestone") + val physicalPostalAddressStreetMilestone: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.street.direction") + val physicalPostalAddressStreetDirection: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.companyPostalCode") + val physicalPostalAddressCompanyPostalCode: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.industrialZone") + val physicalPostalAddressIndustrialZone: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.building") + val physicalPostalAddressBuilding: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.floor") + val physicalPostalAddressFloor: String? = null, + + @CsvBindByName(column = "address.physicalPostalAddress.door") + val physicalPostalAddressDoor: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.geographicCoordinates.longitude") + val alternativePostalAddressLongitude: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.geographicCoordinates.latitude") + val alternativePostalAddressLatitude: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.geographicCoordinates.altitude") + val alternativePostalAddressAltitude: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.country") + val alternativePostalAddressCountry: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.administrativeAreaLevel1") + val alternativePostalAddressAdminArea1: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.postalCode") + val alternativePostalAddressPostalCode: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.city") + val alternativePostalAddressCity: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.deliveryServiceType") + val alternativePostalAddressDeliveryServiceType: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.deliveryServiceQualifier") + val alternativePostalAddressDeliveryServiceQualifier: String? = null, + + @CsvBindByName(column = "address.alternativePostalAddress.deliveryServiceNumber") + val alternativePostalAddressDeliveryServiceNumber: String? = null, + + @CsvBindByName(column = "address.states.validForm") + val addressStatesValidFrom: String? = null, + + @CsvBindByName(column = "address.states.validTo") + val addressStatesValidTo: String? = null, + + @CsvBindByName(column = "address.states.type") + val addressStatesType: String? = null +) diff --git a/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/PartnerUploadService.kt b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/PartnerUploadService.kt new file mode 100644 index 000000000..28096adf7 --- /dev/null +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/PartnerUploadService.kt @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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.service + +import jakarta.validation.Validation +import jakarta.validation.Validator +import mu.KotlinLogging +import org.eclipse.tractusx.bpdm.common.dto.BusinessPartnerRole +import org.eclipse.tractusx.bpdm.gate.api.model.response.BusinessPartnerInputDto +import org.eclipse.tractusx.bpdm.gate.exception.BpdmInvalidPartnerUploadException +import org.eclipse.tractusx.bpdm.gate.model.PartnerUploadFileRow +import org.eclipse.tractusx.bpdm.gate.util.PartnerFileUtil +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile + +@Service +class PartnerUploadService( + private val businessPartnerService: BusinessPartnerService +) { + + private val logger = KotlinLogging.logger { } + + fun processFile(file: MultipartFile, ownerBpnl: String?): ResponseEntity> { + val csvData: List = PartnerFileUtil.parseCsv(file) + validateFileData(csvData) + val businessPartnerDtos = PartnerFileUtil.mapToBusinessPartnerRequests(csvData) + val result = businessPartnerService.upsertBusinessPartnersInput(businessPartnerDtos, ownerBpnl) + return ResponseEntity.ok(result) + } + + /** + * Validates each row in the provided list of PartnerUploadFileRow objects. + * + * This function performs validation on each row of the provided CSV data. It uses + * both Jakarta Bean Validation for standard constraint validations and custom validation + * logic for specific fields. If any validation errors are encountered, an exception is thrown + * immediately, halting further processing. + * + * @param csvData The list of PartnerUploadFileRow objects representing the rows of the CSV file to be validated. + * + * @throws BpdmInvalidPartnerUploadException if any row in the CSV data fails validation. The exception contains + * detailed error messages for each validation failure, specifying the row number and the reason for the error. + */ + private fun validateFileData(csvData: List) { + val validator: Validator = Validation.buildDefaultValidatorFactory().validator + val errors = mutableListOf() + val externalIdSet = mutableSetOf() + + csvData.forEachIndexed { index, rowData -> + val violations = validator.validate(rowData) + logger.debug { "Validating row ${index + 1}: $rowData" } + + if (violations.isNotEmpty()) { + val violationMessages = violations.joinToString("; ") { it.message } + errors.add("Row ${index + 1} having error as $violationMessages") + } + + if (rowData.externalId.isNullOrBlank()) { + errors.add("Row ${index + 1} having error as Column 'externalId' is null or blank.") + } else if (!externalIdSet.add(rowData.externalId)) { + errors.add("Row ${index + 1} having error as Column 'externalId' is already exist.") + } + + if (rowData.roles.isNullOrBlank() || kotlin.runCatching { BusinessPartnerRole.valueOf(rowData.roles) }.isFailure) { + errors.add("Row ${index + 1} having error as Column 'roles' must be one of ${BusinessPartnerRole.entries.joinToString(", ")}.") + } + } + + if (errors.isNotEmpty()) { + throw BpdmInvalidPartnerUploadException(errors) + } + } + +} \ 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 new file mode 100644 index 000000000..b2d69b142 --- /dev/null +++ b/bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/PartnerFileUtil.kt @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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.util + +import com.neovisionaries.i18n.CountryCode +import com.opencsv.bean.CsvToBeanBuilder +import org.eclipse.tractusx.bpdm.common.dto.AddressType +import org.eclipse.tractusx.bpdm.common.dto.BusinessPartnerRole +import org.eclipse.tractusx.bpdm.common.dto.GeoCoordinateDto +import org.eclipse.tractusx.bpdm.common.model.BusinessStateType +import org.eclipse.tractusx.bpdm.common.model.DeliveryServiceType +import org.eclipse.tractusx.bpdm.gate.api.model.* +import org.eclipse.tractusx.bpdm.gate.api.model.request.BusinessPartnerInputRequest +import org.eclipse.tractusx.bpdm.gate.api.model.response.AddressRepresentationInputDto +import org.eclipse.tractusx.bpdm.gate.api.model.response.LegalEntityRepresentationInputDto +import org.eclipse.tractusx.bpdm.gate.api.model.response.SiteRepresentationInputDto +import org.eclipse.tractusx.bpdm.gate.model.PartnerUploadFileRow +import org.springframework.web.multipart.MultipartFile +import java.io.InputStreamReader +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +object PartnerFileUtil { + + fun parseCsv(file: MultipartFile): List { + val reader = InputStreamReader(file.inputStream) + return CsvToBeanBuilder(reader) + .withType(PartnerUploadFileRow::class.java) + .withIgnoreLeadingWhiteSpace(true) + .build() + .parse() + } + + fun mapToBusinessPartnerRequests(csvData: List): List { + val formatter = DateTimeFormatter.ISO_DATE_TIME + + return csvData.map { row -> + BusinessPartnerInputRequest( + externalId = row.externalId?.takeIf { it.isNotEmpty() }.toString(), + nameParts = listOfNotNull(row.nameParts1?.takeIf { it.isNotEmpty() }, row.nameParts2?.takeIf { it.isNotEmpty() }, row.nameParts3?.takeIf { it.isNotEmpty() }, row.nameParts4?.takeIf { it.isNotEmpty() }), + identifiers = listOf( + BusinessPartnerIdentifierDto( + type = row.identifiersType ?.takeIf { it.isNotEmpty() }, + value = row.identifiersValue ?.takeIf { it.isNotEmpty() }, + issuingBody = row.identifiersIssuingBody ?.takeIf { it.isNotEmpty() } + ) + ), + states = listOf(BusinessPartnerStateDto( + validFrom = row.statesValidFrom?.let { LocalDateTime.parse(it, formatter) }, + validTo = row.statesValidTo?.let { LocalDateTime.parse(it, formatter) }, + type = row.statesType?.let { BusinessStateType.valueOf(it) } + )), + roles = listOfNotNull(row.roles?.let { BusinessPartnerRole.valueOf(it) }), + isOwnCompanyData = row.isOwnCompanyData?.toBoolean() ?: false, + legalEntity = LegalEntityRepresentationInputDto( + legalEntityBpn = row.legalEntityBpn?.takeIf { it.isNotEmpty() }, + legalName = row.legalEntityName?.takeIf { it.isNotEmpty() }, + shortName = row.legalEntityShortName ?.takeIf { it.isNotEmpty() }, + legalForm = row.legalEntityLegalForm ?.takeIf { it.isNotEmpty() }, + states = listOf(BusinessPartnerStateDto( + validFrom = row.statesValidFrom?.let { LocalDateTime.parse(it, formatter) }, + validTo = row.statesValidTo?.let { LocalDateTime.parse(it, formatter) }, + type = row.statesType?.let { BusinessStateType.valueOf(it) } + )) + ), + site = SiteRepresentationInputDto( + siteBpn = row.siteBpn ?.takeIf { it.isNotEmpty() }, + name = row.siteName ?.takeIf { it.isNotEmpty() }, + states = listOf(BusinessPartnerStateDto( + validFrom = row.siteStatesValidFrom?.let { LocalDateTime.parse(it, formatter) }, + validTo = row.siteStatesValidFrom?.let { LocalDateTime.parse(it, formatter) }, + type = row.siteStatesType?.let { BusinessStateType.valueOf(it) } + )) + ), + address = AddressRepresentationInputDto( + addressBpn = row.addressBpn ?.takeIf { it.isNotEmpty() }, + name = row.addressName ?.takeIf { it.isNotEmpty() }, + addressType = AddressType.valueOf(row.addressType ?.takeIf { it.isNotEmpty() }.toString()), + physicalPostalAddress = PhysicalPostalAddressDto( + geographicCoordinates = GeoCoordinateDto( + longitude = row.physicalPostalAddressLongitude?.toFloatOrNull() ?: 0f, + latitude = row.physicalPostalAddressLatitude?.toFloatOrNull() ?: 0f, + altitude = row.physicalPostalAddressAltitude?.toFloatOrNull() ?: 0f + ), + country = row.physicalPostalAddressCountry?.let { CountryCode.valueOf(it) }, + administrativeAreaLevel1 = row.physicalPostalAddressAdminArea1 ?.takeIf { it.isNotEmpty() }, + administrativeAreaLevel2 = row.physicalPostalAddressAdminArea2 ?.takeIf { it.isNotEmpty() }, + administrativeAreaLevel3 = row.physicalPostalAddressAdminArea3 ?.takeIf { it.isNotEmpty() }, + postalCode = row.physicalPostalAddressPostalCode ?.takeIf { it.isNotEmpty() }, + city = row.physicalPostalAddressCity ?.takeIf { it.isNotEmpty() }, + district = row.physicalPostalAddressDistrict ?.takeIf { it.isNotEmpty() }, + street = StreetDto( + namePrefix = row.physicalPostalAddressStreetAdditionalNamePrefix ?.takeIf { it.isNotEmpty() }, + additionalNamePrefix = row.physicalPostalAddressStreetAdditionalNameSuffix ?.takeIf { it.isNotEmpty() }, + name = row.physicalPostalAddressStreetName ?.takeIf { it.isNotEmpty() }, + nameSuffix = row.physicalPostalAddressStreetNameSuffix ?.takeIf { it.isNotEmpty() }, + additionalNameSuffix = row.physicalPostalAddressStreetAdditionalNameSuffix ?.takeIf { it.isNotEmpty() }, + houseNumber = row.physicalPostalAddressStreetHouseNumber ?.takeIf { it.isNotEmpty() }, + houseNumberSupplement = row.physicalPostalAddressStreetHouseNumberSupplement ?.takeIf { it.isNotEmpty() }, + milestone = row.physicalPostalAddressStreetMilestone ?.takeIf { it.isNotEmpty() }, + direction = row.physicalPostalAddressStreetDirection ?.takeIf { it.isNotEmpty() } + ), + companyPostalCode = row.physicalPostalAddressCompanyPostalCode ?.takeIf { it.isNotEmpty() }, + industrialZone = row.physicalPostalAddressIndustrialZone ?.takeIf { it.isNotEmpty() }, + building = row.physicalPostalAddressBuilding ?.takeIf { it.isNotEmpty() }, + floor = row.physicalPostalAddressFloor ?.takeIf { it.isNotEmpty() }, + door = row.physicalPostalAddressDoor ?.takeIf { it.isNotEmpty() } + ), + alternativePostalAddress = AlternativePostalAddressDto( + geographicCoordinates = GeoCoordinateDto( + longitude = row.alternativePostalAddressLongitude?.toFloatOrNull() ?: 0f, + latitude = row.alternativePostalAddressLatitude?.toFloatOrNull() ?: 0f, + altitude = row.alternativePostalAddressAltitude?.toFloatOrNull() ?: 0f + ), + country = row.alternativePostalAddressCountry?.let { CountryCode.valueOf(it) }, + administrativeAreaLevel1 = row.alternativePostalAddressAdminArea1 ?.takeIf { it.isNotEmpty() }, + postalCode = row.alternativePostalAddressPostalCode ?.takeIf { it.isNotEmpty() }, + city = row.alternativePostalAddressCity ?.takeIf { it.isNotEmpty() }, + deliveryServiceType = row.alternativePostalAddressDeliveryServiceType?.let { DeliveryServiceType.valueOf(it) }, + deliveryServiceQualifier = row.alternativePostalAddressDeliveryServiceQualifier ?.takeIf { it.isNotEmpty() }, + deliveryServiceNumber = row.alternativePostalAddressDeliveryServiceNumber ?.takeIf { it.isNotEmpty() } + ), + states = listOf(BusinessPartnerStateDto( + validFrom = row.addressStatesValidFrom?.let { LocalDateTime.parse(it, formatter) }, + validTo = row.addressStatesValidTo?.let { LocalDateTime.parse(it, formatter) }, + type = row.addressStatesType?.let { BusinessStateType.valueOf(it) } + )) + ) + ) + } + } +} \ No newline at end of file diff --git a/bpdm-gate/src/main/resources/application.yml b/bpdm-gate/src/main/resources/application.yml index df8dc8b86..628f00906 100644 --- a/bpdm-gate/src/main/resources/application.yml +++ b/bpdm-gate/src/main/resources/application.yml @@ -193,6 +193,10 @@ spring: batch_size: 16 order_inserts: true order_updates: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB springdoc: api-docs: # Generate Open-API document @@ -211,3 +215,4 @@ springdoc: + diff --git a/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/PartnerUploadControllerIT.kt b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/PartnerUploadControllerIT.kt new file mode 100644 index 000000000..8e9dea326 --- /dev/null +++ b/bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/PartnerUploadControllerIT.kt @@ -0,0 +1,170 @@ +/******************************************************************************* + * Copyright (c) 2021,2024 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.controller + +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.junit5.WireMockExtension +import org.eclipse.tractusx.bpdm.gate.api.client.GateClient +import org.eclipse.tractusx.bpdm.gate.api.model.SharingStateType +import org.eclipse.tractusx.bpdm.gate.api.model.response.SharingStateDto +import org.eclipse.tractusx.bpdm.gate.service.GoldenRecordTaskService +import org.eclipse.tractusx.bpdm.gate.util.MockAndAssertUtils +import org.eclipse.tractusx.bpdm.test.containers.PostgreSQLContextInitializer +import org.eclipse.tractusx.bpdm.test.testdata.gate.BusinessPartnerVerboseValues +import org.eclipse.tractusx.bpdm.test.util.AssertHelpers +import org.eclipse.tractusx.bpdm.test.util.DbTestHelpers +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.extension.RegisterExtension +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpStatus +import org.springframework.mock.web.MockMultipartFile +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertySource +import org.springframework.web.reactive.function.client.WebClientResponseException +import java.nio.file.Files +import java.nio.file.Paths + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT +) +@ActiveProfiles("test-no-auth") +@ContextConfiguration(initializers = [PostgreSQLContextInitializer::class]) +class PartnerUploadControllerIT @Autowired constructor( + val testHelpers: DbTestHelpers, + val assertHelpers: AssertHelpers, + val gateClient: GateClient, + val goldenRecordTaskService: GoldenRecordTaskService, + val mockAndAssertUtils: MockAndAssertUtils +){ + + companion object { + + @JvmField + @RegisterExtension + val orchestratorWireMockServer: WireMockExtension = WireMockExtension.newInstance().options(WireMockConfiguration.wireMockConfig().dynamicPort()).build() + + @JvmField + @RegisterExtension + val poolWireMockServer: WireMockExtension = WireMockExtension.newInstance().options(WireMockConfiguration.wireMockConfig().dynamicPort()).build() + + + @JvmStatic + @DynamicPropertySource + fun properties(registry: DynamicPropertyRegistry) { + registry.add("bpdm.client.pool.base-url") { poolWireMockServer.baseUrl() } + registry.add("bpdm.client.orchestrator.base-url") { orchestratorWireMockServer.baseUrl() } + } + } + + @BeforeEach + fun beforeEach() { + testHelpers.truncateDbTables() + orchestratorWireMockServer.resetAll(); + poolWireMockServer.resetAll() + this.mockAndAssertUtils.mockOrchestratorApi(orchestratorWireMockServer) + } + + @Test + fun testPartnerDataFileUploadScenarios() { + // Test valid CSV file upload + testFileUpload("src/test/resources/testData/valid_partner_data.csv", HttpStatus.OK) + + // Test empty CSV file upload + testFileUpload("src/test/resources/testData/empty_partner_data.csv", HttpStatus.BAD_REQUEST) + + // Test non-CSV file upload + testFileUpload("src/test/resources/testData/non_csv_partner_data.xls", HttpStatus.BAD_REQUEST) + + // Test invalid CSV file upload - contains bad business partner data + testFileUpload("src/test/resources/testData/invalid_partner_data.csv", HttpStatus.BAD_REQUEST) + } + + @Test + fun testUploadPartnerDataAndCheckSharingState() { + val bytes = Files.readAllBytes(Paths.get("src/test/resources/testData/valid_partner_data.csv")) + val uploadedFile = MockMultipartFile("valid_partner_data.csv", "valid_partner_data.csv", "text/csv", bytes) + + uploadBusinessPartnerRecordAndShare(uploadedFile) + + val externalId1 = BusinessPartnerVerboseValues.externalId1 + val externalId2 = BusinessPartnerVerboseValues.externalId2 + + val externalIds = listOf(externalId1, externalId2) + + val sharingStatesRequests = listOf( + SharingStateDto( + externalId = externalId1, + sharingStateType = SharingStateType.Pending, + sharingErrorCode = null, + sharingErrorMessage = null, + sharingProcessStarted = null, + taskId = "0" + ), + SharingStateDto( + externalId = externalId2, + sharingStateType = SharingStateType.Pending, + sharingErrorCode = null, + sharingErrorMessage = null, + sharingProcessStarted = null, + taskId = "1" + ) + ) + + val sharingStateResponses = this.mockAndAssertUtils.readSharingStates(externalIds) + assertHelpers.assertRecursively(sharingStateResponses).isEqualTo(sharingStatesRequests) + } + + + + private fun testFileUpload(filePath: String, expectedStatus: HttpStatus) { + val bytes = Files.readAllBytes(Paths.get(filePath)) + val file = MockMultipartFile(filePath.substringAfterLast('/'), filePath.substringAfterLast('/'), determineFileType(filePath), bytes) + + if (expectedStatus == HttpStatus.OK) { + val response = gateClient.partnerUpload.uploadPartnerCsvFile(file) + assertEquals(expectedStatus, response.statusCode) + } else { + val exception = assertThrows { + gateClient.partnerUpload.uploadPartnerCsvFile(file) + } + assertEquals(expectedStatus, exception.statusCode) + } + } + + private fun determineFileType(filePath: String): String { + return when (filePath.substringAfterLast('.')) { + "csv" -> "text/csv" + "xls" -> "application/vnd.ms-excel" + else -> "application/octet-stream" + } + } + + private fun uploadBusinessPartnerRecordAndShare(file: MockMultipartFile) { + gateClient.partnerUpload.uploadPartnerCsvFile(file) + goldenRecordTaskService.createTasksForReadyBusinessPartners() + } + +} diff --git a/bpdm-gate/src/test/resources/testData/empty_partner_data.csv b/bpdm-gate/src/test/resources/testData/empty_partner_data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/bpdm-gate/src/test/resources/testData/invalid_partner_data.csv b/bpdm-gate/src/test/resources/testData/invalid_partner_data.csv new file mode 100644 index 000000000..91b12a79e --- /dev/null +++ b/bpdm-gate/src/test/resources/testData/invalid_partner_data.csv @@ -0,0 +1,6 @@ +externalId,nameParts1,nameParts2,nameParts3,nameParts4,identifiers.type,identifiers.value,identifiers.issuingBody,states.validFrom,states.validTo,states.type,roles,isOwnCompanyData,legalEntity.legalEntityBpn,legalEntity.legalName,legalEntity.shortName,legalEntity.legalForm,legalEntity.states.validFrom,legalEntity.states.validTo,legalEntity.states.type,site.siteBpn,site.name,site.states.validFrom,site.states.validTo,site.states.type,address.addressBpn,address.name,address.addressType,address.physicalPostalAddress.geographicCoordinates.longitude,address.physicalPostalAddress.geographicCoordinates.latitude,address.physicalPostalAddress.geographicCoordinates.altitude,address.physicalPostalAddress.country,address.physicalPostalAddress.administrativeAreaLevel1,address.physicalPostalAddress.administrativeAreaLevel2,address.physicalPostalAddress.administrativeAreaLevel3,address.physicalPostalAddress.postalCode,address.physicalPostalAddress.city,address.physicalPostalAddress.district,address.physicalPostalAddress.street.namePrefix,address.physicalPostalAddress.street.additionalNamePrefix,address.physicalPostalAddress.street.name,address.physicalPostalAddress.street.nameSuffix,address.physicalPostalAddress.street.additionalNameSuffix,address.physicalPostalAddress.street.houseNumber,address.physicalPostalAddress.street.houseNumberSupplement,address.physicalPostalAddress.street.milestone,address.physicalPostalAddress.street.direction,address.physicalPostalAddress.companyPostalCode,address.physicalPostalAddress.industrialZone,address.physicalPostalAddress.building,address.physicalPostalAddress.floor,address.physicalPostalAddress.door,address.alternativePostalAddress.geographicCoordinates.longitude,address.alternativePostalAddress.geographicCoordinates.latitude,address.alternativePostalAddress.geographicCoordinates.altitude,address.alternativePostalAddress.country,address.alternativePostalAddress.administrativeAreaLevel1,address.alternativePostalAddress.postalCode,address.alternativePostalAddress.city,address.alternativePostalAddress.deliveryServiceType,address.alternativePostalAddress.deliveryServiceQualifier,address.alternativePostalAddress.deliveryServiceNumber,address.states.validForm,address.states.validTo,address.states.type +external-1,namePart1-1,namePart1-2,namePart1-3,namePart1-4,type1,value1,issuingBody1,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,SUPPLIER,TRUE,BPNL00000001DEVT,LegalEntityName1,ShortName1,LegalForm1,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNS00000001DEVT,SiteName1,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNA00000001DEVT,AddressName1,LegalAndSiteMainAddress,10.1234,20.5678,100,DE,AdminLevel1-1,AdminLevel1-2,AdminLevel1-3,123456,City1,District1,Prefix1,AdditionalPrefix1,StreetName1,Suffix1,AdditionalSuffix1,123,Supplement1,Milestone1,Direction1,CompanyPostalCode1,IndustrialZone1,Building1,Floor1,Door1,10.1111,20.2222,150,DE,AdminLevel2-1,654321,City1,PO-BOX,DeliveryQualifier1,DeliveryNumber1,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE +external-2,namePart1-1,namePart1-2,namePart1-3,namePart1-4,type2,value2,issuingBody2,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,SUPPLIER,TRUE,BPNL00000001DEVT,LegalEntityName2,ShortName2,LegalForm2,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNS00000001DEVT,SiteName2,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNA00000001DEVT,AddressName2,LegalAndSiteMainAddress,10.1234,20.5678,100,DE,AdminLevel1-1,AdminLevel1-2,AdminLevel1-3,123457,City2,District2,Prefix2,AdditionalPrefix2,StreetName2,Suffix2,AdditionalSuffix2,124,Supplement2,Milestone2,Direction2,CompanyPostalCode2,IndustrialZone2,Building2,Floor2,Door2,10.1111,20.2222,150,DE,AdminLevel2-2,654322,City2,PO-BOX,DeliveryQualifier2,DeliveryNumber2,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE +,namePart1-1,namePart1-2,namePart1-3,namePart1-4,type3,value3,issuingBody3,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,SUPPLIER,TRUE,BPNL00000001DEVT,LegalEntityName3,ShortName3,LegalForm3,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNS00000001DEVT,SiteName3,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNA00000001DEVT,AddressName3,LegalAndSiteMainAddress,10.1234,20.5678,100,DE,AdminLevel1-1,AdminLevel1-2,AdminLevel1-3,123458,City3,District3,Prefix3,AdditionalPrefix3,StreetName3,Suffix3,AdditionalSuffix3,125,Supplement3,Milestone3,Direction3,CompanyPostalCode3,IndustrialZone3,Building3,Floor3,Door3,10.1111,20.2222,150,DE,AdminLevel2-3,654323,City3,PO-BOX,DeliveryQualifier3,DeliveryNumber3,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE +external-4,namePart1-1,namePart1-2,namePart1-3,namePart1-4,type4,value4,issuingBody4,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,TEST,TRUE,BPNL00000001DEVT,LegalEntityName4,ShortName4,LegalForm4,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNS00000001DEVT,SiteName4,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNA00000001DEVT,AddressName4,LegalAndSiteMainAddress,10.1234,20.5678,100,DE,AdminLevel1-1,AdminLevel1-2,AdminLevel1-3,123459,City4,District4,Prefix4,AdditionalPrefix4,StreetName4,Suffix4,AdditionalSuffix4,126,Supplement4,Milestone4,Direction4,CompanyPostalCode4,IndustrialZone4,Building4,Floor4,Door4,10.1111,20.2222,150,DE,AdminLevel2-4,654324,City4,PO-BOX,DeliveryQualifier4,DeliveryNumber4,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE +external-1,namePart1-1,namePart1-2,namePart1-3,namePart1-4,type5,value5,issuingBody5,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,SUPPLIER,TRUE,BPNL00000001DEVT,LegalEntityName5,ShortName5,LegalForm5,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNS00000001DEVT,SiteName5,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNA00000001DEVT,AddressName5,LegalAndSiteMainAddress,10.1234,20.5678,100,DE,AdminLevel1-1,AdminLevel1-2,AdminLevel1-3,123460,City5,District5,Prefix5,AdditionalPrefix5,StreetName5,Suffix5,AdditionalSuffix5,127,Supplement5,Milestone5,Direction5,CompanyPostalCode5,IndustrialZone5,Building5,Floor5,Door5,10.1111,20.2222,150,DE,AdminLevel2-5,654325,City5,PO-BOX,DeliveryQualifier5,DeliveryNumber5,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE diff --git a/bpdm-gate/src/test/resources/testData/non_csv_partner_data.xls b/bpdm-gate/src/test/resources/testData/non_csv_partner_data.xls new file mode 100644 index 000000000..246a3a358 Binary files /dev/null and b/bpdm-gate/src/test/resources/testData/non_csv_partner_data.xls differ diff --git a/bpdm-gate/src/test/resources/testData/valid_partner_data.csv b/bpdm-gate/src/test/resources/testData/valid_partner_data.csv new file mode 100644 index 000000000..e9be1bd8e --- /dev/null +++ b/bpdm-gate/src/test/resources/testData/valid_partner_data.csv @@ -0,0 +1,3 @@ +externalId,nameParts1,nameParts2,nameParts3,nameParts4,identifiers.type,identifiers.value,identifiers.issuingBody,states.validFrom,states.validTo,states.type,roles,isOwnCompanyData,legalEntity.legalEntityBpn,legalEntity.legalName,legalEntity.shortName,legalEntity.legalForm,legalEntity.states.validFrom,legalEntity.states.validTo,legalEntity.states.type,site.siteBpn,site.name,site.states.validFrom,site.states.validTo,site.states.type,address.addressBpn,address.name,address.addressType,address.physicalPostalAddress.geographicCoordinates.longitude,address.physicalPostalAddress.geographicCoordinates.latitude,address.physicalPostalAddress.geographicCoordinates.altitude,address.physicalPostalAddress.country,address.physicalPostalAddress.administrativeAreaLevel1,address.physicalPostalAddress.administrativeAreaLevel2,address.physicalPostalAddress.administrativeAreaLevel3,address.physicalPostalAddress.postalCode,address.physicalPostalAddress.city,address.physicalPostalAddress.district,address.physicalPostalAddress.street.namePrefix,address.physicalPostalAddress.street.additionalNamePrefix,address.physicalPostalAddress.street.name,address.physicalPostalAddress.street.nameSuffix,address.physicalPostalAddress.street.additionalNameSuffix,address.physicalPostalAddress.street.houseNumber,address.physicalPostalAddress.street.houseNumberSupplement,address.physicalPostalAddress.street.milestone,address.physicalPostalAddress.street.direction,address.physicalPostalAddress.companyPostalCode,address.physicalPostalAddress.industrialZone,address.physicalPostalAddress.building,address.physicalPostalAddress.floor,address.physicalPostalAddress.door,address.alternativePostalAddress.geographicCoordinates.longitude,address.alternativePostalAddress.geographicCoordinates.latitude,address.alternativePostalAddress.geographicCoordinates.altitude,address.alternativePostalAddress.country,address.alternativePostalAddress.administrativeAreaLevel1,address.alternativePostalAddress.postalCode,address.alternativePostalAddress.city,address.alternativePostalAddress.deliveryServiceType,address.alternativePostalAddress.deliveryServiceQualifier,address.alternativePostalAddress.deliveryServiceNumber,address.states.validForm,address.states.validTo,address.states.type +external-1,namePart1_test,namePart1_2,namePart1_3,namePart1_4,type1,value1,issuingBody1,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,SUPPLIER,TRUE,BPNL00000001DEVT,LegalEntityName1,ShortName1,LegalForm1,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNS00000001DEVT,SiteName1,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE,BPNA00000001DEVT,AddressName1,LegalAndSiteMainAddress,10.1234,20.5678,100,DE,AdminLevel1_1,AdminLevel1_2,AdminLevel1_3,123456,City1,District1,Prefix1,AdditionalPrefix1,StreetName1,Suffix1,AdditionalSuffix1,123,Supplement1,Milestone1,Direction1,CompanyPostalCode1,IndustrialZone1,Building1,Floor1,Door1,10.1111,20.2222,150,DE,AdminLevel2_1,654321,City1,PO_BOX,DeliveryQualifier1,DeliveryNumber1,2024-05-01T00:00:00Z,2024-05-31T00:00:00Z,ACTIVE +external-2,namePart2_1,namePart2_2,namePart2_3,namePart2_4,type2,value2,issuingBody2,2024-06-01T00:00:00Z,2024-06-30T00:00:00Z,ACTIVE,SUPPLIER,FALSE,BPNL00000002DEVT,LegalEntityName2,ShortName2,LegalForm2,2024-06-01T00:00:00Z,2024-06-30T00:00:00Z,ACTIVE,BPNS00000002DEVT,SiteName2,2024-06-01T00:00:00Z,2024-06-30T00:00:00Z,ACTIVE,BPNA00000002DEVT,AddressName2,LegalAndSiteMainAddress,30.9876,40.6543,200,DE,AdminLevel1_1,AdminLevel1_2,AdminLevel1_3,987654,City2,District2,Prefix2,AdditionalPrefix2,StreetName2,Suffix2,AdditionalSuffix2,456,Supplement2,Milestone2,Direction2,CompanyPostalCode2,IndustrialZone2,Building2,Floor2,Door2,30.3333,40.4444,250,DE,AdminLevel2_2,987456,City2,PO_BOX,DeliveryQualifier2,DeliveryNumber2,2024-06-01T00:00:00Z,2024-06-30T00:00:00Z,ACTIVE