diff --git a/backend/Dockerfile b/backend/Dockerfile index 2dbe81c6..4b18cce4 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -18,7 +18,7 @@ # # SPDX-License-Identifier: Apache-2.0 # -FROM maven:3.9.6-eclipse-temurin-21 as build +FROM maven:3.9.6-eclipse-temurin-21 AS build RUN mkdir -p /app/legal WORKDIR /app COPY pom.xml . diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/DataInjectionCommandLineRunner.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/DataInjectionCommandLineRunner.java index ef6793ab..4e87cbb7 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/DataInjectionCommandLineRunner.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/DataInjectionCommandLineRunner.java @@ -24,9 +24,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.puris.backend.common.domain.model.measurement.ItemUnitEnumeration; import org.eclipse.tractusx.puris.backend.common.util.VariablesService; -import org.eclipse.tractusx.puris.backend.erpadapter.domain.model.ErpAdapterRequest; -import org.eclipse.tractusx.puris.backend.erpadapter.logic.service.ErpAdapterRequestService; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Material; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.MaterialPartnerRelation; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; @@ -38,8 +37,6 @@ import org.eclipse.tractusx.puris.backend.stock.domain.model.ProductItemStock; import org.eclipse.tractusx.puris.backend.stock.domain.model.ReportedMaterialItemStock; import org.eclipse.tractusx.puris.backend.stock.domain.model.ReportedProductItemStock; -import org.eclipse.tractusx.puris.backend.common.domain.model.measurement.ItemUnitEnumeration; -import org.eclipse.tractusx.puris.backend.stock.logic.dto.itemstocksamm.DirectionCharacteristic; import org.eclipse.tractusx.puris.backend.stock.logic.service.MaterialItemStockService; import org.eclipse.tractusx.puris.backend.stock.logic.service.ProductItemStockService; import org.eclipse.tractusx.puris.backend.stock.logic.service.ReportedMaterialItemStockService; @@ -78,8 +75,6 @@ public class DataInjectionCommandLineRunner implements CommandLineRunner { @Autowired private VariablesService variablesService; - @Autowired - private ErpAdapterRequestService erpAdapterRequestService; private ObjectMapper objectMapper; @@ -246,20 +241,6 @@ private void setupCustomerRole() throws JsonProcessingException { .locationBpna(supplierPartner.getSites().first().getAddresses().first().getBpna()) .build(); reportedProductItemStockService.create(reportedProductItemStock); - - // TODO: remove mock - ErpAdapterRequest mockRequest = ErpAdapterRequest - .builder() - .partnerBpnl(supplierPartner.getBpnl()) - .requestDate(new Date(System.currentTimeMillis()-3*60*60*1000)) - .ownMaterialNumber(semiconductorMaterial.getOwnMaterialNumber()) - .directionCharacteristic(DirectionCharacteristic.INBOUND) - .requestType("ItemStock") - .sammVersion("2.0") - .responseCode(201) - .build(); - mockRequest = erpAdapterRequestService.create(mockRequest); - log.info("Created mocked ErpAdapterRequest: \n{}", mockRequest); } /** diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/common/edc/domain/model/AssetType.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/common/edc/domain/model/AssetType.java index f9be1cc3..4e435b28 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/common/edc/domain/model/AssetType.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/common/edc/domain/model/AssetType.java @@ -21,19 +21,23 @@ package org.eclipse.tractusx.puris.backend.common.edc.domain.model; public enum AssetType { - DTR("none", "none"), - ITEM_STOCK_SUBMODEL("urn:samm:io.catenax.item_stock:2.0.0#ItemStock", "$value"), - PRODUCTION_SUBMODEL("urn:samm:io.catenax.planned_production_output:2.0.0#PlannedProductionOutput", "$value"), - DEMAND_SUBMODEL("urn:samm:io.catenax.short_term_material_demand:1.0.0#ShortTermMaterialDemand", "$value"), - DELIVERY_SUBMODEL("urn:samm:io.catenax.delivery_information:2.0.0#DeliveryInformation", "$value"), - NOTIFICATION("urn:samm:io.catenax.demand_and_capacity_notification:2.0.0#DemandAndCapacityNotification", "none"), - PART_TYPE_INFORMATION_SUBMODEL("urn:samm:io.catenax.part_type_information:1.0.0#PartTypeInformation", "$value"); + DTR("none", "none", "none", "none"), + ITEM_STOCK_SUBMODEL("urn:samm:io.catenax.item_stock:2.0.0#ItemStock", "$value", "ItemStock", "2.0"), + PRODUCTION_SUBMODEL("urn:samm:io.catenax.planned_production_output:2.0.0#PlannedProductionOutput", "$value", "PlannedProductionOutput", "2.0"), + DEMAND_SUBMODEL("urn:samm:io.catenax.short_term_material_demand:1.0.0#ShortTermMaterialDemand", "$value", "ShortTermMaterialDemand", "1.0"), + DELIVERY_SUBMODEL("urn:samm:io.catenax.delivery_information:2.0.0#DeliveryInformation", "$value", "DeliveryInformation", "2.0"), + NOTIFICATION("urn:samm:io.catenax.demand_and_capacity_notification:2.0.0#DemandAndCapacityNotification", "none", "none", "2.0"), + PART_TYPE_INFORMATION_SUBMODEL("urn:samm:io.catenax.part_type_information:1.0.0#PartTypeInformation", "$value", "none", "1.0"); public final String URN_SEMANTIC_ID; public final String REPRESENTATION; + public final String ERP_KEYWORD; + public final String ERP_SAMMVERSION; - AssetType(String URN_SEMANTIC_ID, String REPRESENTATION) { + AssetType(String URN_SEMANTIC_ID, String REPRESENTATION, String ERP_KEYWORD, String ERP_SAMMVERSION) { this.URN_SEMANTIC_ID = URN_SEMANTIC_ID; this.REPRESENTATION = REPRESENTATION; + this.ERP_KEYWORD = ERP_KEYWORD; + this.ERP_SAMMVERSION = ERP_SAMMVERSION; } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/ErpAdapterConfiguration.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/ErpAdapterConfiguration.java index edcc6871..fd99682c 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/ErpAdapterConfiguration.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/ErpAdapterConfiguration.java @@ -1,5 +1,6 @@ package org.eclipse.tractusx.puris.backend.erpadapter; +import lombok.AccessLevel; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -40,4 +41,58 @@ public class ErpAdapterConfiguration { */ @Value("${puris.erpadapter.authsecret}") private String erpAdapterAuthSecret; + + @Value("${puris.erpadapter.refreshinterval}") + @Getter(AccessLevel.NONE) + private long refreshInterval; + + @Value("${puris.erpadapter.timelimit}") + @Getter(AccessLevel.NONE) + private long refreshTimeLimit; + + /** + * Period since last received partner request after which no more new update requests to the + * erp adapter will be sent (milliseconds). + * That means: Adding this period to the date and time of the last received request results in that + * point in time, when the ErpAdapterTriggerService assumes, that this specific kind of request + * is no longer relevant and will stop issuing scheduled update requests to the ErpAdapter. + * + *

+ * Example: Let's assume we have set this variable to the equivalent of seven days (in milliseconds). + * Let's also assume that we have received a request from a specific partner for a specific material + * and a specific submodel (and possibly also a specific direction characteristic) on May 15 10:39:21 GMT 2024. + * + *

+ * Then the ErpAdapterTriggerService will issue scheduled requests for new updates from the ErpAdapter, for at least seven days. + * + * After seven days (i.e. at or a few seconds after May 22 10:39:21 GMT 2024), and, of course + * assuming that we didn't receive any requests with the exact same specifics from the same partner in the meantime, + * then no more scheduled requests with these specifics will be sent out to the ErpAdapter. + * + * + * @return the time period + */ + public long getRefreshTimeLimit() { + // translate days to milliseconds + return refreshTimeLimit * 24 * 60 * 60 * 1000; + } + + /** + * Interval between two scheduled requests to the erp adapter for the same issue (milliseconds) + *

+ * Example: Let's assume, that this variable is set to the equivalent of 3 hours (in milliseconds) + * Let's also assume that we have received a request from a specific partner for a specific material + * and a specific submodel (and possibly also a specific direction characteristic) on May 15 10:39:21 GMT 2024. + *

+ * Then ErpAdapterTriggerService will schedule the next request to the ErpAdapter with the specifics of that aforementioned + * request at or a few seconds after May 15 13:39:21 GMT 2024. + * + * These update requests will perpetuate with the given interval, for as long as the refreshTimeLimit has not expired. + * + * @return the interval + */ + public long getRefreshInterval() { + // translate minutes to milliseconds + return refreshInterval * 60 * 1000; + } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/controller/ErpAdapterController.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/controller/ErpAdapterController.java index 3214296e..595e4cf6 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/controller/ErpAdapterController.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/controller/ErpAdapterController.java @@ -28,12 +28,16 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.puris.backend.common.edc.domain.model.AssetType; +import org.eclipse.tractusx.puris.backend.erpadapter.logic.service.ErpAdapterTriggerService; import org.eclipse.tractusx.puris.backend.erpadapter.logic.service.ItemStockErpAdapterService; +import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; +import org.eclipse.tractusx.puris.backend.stock.logic.dto.itemstocksamm.DirectionCharacteristic; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.Arrays; import java.util.Date; import java.util.UUID; @@ -48,11 +52,43 @@ public class ErpAdapterController { @Autowired private ItemStockErpAdapterService itemStockErpAdapterService; + @Autowired + private ErpAdapterTriggerService erpAdapterTriggerService; + + @Autowired + private MaterialPartnerRelationService mprService; + + @Operation(description = "This endpoint is used to trigger scheduled updates from the ErpAdapter. This is useful " + + "if you are expecting a specific request from a partner in the near future and want to make a best-effort attempt to ensure " + + "that your PURIS backend has already obtained current data to respond to that expected request, when it arrives. " + + "Please note, that calling this endpoint has no significant effect, if a request with the exact same specifics is already " + + "currently in place. In that case, a call to this endpoint will only extend the period, after which the scheduled request will " + + "be assumed to be irrelevant (see the puris.erpadapter.timelimit property and its documentation for details in this regard). ") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "accepted"), + @ApiResponse(responseCode = "400", description = "bad request") + }) + @PostMapping("/trigger") + public ResponseEntity scheduleErpUpdate( + @RequestParam("partner-bpnl") String bpnl, + @RequestParam("own-materialnumber") String materialNumber, + @RequestParam("asset-type") AssetType assetType, + @RequestParam(required = false, value = "direction") DirectionCharacteristic directionCharacteristic + ) { + boolean valid = BPNL_PATTERN.matcher(bpnl).matches() + && NON_EMPTY_NON_VERTICAL_WHITESPACE_PATTERN.matcher(materialNumber).matches(); + if (valid && mprService.find(bpnl, materialNumber) != null) { + erpAdapterTriggerService.notifyPartnerRequest(bpnl, materialNumber, assetType, directionCharacteristic); + return ResponseEntity.status(201).build(); + } else { + return ResponseEntity.badRequest().build(); + } + } + + + @Operation(description = "This endpoint accepts responses from the ERP adapter. Incoming messages are expected to " + - "carry a SAMM of the previously requested type. \n\nPlease note that this version currently accepts multiple responses " + - "addressing the same request-id for testing purposes. However, in the near future, this will be enforced strictly. " + - "I.e. only the first response for a given request-id will be accepted. All later responses addressing the same request-id" + - " will be rejected (status code 409)\n\n" + + "carry a SAMM of the previously requested type. \n\n" + "Currently supported: \n\n" + "| response-type | samm-version |\n" + "|---------------|--------------|\n" + @@ -73,24 +109,25 @@ public ResponseEntity putMethod( @RequestParam("response-type") String responseType, @RequestParam("samm-version") String sammVersion, @RequestParam(value = "response-timestamp") - @Parameter(example = "2024-05-28T15:00:00") - @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") Date responseTimestamp, + @Parameter(example = "1719295545654", description = "Represented as the number of milliseconds since January 1, 1970, 00:00:00 GMT") + long responseTimestamp, @io.swagger.v3.oas.annotations.parameters.RequestBody(content = {@Content(examples = { @ExampleObject(itemStock20Sample) })}) @RequestBody JsonNode requestBody ) { - boolean valid = BPNL_PATTERN.matcher(partnerBpnl).matches(); - valid = valid && NON_EMPTY_NON_VERTICAL_WHITESPACE_PATTERN.matcher(responseType).matches(); - valid = valid && NON_EMPTY_NON_VERTICAL_WHITESPACE_PATTERN.matcher(sammVersion).matches(); + boolean valid = BPNL_PATTERN.matcher(partnerBpnl).matches() + && NON_EMPTY_NON_VERTICAL_WHITESPACE_PATTERN.matcher(responseType).matches() + && NON_EMPTY_NON_VERTICAL_WHITESPACE_PATTERN.matcher(sammVersion).matches(); if (!valid) { return ResponseEntity.badRequest().build(); } - Dto dto = new Dto(requestId, partnerBpnl, responseType, sammVersion, responseTimestamp, requestBody); + Dto dto = new Dto(requestId, partnerBpnl, responseType, sammVersion, new Date(responseTimestamp), requestBody); + AssetType assetType = Arrays.stream(AssetType.values()).filter(type -> type.ERP_KEYWORD.equals(responseType)).findFirst().orElse(null); int responseCode = 501; - switch (responseType) { - case "ItemStock" -> responseCode = itemStockErpAdapterService.receiveItemStockUpdate(dto); - default -> { + switch (assetType) { + case ITEM_STOCK_SUBMODEL -> responseCode = itemStockErpAdapterService.receiveItemStockUpdate(dto); + case null, default -> { return ResponseEntity.status(responseCode).body("Unsupported response type: " + responseType); } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/domain/model/ErpAdapterTriggerDataset.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/domain/model/ErpAdapterTriggerDataset.java new file mode 100644 index 00000000..f2e9c4a4 --- /dev/null +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/domain/model/ErpAdapterTriggerDataset.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. + * (represented by Fraunhofer ISST) + * Copyright (c) 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.puris.backend.erpadapter.domain.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import lombok.*; +import org.eclipse.tractusx.puris.backend.common.edc.domain.model.AssetType; + +import java.io.Serializable; +import java.util.Date; + +@Entity +@IdClass(ErpAdapterTriggerDataset.Key.class) +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode +public class ErpAdapterTriggerDataset { + + @Id + private String partnerBpnl; + + @Id + private String ownMaterialNumber; + + @Id + private AssetType assetType; + + @Id + private String directionCharacteristic; + + private long lastPartnerRequest; + + private long nextErpRequestScheduled; + + @Override + public String toString() { + return "ErpAdapterTriggerDataset{" + + "partnerBpnl='" + partnerBpnl + '\'' + + ", ownMaterialNumber='" + ownMaterialNumber + '\'' + + ", assetType=" + assetType + + ", directionCharacteristic='" + directionCharacteristic + '\'' + + ", lastPartnerRequest=" + new Date(lastPartnerRequest) + + ", nextErpRequestScheduled=" + new Date(nextErpRequestScheduled) + + '}'; + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + @EqualsAndHashCode + @ToString + public static class Key implements Serializable { + private String partnerBpnl; + private String ownMaterialNumber; + private AssetType assetType; + private String directionCharacteristic; + + } +} diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/domain/repository/ErpAdapterTriggerDatasetRepository.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/domain/repository/ErpAdapterTriggerDatasetRepository.java new file mode 100644 index 00000000..02fc1d88 --- /dev/null +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/domain/repository/ErpAdapterTriggerDatasetRepository.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. + * (represented by Fraunhofer ISST) + * Copyright (c) 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.puris.backend.erpadapter.domain.repository; + +import org.eclipse.tractusx.puris.backend.erpadapter.domain.model.ErpAdapterTriggerDataset; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ErpAdapterTriggerDatasetRepository extends JpaRepository { +} diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ErpAdapterRequestService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ErpAdapterRequestService.java index 3c7ec974..7595f902 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ErpAdapterRequestService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ErpAdapterRequestService.java @@ -50,13 +50,13 @@ public void createAndSend(ErpAdapterRequest erpAdapterRequest) { if (erpAdapterRequest != null) { Integer responseCode = erpAdapterRequestClient.sendRequest(erpAdapterRequest); if (responseCode != null) { + erpAdapterRequest.setResponseCode(responseCode); + update(erpAdapterRequest); if (responseCode >= 200 && responseCode < 400) { log.info("Successfully sent request to ERP Adapter, got status code {} for request:\n{}", responseCode, erpAdapterRequest); } else { log.warn("Received status code {} from ERP Adapter for request:\n{}", responseCode, erpAdapterRequest); } - erpAdapterRequest.setResponseCode(responseCode); - update(erpAdapterRequest); } else { log.error("Failed to send request to ERP Adapter:\n{}", erpAdapterRequest); } @@ -64,9 +64,7 @@ public void createAndSend(ErpAdapterRequest erpAdapterRequest) { } public ErpAdapterRequest get(UUID id) { - // TODO: Remove when mock is removed - return repository.findById(id).orElse(repository.findAll().getFirst()); -// return repository.findById(id).orElse(null); + return repository.findById(id).orElse(null); } public ErpAdapterRequest update(ErpAdapterRequest erpAdapterRequest) { diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ErpAdapterTriggerService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ErpAdapterTriggerService.java new file mode 100644 index 00000000..a4aa6729 --- /dev/null +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ErpAdapterTriggerService.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. + * (represented by Fraunhofer ISST) + * Copyright (c) 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.puris.backend.erpadapter.logic.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.puris.backend.common.edc.domain.model.AssetType; +import org.eclipse.tractusx.puris.backend.erpadapter.ErpAdapterConfiguration; +import org.eclipse.tractusx.puris.backend.erpadapter.domain.model.ErpAdapterRequest; +import org.eclipse.tractusx.puris.backend.erpadapter.domain.model.ErpAdapterTriggerDataset; +import org.eclipse.tractusx.puris.backend.erpadapter.domain.repository.ErpAdapterTriggerDatasetRepository; +import org.eclipse.tractusx.puris.backend.stock.logic.dto.itemstocksamm.DirectionCharacteristic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ErpAdapterTriggerService { + + @Autowired + private ErpAdapterTriggerDatasetRepository repository; + @Autowired + private ErpAdapterConfiguration erpAdapterConfiguration; + @Autowired + private ErpAdapterRequestService erpAdapterRequestService; + @Autowired + private ExecutorService executorService; + + private final long daemonActivityInterval = 1 * 60 * 1000; // daemon wakes up every minute + + private Future daemonObject; + + private final List datasets = new ArrayList<>(); + + /** + * protect accesses to the datasets list + */ + private final Lock lock = new ReentrantLock(); + + private final Runnable daemon = () -> { + log.info("Daemon thread started"); + while (true) { + lock.lock(); + try { + repository.saveAll(datasets); + datasets.clear(); + } finally { + lock.unlock(); + } + + long timeLimit = erpAdapterConfiguration.getRefreshTimeLimit(); + var allDatasets = repository.findAll(); + long now = new Date().getTime(); + log.info("Daemon waking up, found {} datasets", allDatasets.size()); + for (var dataset : allDatasets) { + if (dataset.getLastPartnerRequest() + timeLimit <= now) { + // too much time has passed since last request of this kind, so + // we will stop triggering further updates from the erp adapter + repository.delete(dataset); + log.info("Stopped scheduling further requests for : {}", dataset); + } else { + if (dataset.getNextErpRequestScheduled() <= now) { + // the time has come for a new erp adapter request + ErpAdapterRequest request = new ErpAdapterRequest(); + request.setOwnMaterialNumber(dataset.getOwnMaterialNumber()); + request.setPartnerBpnl(dataset.getPartnerBpnl()); + request.setRequestDate(new Date(now)); + DirectionCharacteristic directionCharacteristic = dataset.getDirectionCharacteristic().isEmpty() ? + null : DirectionCharacteristic.valueOf(dataset.getDirectionCharacteristic()); + request.setDirectionCharacteristic(directionCharacteristic); + request.setRequestType(dataset.getAssetType().ERP_KEYWORD); + request.setSammVersion(dataset.getAssetType().ERP_SAMMVERSION); + executorService.submit(() -> erpAdapterRequestService.createAndSend(request)); + + // schedule next request + dataset.setNextErpRequestScheduled(now + erpAdapterConfiguration.getRefreshInterval()); + dataset = repository.save(dataset); + log.info("Scheduled next erp adapter request: {}", dataset); + } + } + } + try { + // sleep for the defined interval + Thread.sleep(daemonActivityInterval); + } catch (InterruptedException ignore) { + } + } + }; + + /** + * Send a notification about a just received request from a partner via this + * method in order to schedule regular updates from the erp adapter. + * + * @param partnerBpnl the BPNL of the requesting partner + * @param ownMaterialNumber the material number of the requested material + * @param type the Asset/Submodel type of the request + * @param direction the direction characteristic (if applicable for the given asset type, may be null) + */ + public void notifyPartnerRequest(String partnerBpnl, String ownMaterialNumber, AssetType type, DirectionCharacteristic direction) { + if (!erpAdapterConfiguration.isErpAdapterEnabled()) { + return; + } + String directionString = direction != null ? direction.name() : ""; + + ErpAdapterTriggerDataset dataset = repository.findById(new ErpAdapterTriggerDataset.Key(partnerBpnl, + ownMaterialNumber, type, directionString)) + .orElse(null); + + long now = new Date().getTime(); + if (dataset == null) { + // unknown request specifics, so we trigger a new request right now + ErpAdapterRequest erpAdapterRequest = new ErpAdapterRequest(); + erpAdapterRequest.setRequestDate(new Date(now)); + erpAdapterRequest.setPartnerBpnl(partnerBpnl); + erpAdapterRequest.setOwnMaterialNumber(ownMaterialNumber); + erpAdapterRequest.setDirectionCharacteristic(direction); + erpAdapterRequest.setRequestType(type.ERP_KEYWORD); + erpAdapterRequest.setSammVersion(type.ERP_SAMMVERSION); + executorService.submit(() -> erpAdapterRequestService.createAndSend(erpAdapterRequest)); + + // create dataset for the daemon thread to schedule future erp adapter requests + dataset = new ErpAdapterTriggerDataset(partnerBpnl, ownMaterialNumber, type, directionString, now, + now + erpAdapterConfiguration.getRefreshInterval()); + // it seems safer when only the Daemon thread writes to the repository + lock.lock(); + try { + datasets.add(dataset); + } finally { + lock.unlock(); + } + log.info("Created {}", dataset); + } else { + // we had previous requests of that kind, so we just store the timestamp of this latest request + dataset.setLastPartnerRequest(now); + lock.lock(); + try { + datasets.add(dataset); + } finally { + lock.unlock(); + } + } + if (daemonObject == null) { + daemonObject = executorService.submit(daemon); + } + } + +} diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ItemStockErpAdapterService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ItemStockErpAdapterService.java index 4de30753..1ea34bb1 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ItemStockErpAdapterService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/erpadapter/logic/service/ItemStockErpAdapterService.java @@ -85,11 +85,10 @@ public int receiveItemStockUpdate(ErpAdapterController.Dto dto) { log.error("Unknown request-id {}", dto.requestId()); return 404; } - // TODO: uncomment the following block when removing mock request, also edit swagger description in ErpAdapterController -// if (request.getResponseReceivedDate() != null) { -// log.error("Received duplicate response for messageId {}", request.getId()); -// return 409; -// } + if (request.getResponseReceivedDate() != null) { + log.error("Received duplicate response for messageId {}", request.getId()); + return 409; + } if (request.getResponseCode() == null || request.getResponseCode() < 200 || request.getResponseCode() >= 400) { log.error("Unexpected response, erp adapter had not confirmed request"); return 404; diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/repository/MaterialPartnerRelationRepository.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/repository/MaterialPartnerRelationRepository.java index aab3f8a1..09bf6ca7 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/repository/MaterialPartnerRelationRepository.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/repository/MaterialPartnerRelationRepository.java @@ -55,4 +55,6 @@ public interface MaterialPartnerRelationRepository extends JpaRepository findAllByPartnerAndPartnerMaterialNumberAndPartnerBuysMaterialIsTrue(Partner partner, String partnerMaterialNumber); List findAllByPartnerAndAndPartnerCXNumber(Partner partner, String partnerCXNumber); + + List findAllByPartner_BpnlAndMaterial_OwnMaterialNumber(String bpnl, String ownMaterialNumber); } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialPartnerRelationService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialPartnerRelationService.java index df03d0d7..3d457d2f 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialPartnerRelationService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialPartnerRelationService.java @@ -70,4 +70,6 @@ public interface MaterialPartnerRelationService { List findAllByCustomerPartnerAndPartnerMaterialNumber(Partner partner, String partnerMaterialNumber); MaterialPartnerRelation findByPartnerAndPartnerCXNumber(Partner partner, String partnerCXNumber); + + MaterialPartnerRelation find(String bpnl, String ownMaterialNumber); } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialPartnerRelationServiceImpl.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialPartnerRelationServiceImpl.java index 35bfb431..8c489b06 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialPartnerRelationServiceImpl.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialPartnerRelationServiceImpl.java @@ -619,4 +619,16 @@ public MaterialPartnerRelation findByPartnerAndPartnerCXNumber(Partner partner, return null; } } + + @Override + public MaterialPartnerRelation find(String bpnl, String ownMaterialNumber) { + var results = mprRepository.findAllByPartner_BpnlAndMaterial_OwnMaterialNumber(bpnl, ownMaterialNumber); + if (results.isEmpty()) { + return null; + } + if (results.size() > 1) { + log.warn("Found more than one MPR for {} and ownMaterialNumber {}", bpnl, ownMaterialNumber); + } + return results.getFirst(); + } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ItemStockRequestApiService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ItemStockRequestApiService.java index 185aa0c9..dbea6832 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ItemStockRequestApiService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ItemStockRequestApiService.java @@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.puris.backend.common.edc.domain.model.AssetType; import org.eclipse.tractusx.puris.backend.common.edc.logic.service.EdcAdapterService; +import org.eclipse.tractusx.puris.backend.erpadapter.logic.service.ErpAdapterTriggerService; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Material; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; @@ -56,6 +57,8 @@ public class ItemStockRequestApiService { @Autowired private ReportedMaterialItemStockService reportedMaterialItemStockService; @Autowired + private ErpAdapterTriggerService erpAdapterTriggerService; + @Autowired private EdcAdapterService edcAdapterService; @Autowired private ItemStockSammMapper sammMapper; @@ -76,6 +79,9 @@ public ItemStockSamm handleItemStockSubmodelRequest(String bpnl, String material if (material != null && mprService.find(material, partner).isPartnerBuysMaterial()) { // only send an answer if partner is registered as customer var currentStocks = productItemStockService.findByPartnerAndMaterial(partner, material); + + erpAdapterTriggerService.notifyPartnerRequest(bpnl, material.getOwnMaterialNumber(), AssetType.ITEM_STOCK_SUBMODEL, direction); + return sammMapper.productItemStocksToItemStockSamm(currentStocks, partner, material); } return null; @@ -105,8 +111,10 @@ public ItemStockSamm handleItemStockSubmodelRequest(String bpnl, String material return null; } - // only send an answer if partner is registered as supplier + // request looks valid + erpAdapterTriggerService.notifyPartnerRequest(bpnl, material.getOwnMaterialNumber(), AssetType.ITEM_STOCK_SUBMODEL, direction); var currentStocks = materialItemStockService.findByPartnerAndMaterial(partner, material); + return sammMapper.materialItemStocksToItemStockSamm(currentStocks, partner, material); } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index fd6faaae..841b89f2 100755 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -23,9 +23,18 @@ puris.dtr.idp.edc-client.secret.alias=${PURIS_DTR_IDP_EDC-CLIENT_SECRET_ALIAS} puris.dtr.idp.puris-client.id=${PURIS_DTR_IDP_PURIS-CLIENT_ID:FOSS-DTR-CLIENT} puris.dtr.idp.puris-client.secret=${PURIS_DTR_IDP_PURIS-CLIENT_SECRET} puris.erpadapter.enabled=${PURIS_ERPADAPTER_ENABLED:false} -puris.erpadapter.url=${PURIS_ERPADAPTER_URL:http://my-erpadapter:8080} +puris.erpadapter.url=${PURIS_ERPADAPTER_URL:http://host.docker.internal:5555/} puris.erpadapter.authkey=${PURIS_ERPADAPTER_AUTHKEY:x-api-key} puris.erpadapter.authsecret=${PURIS_ERPADAPTER_AUTHSECRET:erp-password} + +# Interval between two requests to the erp adapter for the same issue (minutes) +puris.erpadapter.refreshinterval=${PURIS_ERPADAPTER_REFRESHINTERVAL:1} + +# Period since last received partner request after which no more new update requests to the +# erp adapter will be sent (days) +puris.erpadapter.timelimit=${PURIS_ERPADAPTER_TIMELIMIT:7} + + # Flag that decides whether the auto-generation feature of the puris backend is enabled. # Since all Material entities are required to have a CatenaX-Id, you must enter any pre-existing CatenaX-Id # via the materials-API of the backend, when you are inserting a new Material entity to the backend's diff --git a/backend/src/test/resources/application.properties b/backend/src/test/resources/application.properties index b778d19b..751d490b 100755 --- a/backend/src/test/resources/application.properties +++ b/backend/src/test/resources/application.properties @@ -23,9 +23,18 @@ puris.dtr.idp.puris-client.id=${PURIS_DTR_IDP_PURIS-CLIENT_ID:FOSS-DTR-CLIENT} puris.dtr.idp.puris-client.secret=${PURIS_DTR_IDP_PURIS-CLIENT_SECRET:test} puris.erpadapter.enabled=${PURIS_ERPADAPTER_ENABLED:false} -puris.erpadapter.url=${PURIS_ERPADAPTER_URL:http://my-erpadapter:8080} +puris.erpadapter.url=${PURIS_ERPADAPTER_URL:http://host.docker.internal:5555/} puris.erpadapter.authkey=${PURIS_ERPADAPTER_AUTHKEY:x-api-key} puris.erpadapter.authsecret=${PURIS_ERPADAPTER_AUTHSECRET:erp-password} + +# Interval between two requests to the erp adapter for the same issue (minutes) +puris.erpadapter.refreshinterval=${PURIS_ERPADAPTER_REFRESHINTERVAL:1} + +# Period since last received partner request after which no more new update requests to the +# erp adapter will be sent (days) +puris.erpadapter.timelimit=${PURIS_ERPADAPTER_TIMELIMIT:7} + + puris.generatematerialcatenaxid=${PURIS_GENERATEMATERIALCATENAXID:true} # DB Configuration diff --git a/charts/puris/Chart.yaml b/charts/puris/Chart.yaml index d79e0ccc..de95772b 100644 --- a/charts/puris/Chart.yaml +++ b/charts/puris/Chart.yaml @@ -35,7 +35,7 @@ dependencies: # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.6.4 +version: 2.7.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/puris/README.md b/charts/puris/README.md index 0bd3fac5..781bac80 100644 --- a/charts/puris/README.md +++ b/charts/puris/README.md @@ -1,6 +1,6 @@ # puris -![Version: 2.6.4](https://img.shields.io/badge/Version-2.6.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.0.2](https://img.shields.io/badge/AppVersion-2.0.2-informational?style=flat-square) +![Version: 2.7.0](https://img.shields.io/badge/Version-2.7.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.0.2](https://img.shields.io/badge/AppVersion-2.0.2-informational?style=flat-square) A helm chart for Kubernetes deployment of PURIS @@ -96,10 +96,11 @@ dependencies: | backend.puris.edc.controlplane.management.url | string | `"https:/your-edc-address:8181/management"` | Url to the EDC controlplane management of the edc | | backend.puris.edc.controlplane.protocol.url | string | `"https://your-edc-address:8184/api/v1/dsp"` | Url to the EDC controlplane protocol API of the edc | | backend.puris.edc.dataplane.public.url | string | `"https://your-data-plane:8285/api/public/"` | Url of one of your data plane's public api | -| backend.puris.erpadapter.authkey | string | `"x-api-key"` | | -| backend.puris.erpadapter.authsecret | string | `"erp-password"` | | -| backend.puris.erpadapter.enabled | bool | `false` | | -| backend.puris.erpadapter.url | string | `"http://my-erpadapter:8080"` | | +| backend.puris.erpadapter.authkey | string | `"x-api-key"` | The auth key to be used on your ERP adapter's request api | +| backend.puris.erpadapter.enabled | bool | `false` | Toggles usage of the ERP adapter | +| backend.puris.erpadapter.refreshinterval | int | `720` | Interval between two requests to the erp adapter for the same issue (minutes) | +| backend.puris.erpadapter.timelimit | int | `7` | Period since last received partner request after which no more new update requests to the erp adapter will be sent (days) | +| backend.puris.erpadapter.url | string | `"http://my-erpadapter:8080"` | The url of your ERP adapter's request api | | backend.puris.existingSecret | string | `"secret-puris-backend"` | Secret for backend passwords. For more information look into 'backend-secrets.yaml' file. | | backend.puris.frameworkagreement.credential | string | `"Puris"` | The name of the framework agreement. Starting with Uppercase and using CamelCase. | | backend.puris.frameworkagreement.version | string | `"1.0"` | The version of the framework agreement, NEEDS TO BE PUT AS "STRING"! | @@ -221,9 +222,6 @@ dependencies: | postgresql.service | object | `{"ports":{"postgresql":5432}}` | Possibility to override the name nameOverride: "" | | postgresql.service.ports.postgresql | int | `5432` | Port of postgres database. | ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1) - ## NOTICE This work is licensed under the [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/charts/puris/templates/backend-deployment.yaml b/charts/puris/templates/backend-deployment.yaml index d4327dd9..3d4d59a8 100644 --- a/charts/puris/templates/backend-deployment.yaml +++ b/charts/puris/templates/backend-deployment.yaml @@ -175,12 +175,16 @@ spec: - name: PURIS_ERPADAPTER_URL value: "{{ .Values.backend.puris.erpadapter.url}}" - name: PURIS_ERPADAPTER_AUTHKEY - value: {{ .Values.backend.puris.erpadapter.authkey }} + value: "{{ .Values.backend.puris.erpadapter.authkey }}" - name: PURIS_ERPADAPTER_AUTHSECRET valueFrom: secretKeyRef: name: "{{ .Values.backend.puris.existingSecret }}" key: "puris-erpadapter-authsecret" + - name: PURIS_ERPADAPTER_TIMELIMIT + value: "{{ .Values.backend.puris.erpadapter.timelimit }}" + - name: PURIS_ERPADAPTER_REFRESHINTERVAL + value: "{{ .Values.backend.puris.erpadapter.refreshinterval }}" ###################################### ## Additional environment variables ## diff --git a/charts/puris/values.yaml b/charts/puris/values.yaml index 17221463..455d30fc 100644 --- a/charts/puris/values.yaml +++ b/charts/puris/values.yaml @@ -475,14 +475,17 @@ backend: # Material entity. generatematerialcatenaxid: true erpadapter: - # Toggles usage of the ERP adapter + # -- Toggles usage of the ERP adapter enabled: false - # The url of your ERP adapter's request api + # -- The url of your ERP adapter's request api url: http://my-erpadapter:8080 - # The auth key to be used on your ERP adapter's request api + # -- The auth key to be used on your ERP adapter's request api authkey: x-api-key - # The auth secret to be used on your ERP adapter's request api - authsecret: erp-password + # -- Interval between two requests to the erp adapter for the same issue (minutes) + refreshinterval: 720 + # -- Period since last received partner request after which no more new update requests to the + # erp adapter will be sent (days) + timelimit: 7 # -- Extra environment variables that will be passed onto the backend deployment pods env: {} diff --git a/local/tractus-x-edc/config/customer/puris-backend.properties b/local/tractus-x-edc/config/customer/puris-backend.properties index af4d7c6d..0731a0be 100644 --- a/local/tractus-x-edc/config/customer/puris-backend.properties +++ b/local/tractus-x-edc/config/customer/puris-backend.properties @@ -24,7 +24,8 @@ puris.dtr.idp.edc-client.secret.alias=${CUSTOMER_KC_DTR_PURIS_CLIENT_ALIAS} puris.dtr.idp.puris-client.id=${KC_MANAGE_CLIENT_ID} puris.dtr.idp.puris-client.secret=${CUSTOMER_KC_DTR_PURIS_CLIENT_SECRET} -puris.erpadapter.url=http://my-erpadapter:8080 +puris.erpadapter.enabled=false +puris.erpadapter.url=http://host.docker.internal:5555/ puris.erpadapter.authkey=x-api-key puris.erpadapter.authsecret=erp-password # diff --git a/local/tractus-x-edc/config/supplier/puris-backend.properties b/local/tractus-x-edc/config/supplier/puris-backend.properties index 2c0a9bd4..7f672909 100644 --- a/local/tractus-x-edc/config/supplier/puris-backend.properties +++ b/local/tractus-x-edc/config/supplier/puris-backend.properties @@ -24,7 +24,8 @@ puris.dtr.idp.edc-client.secret.alias=${SUPPLIER_KC_DTR_PURIS_CLIENT_ALIAS} puris.dtr.idp.puris-client.id=${KC_MANAGE_CLIENT_ID} puris.dtr.idp.puris-client.secret=${SUPPLIER_KC_DTR_PURIS_CLIENT_SECRET} -puris.erpadapter.url=http://my-erpadapter:8080 +puris.erpadapter.enabled=false +puris.erpadapter.url=http://host.docker.internal:5555/ puris.erpadapter.authkey=x-api-key puris.erpadapter.authsecret=erp-password #