diff --git a/CHANGELOG.md b/CHANGELOG.md index 914323561..699682cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The **need for configuration updates** is **marked bold**. ### Added -- / +- Added improved error messages for refresh materials ([#995](https://github.com/eclipse-tractusx/puris/pull/995)) ### Changed diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/DeliveryRequestApiService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/DeliveryRequestApiService.java index e0a177709..a91e848f7 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/DeliveryRequestApiService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/DeliveryRequestApiService.java @@ -26,11 +26,14 @@ import org.eclipse.tractusx.puris.backend.common.edc.logic.service.EdcAdapterService; import org.eclipse.tractusx.puris.backend.delivery.domain.model.DeliveryResponsibilityEnumeration; import org.eclipse.tractusx.puris.backend.delivery.domain.model.OwnDelivery; +import org.eclipse.tractusx.puris.backend.delivery.domain.model.ReportedDelivery; import org.eclipse.tractusx.puris.backend.delivery.logic.adapter.DeliveryInformationSammMapper; import org.eclipse.tractusx.puris.backend.delivery.logic.dto.deliverysamm.DeliveryInformation; 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; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.RefreshError; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.RefreshResult; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; @@ -38,6 +41,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Predicate; @@ -144,7 +148,8 @@ public DeliveryInformation handleDeliverySubmodelRequest(String bpnl, String mat return sammMapper.ownDeliveryToSamm(currentDeliveries, partner, material); } - public void doReportedDeliveryRequest(Partner partner, Material material) { + public RefreshResult doReportedDeliveryRequest(Partner partner, Material material) { + List errors = new ArrayList<>(); try { var mpr = mprService.find(material, partner); if (mpr.getPartnerCXNumber() == null) { @@ -159,10 +164,27 @@ public void doReportedDeliveryRequest(Partner partner, Material material) { var deliveryPartner = delivery.getPartner(); var deliveryMaterial = delivery.getMaterial(); if (!partner.equals(deliveryPartner) || !material.equals(deliveryMaterial)) { - log.warn("Received inconsistent data from " + partner.getBpnl() + "\n" + deliveries); - return; + errors.add(new RefreshError(List.of("Received inconsistent data: partner or material mismatch (expected bpnl=%s, ownMaterialNumber=%s; received bpnl=%s, ownMaterialNumber=%s)".formatted( + partner.getBpnl(), + material.getOwnMaterialNumber(), + deliveryPartner.getBpnl(), + deliveryMaterial.getOwnMaterialNumber() + )))); + continue; + } + + List validationErrors = reportedDeliveryService.validateWithDetails(delivery); + if (!validationErrors.isEmpty()) { + errors.add(new RefreshError(validationErrors)); } } + + if (!errors.isEmpty()) { + log.warn("Validation errors found for ReportedDelivery request from partner {} for material {}: {}", + partner.getBpnl(), material.getOwnMaterialNumber(), errors); + return new RefreshResult("Validation failed for reported deliveries", errors); + } + // delete older data: var oldDeliveries = reportedDeliveryService.findAllByFilters(Optional.of(material.getOwnMaterialNumber()), Optional.empty(), Optional.of(partner.getBpnl()), Optional.empty(), Optional.empty()); for (var oldDelivery : oldDeliveries) { @@ -171,11 +193,14 @@ public void doReportedDeliveryRequest(Partner partner, Material material) { for (var newDelivery : deliveries) { reportedDeliveryService.create(newDelivery); } - log.info("Updated Reported Deliveries for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl()); - + log.info("Successfully updated ReportedDelivery for {} and partner {}", + material.getOwnMaterialNumber(), partner.getBpnl()); materialService.updateTimestamp(material.getOwnMaterialNumber()); + return new RefreshResult("Successfully processed all reported deliveries", errors); } catch (Exception e) { log.error("Error in Reported Deliveries Request for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), e); + errors.add(new RefreshError(List.of("System error: " + e.getMessage()))); + return new RefreshResult("System error occurred during processing", errors); } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/DeliveryService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/DeliveryService.java index 2d601c2f6..0eca77978 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/DeliveryService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/DeliveryService.java @@ -32,6 +32,7 @@ import java.util.stream.Stream; import org.eclipse.tractusx.puris.backend.delivery.domain.model.Delivery; +import org.eclipse.tractusx.puris.backend.delivery.domain.model.EventTypeEnumeration; import org.eclipse.tractusx.puris.backend.delivery.domain.repository.DeliveryRepository; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; @@ -130,4 +131,224 @@ public final T update(T delivery) { public final void delete(UUID id) { repository.deleteById(id); } + + protected List basicValidation(Delivery delivery) { + List errors = new ArrayList<>(); + + if (delivery.getQuantity() < 0) { + errors.add(String.format("Quantity '%d'must be greater than or equal to 0.", delivery.getQuantity())); + } + if (delivery.getMeasurementUnit() == null) { + errors.add("Missing measurement unit."); + } + if (delivery.getLastUpdatedOnDateTime() == null) { + errors.add("Missing lastUpdatedOnTime."); + } else if (delivery.getLastUpdatedOnDateTime().after(new Date())) { + errors.add(String.format("lastUpdatedOnDateTime '%s' must be in the past must be in the past (system time: '%s').", delivery.getLastUpdatedOnDateTime().toInstant().toString(), (new Date()).toInstant().toString())); + } + if (delivery.getMaterial() == null) { + errors.add("Missing material."); + } + if (delivery.getPartner() == null) { + errors.add("Missing partner."); + } + errors.addAll(validateTransitEvent(delivery)); + if (!((delivery.getCustomerOrderNumber() != null && delivery.getCustomerOrderPositionNumber() != null) || + (delivery.getCustomerOrderNumber() == null && delivery.getCustomerOrderPositionNumber() == null && delivery.getSupplierOrderNumber() == null))) { + errors.add("If an order position reference is given, customer order number and customer order position number must be set."); + } + + return errors; + } + + protected List validateOwnPartner(Delivery delivery) { + List errors = new ArrayList<>(); + if (ownPartnerEntity == null) { + ownPartnerEntity = partnerService.getOwnPartnerEntity(); + } + if (delivery.getPartner().equals(ownPartnerEntity)) { + errors.add(String.format("Partner cannot be the same as own partner entity '%s'.", delivery.getPartner().getBpnl())); + } + return errors; + } + + protected List validateTransitEvent(Delivery delivery) { + List errors = new ArrayList<>(); + var now = new Date().getTime(); + + if (delivery.getDepartureType() == null) { + errors.add("Missing departure type."); + } else if (!(delivery.getDepartureType() == EventTypeEnumeration.ESTIMATED_DEPARTURE || delivery.getDepartureType() == EventTypeEnumeration.ACTUAL_DEPARTURE)) { + errors.add("Invalid departure type."); + } + if (delivery.getArrivalType() == null) { + errors.add("Missing arrival type."); + } else if (!(delivery.getArrivalType() == EventTypeEnumeration.ESTIMATED_ARRIVAL || delivery.getArrivalType() == EventTypeEnumeration.ACTUAL_ARRIVAL)) { + errors.add("Invalid arrival type."); + } + if (delivery.getDepartureType() == EventTypeEnumeration.ESTIMATED_DEPARTURE && delivery.getArrivalType() == EventTypeEnumeration.ACTUAL_ARRIVAL) { + errors.add("Delivery with estimated departure cannot have actual arrival."); + } + if (delivery.getDateOfDeparture() == null) { + errors.add("Missing date of departure."); + } + if (delivery.getDateOfArrival() == null) { + errors.add("Missing date of arrival."); + } + if (delivery.getDateOfArrival() != null && delivery.getDateOfDeparture() != null && + delivery.getDateOfDeparture().getTime() >= delivery.getDateOfArrival().getTime()) { + errors.add(String.format("Date of departure '%s' must be before date of arrival '%s'.", delivery.getDateOfDeparture().toInstant().toString(), delivery.getDateOfArrival().toInstant().toString())); + } + if (delivery.getDateOfArrival() != null && + delivery.getArrivalType() == EventTypeEnumeration.ACTUAL_ARRIVAL && delivery.getDateOfArrival().getTime() >= now) { + errors.add(String.format("Actual arrival date '%s' must be in the past (system time: '%s').", delivery.getDateOfArrival().toInstant().toString(), (new Date()).toInstant().toString())); + } + if (delivery.getDateOfDeparture() != null && + delivery.getDepartureType() == EventTypeEnumeration.ACTUAL_DEPARTURE && delivery.getDateOfDeparture().getTime() >= now) { + errors.add(String.format("Actual departure date '%s' must be in the past (system time: '%s').", delivery.getDateOfDeparture().toInstant().toString(), (new Date()).toInstant().toString())); + } + + return errors; + } + + protected List validateOwnResponsibility(Delivery delivery) { + List errors = new ArrayList<>(); + if (ownPartnerEntity == null) { + ownPartnerEntity = partnerService.getOwnPartnerEntity(); + } + var ownSites = ownPartnerEntity.getSites(); + var partnerSites = delivery.getPartner().getSites(); + + if (delivery.getIncoterm() == null) { + errors.add("Missing Incoterm."); + } else { + switch (delivery.getIncoterm().getResponsibility()) { + case SUPPLIER: + var ownSite = ownSites.stream().filter(site -> site.getBpns().equals(delivery.getOriginBpns())).findFirst(); + var partnerSite = partnerSites.stream().filter(site -> site.getBpns().equals(delivery.getDestinationBpns())).findFirst(); + if (!delivery.getMaterial().isProductFlag()) { + errors.add(String.format("Material '%s' must be configured as product via flag (incoterm '%s' with supplier responsibility).", delivery.getMaterial().getOwnMaterialNumber(), delivery.getIncoterm().getValue())); + } + if (!ownSite.isPresent()) { + errors.add(String.format("Origin BPNS '%s' must match one of the own partner entity's site BPNS (incoterm '%s' with supplier responsibility).", delivery.getOriginBpns(), delivery.getIncoterm().getValue())); + } else if (delivery.getOriginBpna() != null && ownSite.get().getAddresses().stream().noneMatch(address -> address.getBpna().equals(delivery.getOriginBpna()))) { + errors.add(String.format("Origin BPNA '%s' not configured for own site '%s' (delivery with supplier responsibility).", delivery.getOriginBpna(), delivery.getOriginBpns())); + } + if (!partnerSite.isPresent()) { + errors.add(String.format("Destination BPNS '%s' must match one of the own partner entity's site BPNS (supplier responsibility).", delivery.getDestinationBpns())); + } else if (delivery.getDestinationBpna() != null && partnerSite.get().getAddresses().stream().noneMatch(address -> address.getBpna().equals(delivery.getDestinationBpna()))) { + errors.add(String.format("Destination BPNA '%s' not configured for own site '%s' (incoterm '%s' with supplier responsibility).", delivery.getDestinationBpna(), delivery.getDestinationBpns(), delivery.getIncoterm().getValue())); + } + break; + case CUSTOMER: + ownSite = ownSites.stream().filter(site -> site.getBpns().equals(delivery.getDestinationBpns())).findFirst(); + partnerSite = partnerSites.stream().filter(site -> site.getBpns().equals(delivery.getOriginBpns())).findFirst(); + if (!delivery.getMaterial().isMaterialFlag()) { + errors.add(String.format("Material '%s' must have customer flag (incoterm '%s' for customer responsibility).", delivery.getMaterial().getOwnMaterialNumber(), delivery.getIncoterm().getValue())); + } + if (!ownSite.isPresent()) { + errors.add(String.format("Destination BPNS '%s' must match one of the own partner entity's site BPNS (incoterm '%s' with customer responsibility).", delivery.getDestinationBpns(), delivery.getIncoterm().getValue())); + } else if (delivery.getDestinationBpna() != null && ownSite.get().getAddresses().stream().noneMatch(address -> address.getBpna().equals(delivery.getDestinationBpna()))) { + errors.add(String.format("Destination BPNA '%s' not configured for own site '%s' (incoterm '%s' with customer responsibility).", delivery.getDestinationBpna(), delivery.getDestinationBpns(), delivery.getIncoterm().getValue())); + } + if (!partnerSite.isPresent()) { + errors.add(String.format("Origin BPNS '%s' must match one of the own partner entity's site BPNS (incoterm '%s' with customer responsibility).", delivery.getOriginBpns(), delivery.getIncoterm().getValue())); + } else if (delivery.getOriginBpna() != null && partnerSite.get().getAddresses().stream().noneMatch(address -> address.getBpna().equals(delivery.getOriginBpna()))) { + errors.add(String.format("Origin BPNA '%s' not configured for own site '%s' (incoterm '%s' with customer responsibility).", delivery.getOriginBpna(), delivery.getOriginBpns(), delivery.getIncoterm().getValue())); + } + break; + case PARTIAL: + if (delivery.getMaterial().isProductFlag()) { + ownSite = ownSites.stream().filter(site -> site.getBpns().equals(delivery.getOriginBpns())).findFirst(); + partnerSite = partnerSites.stream().filter(site -> site.getBpns().equals(delivery.getDestinationBpns())).findFirst(); + if (ownSite.isPresent() && partnerSite.isPresent() && ( + delivery.getOriginBpna() == null || + ownSite.get().getAddresses().stream().anyMatch(address -> address.getBpna().equals(delivery.getOriginBpna())) + ) && ( + delivery.getDestinationBpna() == null || + partnerSite.get().getAddresses().stream().anyMatch(address -> address.getBpna().equals(delivery.getDestinationBpna())) + )) { + return new ArrayList<>(); + } + } + if (delivery.getMaterial().isMaterialFlag()) { + ownSite = ownSites.stream().filter(site -> site.getBpns().equals(delivery.getDestinationBpns())).findFirst(); + partnerSite = partnerSites.stream().filter(site -> site.getBpns().equals(delivery.getOriginBpns())).findFirst(); + if (ownSite.isPresent() && partnerSite.isPresent() && ( + delivery.getDestinationBpna() == null || + ownSite.get().getAddresses().stream().anyMatch(address -> address.getBpna().equals(delivery.getDestinationBpna())) + ) && ( + delivery.getOriginBpna() == null || + partnerSite.get().getAddresses().stream().anyMatch(address -> address.getBpna().equals(delivery.getOriginBpna())) + )) { + return new ArrayList<>(); + } + } + errors.add(String.format("Responsibility conditions for material '%s' for partial responsibility (incoterm '%s') are not met. Either origin site bpns '%s' does not match to own configured sites or destination site bpns '%' does not match to configured sites for partner '%s'. Additionally this behavior might not be applicable to the material configuration as product (%b) or material (%b).", delivery.getMaterial().getOwnMaterialNumber(), delivery.getIncoterm().getValue(), delivery.getOriginBpns(), delivery.getDestinationBpns(), delivery.getPartner().getBpnl(), delivery.getMaterial().isProductFlag(), delivery.getMaterial().isMaterialFlag())); + break; + default: + errors.add(String.format("Invalid incoterm responsibility for incoterm '%s'.", delivery.getIncoterm().getValue())); + break; + } + } + return errors; + } + + protected List validateReportedResponsibility(Delivery delivery) { + List errors = new ArrayList<>(); + if (ownPartnerEntity == null) { + ownPartnerEntity = partnerService.getOwnPartnerEntity(); + } + + if (delivery.getIncoterm() == null) { + errors.add("Missing Incoterm."); + } else { + switch (delivery.getIncoterm().getResponsibility()) { + case SUPPLIER: + if (!delivery.getMaterial().isMaterialFlag()) { + errors.add(String.format("Material '%s' must be configured as material via flag (incoterm '%s' with supplier responsibility).", delivery.getMaterial().getOwnMaterialNumber(), delivery.getIncoterm().getValue())); + } + if (delivery.getPartner().getSites().stream().noneMatch(site -> site.getBpns().equals(delivery.getOriginBpns()))) { + errors.add(String.format("Origin BPNA '%s' not configured for site '%s' of partner '%s' (incoterm '%s' with supplier responsibility).", delivery.getOriginBpna(), delivery.getOriginBpns(), delivery.getPartner().getBpnl(), delivery.getIncoterm().getValue())); + } + if (ownPartnerEntity.getSites().stream().noneMatch(site -> site.getBpns().equals(delivery.getDestinationBpns()))) { + errors.add(String.format("Destination BPNA '%s' not configured for site '%s' of partner '%s' (incoterm '%s' with supplier responsibility).", delivery.getDestinationBpna(), delivery.getDestinationBpns(), delivery.getPartner().getBpnl(), delivery.getIncoterm().getValue())); + } + + break; + case CUSTOMER: + if (!delivery.getMaterial().isProductFlag()) { + errors.add(String.format("Material '%s' must be configured as product via flag (incoterm '%s' with customer responsibility).", delivery.getMaterial().getOwnMaterialNumber(), delivery.getIncoterm().getValue())); + } + if (ownPartnerEntity.getSites().stream().noneMatch(site -> site.getBpns().equals(delivery.getOriginBpns()))) { + errors.add(String.format("Origin BPNS '%s' must match one of the own partner entity's site BPNS (incoterm '%s' with customer responsibility).", delivery.getOriginBpns(), delivery.getIncoterm().getValue())); + } + if (delivery.getPartner().getSites().stream().noneMatch(site -> site.getBpns().equals(delivery.getDestinationBpns()))) { + errors.add(String.format("Destination BPNA '%s' not configured for site '%s' of partner '%s' (incoterm '%s' with supplier responsibility).", delivery.getDestinationBpna(), delivery.getDestinationBpns(), delivery.getPartner().getBpnl(), delivery.getIncoterm().getValue())); + } + + break; + case PARTIAL: + if (delivery.getMaterial().isProductFlag()) { + if (delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())) && + ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())) + ) { + return new ArrayList<>(); + } + } + if (delivery.getMaterial().isMaterialFlag()) { + if (ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())) && + delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns()))) { + return new ArrayList<>(); + } + } + errors.add(String.format("Responsibility conditions for material '%s' for partial responsibility (incoterm '%s') are not met. Either origin site bpns '%s' does not match to own configured sites or destination site bpns '%' does not match to configured sites for partner '%s'. Additionally this behavior might not be applicable to the material configuration as product (%b) or material (%b).", delivery.getMaterial().getOwnMaterialNumber(), delivery.getIncoterm().getValue(), delivery.getOriginBpns(), delivery.getDestinationBpns(), delivery.getPartner().getBpnl(), delivery.getMaterial().isProductFlag(), delivery.getMaterial().isMaterialFlag())); + break; + default: + errors.add("Invalid incoterm responsibility."); + break; + } + } + return errors; + } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/OwnDeliveryService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/OwnDeliveryService.java index e4a45ccd8..dfa2503d4 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/OwnDeliveryService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/OwnDeliveryService.java @@ -21,16 +21,13 @@ package org.eclipse.tractusx.puris.backend.delivery.logic.service; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.function.Function; import javax.management.openmbean.KeyAlreadyExistsException; -import org.eclipse.tractusx.puris.backend.delivery.domain.model.EventTypeEnumeration; import org.eclipse.tractusx.puris.backend.delivery.domain.model.OwnDelivery; import org.eclipse.tractusx.puris.backend.delivery.domain.repository.OwnDeliveryRepository; -import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.springframework.stereotype.Service; @@ -42,8 +39,6 @@ public class OwnDeliveryService extends DeliveryService { protected final Function validator; - private Partner ownPartnerEntity; - public OwnDeliveryService(OwnDeliveryRepository repository, PartnerService partnerService) { this.repository = repository; this.partnerService = partnerService; @@ -86,160 +81,10 @@ public boolean validate(OwnDelivery delivery) { } public List validateWithDetails(OwnDelivery delivery) { - List errors = new ArrayList<>(); - if (ownPartnerEntity == null) { - ownPartnerEntity = partnerService.getOwnPartnerEntity(); - } - - if (delivery.getQuantity() < 0) { - errors.add("Quantity must be greater than or equal to 0."); - } - if (delivery.getMeasurementUnit() == null) { - errors.add("Missing measurement unit."); - } - if (delivery.getLastUpdatedOnDateTime() == null) { - errors.add("Missing lastUpdatedOnTime."); - } else if (delivery.getLastUpdatedOnDateTime().after(new Date())) { - errors.add("lastUpdatedOnDateTime cannot be in the future."); - } - if (delivery.getMaterial() == null) { - errors.add("Missing material."); - } - if (delivery.getPartner() == null) { - errors.add("Missing partner."); - } - errors.addAll(validateResponsibility(delivery)); - errors.addAll(validateTransitEvent(delivery)); - if (delivery.getPartner().equals(ownPartnerEntity)) { - errors.add("Partner cannot be the same as own partner entity."); - } - if (!((delivery.getCustomerOrderNumber() != null && delivery.getCustomerOrderPositionNumber() != null) || - (delivery.getCustomerOrderNumber() == null && delivery.getCustomerOrderPositionNumber() == null && delivery.getSupplierOrderNumber() == null))) { - errors.add("If an order position reference is given, customer order number and customer order position number must be set."); - } - - return errors; - } - - private List validateTransitEvent(OwnDelivery delivery) { - List errors = new ArrayList<>(); - var now = new Date().getTime(); - - if (delivery.getDepartureType() == null) { - errors.add("Missing departure type."); - } else if (!(delivery.getDepartureType() == EventTypeEnumeration.ESTIMATED_DEPARTURE || delivery.getDepartureType() == EventTypeEnumeration.ACTUAL_DEPARTURE)) { - errors.add("Invalid departure type."); - } - if (delivery.getArrivalType() == null) { - errors.add("Missing arrival type."); - } else if (!(delivery.getArrivalType() == EventTypeEnumeration.ESTIMATED_ARRIVAL || delivery.getArrivalType() == EventTypeEnumeration.ACTUAL_ARRIVAL)) { - errors.add("Invalid arrival type."); - } - if (delivery.getDepartureType() == EventTypeEnumeration.ESTIMATED_DEPARTURE && delivery.getArrivalType() == EventTypeEnumeration.ACTUAL_ARRIVAL) { - errors.add("Estimated departure cannot have actual arrival."); - } - if (delivery.getDateOfDeparture() == null) { - errors.add("Missing date of departure."); - } - if (delivery.getDateOfArrival() == null) { - errors.add("Missing date of arrival."); - } - if (delivery.getDateOfArrival() != null && delivery.getDateOfDeparture() != null && - delivery.getDateOfDeparture().getTime() >= delivery.getDateOfArrival().getTime()) { - errors.add("Date of departure must be before date of arrival."); - } - if (delivery.getDateOfArrival() != null && - delivery.getArrivalType() == EventTypeEnumeration.ACTUAL_ARRIVAL && delivery.getDateOfArrival().getTime() >= now) { - errors.add("Actual arrival date must be in the past."); - } - if (delivery.getDateOfDeparture() != null && - delivery.getDepartureType() == EventTypeEnumeration.ACTUAL_DEPARTURE && delivery.getDateOfDeparture().getTime() >= now) { - errors.add("Actual departure date must be in the past."); - } - - return errors; - } - - private List validateResponsibility(OwnDelivery delivery) { - List errors = new ArrayList<>(); - if (ownPartnerEntity == null) { - ownPartnerEntity = partnerService.getOwnPartnerEntity(); - } - var ownSites = ownPartnerEntity.getSites(); - var partnerSites = delivery.getPartner().getSites(); - - if (delivery.getIncoterm() == null) { - errors.add("Missing Incoterm."); - } else { - switch (delivery.getIncoterm().getResponsibility()) { - case SUPPLIER: - var ownSite = ownSites.stream().filter(site -> site.getBpns().equals(delivery.getOriginBpns())).findFirst(); - var partnerSite = partnerSites.stream().filter(site -> site.getBpns().equals(delivery.getDestinationBpns())).findFirst(); - if (!delivery.getMaterial().isProductFlag()) { - errors.add("Material must have product flag for supplier responsibility."); - } - if (!ownSite.isPresent()) { - errors.add("Origin BPNS must match one of the own partner entity's site BPNS for supplier responsibility."); - } else if (delivery.getOriginBpna() != null && ownSite.get().getAddresses().stream().noneMatch(address -> address.getBpna().equals(delivery.getOriginBpna()))) { - errors.add("Origin BPNA must match one of the own partner entity's site' address BPNAs for supplier responsibility."); - } - if (!partnerSite.isPresent()) { - errors.add("Destination BPNS must match one of the partner's site BPNS for supplier responsibility."); - } else if (delivery.getDestinationBpna() != null && partnerSite.get().getAddresses().stream().noneMatch(address -> address.getBpna().equals(delivery.getDestinationBpna()))) { - errors.add("Destination BPNA must match one of the own partner entity's site' address BPNAs for supplier responsibility."); - } - break; - case CUSTOMER: - ownSite = ownSites.stream().filter(site -> site.getBpns().equals(delivery.getDestinationBpns())).findFirst(); - partnerSite = partnerSites.stream().filter(site -> site.getBpns().equals(delivery.getOriginBpns())).findFirst(); - if (!delivery.getMaterial().isMaterialFlag()) { - errors.add("Material must have material flag for customer responsibility."); - } - if (!ownSite.isPresent()) { - errors.add("Destination BPNS must match one of the own partner entity's site BPNS for customer responsibility."); - } else if (delivery.getDestinationBpna() != null && ownSite.get().getAddresses().stream().noneMatch(address -> address.getBpna().equals(delivery.getDestinationBpna()))) { - errors.add("Destination BPNA must match one of the own partner entity's site' address BPNAs for customer responsibility."); - } - if (!partnerSite.isPresent()) { - errors.add("Origin BPNS must match one of the partner's site BPNS for customer responsibility."); - } else if (delivery.getOriginBpna() != null && partnerSite.get().getAddresses().stream().noneMatch(address -> address.getBpna().equals(delivery.getOriginBpna()))) { - errors.add("Origin BPNA must match one of the own partner entity's site' address BPNAs for customer responsibility."); - } - break; - case PARTIAL: - if (delivery.getMaterial().isProductFlag()) { - ownSite = ownSites.stream().filter(site -> site.getBpns().equals(delivery.getOriginBpns())).findFirst(); - partnerSite = partnerSites.stream().filter(site -> site.getBpns().equals(delivery.getDestinationBpns())).findFirst(); - if (ownSite.isPresent() && partnerSite.isPresent() && ( - delivery.getOriginBpna() == null || - ownSite.get().getAddresses().stream().anyMatch(address -> address.getBpna().equals(delivery.getOriginBpna())) - ) && ( - delivery.getDestinationBpna() == null || - partnerSite.get().getAddresses().stream().anyMatch(address -> address.getBpna().equals(delivery.getDestinationBpna())) - )) { - return new ArrayList<>(); - } - } - if (delivery.getMaterial().isMaterialFlag()) { - ownSite = ownSites.stream().filter(site -> site.getBpns().equals(delivery.getDestinationBpns())).findFirst(); - partnerSite = partnerSites.stream().filter(site -> site.getBpns().equals(delivery.getOriginBpns())).findFirst(); - if (ownSite.isPresent() && partnerSite.isPresent() && ( - delivery.getDestinationBpna() == null || - ownSite.get().getAddresses().stream().anyMatch(address -> address.getBpna().equals(delivery.getDestinationBpna())) - ) && ( - delivery.getOriginBpna() == null || - partnerSite.get().getAddresses().stream().anyMatch(address -> address.getBpna().equals(delivery.getOriginBpna())) - )) { - return new ArrayList<>(); - } - } - errors.add("Responsibility conditions for partial responsibility are not met."); - break; - default: - errors.add("Invalid incoterm responsibility."); - break; - } - } - return errors; + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(delivery)); + validationErrors.addAll(validateOwnPartner(delivery)); + validationErrors.addAll(validateOwnResponsibility(delivery)); + return validationErrors; } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/ReportedDeliveryService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/ReportedDeliveryService.java index 6d2e88624..8891a1d4d 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/ReportedDeliveryService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/ReportedDeliveryService.java @@ -21,15 +21,13 @@ package org.eclipse.tractusx.puris.backend.delivery.logic.service; -import java.util.Date; +import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.function.Function; -import org.eclipse.tractusx.puris.backend.delivery.domain.model.EventTypeEnumeration; import org.eclipse.tractusx.puris.backend.delivery.domain.model.ReportedDelivery; import org.eclipse.tractusx.puris.backend.delivery.domain.repository.ReportedDeliveryRepository; -import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.springframework.stereotype.Service; @@ -41,8 +39,6 @@ public class ReportedDeliveryService extends DeliveryService { protected final Function validator; - private Partner ownPartnerEntity; - public ReportedDeliveryService(ReportedDeliveryRepository repository, PartnerService partnerService) { this.repository = repository; this.partnerService = partnerService; @@ -76,60 +72,13 @@ public final List createAll(List deliveries) } public boolean validate(ReportedDelivery delivery) { - return - delivery.getQuantity() >= 0 && - delivery.getMeasurementUnit() != null && - delivery.getMaterial() != null && - delivery.getPartner() != null && - validateResponsibility(delivery) && - validateTransitEvent(delivery) && - (( - delivery.getCustomerOrderNumber() != null && - delivery.getCustomerOrderPositionNumber() != null - ) || ( - delivery.getCustomerOrderNumber() == null && - delivery.getCustomerOrderPositionNumber() == null && - delivery.getSupplierOrderNumber() == null - )); - } - - private boolean validateTransitEvent(ReportedDelivery delivery) { - var now = new Date().getTime(); - return - delivery.getDepartureType() != null && - (delivery.getDepartureType() == EventTypeEnumeration.ESTIMATED_DEPARTURE || delivery.getDepartureType() == EventTypeEnumeration.ACTUAL_DEPARTURE) && - delivery.getArrivalType() != null && - (delivery.getArrivalType() == EventTypeEnumeration.ESTIMATED_ARRIVAL || delivery.getArrivalType() == EventTypeEnumeration.ACTUAL_ARRIVAL) && - !(delivery.getDepartureType() == EventTypeEnumeration.ESTIMATED_DEPARTURE && delivery.getArrivalType() == EventTypeEnumeration.ACTUAL_ARRIVAL) && - delivery.getDateOfDeparture().getTime() < delivery.getDateOfArrival().getTime() && - (delivery.getArrivalType() != EventTypeEnumeration.ACTUAL_ARRIVAL || delivery.getDateOfArrival().getTime() < now) && - (delivery.getDepartureType() != EventTypeEnumeration.ACTUAL_DEPARTURE || delivery.getDateOfDeparture().getTime() < now); + return validateWithDetails(delivery).isEmpty(); } - private boolean validateResponsibility(ReportedDelivery delivery) { - if (ownPartnerEntity == null) { - ownPartnerEntity = partnerService.getOwnPartnerEntity(); - } - return delivery.getIncoterm() != null && switch (delivery.getIncoterm().getResponsibility()) { - case CUSTOMER -> - delivery.getMaterial().isProductFlag() && - ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())) && - delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())); - case SUPPLIER -> - delivery.getMaterial().isMaterialFlag() && - delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())) && - ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())); - case PARTIAL -> - ( - delivery.getMaterial().isMaterialFlag() && - ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())) && - delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())) - - ) || ( - delivery.getMaterial().isProductFlag() && - delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())) && - ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())) - ); - }; + public List validateWithDetails(ReportedDelivery delivery) { + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(delivery)); + validationErrors.addAll(validateReportedResponsibility(delivery)); + return validationErrors; } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/DemandRequestApiService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/DemandRequestApiService.java index 32784e007..62983584a 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/DemandRequestApiService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/DemandRequestApiService.java @@ -28,6 +28,8 @@ import org.eclipse.tractusx.puris.backend.demand.logic.dto.demandsamm.ShortTermMaterialDemand; 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.domain.model.RefreshError; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.RefreshResult; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; @@ -35,6 +37,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; @Service @@ -93,7 +97,8 @@ public ShortTermMaterialDemand handleDemandSubmodelRequest(String bpnl, String m return sammMapper.ownDemandToSamm(currentDemands, partner, material); } - public void doReportedDemandRequest(Partner partner, Material material) { + public RefreshResult doReportedDemandRequest(Partner partner, Material material) { + List errors = new ArrayList<>(); try { var mpr = mprService.find(material, partner); if (mpr.getPartnerCXNumber() == null) { @@ -103,14 +108,32 @@ public void doReportedDemandRequest(Partner partner, Material material) { var data = edcAdapterService.doSubmodelRequest(AssetType.DEMAND_SUBMODEL, mpr, DirectionCharacteristic.INBOUND, 1); var samm = objectMapper.treeToValue(data, ShortTermMaterialDemand.class); var demands = sammMapper.sammToReportedDemand(samm, partner); + for (var demand : demands) { var demandPartner = demand.getPartner(); var demandMaterial = demand.getMaterial(); if (!partner.equals(demandPartner) || !material.equals(demandMaterial)) { - log.warn("Received inconsistent data from " + partner.getBpnl() + "\n" + demands); - return; + errors.add(new RefreshError(List.of("Received inconsistent data: partner or material mismatch (expected bpnl=%s, ownMaterialNumber=%s; received bpnl=%s, ownMaterialNumber=%s)".formatted( + partner.getBpnl(), + material.getOwnMaterialNumber(), + demandPartner.getBpnl(), + demandMaterial.getOwnMaterialNumber() + )))); + continue; } + + List validationErrors = reportedDemandService.validateWithDetails(demand); + if (!validationErrors.isEmpty()) { + errors.add(new RefreshError(validationErrors)); + } + } + + if (!errors.isEmpty()) { + log.warn("Validation errors found for ReportedDemand request from partner {} for material {}: {}", + partner.getBpnl(), material.getOwnMaterialNumber(), errors); + return new RefreshResult("Validation failed for reported demands", errors); } + // delete older data: var oldDemands = reportedDemandService.findAllByFilters(Optional.of(material.getOwnMaterialNumber()), Optional.of(partner.getBpnl()), Optional.empty()); for (var oldDemand : oldDemands) { @@ -119,11 +142,14 @@ public void doReportedDemandRequest(Partner partner, Material material) { for (var newDemand : demands) { reportedDemandService.create(newDemand); } - log.info("Updated ReportedDemand for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl()); - - materialService.updateTimestamp(material.getOwnMaterialNumber()); + log.info("Successfully updated ReportedDemand for {} and partner {}", + material.getOwnMaterialNumber(), partner.getBpnl()); + materialService.updateTimestamp(material.getOwnMaterialNumber()); + return new RefreshResult("Successfully processed all reported demands", errors); } catch (Exception e) { log.error("Error in ReportedDemandRequest for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), e); + errors.add(new RefreshError(List.of("System error: " + e.getMessage()))); + return new RefreshResult("Error in ReportedDemandRequest for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), errors); } } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/DemandService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/DemandService.java index 3472bd10d..1d1779b4f 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/DemandService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/DemandService.java @@ -19,6 +19,8 @@ See the NOTICE file(s) distributed with this work for additional */ package org.eclipse.tractusx.puris.backend.demand.logic.services; +import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -27,6 +29,7 @@ See the NOTICE file(s) distributed with this work for additional import javax.management.openmbean.KeyAlreadyExistsException; import org.eclipse.tractusx.puris.backend.demand.domain.model.Demand; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.springframework.data.jpa.repository.JpaRepository; @@ -80,6 +83,72 @@ public final List findAllByFilters( return stream.toList(); } + protected List basicValidation(Demand demand) { + List errors = new ArrayList<>(); + Partner ownPartnerEntity = partnerService.getOwnPartnerEntity(); + + if (demand.getMaterial() == null) { + errors.add("Missing Material."); + } + if (demand.getPartner() == null) { + errors.add("Missing Partner."); + } + if (demand.getQuantity() < 0) { + errors.add(String.format("Quantity '%s' must be greater than or equal to 0.", demand.getQuantity())); + } + if (demand.getMeasurementUnit() == null) { + errors.add("Missing measurement unit."); + } + if (demand.getLastUpdatedOnDateTime() == null) { + errors.add("Missing lastUpdatedOnTime."); + } else if (demand.getLastUpdatedOnDateTime().after(new Date())) { + errors.add(String.format("lastUpdatedOnDateTime '%s' must be in the past must be in the past (system time: '%s').", demand.getLastUpdatedOnDateTime().toInstant().toString(), (new Date()).toInstant().toString())); + } + if (demand.getDay() == null) { + errors.add("Missing day."); + } + if (demand.getDemandCategoryCode() == null) { + errors.add("Missing demand category code."); + } + if (demand.getDemandLocationBpns() == null) { + errors.add("Missing demand location BPNS."); + } + if (demand.getPartner().equals(ownPartnerEntity)) { + errors.add(String.format("Partner cannot be the same as own partner entity '%s'.", demand.getPartner().getBpnl())); + } + return errors; + } + + protected List validateReportedDemand(Demand demand) { + List errors = new ArrayList<>(); + Partner ownPartnerEntity = partnerService.getOwnPartnerEntity(); + if (!mprService.partnerOrdersProduct(demand.getMaterial(), demand.getPartner())) { + errors.add(String.format("Partner '%s' is not configured to buy your material '%s'.", demand.getPartner().getBpnl(), demand.getMaterial().getOwnMaterialNumber())); + } + if ((demand.getSupplierLocationBpns() != null && + ownPartnerEntity.getSites().stream().noneMatch(site -> site.getBpns().equals(demand.getSupplierLocationBpns()))) + || demand.getPartner().getSites().stream().noneMatch(site -> site.getBpns().equals(demand.getDemandLocationBpns()))) { + errors.add("Supplier or demand location is not valid."); + } + return errors; + } + + protected List validateOwnDemand(Demand demand) { + List errors = new ArrayList<>(); + Partner ownPartnerEntity = partnerService.getOwnPartnerEntity(); + if (!mprService.partnerSuppliesMaterial(demand.getMaterial(), demand.getPartner())) { + errors.add(String.format("Partner '%s' is not configured to supply you the specified material '%s'.", demand.getPartner().getBpnl(), demand.getMaterial().getOwnMaterialNumber())); + } + if (ownPartnerEntity.getSites().stream().noneMatch(site -> site.getBpns().equals(demand.getDemandLocationBpns()))) { + errors.add(String.format("Demand location BPNS '%s' must match to one site configured for your own partner '%s' .", demand.getDemandLocationBpns(), ownPartnerEntity.getBpnl())); + } + if (demand.getSupplierLocationBpns() != null && + demand.getPartner().getSites().stream().noneMatch(site -> site.getBpns().equals(demand.getSupplierLocationBpns()))) { + errors.add(String.format("Expected supplier location BPNS '%s' must match to one site of the partner '%s' .", demand.getSupplierLocationBpns(), demand.getPartner().getBpnl())); + } + return errors; + } + public final TEntity create(TEntity demand) { if (!validator.apply(demand)) { throw new IllegalArgumentException("Invalid demand"); diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/OwnDemandService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/OwnDemandService.java index 7b0ec191c..eefd769d7 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/OwnDemandService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/OwnDemandService.java @@ -28,10 +28,8 @@ See the NOTICE file(s) distributed with this work for additional import java.util.Date; import java.util.List; import java.util.Optional; - import org.eclipse.tractusx.puris.backend.demand.domain.model.OwnDemand; import org.eclipse.tractusx.puris.backend.demand.domain.repository.OwnDemandRepository; -import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.springframework.stereotype.Service; @@ -70,49 +68,10 @@ public boolean validate(OwnDemand demand) { } public List validateWithDetails(OwnDemand demand) { - List errors = new ArrayList<>(); - Partner ownPartnerEntity = partnerService.getOwnPartnerEntity(); - - if (demand.getMaterial() == null) { - errors.add("Missing Material."); - } - if (demand.getPartner() == null) { - errors.add("Missing Partner."); - } - if (!mprService.partnerSuppliesMaterial(demand.getMaterial(), demand.getPartner())) { - errors.add("Partner does not supply the specified material."); - } - if (demand.getQuantity() < 0) { - errors.add("Quantity must be greater than or equal to 0."); - } - if (demand.getMeasurementUnit() == null) { - errors.add("Missing measurement unit."); - } - if (demand.getLastUpdatedOnDateTime() == null) { - errors.add("Missing lastUpdatedOnTime."); - } else if (demand.getLastUpdatedOnDateTime().after(new Date())) { - errors.add("lastUpdatedOnDateTime cannot be in the future."); - } - if (demand.getDay() == null) { - errors.add("Missing day."); - } - if (demand.getDemandCategoryCode() == null) { - errors.add("Missing demand category code."); - } - if (demand.getDemandLocationBpns() == null) { - errors.add("Missing demand location BPNS."); - } - if (demand.getPartner().equals(ownPartnerEntity)) { - errors.add("Partner cannot be the same as own partner entity."); - } - if (ownPartnerEntity.getSites().stream().noneMatch(site -> site.getBpns().equals(demand.getDemandLocationBpns()))) { - errors.add("Demand location BPNS must match one of the own partner entity's site BPNS."); - } - if (demand.getSupplierLocationBpns() != null && - demand.getPartner().getSites().stream().noneMatch(site -> site.getBpns().equals(demand.getSupplierLocationBpns()))) { - errors.add("Supplier location BPNS must match one of the partner's site BPNS."); - } - return errors; + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(demand)); + validationErrors.addAll(validateOwnDemand(demand)); + return validationErrors; } private final double getSumOfQuantities(List demands) { diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/ReportedDemandService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/ReportedDemandService.java index a6034ef86..2e0b7f595 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/ReportedDemandService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/ReportedDemandService.java @@ -20,9 +20,10 @@ See the NOTICE file(s) distributed with this work for additional */ package org.eclipse.tractusx.puris.backend.demand.logic.services; +import java.util.ArrayList; +import java.util.List; import org.eclipse.tractusx.puris.backend.demand.domain.model.ReportedDemand; import org.eclipse.tractusx.puris.backend.demand.domain.repository.ReportedDemandRepository; -import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.springframework.stereotype.Service; @@ -36,18 +37,13 @@ public ReportedDemandService(ReportedDemandRepository repository, PartnerService @Override public boolean validate(ReportedDemand demand) { - Partner ownPartnerEntity = partnerService.getOwnPartnerEntity(); - return - demand.getMaterial() != null && - demand.getPartner() != null && - mprService.partnerOrdersProduct(demand.getMaterial(), demand.getPartner()) && - demand.getQuantity() >= 0 && - demand.getMeasurementUnit() != null && - demand.getDay() != null && - demand.getDemandCategoryCode() != null && - demand.getDemandLocationBpns() != null && - !demand.getPartner().equals(ownPartnerEntity) && - (demand.getSupplierLocationBpns() == null || ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(demand.getSupplierLocationBpns()))) && - demand.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(demand.getDemandLocationBpns())); + return validateWithDetails(demand).isEmpty(); + } + + public List validateWithDetails(ReportedDemand demand) { + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(demand)); + validationErrors.addAll(validateReportedDemand(demand)); + return validationErrors; } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/model/RefreshError.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/model/RefreshError.java new file mode 100644 index 000000000..0d0a6197a --- /dev/null +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/model/RefreshError.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 Volkswagen AG + * Copyright (c) 2025 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.masterdata.domain.model; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class RefreshError { + private List errors; +} diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/model/RefreshResult.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/model/RefreshResult.java new file mode 100644 index 000000000..e1bd1b56c --- /dev/null +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/domain/model/RefreshResult.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Volkswagen AG + * Copyright (c) 2025 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.masterdata.domain.model; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class RefreshResult { + private String message; + private List errors; +} \ No newline at end of file diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialRefreshService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialRefreshService.java index 8dedd9915..c4c370382 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialRefreshService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/logic/service/MaterialRefreshService.java @@ -22,12 +22,16 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.tractusx.puris.backend.delivery.logic.service.DeliveryRequestApiService; import org.eclipse.tractusx.puris.backend.demand.logic.services.DemandRequestApiService; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.RefreshResult; import org.eclipse.tractusx.puris.backend.production.logic.service.ProductionRequestApiService; import org.eclipse.tractusx.puris.backend.stock.logic.dto.itemstocksamm.DirectionCharacteristic; import org.eclipse.tractusx.puris.backend.stock.logic.service.ItemStockRequestApiService; @@ -65,6 +69,9 @@ public class MaterialRefreshService { @Autowired private SimpMessagingTemplate messagingTemplate; + @Autowired + private ObjectMapper objectMapper; + public void refreshPartnerData(String ownMaterialNumber) { var material = materialService.findByOwnMaterialNumber(ownMaterialNumber); var customers = partnerService.findAllCustomerPartnersForMaterialId(ownMaterialNumber); @@ -73,54 +80,83 @@ public void refreshPartnerData(String ownMaterialNumber) { allPartners.addAll(suppliers); var numberOfTasks = (customers.size() + suppliers.size()) * 3 + allPartners.size(); ExecutorService executorService = Executors.newFixedThreadPool(numberOfTasks); - List> futures = new ArrayList<>(); + List> futures = new ArrayList<>(); + // customers customers.forEach(customer -> { - futures.add(CompletableFuture.runAsync( + futures.add(CompletableFuture.supplyAsync( () -> demandRequestApiService.doReportedDemandRequest(customer, material), executorService)); futures.add( - CompletableFuture.runAsync( + CompletableFuture.supplyAsync( () -> itemStockRequestApiService .doItemStockSubmodelReportedProductItemStockRequest( customer, material), executorService)); futures.add( - CompletableFuture.runAsync( + CompletableFuture.supplyAsync( () -> daysOfSupplyRequestApiService .doReportedDaysOfSupplyRequest(customer, material, DirectionCharacteristic.INBOUND), executorService)); }); + // suppliers suppliers.forEach(supplier -> { - futures.add(CompletableFuture.runAsync( + futures.add(CompletableFuture.supplyAsync( () -> productionRequestApiService.doReportedProductionRequest(supplier, material), executorService)); futures.add( - CompletableFuture.runAsync( + CompletableFuture.supplyAsync( () -> itemStockRequestApiService .doItemStockSubmodelReportedMaterialItemStockRequest( supplier, material), executorService)); futures.add( - CompletableFuture.runAsync( + CompletableFuture.supplyAsync( () -> daysOfSupplyRequestApiService .doReportedDaysOfSupplyRequest(supplier, material, DirectionCharacteristic.OUTBOUND), executorService)); }); + // deliveries allPartners.forEach(partner -> { - futures.add(CompletableFuture.runAsync( + futures.add(CompletableFuture.supplyAsync( () -> deliveryRequestApiService.doReportedDeliveryRequest(partner, material), executorService)); }); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenRun(() -> { - var topic = "/topic/material/" + material.getOwnMaterialNumber(); - messagingTemplate.convertAndSend(topic, "SUCCESS"); - log.info("Refreshed Material data for " + material.getOwnMaterialNumber()); - }); + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream().map(CompletableFuture::join).toList()) + .thenAccept(results -> { + var allErrors = results.stream() + .filter(r -> r.getErrors() != null && !r.getErrors().isEmpty()) + .map(r -> Map.of( + "message", r.getMessage(), + "errors", r.getErrors() + )) + .toList(); + + var topic = "/topic/material/" + material.getOwnMaterialNumber(); + if (allErrors.isEmpty()) { + messagingTemplate.convertAndSend(topic, "SUCCESS"); + log.info("Successfully refreshed material {}", material.getOwnMaterialNumber()); + } else { + try { + var json = objectMapper.writeValueAsString(allErrors); + messagingTemplate.convertAndSend(topic, json); + log.warn("Refresh completed with errors for material {}: {}", + material.getOwnMaterialNumber(), json); + } catch (Exception e) { + messagingTemplate.convertAndSend(topic, "[{\"errors\":[\"Serialization error: " + + e.getMessage().replace("\"","\\\"") + "\"]}]"); + log.error("Failed to serialize error payload for material {}", + material.getOwnMaterialNumber(), e); + } + } + }); + executorService.shutdown(); } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/OwnProductionService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/OwnProductionService.java index ba70ad2b7..f47470e09 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/OwnProductionService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/OwnProductionService.java @@ -22,13 +22,10 @@ See the NOTICE file(s) distributed with this work for additional package org.eclipse.tractusx.puris.backend.production.logic.service; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.function.Function; import javax.management.openmbean.KeyAlreadyExistsException; - -import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.eclipse.tractusx.puris.backend.production.domain.model.OwnProduction; import org.eclipse.tractusx.puris.backend.production.domain.repository.OwnProductionRepository; @@ -75,43 +72,9 @@ public boolean validate(OwnProduction production) { } public List validateWithDetails(OwnProduction production) { - List errors = new ArrayList<>(); - Partner ownPartnerEntity = partnerService.getOwnPartnerEntity(); - - if (production.getQuantity() < 0) { - errors.add("Quantity must be greater than or equal to 0."); - } - if (production.getMeasurementUnit() == null) { - errors.add("Missing measurement unit."); - } - if (production.getLastUpdatedOnDateTime() == null) { - errors.add("Missing lastUpdatedOnTime."); - } else if (production.getLastUpdatedOnDateTime().after(new Date())) { - errors.add("lastUpdatedOnDateTime cannot be in the future."); - } - if (production.getEstimatedTimeOfCompletion() == null) { - errors.add("Missing estimated time of completion."); - } - if (production.getMaterial() == null) { - errors.add("Missing material."); - } - if (production.getPartner() == null) { - errors.add("Missing partner."); - } - if (production.getPartner().equals(ownPartnerEntity)) { - errors.add("Partner cannot be the same as own partner entity."); - } - if (production.getProductionSiteBpns() == null) { - errors.add("Missing production site BPNS."); - } - if (ownPartnerEntity.getSites().stream().noneMatch(site -> site.getBpns().equals(production.getProductionSiteBpns()))) { - errors.add("Production site BPNS must match one of the own partner entity's site BPNS."); - } - if (!((production.getCustomerOrderNumber() != null && production.getCustomerOrderPositionNumber() != null) || - (production.getCustomerOrderNumber() == null && production.getCustomerOrderPositionNumber() == null && production.getSupplierOrderNumber() == null))) { - errors.add("If an order position reference is given, customer order number and customer order position number must be set."); - } - - return errors; + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(production)); + validationErrors.addAll(validateOwnProduction(production, partnerService.getOwnPartnerEntity())); + return validationErrors; } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ProductionRequestApiService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ProductionRequestApiService.java index c89a2296c..f0bc96873 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ProductionRequestApiService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ProductionRequestApiService.java @@ -26,15 +26,20 @@ import org.eclipse.tractusx.puris.backend.common.edc.logic.service.EdcAdapterService; 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.domain.model.RefreshError; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.RefreshResult; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; +import org.eclipse.tractusx.puris.backend.production.domain.model.ReportedProduction; import org.eclipse.tractusx.puris.backend.production.logic.adapter.PlannedProductionSammMapper; import org.eclipse.tractusx.puris.backend.production.logic.dto.plannedproductionsamm.PlannedProductionOutput; 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.List; import java.util.Optional; @Service @@ -77,7 +82,8 @@ public PlannedProductionOutput handleProductionSubmodelRequest(String bpnl, Stri return sammMapper.ownProductionToSamm(currentProduction, partner, material); } - public void doReportedProductionRequest(Partner partner, Material material) { + public RefreshResult doReportedProductionRequest(Partner partner, Material material) { + List errors = new ArrayList<>(); try { var mpr = mprService.find(material, partner); var data = edcAdapterService.doSubmodelRequest(AssetType.PRODUCTION_SUBMODEL, mpr, DirectionCharacteristic.OUTBOUND, 1); @@ -87,10 +93,27 @@ public void doReportedProductionRequest(Partner partner, Material material) { var productionPartner = production.getPartner(); var productionMaterial = production.getMaterial(); if (!partner.equals(productionPartner) || !material.equals(productionMaterial)) { - log.warn("Received inconsistent data from " + partner.getBpnl()); - return; + errors.add(new RefreshError(List.of("Received inconsistent data: partner or material mismatch (expected bpnl=%s, ownMaterialNumber=%s; received bpnl=%s, ownMaterialNumber=%s)".formatted( + partner.getBpnl(), + material.getOwnMaterialNumber(), + productionPartner.getBpnl(), + productionMaterial.getOwnMaterialNumber() + )))); + continue; + } + + List validationErrors = reportedProductionService.validateWithDetails(production); + if (!validationErrors.isEmpty()) { + errors.add(new RefreshError(validationErrors)); } } + + if (!errors.isEmpty()) { + log.warn("Validation errors found for ReportedProduction request from partner {} for material {}: {}", + partner.getBpnl(), material.getOwnMaterialNumber(), errors); + return new RefreshResult("Validation failed for reported productions", errors); + } + // delete older data: var oldProductions = reportedProductionService.findAllByFilters(Optional.of(material.getOwnMaterialNumber()), Optional.of(partner.getBpnl()), Optional.empty(), Optional.empty()); for (var oldProduction : oldProductions) { @@ -99,11 +122,14 @@ public void doReportedProductionRequest(Partner partner, Material material) { for (var newProduction : productions) { reportedProductionService.create(newProduction); } - log.info("Updated ReportedProduction for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl()); - + log.info("Successfully updated ReportedProduction for {} and partner {}", + material.getOwnMaterialNumber(), partner.getBpnl()); materialService.updateTimestamp(material.getOwnMaterialNumber()); + return new RefreshResult("Successfully processed all reported productions", errors); } catch (Exception e) { log.error("Error in ReportedProductionRequest for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), e); + errors.add(new RefreshError(List.of("System error: " + e.getMessage()))); + return new RefreshResult("System error occurred during processing", errors); } } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ProductionService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ProductionService.java index 790f510b6..9c389105d 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ProductionService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ProductionService.java @@ -19,6 +19,7 @@ */ package org.eclipse.tractusx.puris.backend.production.logic.service; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.production.domain.model.Production; import org.eclipse.tractusx.puris.backend.production.domain.repository.ProductionRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -96,6 +97,60 @@ public final List getQuantityForDays(String material, Optional p return quantities; } + protected List basicValidation(Production production) { + List errors = new ArrayList<>(); + + if (production.getQuantity() < 0) { + errors.add(String.format("Quantity '%s' must be greater than or equal to 0.", production.getQuantity())); + } + if (production.getMeasurementUnit() == null) { + errors.add("Missing measurement unit."); + } + if (production.getLastUpdatedOnDateTime() == null) { + errors.add("Missing lastUpdatedOnTime."); + } else if (production.getLastUpdatedOnDateTime().after(new Date())) { + errors.add(String.format("lastUpdatedOnDateTime '%s' must be in the past must be in the past (system time: '%s').", production.getLastUpdatedOnDateTime().toInstant().toString(), (new Date()).toInstant().toString())); + } + if (production.getEstimatedTimeOfCompletion() == null) { + errors.add("Missing estimated time of completion."); + } + if (production.getMaterial() == null) { + errors.add("Missing material."); + } + if (production.getPartner() == null) { + errors.add("Missing partner."); + } + if (production.getProductionSiteBpns() == null) { + errors.add("Missing production site BPNS."); + } + if (!((production.getCustomerOrderNumber() != null && production.getCustomerOrderPositionNumber() != null) || + (production.getCustomerOrderNumber() == null && production.getCustomerOrderPositionNumber() == null && production.getSupplierOrderNumber() == null))) { + errors.add(String.format("If an order position reference is given, customer order number '%s' and customer order position number '%s' must be set. Supplier order number '%' then can be set, too", production.getCustomerOrderNumber(), production.getCustomerOrderPositionNumber(), production.getSupplierOrderNumber())); + } + + return errors; + } + + protected List validateOwnProduction(Production production, Partner ownPartnerEntity) { + List errors = new ArrayList<>(); + + if (production.getPartner().equals(ownPartnerEntity)) { + errors.add(String.format("Partner cannot be the same as own partner entity '%s'.", production.getPartner().getBpnl())); + } + if (ownPartnerEntity.getSites().stream().noneMatch(site -> site.getBpns().equals(production.getProductionSiteBpns()))) { + errors.add(String.format("Production site BPNS '%s' must match to one site of the partner '%s' .", production.getProductionSiteBpns(), production.getPartner().getBpnl())); + } + return errors; + } + + protected List validateReportedProduction(Production production) { + List errors = new ArrayList<>(); + if (production.getPartner().getSites().stream().noneMatch(site -> site.getBpns().equals(production.getProductionSiteBpns()))) { + errors.add(String.format("Production site BPNS '%s' must match to one site of the partner '%s' .", production.getProductionSiteBpns(), production.getPartner().getBpnl())); + } + return errors; + } + public final T update(T production) { if (production.getUuid() == null || repository.findById(production.getUuid()).isEmpty()) { return null; diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ReportedProductionService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ReportedProductionService.java index 032074321..b70813153 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ReportedProductionService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ReportedProductionService.java @@ -21,6 +21,7 @@ See the NOTICE file(s) distributed with this work for additional package org.eclipse.tractusx.puris.backend.production.logic.service; +import java.util.ArrayList; import java.util.List; import java.util.function.Function; @@ -61,21 +62,13 @@ public final List createAll(List product } public boolean validate(ReportedProduction production) { - return - production.getQuantity() >= 0 && - production.getMeasurementUnit() != null && - production.getEstimatedTimeOfCompletion() != null && - production.getMaterial() != null && - production.getPartner() != null && - production.getProductionSiteBpns() != null && - production.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(production.getProductionSiteBpns())) && - (( - production.getCustomerOrderNumber() != null && - production.getCustomerOrderPositionNumber() != null - ) || ( - production.getCustomerOrderNumber() == null && - production.getCustomerOrderPositionNumber() == null && - production.getSupplierOrderNumber() == null - )); + return validateWithDetails(production).isEmpty(); + } + + public List validateWithDetails(ReportedProduction production) { + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(production)); + validationErrors.addAll(validateReportedProduction(production)); + return validationErrors; } } 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 9ed05a668..113f42f15 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 @@ -22,11 +22,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + 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.domain.model.RefreshError; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.RefreshResult; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; @@ -125,7 +131,8 @@ public ItemStockSamm handleItemStockSubmodelRequest(String bpnl, String material } - public void doItemStockSubmodelReportedMaterialItemStockRequest(Partner partner, Material material) { + public RefreshResult doItemStockSubmodelReportedMaterialItemStockRequest(Partner partner, Material material) { + List errors = new ArrayList<>(); try { var mpr = mprService.find(material, partner); var data = edcAdapterService.doSubmodelRequest(AssetType.ITEM_STOCK_SUBMODEL, mpr, DirectionCharacteristic.OUTBOUND, 1); @@ -135,10 +142,26 @@ public void doItemStockSubmodelReportedMaterialItemStockRequest(Partner partner, var stockPartner = stock.getPartner(); var stockMaterial = stock.getMaterial(); if (!partner.equals(stockPartner) || !material.equals(stockMaterial)) { - log.warn("Received inconsistent data from " + partner.getBpnl() + "\n" + stocks); - return; + errors.add(new RefreshError(List.of("Received inconsistent data: partner or material mismatch (expected bpnl=%s, ownMaterialNumber=%s; received bpnl=%s, ownMaterialNumber=%s)".formatted( + partner.getBpnl(), + material.getOwnMaterialNumber(), + stockPartner.getBpnl(), + stockMaterial.getOwnMaterialNumber() + )))); + continue; } + + List validationErrors = reportedMaterialItemStockService.validateWithDetails(stock); + if (!validationErrors.isEmpty()) { + errors.add(new RefreshError(validationErrors)); + } + } + if (!errors.isEmpty()) { + log.warn("Validation errors found for ReportedMaterialItemStock request from partner {}: {}", + partner.getBpnl(), errors); + return new RefreshResult("Validation failed for reported materials", errors); } + // delete older data: var oldStocks = reportedMaterialItemStockService.findByPartnerAndMaterial(partner, material); for (var oldStock : oldStocks) { reportedMaterialItemStockService.delete(oldStock.getUuid()); @@ -149,12 +172,16 @@ public void doItemStockSubmodelReportedMaterialItemStockRequest(Partner partner, log.info("Updated ReportedMaterialItemStocks for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl()); materialService.updateTimestamp(material.getOwnMaterialNumber()); + return new RefreshResult("Updated ReportedMaterialItemStocks for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), errors); } catch (Exception e) { log.error("Error in ReportedMaterialItemStockRequest for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), e); + errors.add(new RefreshError(List.of("System error: " + e.getMessage()))); + return new RefreshResult("Error in ReportedMaterialItemStockRequest for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), errors); } } - public void doItemStockSubmodelReportedProductItemStockRequest(Partner partner, Material material) { + public RefreshResult doItemStockSubmodelReportedProductItemStockRequest(Partner partner, Material material) { + List errors = new ArrayList<>(); try { var mpr = mprService.find(material, partner); if (mpr.getPartnerCXNumber() == null) { @@ -168,10 +195,21 @@ public void doItemStockSubmodelReportedProductItemStockRequest(Partner partner, var stockPartner = stock.getPartner(); var stockMaterial = stock.getMaterial(); if (!partner.equals(stockPartner) || !material.equals(stockMaterial)) { - log.warn("Received inconsistent data from " + partner.getBpnl() + "\n" + stocks); - return; + errors.add(new RefreshError(List.of("Received inconsistent data from " + partner.getBpnl() + "\n" + stocks))); + continue; + } + + List validationErrors = reportedProductItemStockService.validateWithDetails(stock); + if (!validationErrors.isEmpty()) { + errors.add(new RefreshError(validationErrors)); } } + + if (!errors.isEmpty()) { + log.warn("Validation errors found for ReportedProductItemStock request from partner {}: {}", + partner.getBpnl(), errors); + return new RefreshResult("Validation failed for reported item stocks", errors); + } // delete older data: var oldStocks = reportedProductItemStockService.findByPartnerAndMaterial(partner, material); for (var oldStock : oldStocks) { @@ -183,8 +221,11 @@ public void doItemStockSubmodelReportedProductItemStockRequest(Partner partner, log.info("Updated ReportedProductItemStocks for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl()); materialService.updateTimestamp(material.getOwnMaterialNumber()); + return new RefreshResult("Updated ReportedProductItemStocks for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), errors); } catch (Exception e) { log.error("Error in ReportedProductItemStockRequest for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), e); + errors.add(new RefreshError(List.of("System error: " + e.getMessage()))); + return new RefreshResult("Error in ReportedProductItemStockRequest for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), errors); } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ItemStockService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ItemStockService.java index 323090cde..9c654c305 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ItemStockService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ItemStockService.java @@ -156,7 +156,7 @@ protected List basicValidation(ItemStock itemStock) { errors.add("Missing locationBpns."); } if (itemStock.getQuantity() < 0){ - errors.add("Quantity must be greater than or equal to 0."); + errors.add(String.format("Quantity '%s' must be greater than or equal to 0.", itemStock.getQuantity())); } if (itemStock.getMeasurementUnit() == null) { errors.add("Missing measurementUnit."); @@ -164,11 +164,14 @@ protected List basicValidation(ItemStock itemStock) { if (itemStock.getLastUpdatedOnDateTime() == null) { errors.add("Missing lastUpdatedOnTime."); } else if (itemStock.getLastUpdatedOnDateTime().after(new Date())) { - errors.add("lastUpdatedOnDateTime cannot be in the future."); + errors.add(String.format("lastUpdatedOnDateTime '%s' must be in the past must be in the past (system time: '%s').", itemStock.getLastUpdatedOnDateTime().toInstant().toString(), (new Date()).toInstant().toString())); } if (!((itemStock.getCustomerOrderId() != null && itemStock.getCustomerOrderPositionId() != null) || (itemStock.getCustomerOrderId() == null && itemStock.getCustomerOrderPositionId() == null && itemStock.getSupplierOrderId() == null))) { - errors.add("If an order position reference is given, customer order number and customer order position number must be set."); + errors.add(String.format( + "Invalid order reference configuration for item stock: customerOrderId='%s', customerOrderPositionId='%s', supplierOrderId='%s'. If a customer order reference is provided, both customerOrderId and customerOrderPositionId must be set, if none are provided then they must be null. ", + itemStock.getCustomerOrderId(), itemStock.getCustomerOrderPositionId(), itemStock.getSupplierOrderId() + )); } } catch (Exception e) { log.error("Basic Validation failed: " + itemStock + "\n" + e.getMessage()); @@ -195,10 +198,10 @@ protected final List validateMaterialItemStock(ItemStock itemStock) { errors.add("Missing MaterialPartnerRelation."); } if (!material.isMaterialFlag()) { - errors.add("Material flag is missing."); + errors.add(String.format("Material flag is missing for Material '%s'.", material.getOwnMaterialNumber())); } if (relation != null && !relation.isPartnerSuppliesMaterial()) { - errors.add("Partner does not supply material."); + errors.add(String.format("Partner '%s' does not supply material '%s'. ", partner.getBpnl(), material.getOwnMaterialNumber())); } } catch (Exception e) { log.error("MaterialItemStock Validation failed: " + itemStock + "\n" + e.getMessage()); @@ -217,10 +220,10 @@ protected final List validateProductItemStock(ItemStock itemStock) { errors.add("Missing MaterialPartnerRelation."); } if (!material.isProductFlag()) { - errors.add("Product flag is missing."); + errors.add(String.format("Product flag is missing for Material '%s'.", material.getOwnMaterialNumber())); } if (relation != null && !relation.isPartnerBuysMaterial()) { - errors.add("Partner does not buy material."); + errors.add(String.format("Partner '%s' does not supply material '%s'. ", partner.getBpnl(), material.getOwnMaterialNumber())); } } catch (Exception e) { log.error("ProductItemStock Validation failed: " + itemStock + "\n" + e.getMessage()); diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/MaterialItemStockService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/MaterialItemStockService.java index e215fa6e9..4af331d12 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/MaterialItemStockService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/MaterialItemStockService.java @@ -45,8 +45,7 @@ public MaterialItemStockService(PartnerService partnerService, MaterialPartnerRe @Override public boolean validate(MaterialItemStock materialItemStock) { - return basicValidation(materialItemStock).isEmpty() && validateLocalStock(materialItemStock).isEmpty() - && validateMaterialItemStock(materialItemStock).isEmpty(); + return validateWithDetails(materialItemStock).isEmpty(); } public List validateWithDetails(MaterialItemStock materialItemStock) { diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ReportedMaterialItemStockService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ReportedMaterialItemStockService.java index de8f6cdff..a516bc483 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ReportedMaterialItemStockService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ReportedMaterialItemStockService.java @@ -21,6 +21,9 @@ package org.eclipse.tractusx.puris.backend.stock.logic.service; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.List; + import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.eclipse.tractusx.puris.backend.stock.domain.model.ReportedMaterialItemStock; @@ -42,4 +45,12 @@ public ReportedMaterialItemStockService(PartnerService partnerService, MaterialP public boolean validate(ReportedMaterialItemStock itemStock) { return basicValidation(itemStock).isEmpty() && validateMaterialItemStock(itemStock).isEmpty() && validateRemoteStock(itemStock).isEmpty(); } + + public List validateWithDetails(ReportedMaterialItemStock itemStock) { + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(itemStock)); + validationErrors.addAll(validateMaterialItemStock(itemStock)); + validationErrors.addAll(validateRemoteStock(itemStock)); + return validationErrors; + } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ReportedProductItemStockService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ReportedProductItemStockService.java index 08124ed78..eacea63d0 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ReportedProductItemStockService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/stock/logic/service/ReportedProductItemStockService.java @@ -21,6 +21,9 @@ package org.eclipse.tractusx.puris.backend.stock.logic.service; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.List; + import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.eclipse.tractusx.puris.backend.stock.domain.model.ReportedProductItemStock; @@ -43,4 +46,12 @@ public ReportedProductItemStockService(PartnerService partnerService, MaterialPa public boolean validate(ReportedProductItemStock itemStock) { return basicValidation(itemStock).isEmpty() && validateProductItemStock(itemStock).isEmpty() && validateRemoteStock(itemStock).isEmpty(); } + + public List validateWithDetails(ReportedProductItemStock itemStock) { + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(itemStock)); + validationErrors.addAll(validateProductItemStock(itemStock)); + validationErrors.addAll(validateRemoteStock(itemStock)); + return validationErrors; + } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/CustomerSupplyService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/CustomerSupplyService.java index 5cc3df8a6..603e0ea03 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/CustomerSupplyService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/CustomerSupplyService.java @@ -21,6 +21,7 @@ package org.eclipse.tractusx.puris.backend.supply.logic.service; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -110,17 +111,12 @@ public final List findAllByFilters(Optional ownM } public boolean validate(ReportedCustomerSupply daysOfSupply) { - return - daysOfSupply.getPartner() != null && - daysOfSupply.getMaterial() != null && - daysOfSupply.getDate() != null && - daysOfSupply.getDaysOfSupply() >= 0 && - daysOfSupply.getStockLocationBPNS() != null && - daysOfSupply.getStockLocationBPNA() != null && - daysOfSupply.getPartner() != partnerService.getOwnPartnerEntity() && - daysOfSupply.getPartner().getSites().stream().anyMatch(site -> - site.getBpns().equals(daysOfSupply.getStockLocationBPNS()) && - site.getAddresses().stream().anyMatch(address -> address.getBpna().equals(daysOfSupply.getStockLocationBPNA())) - ); + return validateWithDetails(daysOfSupply).isEmpty(); + } + + public List validateWithDetails(ReportedCustomerSupply daysOfSupply) { + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(daysOfSupply)); + return validationErrors; } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/DaysOfSupplyRequestApiService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/DaysOfSupplyRequestApiService.java index 2b1c764cc..f5bfa0aed 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/DaysOfSupplyRequestApiService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/DaysOfSupplyRequestApiService.java @@ -27,6 +27,8 @@ 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; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.RefreshError; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.RefreshResult; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.eclipse.tractusx.puris.backend.stock.logic.dto.itemstocksamm.DirectionCharacteristic; @@ -110,7 +112,8 @@ public DaysOfSupply handleDaysOfSupplySubmodelRequest(String bpnl, String materi } } - public void doReportedDaysOfSupplyRequest(Partner partner, Material material, DirectionCharacteristic direction) { + public RefreshResult doReportedDaysOfSupplyRequest(Partner partner, Material material, DirectionCharacteristic direction) { + List errors = new ArrayList<>(); try { var mpr = mprService.find(material, partner); if (mpr.getPartnerCXNumber() == null) { @@ -125,10 +128,24 @@ public void doReportedDaysOfSupplyRequest(Partner partner, Material material, Di var supplyPartner = reportedCustomerSupply.getPartner(); var supplyMaterial = reportedCustomerSupply.getMaterial(); if (!partner.equals(supplyPartner) || !material.equals(supplyMaterial)) { - log.warn("Received inconsistent data from " + partner.getBpnl() + "\n" - + reportedCustomerSupplies); - return; + errors.add(new RefreshError(List.of("Received inconsistent data: partner or material mismatch (expected bpnl=%s, ownMaterialNumber=%s; received bpnl=%s, ownMaterialNumber=%s)".formatted( + partner.getBpnl(), + material.getOwnMaterialNumber(), + supplyPartner.getBpnl(), + supplyMaterial.getOwnMaterialNumber() + )))); + continue; } + + List validationErrors = customerSupplyService.validateWithDetails(reportedCustomerSupply); + if (!validationErrors.isEmpty()) { + errors.add(new RefreshError(validationErrors)); + } + } + if (!errors.isEmpty()) { + log.warn("Validation errors found for ReportedSupply request from partner {}: {}", + partner.getBpnl(), errors); + return new RefreshResult("Validation failed for reported supplies", errors); } var oldSupplies = customerSupplyService.findAllByFilters(Optional.of(material.getOwnMaterialNumber()), Optional.of(partner.getBpnl()), Optional.empty()); for (var oldSupply : oldSupplies) { @@ -143,9 +160,13 @@ public void doReportedDaysOfSupplyRequest(Partner partner, Material material, Di var supplyPartner = reportedSupplierSupply.getPartner(); var supplyMaterial = reportedSupplierSupply.getMaterial(); if (!partner.equals(supplyPartner) || !material.equals(supplyMaterial)) { - log.warn("Received inconsistent data from " + partner.getBpnl() + "\n" - + reportedSupplierSupplies); - return; + errors.add(new RefreshError(List.of("Received inconsistent data from " + partner.getBpnl()))); + continue; + } + + List validationErrors = supplierSupplyService.validateWithDetails(reportedSupplierSupply); + if (!validationErrors.isEmpty()) { + errors.add(new RefreshError(validationErrors)); } } var oldSupplies = supplierSupplyService.findAllByFilters(Optional.of(material.getOwnMaterialNumber()), Optional.of(partner.getBpnl()), Optional.empty()); @@ -157,8 +178,11 @@ public void doReportedDaysOfSupplyRequest(Partner partner, Material material, Di } } log.info("Updated ReportedSupply for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl()); + return new RefreshResult("Updated ReportedSupply for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), errors); } catch (Exception e) { log.error("Error in ReportedDaysOfSupply request for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), e); + errors.add(new RefreshError(List.of("System error: " + e.getMessage()))); + return new RefreshResult("Error in ReportedDaysOfSupply request for " + material.getOwnMaterialNumber() + " and partner " + partner.getBpnl(), errors); } } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/SupplierSupplyService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/SupplierSupplyService.java index 76eb5a312..23583712b 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/SupplierSupplyService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/SupplierSupplyService.java @@ -21,6 +21,7 @@ package org.eclipse.tractusx.puris.backend.supply.logic.service; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -109,19 +110,13 @@ public final List findAllByFilters(Optional ownM return stream.toList(); } - public boolean validate(ReportedSupplierSupply daysOfSupply) { - return - daysOfSupply.getPartner() != null && - daysOfSupply.getMaterial() != null && - daysOfSupply.getDate() != null && - daysOfSupply.getDaysOfSupply() >= 0 && - daysOfSupply.getStockLocationBPNS() != null && - daysOfSupply.getStockLocationBPNA() != null && - daysOfSupply.getPartner() != partnerService.getOwnPartnerEntity() && - daysOfSupply.getPartner().getSites().stream().anyMatch(site -> - site.getBpns().equals(daysOfSupply.getStockLocationBPNS()) && - site.getAddresses().stream().anyMatch(address -> address.getBpna().equals(daysOfSupply.getStockLocationBPNA())) - ); + return validateWithDetails(daysOfSupply).isEmpty(); + } + + public List validateWithDetails(ReportedSupplierSupply daysOfSupply) { + List validationErrors = new ArrayList<>(); + validationErrors.addAll(basicValidation(daysOfSupply)); + return validationErrors; } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/SupplyService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/SupplyService.java index 8def33796..073b886b8 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/SupplyService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/supply/logic/service/SupplyService.java @@ -175,4 +175,35 @@ private double getDaysOfSupply(double stockQuantity, List consumedValues } return daysOfSupply; } + + protected List basicValidation(Supply supply) { + List errors = new ArrayList<>(); + Partner ownPartnerEntity = partnerService.getOwnPartnerEntity(); + + if (supply.getMaterial() == null) { + errors.add("Missing Material."); + } + if (supply.getPartner() == null) { + errors.add("Missing Partner."); + } + if (supply.getDate() == null) { + errors.add("Missing date."); + } + if (supply.getStockLocationBPNS() == null) { + errors.add("Missing stock location BPNS."); + } + if (supply.getStockLocationBPNA() == null) { + errors.add("Missing stock location BPNA."); + } + if (supply.getPartner().equals(ownPartnerEntity)) { + errors.add(String.format("Partner cannot be the same as own partner entity '%s'.", supply.getPartner().getBpnl())); + } + if (supply.getPartner().getSites().stream().noneMatch(site -> + site.getBpns().equals(supply.getStockLocationBPNS()) || + site.getAddresses().stream().noneMatch(address -> address.getBpna().equals(supply.getStockLocationBPNA())) + )) { + errors.add(String.format("Stock location '%s' and or stock address '%s' don't belong to each other or partner '%s'.", supply.getStockLocationBPNS(), supply.getStockLocationBPNA(), supply.getPartner().getBpnl())); + } + return errors; + } } diff --git a/frontend/src/features/material-details/components/MaterialDetails.tsx b/frontend/src/features/material-details/components/MaterialDetails.tsx index 5a3d440b9..38db92997 100644 --- a/frontend/src/features/material-details/components/MaterialDetails.tsx +++ b/frontend/src/features/material-details/components/MaterialDetails.tsx @@ -36,7 +36,7 @@ import { scheduleErpUpdateStocks } from '@services/stocks-service'; import { NotFoundView } from '@views/errors/NotFoundView'; import { Material } from '@models/types/data/stock'; import { BPNS } from '@models/types/edc/bpn'; -import { useSubscription } from 'react-stomp-hooks'; +import { IMessage, useSubscription } from 'react-stomp-hooks'; import { refreshPartnerData } from '@services/refresh-service'; type SummaryContainerProps = { @@ -58,6 +58,61 @@ function SummaryContainer({ children }: SummaryContainerProps) { ); } +function makeErrorDownload(payloadText: string, materialNo: string) { + let contents: string; + try { + contents = JSON.stringify(JSON.parse(payloadText), null, 2); + } catch { + contents = JSON.stringify( + { material: materialNo, errors: [payloadText] }, + null, + 2 + ); + } + + const blob = new Blob([contents], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `material-${materialNo}-refresh-errors-${ts}.json`; + + return { url, filename }; +} + +function parseRefreshWsMessage(msg?: string): { ok: boolean; errors: string[] } { + const text = (msg ?? '').trim(); + if (!text || text === 'SUCCESS') return { ok: true, errors: [] }; + + try { + const arr = JSON.parse(text) as unknown; + if (Array.isArray(arr)) { + const a = arr as any[]; + + const hasMessage = a.some(e => e && typeof e === 'object' && 'message' in e); + const errors = hasMessage + ? a.flatMap(entry => { + const msg = typeof entry?.message === 'string' ? entry.message.trim() : ''; + const prefix = msg ? `${msg}: ` : ''; + const errs = Array.isArray(entry?.errors) ? entry.errors : []; + return errs.flatMap((e: any) => { + if (Array.isArray(e?.errors)) { + return e.errors.filter((s: any) => typeof s === 'string' && s.length > 0) + .map((s: string) => prefix + s); + } + return typeof e === 'string' && e.length > 0 ? [prefix + e] : []; + }); + }) + : (a as { errors?: string[] }[]) + .flatMap(e => Array.isArray(e?.errors) ? e.errors! : []) + .filter(s => typeof s === 'string' && s.length > 0); + return { ok: errors.length === 0, errors: errors.length ? errors : [text] }; + } + return { ok: false, errors: [text] }; + } catch { + return { ok: false, errors: [text] }; + } +} + type MaterialDetailsProps = { material: Material; direction: DirectionType; @@ -131,25 +186,44 @@ export function MaterialDetails({ material, direction }: MaterialDetailsProps) { }; }, {}), [createSummaryByPartnerAndDirection, direction, expandablePartners]); - const handleRefresh = async () => { - refresh(['partner-data']) - .then(() => { + const handleRefreshMessage = async (message?: string) => { + const raw = (message ?? '').trim(); + const { ok, errors } = parseRefreshWsMessage(raw); + try { + await refresh(['partner-data']); + + if (ok) { notify({ title: 'Partner data updated', - description: `The partner data for ${material.name} was updated as requested`, - severity: 'success' - }) - }) - .catch(() => { + description: `The partner data for ${material.name} was updated as requested.`, + severity: 'success', + }); + } else { + const { url, filename } = makeErrorDownload(raw, material.ownMaterialNumber || ''); notify({ - title: 'Partner data refresh error', - description: `There was an error refreshing the requested partner data. Please try manually reloading the page`, - severity: 'error' - }) - }); - setIsRefreshing(false); + title: 'Partner data updated with errors', + description: ( + + Found {errors.length} issue{errors.length === 1 ? '' : 's'}.{' '} + { + setTimeout(() => URL.revokeObjectURL(url), 3000); + }} + > + Download error details + + + ) as unknown as string, + severity: 'error', + }); + } + } finally { + setIsRefreshing(false); + } }; - useSubscription('/topic/material/' + material.ownMaterialNumber, handleRefresh); + useSubscription('/topic/material/' + material.ownMaterialNumber, (msg: IMessage) => handleRefreshMessage(msg?.body)); useEffect(() => { const callback = (category: DataCategory) => refresh([category]);