Skip to content

Commit

Permalink
Merge pull request #474 from FraunhoferISST/feat/erp_adapter_trigger
Browse files Browse the repository at this point in the history
Feat: Erpadapter Trigger Service
  • Loading branch information
tom-rm-meyer-ISST authored Jul 11, 2024
2 parents b054efe + 579da73 commit 040ef55
Show file tree
Hide file tree
Showing 22 changed files with 484 additions and 73 deletions.
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -78,8 +75,6 @@ public class DataInjectionCommandLineRunner implements CommandLineRunner {

@Autowired
private VariablesService variablesService;
@Autowired
private ErpAdapterRequestService erpAdapterRequestService;

private ObjectMapper objectMapper;

Expand Down Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
*
* <p>
* 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.
*
* <p>
* 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)
* <p>
* 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.
* <p>
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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" +
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

}
}
Original file line number Diff line number Diff line change
@@ -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<ErpAdapterTriggerDataset, ErpAdapterTriggerDataset.Key> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,21 @@ 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);
}
}
}

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) {
Expand Down
Loading

0 comments on commit 040ef55

Please sign in to comment.