Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat: Erpadapter Trigger Service #474

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
tom-rm-meyer-ISST marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading