Skip to content

Commit

Permalink
[MODINVOICE-537] Update po lines paymentStatus when cancelling invoic…
Browse files Browse the repository at this point in the history
…es (#484)
  • Loading branch information
damien-git authored Apr 3, 2024
1 parent a2476fd commit 2f8ef52
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 33 deletions.
1 change: 1 addition & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
],
"modulePermissions": [
"configuration.entries.collection.get",
"invoice-storage.invoices.collection.get",
"invoice-storage.invoices.item.put",
"invoice-storage.invoices.item.get",
"invoice-storage.invoice-lines.item.put",
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/org/folio/config/ServicesConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,11 @@ InvoiceCancelService invoiceCancelService(BaseTransactionService baseTransaction
VoucherService voucherService,
OrderLineService orderLineService,
OrderService orderService,
InvoiceLineService invoiceLineService,
BaseInvoiceService baseInvoiceService,
InvoiceWorkflowDataHolderBuilder invoiceWorkflowDataHolderBuilder) {
return new InvoiceCancelService(baseTransactionService, encumbranceService,
voucherService, orderLineService, orderService, invoiceWorkflowDataHolderBuilder);
return new InvoiceCancelService(baseTransactionService, encumbranceService, voucherService, orderLineService,
orderService, invoiceLineService, baseInvoiceService, invoiceWorkflowDataHolderBuilder);
}
@Bean
BatchVoucherService batchVoucherService(RestClient restClient) {
Expand Down
196 changes: 167 additions & 29 deletions src/main/java/org/folio/services/invoice/InvoiceCancelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import static io.vertx.core.Future.succeededFuture;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNullElse;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.groupingBy;
import static org.folio.invoices.utils.ErrorCodes.CANCEL_TRANSACTIONS_ERROR;
import static org.folio.invoices.utils.ErrorCodes.CANNOT_CANCEL_INVOICE;
import static org.folio.invoices.utils.ErrorCodes.ERROR_UNRELEASING_ENCUMBRANCES;
Expand All @@ -15,10 +15,17 @@
import static org.folio.rest.acq.model.finance.Transaction.TransactionType.CREDIT;
import static org.folio.rest.acq.model.finance.Transaction.TransactionType.PAYMENT;
import static org.folio.rest.acq.model.finance.Transaction.TransactionType.PENDING_PAYMENT;
import static org.folio.rest.acq.model.orders.PoLine.PaymentStatus.AWAITING_PAYMENT;
import static org.folio.rest.acq.model.orders.PoLine.PaymentStatus.PARTIALLY_PAID;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import one.util.streamex.StreamEx;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -28,10 +35,12 @@
import org.folio.rest.acq.model.finance.Transaction;
import org.folio.rest.acq.model.finance.Transaction.TransactionType;
import org.folio.rest.acq.model.finance.TransactionCollection;
import org.folio.rest.acq.model.orders.CompositePoLine;
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.Invoice;
import org.folio.rest.jaxrs.model.InvoiceCollection;
import org.folio.rest.jaxrs.model.InvoiceLine;
import org.folio.rest.jaxrs.model.Parameter;
import org.folio.services.finance.transaction.BaseTransactionService;
Expand All @@ -43,28 +52,36 @@
public class InvoiceCancelService {
private static final String PO_LINES_WITH_RIGHT_PAYMENT_STATUS_QUERY =
"paymentStatus==(\"Awaiting Payment\" OR \"Partially Paid\" OR \"Fully Paid\" OR \"Ongoing\")";
private static final String PAYMENT_STATUS_PAID_QUERY = "paymentStatus==(\"Fully Paid\" OR \"Partially Paid\")";
private static final String OPEN_ORDERS_QUERY = "workflowStatus==\"Open\"";
private static final String AND = " AND ";

private static final Logger logger = LogManager.getLogger(InvoiceCancelService.class);
private static final Logger logger = LogManager.getLogger();

private final BaseTransactionService baseTransactionService;
private final EncumbranceService encumbranceService;
private final VoucherService voucherService;
private final OrderLineService orderLineService;
private final OrderService orderService;
private final InvoiceLineService invoiceLineService;
private final BaseInvoiceService invoiceService;
private final InvoiceWorkflowDataHolderBuilder holderBuilder;

public InvoiceCancelService(BaseTransactionService baseTransactionService,
EncumbranceService encumbranceService,
VoucherService voucherService,
OrderLineService orderLineService,
OrderService orderService,
InvoiceLineService invoiceLineService,
BaseInvoiceService baseInvoiceService,
InvoiceWorkflowDataHolderBuilder holderBuilder) {
this.baseTransactionService = baseTransactionService;
this.encumbranceService = encumbranceService;
this.voucherService = voucherService;
this.orderLineService = orderLineService;
this.orderService = orderService;
this.invoiceLineService = invoiceLineService;
this.invoiceService = baseInvoiceService;
this.holderBuilder = holderBuilder;
}

Expand All @@ -81,6 +98,7 @@ public InvoiceCancelService(BaseTransactionService baseTransactionService,
*/
public Future<Void> cancelInvoice(Invoice invoiceFromStorage, List<InvoiceLine> lines, RequestContext requestContext) {
String invoiceId = invoiceFromStorage.getId();
logger.info("cancelInvoice:: Cancelling invoice {}...", invoiceId);

return Future.succeededFuture()
.map(v -> {
Expand All @@ -95,7 +113,10 @@ public Future<Void> cancelInvoice(Invoice invoiceFromStorage, List<InvoiceLine>
return null;
})
.compose(v -> cancelVoucher(invoiceId, requestContext))
.compose(v -> unreleaseEncumbrances(lines, invoiceFromStorage, requestContext));
.compose(v -> unreleaseEncumbrances(lines, invoiceFromStorage, requestContext))
.compose(v -> updatePoLinePaymentStatus(invoiceFromStorage, lines, requestContext))
.onSuccess(v -> logger.info("cancelInvoice:: Invoice {} cancelled successfully", invoiceId))
.onFailure(t -> logger.error("cancelInvoice:: Failed to cancel invoice {}", invoiceId, t));
}

private void validateCancelInvoice(Invoice invoiceFromStorage) {
Expand All @@ -121,7 +142,8 @@ private void validateCancelInvoice(Invoice invoiceFromStorage) {
private Future<Void> validateBudgetsStatus(Invoice invoice, List<InvoiceLine> lines, RequestContext requestContext) {
List<InvoiceWorkflowDataHolder> dataHolders = holderBuilder.buildHoldersSkeleton(lines, invoice);
return holderBuilder.withBudgets(dataHolders, requestContext)
.onFailure(t -> logger.error("Could not find an active budget for the invoice with id {}", invoice.getId(), t))
.onFailure(t -> logger.error("validateBudgetsStatus:: Could not find an active budget for the invoice with id {}",
invoice.getId(), t))
.mapEmpty();
}

Expand All @@ -131,16 +153,17 @@ private Future<List<Transaction>> getTransactions(String invoiceId, RequestConte
return baseTransactionService.getTransactions(query, 0, Integer.MAX_VALUE, requestContext)
.map(TransactionCollection::getTransactions)
.map(transactions -> transactions.stream()
.filter(tr -> relevantTransactionTypes.contains(tr.getTransactionType())).collect(toList()));
.filter(tr -> relevantTransactionTypes.contains(tr.getTransactionType())).toList());
}

private Future<Void> cancelTransactions(String invoiceId, List<Transaction> transactions,
RequestContext requestContext) {
if (transactions.isEmpty())
private Future<Void> cancelTransactions(String invoiceId, List<Transaction> transactions, RequestContext requestContext) {
if (transactions.isEmpty()) {
return succeededFuture(null);
}
logger.info("cancelTransactions:: Cancelling invoice transactions, invoiceId={}...", invoiceId);
return baseTransactionService.batchCancel(transactions, requestContext)
.recover(t -> {
logger.error("Failed to cancel transactions for invoice with id {}", invoiceId, t);
logger.error("cancelTransactions:: Failed to cancel transactions for invoice with id {}", invoiceId, t);
var param = new Parameter().withKey(INVOICE_ID).withValue(invoiceId);
var causeParam = new Parameter().withKey("cause").withValue(t.getMessage());
throw new HttpException(500, CANCEL_TRANSACTIONS_ERROR, List.of(param, causeParam));
Expand All @@ -152,6 +175,7 @@ private void cancelInvoiceLines(List<InvoiceLine> lines) {
}

private Future<Void> cancelVoucher(String invoiceId, RequestContext requestContext) {
logger.info("cancelVoucher:: Cancelling voucher, invoiceId={}...", invoiceId);
return voucherService.cancelInvoiceVoucher(invoiceId, requestContext);
}

Expand All @@ -161,30 +185,26 @@ private Future<Void> unreleaseEncumbrances(List<InvoiceLine> invoiceLines, Invoi
.filter(InvoiceLine::getReleaseEncumbrance)
.map(InvoiceLine::getPoLineId)
.distinct()
.collect(toList());
if (poLineIds.isEmpty())
.toList();
if (poLineIds.isEmpty()) {
return succeededFuture();
List<Future<List<PoLine>>> futureList = StreamEx
.ofSubLists(poLineIds, MAX_IDS_FOR_GET_RQ)
.map(this::queryToGetPoLinesWithRightPaymentStatusByIds)
.map(query -> orderLineService.getPoLines(query, requestContext))
.collect(toList());

Future<List<PoLine>> poLinesFuture = collectResultsOnSuccess(futureList)
.map(col -> col.stream().flatMap(List::stream).collect(toList()));

return poLinesFuture.compose(poLines -> selectPoLinesWithOpenOrders(poLines, requestContext))
}
String invoiceId = invoiceFromStorage.getId();
logger.info("unreleaseEncumbrances:: Unreleasing encumbrances, invoiceId={}...", invoiceId);
return getPoLinesByIdAndQuery(poLineIds, this::queryToGetPoLinesWithRightPaymentStatusByIds, requestContext)
.compose(poLines -> selectPoLinesWithOpenOrders(poLines, requestContext))
.compose(poLines -> unreleaseEncumbrancesForPoLines(poLines, invoiceFromStorage, requestContext))
.recover(t -> {
logger.error("Failed to unrelease encumbrance for po lines", t);
var param = new Parameter().withKey("cause").withValue(requireNonNullElse(t.getCause(), t).toString());
var error = ERROR_UNRELEASING_ENCUMBRANCES.toError().withParameters(List.of(param));
logger.error("unreleaseEncumbrances:: Failed to unrelease encumbrance for po lines, invoiceId={}", invoiceId, t);
var causeParam = new Parameter().withKey("cause").withValue(requireNonNullElse(t.getCause(), t).toString());
var invoiceIdParam = new Parameter().withKey("invoiceId").withValue(invoiceId);
var error = ERROR_UNRELEASING_ENCUMBRANCES.toError().withParameters(List.of(causeParam, invoiceIdParam));
throw new HttpException(500, error);
});
}

private String queryToGetPoLinesWithRightPaymentStatusByIds(List<String> poLineIds) {
return PO_LINES_WITH_RIGHT_PAYMENT_STATUS_QUERY + " AND " + convertIdsToCqlQuery(poLineIds);
return PO_LINES_WITH_RIGHT_PAYMENT_STATUS_QUERY + AND + convertIdsToCqlQuery(poLineIds);
}

private Future<List<PoLine>> selectPoLinesWithOpenOrders(List<PoLine> poLines, RequestContext requestContext) {
Expand All @@ -193,25 +213,25 @@ private Future<List<PoLine>> selectPoLinesWithOpenOrders(List<PoLine> poLines, R
List<String> orderIds = poLines.stream()
.map(PoLine::getPurchaseOrderId)
.distinct()
.collect(toList());
.toList();
return orderService.getOrders(queryToGetOpenOrdersByIds(orderIds), requestContext)
.map(orders -> {
List<String> openOrderIds = orders.stream().map(PurchaseOrder::getId).toList();
return poLines.stream()
.filter(poLine -> openOrderIds.contains(poLine.getPurchaseOrderId()))
.collect(toList());
.toList();
});
}

private String queryToGetOpenOrdersByIds(List<String> orderIds) {
return OPEN_ORDERS_QUERY + " AND " + convertIdsToCqlQuery(orderIds);
return OPEN_ORDERS_QUERY + AND + convertIdsToCqlQuery(orderIds);
}

private Future<Void> unreleaseEncumbrancesForPoLines(List<PoLine> poLines, Invoice invoiceFromStorage,
RequestContext requestContext) {
if (poLines.isEmpty())
return succeededFuture(null);
List<String> poLineIds = poLines.stream().map(PoLine::getId).collect(toList());
List<String> poLineIds = poLines.stream().map(PoLine::getId).toList();
String fiscalYearId = invoiceFromStorage.getFiscalYearId();
return encumbranceService.getEncumbrancesByPoLineIds(poLineIds, fiscalYearId, requestContext)
.map(transactions -> transactions.stream()
Expand All @@ -224,4 +244,122 @@ private Future<Void> unreleaseEncumbrancesForPoLines(List<PoLine> poLines, Invoi
});
}

private Future<Void> updatePoLinePaymentStatus(Invoice invoiceFromStorage, List<InvoiceLine> invoiceLines,
RequestContext requestContext) {
String invoiceId = invoiceFromStorage.getId();
if (!Invoice.Status.PAID.equals(invoiceFromStorage.getStatus()) || invoiceFromStorage.getFiscalYearId() == null) {
// in the unlikely case the fiscal year is undefined, the payment status update is skipped
// (the MODINVOSTO-177 script should fill it up for pre-Poppy invoices)
logger.info("updatePoLinePaymentStatus:: The invoice was not paid or the fiscal year is unknown; " +
"skipping po line paymentStatus update. invoiceId={}", invoiceId);
return succeededFuture();
}
List<String> allPoLineIds = invoiceLines.stream()
.map(InvoiceLine::getPoLineId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (allPoLineIds.isEmpty()) {
return succeededFuture();
}
logger.info("updatePoLinePaymentStatus:: Retrieving linked po lines that might need a paymentStatus update, " +
"invoiceId={}...", invoiceId);
return getPoLinesByIdAndQuery(allPoLineIds, this::queryToGetPoLinesWithFullyOrPartiallyPaidPaymentStatusByIds, requestContext)
.compose(filteredPoLines -> {
if (filteredPoLines.isEmpty()) {
logger.info("updatePoLinePaymentStatus:: No matching po line, no paymentStatus update needed, invoiceId={}",
invoiceId);
return succeededFuture();
}
logger.info("updatePoLinePaymentStatus:: There are po lines linked to the invoice that might need a paymentStatus update;" +
" retrieving paid invoice lines linked to the po lines...");
List<String> filteredPoLineIds = filteredPoLines.stream().map(PoLine::getId).toList();
return getInvoiceLinesByPoLineIdsAndQuery(filteredPoLineIds,
ids -> queryToGetRelatedPaidInvoiceLinesByPoLineIds(invoiceId, ids), requestContext)
.compose(relatedInvoiceLines -> {
if (relatedInvoiceLines.isEmpty()) {
logger.info("updatePoLinePaymentStatus:: No linked paid invoice line; setting paymentStatus to Awaiting Payment...");
return updateRelatedPoLines(relatedInvoiceLines, filteredPoLines, emptyList(), invoiceId, requestContext);
}
logger.info("updatePoLinePaymentStatus:: There are linked paid invoice lines; " +
"retrieving the related invoices to select only invoices with the same fiscal year as the cancelled invoice...");
List<String> relatedInvoiceIds = relatedInvoiceLines.stream().map(InvoiceLine::getInvoiceId).distinct().toList();
String invoiceQuery = "status==Paid AND fiscalYearId==" + invoiceFromStorage.getFiscalYearId();
return getInvoicesByIdsAndQuery(relatedInvoiceIds, invoiceQuery, requestContext)
.compose(relatedInvoices -> updateRelatedPoLines(relatedInvoiceLines, filteredPoLines, relatedInvoices,
invoiceId, requestContext));
});
});
}

private String queryToGetPoLinesWithFullyOrPartiallyPaidPaymentStatusByIds(List<String> poLineIds) {
return PAYMENT_STATUS_PAID_QUERY + AND + convertIdsToCqlQuery(poLineIds);
}

private String queryToGetRelatedPaidInvoiceLinesByPoLineIds(String invoiceId, List<String> poLineIds) {
return "invoiceId<>" + invoiceId + " AND invoiceLineStatus==(\"Paid\") AND " +
convertIdsToCqlQuery(poLineIds, "poLineId", true);
}

private Future<List<PoLine>> getPoLinesByIdAndQuery(List<String> poLineIds, Function<List<String>, String> queryFunction,
RequestContext requestContext) {
List<Future<List<PoLine>>> futureList = StreamEx
.ofSubLists(poLineIds, MAX_IDS_FOR_GET_RQ)
.map(queryFunction)
.map(query -> orderLineService.getPoLines(query, requestContext))
.toList();

return collectResultsOnSuccess(futureList)
.map(col -> col.stream().flatMap(List::stream).toList());
}

private Future<List<InvoiceLine>> getInvoiceLinesByPoLineIdsAndQuery(List<String> poLineIds,
Function<List<String>, String> queryFunction, RequestContext requestContext) {
List<Future<List<InvoiceLine>>> futureList = StreamEx
.ofSubLists(poLineIds, MAX_IDS_FOR_GET_RQ)
.map(queryFunction)
.map(query -> invoiceLineService.getInvoiceLinesByQuery(query, requestContext))
.toList();

return collectResultsOnSuccess(futureList)
.map(col -> col.stream().flatMap(List::stream).toList());
}

private Future<List<Invoice>> getInvoicesByIdsAndQuery(List<String> invoiceIds, String query, RequestContext requestContext) {
String query2 = "(" + query + ") AND " + convertIdsToCqlQuery(invoiceIds);
return invoiceService.getInvoices(query2, 0, Integer.MAX_VALUE, requestContext)
.map(InvoiceCollection::getInvoices);
}

private Future<Void> updateRelatedPoLines(List<InvoiceLine> allRelatedInvoiceLines, List<PoLine> filteredPoLines,
List<Invoice> relatedInvoices, String invoiceId, RequestContext requestContext) {
List<String> relatedInvoiceIds = relatedInvoices.stream().map(Invoice::getId).toList();
List<InvoiceLine> filteredInvoiceLines = allRelatedInvoiceLines.stream()
.filter(invoiceLine -> relatedInvoiceIds.contains(invoiceLine.getInvoiceId()))
.toList();
Map<String, List<InvoiceLine>> poLineIdToInvoiceLines = filteredInvoiceLines.stream()
.collect(groupingBy(InvoiceLine::getPoLineId));
List<PoLine> modifiedPoLines = new ArrayList<>();
filteredPoLines.forEach(poLine -> {
List<InvoiceLine> relatedInvoiceLines = poLineIdToInvoiceLines.get(poLine.getId());
if (relatedInvoiceLines == null || relatedInvoiceLines.isEmpty()) {
poLine.setPaymentStatus(AWAITING_PAYMENT);
modifiedPoLines.add(poLine);
} else if (relatedInvoiceLines.stream().noneMatch(InvoiceLine::getReleaseEncumbrance) &&
!PARTIALLY_PAID.equals(poLine.getPaymentStatus())) {
poLine.setPaymentStatus(PARTIALLY_PAID);
modifiedPoLines.add(poLine);
}
});
if (modifiedPoLines.isEmpty()) {
logger.info("updateRelatedPoLines:: No po line paymentStatus update needed, invoiceId={}", invoiceId);
return succeededFuture();
}
logger.info("updateRelatedPoLines:: {} po lines need a paymentStatus update for invoice {}. Updating...",
modifiedPoLines.size(), invoiceId);
List<CompositePoLine> compositePoLines = modifiedPoLines.stream()
.map(poLine -> JsonObject.mapFrom(poLine).mapTo(CompositePoLine.class))
.toList();
return orderLineService.updateCompositePoLines(compositePoLines, requestContext);
}
}
Loading

0 comments on commit 2f8ef52

Please sign in to comment.