Skip to content

Commit

Permalink
[MODORDERS-969] - Opening and editing POs with location-restricted fu…
Browse files Browse the repository at this point in the history
…nds (#805)

* [MODORDERS-969] - Opening and editing POs with location-restricted funds
  • Loading branch information
imerabishvili authored Dec 14, 2023
1 parent bca97c0 commit 10c04b0
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 12 deletions.
5 changes: 3 additions & 2 deletions src/main/java/org/folio/config/ApplicationConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -599,10 +599,11 @@ ReceivingEncumbranceStrategy receivingEncumbranceStrategy(EncumbranceService enc
return new OpenCompositeOrderInventoryService(inventoryManager, openCompositeOrderPieceService, processInventoryStrategyResolver, restClient) ;
}

@Bean OpenCompositeOrderFlowValidator openCompositeOrderFlowValidator(ExpenseClassValidationService expenseClassValidationService,
@Bean OpenCompositeOrderFlowValidator openCompositeOrderFlowValidator(FundService fundService,
ExpenseClassValidationService expenseClassValidationService,
PieceStorageService pieceStorageService, EncumbranceWorkflowStrategyFactory encumbranceWorkflowStrategyFactory,
CompositePoLineValidationService compositePoLineValidationService) {
return new OpenCompositeOrderFlowValidator(expenseClassValidationService, pieceStorageService,
return new OpenCompositeOrderFlowValidator(fundService, expenseClassValidationService, pieceStorageService,
encumbranceWorkflowStrategyFactory, compositePoLineValidationService);
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/java/org/folio/helper/PurchaseOrderHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,10 @@ public Future<Void> updateOrder(CompositePurchaseOrder compPO, boolean deleteHol
compPO.setCompositePoLines(clonedPoFromStorage.getCompositePoLines());
}
compPO.getCompositePoLines().forEach(poLine -> PoLineCommonUtil.updateLocationsQuantity(poLine.getLocations()));
return openCompositeOrderFlowValidator.checkLocationsAndPiecesConsistency(compPO.getCompositePoLines(), requestContext);
} else {
return Future.succeededFuture();
return openCompositeOrderFlowValidator.checkLocationsAndPiecesConsistency(compPO.getCompositePoLines(), requestContext)
.compose(ok -> openCompositeOrderFlowValidator.checkFundLocationRestrictions(compPO.getCompositePoLines(), requestContext));
}
return Future.succeededFuture();
})
.compose(ok -> {
if (isTransitionToReopen(poFromStorage, compPO)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public enum ErrorCodes {
PO_LINE_HAS_RELATED_APPROVED_INVOICE_ERROR("poLineHasRelatedApprovedInvoice", "Composite POL related invoice lines contains lines which has status APPROVED for the current fiscal year, invoice line ids: %s"),
MULTIPLE_FISCAL_YEARS("multipleFiscalYears", "Order line fund distributions have active budgets in multiple fiscal years."),
INSTANCE_INVALID_PRODUCT_ID_ERROR("instanceInvalidProductIdError", "Instance connection could not be changed, the chosen instance contains an invalid Product ID."),
FUND_LOCATION_RESTRICTION_VIOLATION("fundLocationRestrictionViolation", "One of the funds is restricted to be used for one of the locations."),
ENCUMBRANCES_FOR_RE_ENCUMBER_NOT_FOUND("encumbrancesForReEncumberNotFound", "The encumbrances were correctly created during the rollover or have already been updated."),
CLAIMING_CONFIG_INVALID("claimingConfigInvalid", "Claiming interval should be set and greater than 0 if claiming is active");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
package org.folio.service.orders.flows.update.open;

import static java.util.stream.Collectors.toList;
import static org.folio.orders.utils.validators.LocationsAndPiecesConsistencyValidator.verifyLocationsAndPiecesConsistency;
import static org.folio.rest.jaxrs.model.CompositePurchaseOrder.WorkflowStatus.PENDING;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.folio.okapi.common.GenericCompositeFuture;
import org.folio.orders.utils.FundDistributionUtils;
import org.folio.rest.acq.model.finance.Fund;
import org.folio.rest.core.exceptions.ErrorCodes;
import org.folio.rest.core.exceptions.HttpException;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.jaxrs.model.CompositePoLine;
import org.folio.rest.jaxrs.model.CompositePurchaseOrder;
import org.folio.rest.jaxrs.model.Error;
import org.folio.rest.jaxrs.model.FundDistribution;
import org.folio.rest.jaxrs.model.Location;
import org.folio.rest.jaxrs.model.Parameter;
import org.folio.rest.jaxrs.model.PieceCollection;
import org.folio.service.finance.FundService;
import org.folio.service.finance.expenceclass.ExpenseClassValidationService;
import org.folio.service.finance.transaction.EncumbranceWorkflowStrategyFactory;
import org.folio.service.orders.CompositePoLineValidationService;
Expand All @@ -30,22 +37,26 @@
public class OpenCompositeOrderFlowValidator {
private static final Logger logger = LogManager.getLogger(OpenCompositeOrderFlowValidator.class);

private final FundService fundService;
private final ExpenseClassValidationService expenseClassValidationService;
private final PieceStorageService pieceStorageService;
private final EncumbranceWorkflowStrategyFactory encumbranceWorkflowStrategyFactory;
private final CompositePoLineValidationService compositePoLineValidationService;

public OpenCompositeOrderFlowValidator(ExpenseClassValidationService expenseClassValidationService,
PieceStorageService pieceStorageService, EncumbranceWorkflowStrategyFactory encumbranceWorkflowStrategyFactory,
CompositePoLineValidationService compositePoLineValidationService) {
public OpenCompositeOrderFlowValidator(FundService fundService,
ExpenseClassValidationService expenseClassValidationService,
PieceStorageService pieceStorageService,
EncumbranceWorkflowStrategyFactory encumbranceWorkflowStrategyFactory,
CompositePoLineValidationService compositePoLineValidationService) {
this.fundService = fundService;
this.expenseClassValidationService = expenseClassValidationService;
this.pieceStorageService = pieceStorageService;
this.encumbranceWorkflowStrategyFactory = encumbranceWorkflowStrategyFactory;
this.compositePoLineValidationService = compositePoLineValidationService;
}

public Future<Void> validate(CompositePurchaseOrder compPO, CompositePurchaseOrder poFromStorage,
RequestContext requestContext) {
RequestContext requestContext) {
List<Future<Void>> futures = new ArrayList<>();

Future<Void> validateMaterialTypesFuture = Future.succeededFuture()
Expand Down Expand Up @@ -83,7 +94,7 @@ public Future<Void> checkLocationsAndPiecesConsistency(List<CompositePoLine> poL
List<CompositePoLine> linesWithIdWithoutManualPieceReceived = poLines.stream().filter(
compositePoLine -> StringUtils.isNotEmpty(compositePoLine.getId()) && Boolean.FALSE.equals(compositePoLine.getCheckinItems()))
.collect(Collectors.toList());
List<String> lineIds = linesWithIdWithoutManualPieceReceived.stream().map(CompositePoLine::getId).collect(toList());
List<String> lineIds = linesWithIdWithoutManualPieceReceived.stream().map(CompositePoLine::getId).toList();
return pieceStorageService.getPiecesByLineIdsByChunks(lineIds, requestContext)
.map(pieces -> new PieceCollection().withPieces(pieces).withTotalRecords(pieces.size()))
.map(pieces -> {
Expand All @@ -92,12 +103,50 @@ public Future<Void> checkLocationsAndPiecesConsistency(List<CompositePoLine> poL
});
}

private void validateMaterialTypes(CompositePurchaseOrder purchaseOrder){
private void validateMaterialTypes(CompositePurchaseOrder purchaseOrder) {
if (purchaseOrder.getWorkflowStatus() != PENDING) {
List<Error> errors = compositePoLineValidationService.checkMaterialsAvailability(purchaseOrder.getCompositePoLines());
if (!errors.isEmpty()) {
throw new HttpException(422, errors.get(0));
}
}
}

public Future<Void> checkFundLocationRestrictions(List<CompositePoLine> poLines, RequestContext requestContext) {
logger.debug("checkFundLocationRestrictions start");
List<Future<Void>> checkFunds = poLines
.stream()
.filter(poLine -> CollectionUtils.isNotEmpty(poLine.getLocations()))
.filter(poLine -> CollectionUtils.isNotEmpty(poLine.getFundDistribution()))
.map(poLine -> {
List<String> fundIdList = poLine.getFundDistribution().stream()
.map(FundDistribution::getFundId)
.toList();
return fundService.getFunds(fundIdList, requestContext)
.compose(funds -> validateLocationRestrictions(poLine, funds));
}).toList();
return GenericCompositeFuture.join(checkFunds).mapEmpty();
}

private Future<Void> validateLocationRestrictions(CompositePoLine poLine, List<Fund> funds) {
List<String> polLocationIds = poLine.getLocations().stream().map(Location::getLocationId).toList();
for (Fund fund : funds) {
if (Boolean.TRUE.equals(fund.getRestrictByLocations()) && CollectionUtils.containsAny(fund.getLocationIds(), polLocationIds)) {
String poLineId = poLine.getId();
String fundId = fund.getId();
Collection<String> restrictedLocations = CollectionUtils.intersection(fund.getLocationIds(), polLocationIds);
logger.error("For POL {} fund {} is restricted to be used for locations {}", poLineId, fundId, restrictedLocations);
List<Parameter> parameters = List.of(
new Parameter().withKey("poLineId").withValue(poLineId),
new Parameter().withKey("poLineNumber").withValue(poLine.getPoLineNumber()),
new Parameter().withKey("fundId").withValue(fundId),
new Parameter().withKey("fundCode").withValue(fund.getCode()),
new Parameter().withKey("restrictedLocations").withValue(restrictedLocations.toString())
);
return Future.failedFuture(new HttpException(422, ErrorCodes.FUND_LOCATION_RESTRICTION_VIOLATION, parameters));
}
}
return Future.succeededFuture();
}

}
5 changes: 5 additions & 0 deletions src/test/java/org/folio/ApiTestSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.folio.service.orders.PurchaseOrderStorageServiceTest;
import org.folio.service.orders.ReEncumbranceHoldersBuilderTest;
import org.folio.service.orders.TransactionsTotalFieldsPopulateServiceTest;
import org.folio.service.orders.flows.update.open.OpenCompositeOrderFlowValidatorTest;
import org.folio.service.orders.flows.update.open.OpenCompositeOrderHolderBuilderTest;
import org.folio.service.orders.flows.update.open.OpenCompositeOrderInventoryServiceTest;
import org.folio.service.orders.flows.update.open.OpenCompositeOrderManagerTest;
Expand Down Expand Up @@ -398,6 +399,10 @@ class PieceUpdateFlowPoLineServiceTestNested extends PieceUpdateFlowPoLineServic
class OpenCompositeOrderInventoryServiceTestNested extends OpenCompositeOrderInventoryServiceTest {
}

@Nested
class OpenCompositeOrderFlowValidatorTestNested extends OpenCompositeOrderFlowValidatorTest {
}

@Nested
class OpenCompositeOrderManagerTestNested extends OpenCompositeOrderManagerTest {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.folio.service.orders.flows.update.open;

import static org.folio.rest.core.exceptions.ErrorCodes.FUND_LOCATION_RESTRICTION_VIOLATION;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.folio.rest.acq.model.finance.Fund;
import org.folio.rest.core.exceptions.HttpException;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.jaxrs.model.CompositePoLine;
import org.folio.rest.jaxrs.model.FundDistribution;
import org.folio.rest.jaxrs.model.Location;
import org.folio.rest.jaxrs.model.Parameter;
import org.folio.service.finance.FundService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import io.vertx.core.Future;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;

@ExtendWith(VertxExtension.class)
public class OpenCompositeOrderFlowValidatorTest {

@Mock
private RequestContext requestContext;
@Mock
private FundService fundService;

@InjectMocks
private OpenCompositeOrderFlowValidator openCompositeOrderFlowValidator;

@BeforeEach
public void initMocks() {
MockitoAnnotations.openMocks(this);
}

@Test
public void testCheckFundLocationRestrictions(VertxTestContext vertxTestContext) {

// given
List<String> fundIds = List.of("F1", "F2");
List<String> locationIds = List.of("L1", "L2", "L3");
CompositePoLine poLine = new CompositePoLine()
.withId("ID")
.withPoLineNumber("number")
.withFundDistribution(
fundIds.stream().map(id -> new FundDistribution().withFundId(id)).toList()
)
.withLocations(
locationIds.stream().map(id -> new Location().withLocationId(id)).toList()
);
Mockito.when(fundService.getFunds(fundIds, requestContext)).thenReturn(
Future.succeededFuture(List.of(
new Fund().withId("F1").withCode("FC").withRestrictByLocations(true).withLocationIds(List.of("L4")),
new Fund().withId("F2").withCode("FC").withRestrictByLocations(true).withLocationIds(List.of("L2", "L3"))
))
);

// when
Future<Void> future = openCompositeOrderFlowValidator.checkFundLocationRestrictions(List.of(poLine), requestContext);

// then
vertxTestContext.assertFailure(future)
.onComplete(result -> {
assertTrue(result.failed());
HttpException exception = (HttpException) result.cause();
assertEquals(422, exception.getCode());
List<Parameter> expectedParameters = List.of(
new Parameter().withKey("poLineId").withValue(poLine.getId()),
new Parameter().withKey("poLineNumber").withValue(poLine.getPoLineNumber()),
new Parameter().withKey("fundId").withValue("F2"),
new Parameter().withKey("fundCode").withValue("FC"),
new Parameter().withKey("restrictedLocations").withValue("[L2, L3]")
);
assertEquals(FUND_LOCATION_RESTRICTION_VIOLATION.toError().withParameters(expectedParameters), exception.getError());
vertxTestContext.completeNow();
});
}

}

0 comments on commit 10c04b0

Please sign in to comment.