From afc50ead46e545cc6f310adcd4cbb9a0d58a47ac Mon Sep 17 00:00:00 2001 From: SujitMBRDI Date: Wed, 12 Jun 2024 17:00:29 +0530 Subject: [PATCH] feat(bpdm): post endpoint to upload business partner data using csv file --- .../bpdm/gate/api/GatePartnerUploadApi.kt | 66 ++++++ .../bpdm/gate/api/client/GateClient.kt | 2 + .../bpdm/gate/api/client/GateClientImpl.kt | 2 + .../gate/api/client/PartnerUploadApiClient.kt | 43 ++++ .../response/PartnerUploadErrorResponse.kt | 36 +++ bpdm-gate/pom.xml | 5 + .../controller/PartnerUploadController.kt | 53 +++++ .../BpdmInvalidPartnerUploadException.kt | 24 ++ .../gate/exception/GateExceptionHandler.kt | 20 +- .../bpdm/gate/model/PartnerUploadFileRow.kt | 214 ++++++++++++++++++ .../bpdm/gate/service/PartnerUploadService.kt | 92 ++++++++ .../bpdm/gate/util/PartnerFileUtil.kt | 149 ++++++++++++ bpdm-gate/src/main/resources/application.yml | 5 + .../controller/PartnerUploadControllerIT.kt | 170 ++++++++++++++ .../resources/testData/empty_partner_data.csv | 0 .../testData/invalid_partner_data.csv | 6 + .../testData/non_csv_partner_data.xls | Bin 0 -> 13312 bytes .../resources/testData/valid_partner_data.csv | 3 + 18 files changed, 889 insertions(+), 1 deletion(-) create mode 100644 bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/GatePartnerUploadApi.kt create mode 100644 bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/client/PartnerUploadApiClient.kt create mode 100644 bpdm-gate-api/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/api/model/response/PartnerUploadErrorResponse.kt create mode 100644 bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/controller/PartnerUploadController.kt create mode 100644 bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/exception/BpdmInvalidPartnerUploadException.kt create mode 100644 bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/model/PartnerUploadFileRow.kt create mode 100644 bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/service/PartnerUploadService.kt create mode 100644 bpdm-gate/src/main/kotlin/org/eclipse/tractusx/bpdm/gate/util/PartnerFileUtil.kt create mode 100644 bpdm-gate/src/test/kotlin/org/eclipse/tractusx/bpdm/gate/controller/PartnerUploadControllerIT.kt create mode 100644 bpdm-gate/src/test/resources/testData/empty_partner_data.csv create mode 100644 bpdm-gate/src/test/resources/testData/invalid_partner_data.csv create mode 100644 bpdm-gate/src/test/resources/testData/non_csv_partner_data.xls create mode 100644 bpdm-gate/src/test/resources/testData/valid_partner_data.csv 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 0000000000000000000000000000000000000000..246a3a358c150d77365b13b460849dbac4b227e2 GIT binary patch literal 13312 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 Bx6=Rs literal 0 HcmV?d00001 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