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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/main/java/org/folio/config/ServicesConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,10 @@ InvoiceCancelService invoiceCancelService(BaseTransactionService baseTransaction
OrderLineService orderLineService,
OrderService orderService,
PoLinePaymentStatusUpdateService poLinePaymentStatusUpdateService,
InvoiceWorkflowDataHolderBuilder invoiceWorkflowDataHolderBuilder) {
InvoiceWorkflowDataHolderBuilder invoiceWorkflowDataHolderBuilder,
BudgetService budgetService) {
return new InvoiceCancelService(baseTransactionService, encumbranceService, voucherService, orderLineService,
orderService, poLinePaymentStatusUpdateService, invoiceWorkflowDataHolderBuilder);
orderService, poLinePaymentStatusUpdateService, invoiceWorkflowDataHolderBuilder, budgetService);
}
@Bean
BatchVoucherService batchVoucherService(RestClient restClient) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import static org.folio.rest.acq.model.finance.Transaction.TransactionType.PAYMENT;
import static org.folio.rest.acq.model.finance.Transaction.TransactionType.PENDING_PAYMENT;

import java.util.Collection;
import java.util.List;
import java.util.Objects;

import io.vertx.core.Future;
import lombok.extern.log4j.Log4j2;
Expand All @@ -26,10 +28,12 @@
import org.folio.rest.acq.model.orders.PoLine;
import org.folio.rest.acq.model.orders.PurchaseOrder;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.jaxrs.model.FundDistribution;
import org.folio.rest.jaxrs.model.Invoice;
import org.folio.rest.jaxrs.model.InvoiceLine;
import org.folio.rest.jaxrs.model.Parameter;
import org.folio.services.finance.EncumbranceUtils;
import org.folio.services.finance.budget.BudgetService;
import org.folio.services.finance.transaction.BaseTransactionService;
import org.folio.services.finance.transaction.EncumbranceService;
import org.folio.services.order.OrderLineService;
Expand All @@ -52,21 +56,24 @@
private final OrderService orderService;
private final PoLinePaymentStatusUpdateService poLinePaymentStatusUpdateService;
private final InvoiceWorkflowDataHolderBuilder holderBuilder;
private final BudgetService budgetService;

public InvoiceCancelService(BaseTransactionService baseTransactionService,

Check warning on line 61 in src/main/java/org/folio/services/invoice/InvoiceCancelService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Constructor has 8 parameters, which is greater than 7 authorized.

See more on https://sonarcloud.io/project/issues?id=org.folio%3Amod-invoice&issues=AZy6B7S1Jp304WWP7inm&open=AZy6B7S1Jp304WWP7inm&pullRequest=601
EncumbranceService encumbranceService,
VoucherService voucherService,
OrderLineService orderLineService,
OrderService orderService,
PoLinePaymentStatusUpdateService poLinePaymentStatusUpdateService,
InvoiceWorkflowDataHolderBuilder holderBuilder) {
InvoiceWorkflowDataHolderBuilder holderBuilder,
BudgetService budgetService) {
this.baseTransactionService = baseTransactionService;
this.encumbranceService = encumbranceService;
this.voucherService = voucherService;
this.orderLineService = orderLineService;
this.orderService = orderService;
this.poLinePaymentStatusUpdateService = poLinePaymentStatusUpdateService;
this.holderBuilder = holderBuilder;
this.budgetService = budgetService;
}

/**
Expand Down Expand Up @@ -128,11 +135,49 @@
private Future<Void> validateBudgetsStatus(Invoice invoice, List<InvoiceLine> lines, RequestContext requestContext) {
List<InvoiceWorkflowDataHolder> dataHolders = holderBuilder.buildHoldersSkeleton(lines, invoice);
return holderBuilder.withBudgets(dataHolders, false, requestContext)
.compose(holders -> checkBudgetsForUnlinkedEncumbrancesToUnrelease(invoice, lines, requestContext))
.onFailure(t -> log.error("validateBudgetsStatus:: Could not find an active budget for the invoice with id {}",
invoice.getId(), t))
.mapEmpty();
}

private Future<Void> checkBudgetsForUnlinkedEncumbrancesToUnrelease(Invoice invoice, List<InvoiceLine> invoiceLines,
RequestContext requestContext) {
var poLineIds = invoiceLines.stream()
.filter(InvoiceLine::getReleaseEncumbrance)
.map(InvoiceLine::getPoLineId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (poLineIds.isEmpty()) {
return succeededFuture();
}
var fundIdsFromInvoiceLines = invoiceLines.stream()
.filter(InvoiceLine::getReleaseEncumbrance)
.map(InvoiceLine::getFundDistributions)
.flatMap(Collection::stream)
.map(FundDistribution::getFundId)
.distinct()
.toList();
String invoiceFiscalYearId = invoice.getFiscalYearId();
return orderLineService.getPoLinesByIdAndQuery(poLineIds, this::queryToGetPoLinesWithRightPaymentStatusByIds, requestContext)
.compose(poLines -> selectPoLinesWithOpenOrders(poLines, requestContext))
.map(poLines -> poLines.stream()
.map(PoLine::getFundDistribution)
.flatMap(Collection::stream)
.map(org.folio.rest.acq.model.orders.FundDistribution::getFundId)
.distinct()
.filter(fundId-> !fundIdsFromInvoiceLines.contains(fundId))
.toList())
.compose(fundIds -> {
if (fundIds.isEmpty()) {
return succeededFuture();
}
return budgetService.getBudgetsByFundIds(fundIds, invoiceFiscalYearId, false, requestContext);
})
.mapEmpty();
}

private Future<List<Transaction>> getTransactions(String invoiceId, RequestContext requestContext) {
String query = String.format("sourceInvoiceId==%s", invoiceId);
List<TransactionType> relevantTransactionTypes = List.of(PENDING_PAYMENT, PAYMENT, CREDIT);
Expand Down Expand Up @@ -168,7 +213,9 @@
private Future<Void> updateOrUnreleaseEncumbrances(List<InvoiceLine> invoiceLines, Invoice invoiceFromStorage,
RequestContext requestContext) {
var poLineIds = invoiceLines.stream()
.filter(InvoiceLine::getReleaseEncumbrance)
.map(InvoiceLine::getPoLineId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (poLineIds.isEmpty()) {
Expand All @@ -178,7 +225,7 @@
log.info("updateOrUnreleaseEncumbrances:: Updating or unreleasing encumbrances, invoiceId={}...", invoiceId);
return orderLineService.getPoLinesByIdAndQuery(poLineIds, this::queryToGetPoLinesWithRightPaymentStatusByIds, requestContext)
.compose(poLines -> selectPoLinesWithOpenOrders(poLines, requestContext))
.compose(poLines -> unreleaseEncumbrancesForPoLines(invoiceLines, poLines, invoiceFromStorage, requestContext))
.compose(poLines -> unreleaseEncumbrancesForPoLines(poLines, invoiceFromStorage, requestContext))
.recover(t -> {
log.error("updateOrUnreleaseEncumbrances:: Failed to update or unrelease encumbrance for po lines, invoiceId={}", invoiceId, t);
var causeParam = new Parameter().withKey("cause").withValue(requireNonNullElse(t.getCause(), t).toString());
Expand Down Expand Up @@ -213,15 +260,13 @@
return OPEN_ORDERS_QUERY + AND + convertIdsToCqlQuery(orderIds);
}

private Future<Void> unreleaseEncumbrancesForPoLines(List<InvoiceLine> invoiceLines, List<PoLine> poLines,
Invoice invoiceFromStorage, RequestContext requestContext) {
private Future<Void> unreleaseEncumbrancesForPoLines(List<PoLine> poLines, Invoice invoiceFromStorage,
RequestContext requestContext) {
if (CollectionUtils.isEmpty(poLines)) {
return succeededFuture(null);
}
var poLineIds = invoiceLines.stream()
.filter(InvoiceLine::getReleaseEncumbrance)
.map(InvoiceLine::getPoLineId)
.distinct()
var poLineIds = poLines.stream()
.map(PoLine::getId)
.toList();
var fiscalYearId = invoiceFromStorage.getFiscalYearId();
return encumbranceService.getEncumbrancesByPoLineIds(poLineIds, fiscalYearId, requestContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import static org.folio.rest.impl.ApiTestBase.X_OKAPI_USER_ID;
import static org.folio.services.finance.transaction.BaseTransactionServiceTest.X_OKAPI_TENANT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
Expand Down Expand Up @@ -61,10 +62,12 @@
import org.folio.rest.core.RestClient;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.core.models.RequestEntry;
import org.folio.rest.jaxrs.model.FundDistribution;
import org.folio.rest.jaxrs.model.Invoice;
import org.folio.rest.jaxrs.model.InvoiceCollection;
import org.folio.rest.jaxrs.model.InvoiceLine;
import org.folio.rest.jaxrs.model.InvoiceLineCollection;
import org.folio.rest.jaxrs.model.Parameter;
import org.folio.rest.jaxrs.model.Voucher;
import org.folio.rest.jaxrs.model.VoucherCollection;
import org.folio.services.exchange.CacheableExchangeRateService;
Expand Down Expand Up @@ -145,8 +148,8 @@ public void initMocks() {
fiscalYearService, fundService, ledgerService, baseTransactionService,
budgetService, expenseClassRetrieveService, cacheableExchangeRateService);

cancelService = new InvoiceCancelService(baseTransactionService, encumbranceService,
voucherService, orderLineService, orderService, poLinePaymentStatusUpdateService, holderBuilder);
cancelService = new InvoiceCancelService(baseTransactionService, encumbranceService, voucherService,
orderLineService, orderService, poLinePaymentStatusUpdateService, holderBuilder, budgetService);
}

@AfterEach
Expand Down Expand Up @@ -230,6 +233,59 @@ public void validateBudgetWhenCancelInvoiceAndUndefinedFiscalYearTest(VertxTestC
});
}

@Test
public void errorWithInactiveBudgetInLinkedPol(VertxTestContext vertxTestContext) {
String invoiceId = UUID.randomUUID().toString();
String fiscalYearId = UUID.randomUUID().toString();
String fundId1 = UUID.randomUUID().toString();
String fundId2 = UUID.randomUUID().toString();
String orderId = UUID.randomUUID().toString();
String poLineId = UUID.randomUUID().toString();
Invoice invoice = new Invoice()
.withId(invoiceId)
.withFiscalYearId(fiscalYearId)
.withStatus(Invoice.Status.PAID);
FundDistribution invoiceLineFundDistribution = new FundDistribution()
.withFundId(fundId1);
InvoiceLine invoiceLine = new InvoiceLine()
.withId(UUID.randomUUID().toString())
.withInvoiceId(invoiceId)
.withFundDistributions(List.of(invoiceLineFundDistribution))
.withPoLineId(poLineId)
.withReleaseEncumbrance(true);
List<InvoiceLine> invoiceLines = List.of(invoiceLine);
Budget budget = new Budget()
.withId(UUID.randomUUID().toString())
.withBudgetStatus(Budget.BudgetStatus.ACTIVE);
var poLineFundDistribution = new org.folio.rest.acq.model.orders.FundDistribution()
.withFundId(fundId2);
PurchaseOrder order = new PurchaseOrder()
.withId(orderId)
.withWorkflowStatus(PurchaseOrder.WorkflowStatus.OPEN);
PoLine poLine = new PoLine()
.withId(poLineId)
.withPurchaseOrderId(orderId)
.withFundDistribution(List.of(poLineFundDistribution));

when(restClient.get(any(RequestEntry.class), eq(BudgetCollection.class), eq(requestContext)))
.thenReturn(succeededFuture(new BudgetCollection().withBudgets(List.of(budget)).withTotalRecords(1)))
.thenReturn(succeededFuture(new BudgetCollection().withBudgets(List.of()).withTotalRecords(0)));
when(restClient.get(any(RequestEntry.class), eq(PoLineCollection.class), eq(requestContext)))
.thenReturn(succeededFuture(new PoLineCollection().withPoLines(List.of(poLine)).withTotalRecords(1)));
when(restClient.get(any(RequestEntry.class), eq(PurchaseOrderCollection.class), eq(requestContext)))
.thenReturn(succeededFuture(new PurchaseOrderCollection().withPurchaseOrders(List.of(order)).withTotalRecords(1)));

Future<Void> future = cancelService.cancelInvoice(invoice, invoiceLines, null, requestContext);
assertTrue(future.failed());
var exception = (HttpException)future.cause();
assertEquals(404, exception.getCode());
assertEquals(BUDGET_NOT_FOUND_USING_FISCAL_YEAR_ID.getDescription(), exception.getMessage());
List<Parameter> parameters = exception.getErrors().getErrors().getFirst().getParameters();
assertEquals(fundId2, parameters.getFirst().getValue());
assertEquals(fiscalYearId, parameters.get(1).getValue());
vertxTestContext.completeNow();
}

@Test
public void errorUnreleasingEncumbrances(VertxTestContext vertxTestContext) throws IOException {
Invoice invoice = getMockAs(APPROVED_INVOICE_SAMPLE_PATH, Invoice.class);
Expand Down