diff --git a/src/main/java/org/folio/dataimport/handlers/actions/CreateInvoiceEventHandler.java b/src/main/java/org/folio/dataimport/handlers/actions/CreateInvoiceEventHandler.java index a66226959..166fa18e0 100644 --- a/src/main/java/org/folio/dataimport/handlers/actions/CreateInvoiceEventHandler.java +++ b/src/main/java/org/folio/dataimport/handlers/actions/CreateInvoiceEventHandler.java @@ -11,7 +11,6 @@ import static org.folio.invoices.utils.HelperUtils.collectResultsOnSuccess; import static org.folio.invoices.utils.ResourcePathResolver.ORDER_LINES; import static org.folio.invoices.utils.ResourcePathResolver.resourcesPath; -import static org.folio.rest.RestConstants.SEMAPHORE_MAX_ACTIVE_THREADS; import static org.folio.rest.jaxrs.model.EntityType.EDIFACT_INVOICE; import static org.folio.rest.jaxrs.model.InvoiceLine.InvoiceLineStatus.OPEN; import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.ACTION_PROFILE; @@ -23,11 +22,11 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; @@ -52,7 +51,6 @@ import org.folio.rest.core.models.RequestContext; import org.folio.rest.core.models.RequestEntry; import org.folio.rest.impl.InvoiceHelper; -import org.folio.rest.impl.InvoiceLineHelper; import org.folio.rest.jaxrs.model.Invoice; import org.folio.rest.jaxrs.model.InvoiceLine; import org.folio.rest.jaxrs.model.InvoiceLineCollection; @@ -62,7 +60,6 @@ import io.vertx.core.json.Json; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import io.vertxconcurrent.Semaphore; public class CreateInvoiceEventHandler implements EventHandler { @@ -106,6 +103,7 @@ public CompletableFuture handle(DataImportEventPayload d Map okapiHeaders = DataImportUtils.getOkapiHeaders(dataImportEventPayload); Future> poLinesFuture = getAssociatedPoLines(dataImportEventPayload, okapiHeaders); + InvoiceHelper invoiceHelper = new InvoiceHelper(okapiHeaders, Vertx.currentContext()); poLinesFuture .map(invLineNoToPoLine -> { @@ -115,47 +113,26 @@ public CompletableFuture handle(DataImportEventPayload d prepareMappingResult(dataImportEventPayload); return null; }) - .compose(v -> saveInvoice(dataImportEventPayload, okapiHeaders)) - .map(savedInvoice -> prepareInvoiceLinesToSave(savedInvoice.getId(), dataImportEventPayload, poLinesFuture.result())) - .compose(preparedInvoiceLines -> saveInvoiceLines(preparedInvoiceLines, okapiHeaders)) + .map(v -> prepareInvoiceAndLinesToSave(dataImportEventPayload, poLinesFuture.result())) + .compose(invoiceAndLines -> createInvoiceAndLines(dataImportEventPayload, invoiceAndLines, invoiceHelper)) .onComplete(result -> { makeLightweightReturnPayload(dataImportEventPayload); if (result.succeeded()) { - List invoiceLines = result.result().stream().map(Pair::getLeft).collect(Collectors.toList()); - InvoiceLineCollection invoiceLineCollection = new InvoiceLineCollection().withInvoiceLines(invoiceLines).withTotalRecords(invoiceLines.size()); - dataImportEventPayload.getContext().put(INVOICE_LINES_KEY, Json.encode(invoiceLineCollection)); - Map invoiceLinesErrors = prepareInvoiceLinesErrors(result.result()); - if (!invoiceLinesErrors.isEmpty()) { - dataImportEventPayload.getContext().put(INVOICE_LINES_ERRORS_KEY, Json.encode(invoiceLinesErrors)); - future.completeExceptionally(new EventProcessingException("Error during invoice lines creation")); - return; - } future.complete(dataImportEventPayload); } else { preparePayloadWithMappedInvoiceLines(dataImportEventPayload); - logger.error("Error during invoice creation", result.cause()); + logger.error("Error when creating the invoice and lines", result.cause()); future.completeExceptionally(result.cause()); } }); } catch (Exception e) { - logger.error("Error during creation invoice and invoice lines", e); + logger.error("Error when creating the invoice and lines", e); future.completeExceptionally(e); } return future; } - private Map prepareInvoiceLinesErrors(List> invoiceLinesSavingResult) { - Map invoiceLinesErrors = new HashMap<>(); - for (int i = 0; i < invoiceLinesSavingResult.size(); i++) { - Pair invLineResult = invoiceLinesSavingResult.get(i); - if (invLineResult.getRight() != null) { - invoiceLinesErrors.put(i + 1, invLineResult.getRight()); - } - } - return invoiceLinesErrors; - } - private Future> getAssociatedPoLines(DataImportEventPayload eventPayload, Map okapiHeaders) { String recordAsString = eventPayload.getContext().get(EDIFACT_INVOICE.value()); Record sourceRecord = Json.decodeValue(recordAsString, Record.class); @@ -264,23 +241,9 @@ private Future> getAssociatedPoLinesByRefNumbers(Map()); } - return requestContext.getContext() - .>>>executeBlocking(promise -> { - List>> futures = new ArrayList<>(); - Semaphore semaphore = new Semaphore(SEMAPHORE_MAX_ACTIVE_THREADS, Vertx.currentContext().owner()); - for (Map.Entry> entry : refNumberList.entrySet()) { - semaphore.acquire(() -> { - var future = getLinePair(entry, requestContext) - .onComplete(asyncResult -> semaphore.release()); - futures.add(future); - // complete executeBlocking promise when all operations started - if (futures.size() == refNumberList.size()) { - promise.complete(futures); - } - }); - } - }) - .compose(HelperUtils::collectResultsOnSuccess) + return HelperUtils.executeWithSemaphores(refNumberList.entrySet(), + entry -> getLinePair(entry, requestContext), + requestContext) .map(poLinePairs -> poLinePairs.stream() .filter(Objects::nonNull) .collect(Collectors.toMap(Pair::getKey, Pair::getValue))); @@ -320,32 +283,51 @@ private String prepareQueryGetPoLinesByRefNumber(List referenceNumbers) return format(PO_LINES_BY_REF_NUMBER_CQL, valueString); } - private Future saveInvoice(DataImportEventPayload dataImportEventPayload, Map okapiHeaders) { - Invoice invoiceToSave = Json.decodeValue(dataImportEventPayload.getContext().get(INVOICE.value()), Invoice.class); - invoiceToSave.setSource(Invoice.Source.EDI); - InvoiceHelper invoiceHelper = new InvoiceHelper(okapiHeaders, Vertx.currentContext()); - RequestContext requestContext = new RequestContext(Vertx.currentContext(), okapiHeaders); - - return invoiceHelper.createInvoice(invoiceToSave, requestContext) - .onComplete(result -> { - if (result.succeeded()) { - dataImportEventPayload.getContext().put(INVOICE.value(), Json.encode(result.result())); - return; - } - logger.error("Error during creation invoice in the storage", result.cause()); - }); - } - - private List prepareInvoiceLinesToSave(String invoiceId, DataImportEventPayload dataImportEventPayload, Map associatedPoLines) { + private InvoiceAndLines prepareInvoiceAndLinesToSave(DataImportEventPayload dataImportEventPayload, + Map associatedPoLines) { + Invoice invoice = Json.decodeValue(dataImportEventPayload.getContext().get(INVOICE.value()), Invoice.class); + if (invoice.getId() == null) { + invoice.setId(UUID.randomUUID().toString()); + } + invoice.setSource(Invoice.Source.EDI); List invoiceLines = new JsonArray(dataImportEventPayload.getContext().get(INVOICE_LINES_KEY)) .stream() .map(JsonObject.class::cast) .map(json -> json.mapTo(InvoiceLine.class)) - .map(invoiceLine -> invoiceLine.withInvoiceId(invoiceId).withInvoiceLineStatus(OPEN)) + .map(invoiceLine -> invoiceLine.withInvoiceLineStatus(OPEN) + .withInvoiceId(invoice.getId())) .collect(Collectors.toList()); linkInvoiceLinesToPoLines(invoiceLines, associatedPoLines); - return invoiceLines; + + InvoiceAndLines invoiceAndLines = new InvoiceAndLines(); + invoiceAndLines.invoice = invoice; + invoiceAndLines.invoiceLines = invoiceLines; + return invoiceAndLines; + } + + private Future createInvoiceAndLines(DataImportEventPayload dataImportEventPayload, + InvoiceAndLines invoiceAndLines, InvoiceHelper invoiceHelper) { + return invoiceHelper.createInvoiceAndLines(invoiceAndLines.invoice, invoiceAndLines.invoiceLines) + .onFailure(t -> logger.error("Error during creation of invoice and lines", t)) + .map(result -> { + Map invoiceLinesErrors = result.getInvoiceLinesErrors(); + if (!invoiceLinesErrors.isEmpty()) { + String errorsAsString = Json.encode(invoiceLinesErrors); + dataImportEventPayload.getContext().put(INVOICE_LINES_ERRORS_KEY, errorsAsString); + throw new EventProcessingException("Error during invoice lines creation: " + errorsAsString); + } + if (result.getNewInvoice() != null) { + dataImportEventPayload.getContext().put(INVOICE.value(), Json.encode(result.getNewInvoice())); + } + if (result.getNewInvoiceLines() != null) { + InvoiceLineCollection invoiceLineCollection = new InvoiceLineCollection() + .withInvoiceLines(result.getNewInvoiceLines()) + .withTotalRecords(result.getNewInvoiceLines().size()); + dataImportEventPayload.getContext().put(INVOICE_LINES_KEY, Json.encode(invoiceLineCollection)); + } + return null; + }); } private void linkInvoiceLinesToPoLines(List invoiceLines, Map associatedPoLines) { @@ -358,40 +340,6 @@ private void linkInvoiceLinesToPoLines(List invoiceLines, Map>> saveInvoiceLines(List invoiceLines, Map okapiHeaders) { - List>> futures = new ArrayList<>(); - - if (CollectionUtils.isEmpty(invoiceLines)) { - return Future.succeededFuture(new ArrayList<>()); - } - - InvoiceLineHelper helper = new InvoiceLineHelper(okapiHeaders, Vertx.currentContext()); - return Vertx.currentContext() - .>>>executeBlocking(promise -> { - Semaphore semaphore = new Semaphore(SEMAPHORE_MAX_ACTIVE_THREADS, Vertx.currentContext().owner()); - for (InvoiceLine invoiceLine : invoiceLines) { - semaphore.acquire(() -> { - var future = helper.createInvoiceLine(invoiceLine) - .compose(createdInvoiceLine -> { - Pair invoiceLineToMsg = Pair.of(createdInvoiceLine, null); - return Future.succeededFuture(invoiceLineToMsg); - }, err -> { - logger.error("Failed to create invoice line {}, {}", invoiceLine, err); - Pair invoiceLineToMsg = Pair.of(invoiceLine, err.getMessage()); - return Future.succeededFuture(invoiceLineToMsg); - }) - .onComplete(asyncResult -> semaphore.release()); - futures.add(future); - // complete executeBlocking promise when all operations processed - if (futures.size() == invoiceLines.size()) { - promise.complete(futures); - } - }); - } - }) - .compose(HelperUtils::collectResultsOnSuccess); - } - private List mapInvoiceLinesArrayToList(JsonArray invoiceLinesArray) { return invoiceLinesArray.stream() .map(JsonObject.class::cast) @@ -457,4 +405,9 @@ public boolean isEligible(DataImportEventPayload dataImportEventPayload) { } return false; } + + private static class InvoiceAndLines { + Invoice invoice; + List invoiceLines; + } } diff --git a/src/main/java/org/folio/invoices/utils/ErrorCodes.java b/src/main/java/org/folio/invoices/utils/ErrorCodes.java index 7ab669cf0..48f22e553 100644 --- a/src/main/java/org/folio/invoices/utils/ErrorCodes.java +++ b/src/main/java/org/folio/invoices/utils/ErrorCodes.java @@ -61,7 +61,8 @@ public enum ErrorCodes { MULTIPLE_FISCAL_YEARS("multipleFiscalYears", "Multiple fiscal years are used with the funds %s and %s."), COULD_NOT_FIND_VALID_FISCAL_YEAR("couldNotFindValidFiscalYear", "Could not find any valid fiscal year with a budget for all funds in the invoice"), MORE_THAN_ONE_FISCAL_YEAR_SERIES("moreThanOneFiscalYearSeries", "Fund distributions cannot reference more than one fiscal year series. Please edit fund distributions so they all come from the same fiscal year series."), - CANNOT_RESET_INVOICE_FISCAL_YEAR("cannotResetInvoiceFiscalYear", "Invoice fiscal year cannot be set to null if it was previously defined"); + CANNOT_RESET_INVOICE_FISCAL_YEAR("cannotResetInvoiceFiscalYear", "Invoice fiscal year cannot be set to null if it was previously defined"), + ERROR_CREATING_INVOICE_LINE("errorCreatingInvoiceLine", "Error creating invoice line"); private final String code; private final String description; diff --git a/src/main/java/org/folio/invoices/utils/HelperUtils.java b/src/main/java/org/folio/invoices/utils/HelperUtils.java index 0a521fc5e..ff5ea6405 100755 --- a/src/main/java/org/folio/invoices/utils/HelperUtils.java +++ b/src/main/java/org/folio/invoices/utils/HelperUtils.java @@ -6,6 +6,7 @@ import static org.folio.invoices.utils.ResourcePathResolver.INVOICE_LINES; import static org.folio.invoices.utils.ResourcePathResolver.VOUCHERS_STORAGE; import static org.folio.invoices.utils.ResourcePathResolver.VOUCHER_LINES; +import static org.folio.rest.RestConstants.SEMAPHORE_MAX_ACTIVE_THREADS; import static org.folio.rest.impl.AbstractHelper.ID; import static org.folio.rest.jaxrs.model.FundDistribution.DistributionType.PERCENTAGE; import static org.folio.services.exchange.ExchangeRateProviderResolver.RATE_KEY; @@ -28,12 +29,15 @@ import javax.money.convert.ConversionQuery; import javax.money.convert.ConversionQueryBuilder; +import io.vertx.core.Vertx; +import io.vertxconcurrent.Semaphore; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.folio.invoices.rest.exceptions.HttpException; import org.folio.okapi.common.GenericCompositeFuture; import org.folio.rest.acq.model.finance.ExchangeRate; +import org.folio.rest.core.models.RequestContext; import org.folio.rest.impl.ProtectionHelper; import org.folio.rest.jaxrs.model.Adjustment; import org.folio.rest.jaxrs.model.FundDistribution; @@ -168,6 +172,30 @@ public static Future> collectResultsOnSuccess(List> future .map(CompositeFuture::list); } + public static Future> executeWithSemaphores(Collection collection, + FunctionReturningFuture f, RequestContext requestContext) { + if (collection.isEmpty()) + return Future.succeededFuture(List.of()); + return requestContext.getContext().>>executeBlocking(promise -> { + Semaphore semaphore = new Semaphore(SEMAPHORE_MAX_ACTIVE_THREADS, Vertx.currentContext().owner()); + List> futures = new ArrayList<>(); + for (I item : collection) { + semaphore.acquire(() -> { + Future future = f.apply(item) + .onComplete(asyncResult -> semaphore.release()); + futures.add(future); + if (futures.size() == collection.size()) { + promise.complete(futures); + } + }); + } + }).compose(HelperUtils::collectResultsOnSuccess); + } + + public interface FunctionReturningFuture { + Future apply(I item); + } + public static double calculateVoucherAmount(Voucher voucher, List voucherLines) { CurrencyUnit currency = Monetary.getCurrency(voucher.getSystemCurrency()); diff --git a/src/main/java/org/folio/invoices/utils/ResourcePathResolver.java b/src/main/java/org/folio/invoices/utils/ResourcePathResolver.java index 4bd7cbda7..495795ee2 100644 --- a/src/main/java/org/folio/invoices/utils/ResourcePathResolver.java +++ b/src/main/java/org/folio/invoices/utils/ResourcePathResolver.java @@ -14,7 +14,7 @@ private ResourcePathResolver() { public static final String ACQUISITIONS_MEMBERSHIPS = "acquisitionsMemberships"; public static final String INVOICES = "invoices"; public static final String INVOICE_LINES = "invoiceLines"; - public static final String COMPOSITE_ORDER = "compositeOrder"; + public static final String COMPOSITE_ORDERS = "compositeOrders"; public static final String ORDER_LINES = "orderLines"; public static final String ORDER_INVOICE_RELATIONSHIP = "orderInvoiceRelationship"; public static final String VOUCHER_LINES = "voucherLines"; @@ -56,7 +56,7 @@ private ResourcePathResolver() { apis.put(ACQUISITIONS_UNITS, "/acquisitions-units-storage/units"); apis.put(ACQUISITIONS_MEMBERSHIPS, "/acquisitions-units-storage/memberships"); apis.put(INVOICES, "/invoice-storage/invoices"); - apis.put(COMPOSITE_ORDER, "/orders/composite-orders"); + apis.put(COMPOSITE_ORDERS, "/orders/composite-orders"); apis.put(INVOICE_LINES, "/invoice-storage/invoice-lines"); apis.put(INVOICE_LINE_NUMBER, "/invoice-storage/invoice-line-number"); apis.put(ORDER_LINES, "/orders/order-lines"); diff --git a/src/main/java/org/folio/rest/impl/InvoiceHelper.java b/src/main/java/org/folio/rest/impl/InvoiceHelper.java index 6572e51af..aa3ae0935 100644 --- a/src/main/java/org/folio/rest/impl/InvoiceHelper.java +++ b/src/main/java/org/folio/rest/impl/InvoiceHelper.java @@ -26,12 +26,14 @@ import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -39,6 +41,10 @@ import javax.money.convert.ConversionQuery; import javax.money.convert.CurrencyConversion; import javax.money.convert.ExchangeRateProvider; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; @@ -53,8 +59,11 @@ import org.folio.okapi.common.GenericCompositeFuture; import org.folio.rest.acq.model.Organization; import org.folio.rest.acq.model.finance.ExpenseClass; +import org.folio.rest.acq.model.finance.FiscalYear; import org.folio.rest.acq.model.finance.Fund; 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.Adjustment; import org.folio.rest.jaxrs.model.Error; @@ -83,6 +92,9 @@ import org.folio.services.invoice.InvoiceLineService; import org.folio.services.invoice.InvoicePaymentService; import org.folio.services.invoice.InvoiceService; +import org.folio.services.order.OrderLineService; +import org.folio.services.order.OrderService; +import org.folio.services.validator.InvoiceLineValidator; import org.folio.services.validator.InvoiceValidator; import org.folio.services.voucher.VoucherCommandService; import org.folio.services.voucher.VoucherService; @@ -140,6 +152,10 @@ public class InvoiceHelper extends AbstractHelper { private VoucherLineService voucherLineService; @Autowired private InvoiceFiscalYearsService invoiceFiscalYearsService; + @Autowired + private OrderService orderService; + @Autowired + private OrderLineService orderLineService; private RequestContext requestContext; public InvoiceHelper(Map okapiHeaders, Context ctx) { @@ -149,7 +165,7 @@ public InvoiceHelper(Map okapiHeaders, Context ctx) { SpringContextUtil.autowireDependencies(this, Vertx.currentContext()); } - public Future createInvoice(Invoice invoice, RequestContext requestContext) { + public Future createInvoice(Invoice invoice) { return Future.succeededFuture() .map(v -> { validator.validateIncomingInvoice(invoice); @@ -157,9 +173,7 @@ public Future createInvoice(Invoice invoice, RequestContext requestCont }) .compose(v -> validateAcqUnitsOnCreate(invoice.getAcqUnitIds())) .compose(v -> updateWithSystemGeneratedData(invoice)) - .compose(v -> invoiceService.createInvoice(invoice, requestContext)) - .map(Invoice::getId) - .map(invoice::withId); + .compose(v -> invoiceService.createInvoice(invoice, requestContext)); } /** @@ -278,6 +292,172 @@ public Future getFiscalYearsByInvoiceId(String invoiceId) .onFailure(t -> logger.error("Error getting fiscal years for invoice {}", invoiceId, t)); } + public Future createInvoiceAndLines(Invoice invoice, List invoiceLines) { + CreateInvoiceAndLinesResult result = new CreateInvoiceAndLinesResult(); + + // Validate the lines before creating the invoice + Map invoiceLinesErrors = validateInvoiceLines(invoiceLines); + InvoiceLineValidator invoiceLineValidator = new InvoiceLineValidator(); + for (int i=0; i { + result.setNewInvoice(newInvoice); + return null; + }) + .compose(v -> createInvoiceLines(result.getNewInvoice(), invoiceLines, result)) + .map(v -> result); + } + + public static class CreateInvoiceAndLinesResult { + private Invoice newInvoice; + private List newInvoiceLines; + private Map invoiceLinesErrors; + public void setNewInvoice(Invoice newInvoice) { + this.newInvoice = newInvoice; + } + public void setNewInvoiceLines(List newInvoiceLines) { + this.newInvoiceLines = newInvoiceLines; + } + public void setInvoiceLinesErrors(Map invoiceLinesErrors) { + this.invoiceLinesErrors = invoiceLinesErrors; + } + public Invoice getNewInvoice() { + return newInvoice; + } + public List getNewInvoiceLines() { + return newInvoiceLines; + } + public Map getInvoiceLinesErrors() { + return invoiceLinesErrors; + } + } + + private Future createInvoiceLines(Invoice invoice, List invoiceLines, + CreateInvoiceAndLinesResult result) { + // NOTE: this should be kept consistent with InvoiceLineHelper.createInvoiceLine() + // protectionHelper.isOperationRestricted(acqUnitIds, ProtectedOperationType.CREATE) has already been called at this point + return getInvoiceWorkflowDataHolders(invoice, invoiceLines, requestContext) + .compose(holders -> budgetExpenseClassService.checkExpenseClasses(holders, requestContext)) + .compose(holders -> generateNewInvoiceLineNumbers(invoice.getId(), invoiceLines) + .map(newInvoice -> { + result.setNewInvoice(newInvoice); + updateInvoiceFiscalYear(newInvoice, holders); + return holders; + })) + .compose(holders -> encumbranceService.updateEncumbranceLinksForFiscalYear(result.getNewInvoice(), holders, + requestContext)) + .map(v -> { + adjustmentsService.processProratedAdjustments(invoiceLines, result.getNewInvoice()); + invoiceService.recalculateTotals(result.getNewInvoice(), invoiceLines); + return null; + }) + .compose(v -> invoiceLineService.createInvoiceLines(invoiceLines, requestContext)) + .map(newInvoiceLines -> { + result.setNewInvoiceLines(newInvoiceLines); + return null; + }) + .compose(v -> getPurchaseOrders(result.getNewInvoiceLines())) + .compose(pos -> createInvoiceOrderRelations(result.getNewInvoice().getId(), pos) + .map(v -> { + updateInvoicePoNumbers(result.getNewInvoice(), pos); + return null; + })) + .compose(v -> invoiceService.updateInvoice(result.getNewInvoice(), requestContext)) + .map(v -> result) + .onFailure(t -> logger.error("Error when creating invoice lines", t)); + } + + public Map validateInvoiceLines(List invoiceLines) { + Map invoiceLinesErrors = new HashMap<>(); + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + Validator schemaValidator = factory.getValidator(); + for (int i = 0; i < invoiceLines.size(); i++) { + InvoiceLine invoiceLine = invoiceLines.get(i); + Set> violations = schemaValidator.validate(invoiceLine); + if (!violations.isEmpty()) { + String errorsAsString = violations.stream() + .map(v -> String.format("%s: %s", v.getPropertyPath().toString(), v.getMessage())) + .collect(Collectors.joining("; ")); + invoiceLinesErrors.put(i + 1, errorsAsString); + } + } + } + return invoiceLinesErrors; + } + + private Future generateNewInvoiceLineNumbers(String invoiceId, List invoiceLines) { + // NOTE: currently we don't have a way to generate several numbers at a time like with order lines, + // and getting a new number saves the invoice each time; this could be optimized + return HelperUtils.executeWithSemaphores(invoiceLines, + invoiceLine -> invoiceLineService.generateLineNumber(invoiceId, requestContext), + requestContext) + .map(numbers -> { + numbers.sort(Comparator.comparing(Integer::valueOf)); + for (int i=0; i invoiceService.getInvoiceById(invoiceId, requestContext)) + .onFailure(t -> logger.error("Error when generating new invoice line numbers", t)); + } + + private void updateInvoiceFiscalYear(Invoice invoice, List holders) { + if (holders.isEmpty()) + return; + if (invoice.getFiscalYearId() != null) + return; + holders.stream() + .map(InvoiceWorkflowDataHolder::getFiscalYear) + .filter(Objects::nonNull) + .map(FiscalYear::getId) + .filter(Objects::nonNull) + .findFirst() + .ifPresent(invoice::setFiscalYearId); + } + + private Future> getPurchaseOrders(List invoiceLines) { + List poLineIds = invoiceLines.stream() + .map(InvoiceLine::getPoLineId) + .filter(Objects::nonNull) + .collect(toList()); + if (poLineIds.isEmpty()) { + return succeededFuture(List.of()); + } + return orderLineService.getPoLinesByIds(poLineIds, requestContext) + .map(poLines -> poLines.stream().map(PoLine::getPurchaseOrderId).distinct().collect(toList())) + .compose(poIds -> orderService.getOrdersByIds(poIds, requestContext)) + .onFailure(t -> logger.error("Error when getting order lines and orders", t)); + } + + private Future createInvoiceOrderRelations(String invoiceId, List pos) { + if (pos.isEmpty()) + return succeededFuture(null); + List poIds = pos.stream().map(PurchaseOrder::getId).collect(toList()); + return orderService.createInvoiceOrderRelations(invoiceId, poIds, requestContext) + .onFailure(t -> logger.error("Error when creating invoice order relations", t)); + } + + private void updateInvoicePoNumbers(Invoice invoice, List pos) { + List numbers = pos.stream().map(PurchaseOrder::getPoNumber).collect(toList()); + invoice.setPoNumbers(numbers); + } + private Future handleExchangeRateChange(Invoice invoice, List invoiceLines) { return getInvoiceWorkflowDataHolders(invoice, invoiceLines, requestContext) @@ -322,7 +502,7 @@ private Future validateAndHandleInvoiceStatusTransition(Invoice invoice, I return null; }) .map(aVoid -> filterUpdatedLines(invoiceLines, updatedInvoiceLines)) - .compose(lines -> invoiceLineService.persistInvoiceLines(lines, requestContext))) + .compose(lines -> invoiceLineService.updateInvoiceLines(lines, requestContext))) .compose(lines -> updateEncumbranceLinksWhenFiscalYearIsChanged(invoice, invoiceFromStorage, invoiceLines))); } @@ -458,7 +638,7 @@ private Future approveInvoice(Invoice invoice, List lines) { .compose(v -> getInvoiceWorkflowDataHolders(invoice, lines, requestContext)) .compose(holders -> encumbranceService.updateInvoiceLinesEncumbranceLinks(holders, holders.get(0).getFiscalYear().getId(), requestContext) - .compose(linesToUpdate -> invoiceLineService.persistInvoiceLines(linesToUpdate, requestContext)) + .compose(linesToUpdate -> invoiceLineService.updateInvoiceLines(linesToUpdate, requestContext)) .map(v -> holders)) .compose(holders -> budgetExpenseClassService.checkExpenseClasses(holders, requestContext)) .compose(holders -> pendingPaymentWorkflowService.handlePendingPaymentsCreation(holders, invoice, requestContext)) @@ -844,7 +1024,7 @@ private Future updateEncumbranceLinksWhenFiscalYearIsChanged(Invoice invoi List dataHolders = holderBuilder.buildHoldersSkeleton(lines, invoice); return holderBuilder.withEncumbrances(dataHolders, requestContext) .compose(holders -> encumbranceService.updateInvoiceLinesEncumbranceLinks(holders, newFiscalYearId, requestContext)) - .compose(linesToUpdate -> invoiceLineService.persistInvoiceLines(linesToUpdate, requestContext)); + .compose(linesToUpdate -> invoiceLineService.updateInvoiceLines(linesToUpdate, requestContext)); } @VisibleForTesting diff --git a/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java b/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java index 3417fdff1..793cf1dd6 100644 --- a/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java +++ b/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java @@ -340,6 +340,7 @@ private Future getInvoiceIfExists(ILProcessing ilProcessing, String lineId * @return completable future which {@link InvoiceLine} on success */ public Future createInvoiceLine(InvoiceLine invoiceLine) { + // NOTE: this should be kept consistent with InvoiceHelper.createInvoiceLines() RequestContext requestContext = new RequestContext(ctx, okapiHeaders); ILProcessing ilProcessing = new ILProcessing(); ilProcessing.setInvoiceLine(invoiceLine); diff --git a/src/main/java/org/folio/rest/impl/InvoicesImpl.java b/src/main/java/org/folio/rest/impl/InvoicesImpl.java index 86b08cddf..689de9b16 100644 --- a/src/main/java/org/folio/rest/impl/InvoicesImpl.java +++ b/src/main/java/org/folio/rest/impl/InvoicesImpl.java @@ -53,8 +53,7 @@ public class InvoicesImpl extends BaseApi implements org.folio.rest.jaxrs.resour public void postInvoiceInvoices(Invoice invoice, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { InvoiceHelper helper = new InvoiceHelper(okapiHeaders, vertxContext); - RequestContext requestContext = new RequestContext(vertxContext, okapiHeaders); - helper.createInvoice(invoice, requestContext) + helper.createInvoice(invoice) .onSuccess(invoiceWithId -> asyncResultHandler.handle(succeededFuture(helper.buildResponseWithLocation(String.format(INVOICE_LOCATION_PREFIX, invoiceWithId.getId()), invoiceWithId)))) .onFailure(t -> { logger.error("Failed to create invoice ", t); diff --git a/src/main/java/org/folio/services/invoice/InvoiceLineService.java b/src/main/java/org/folio/services/invoice/InvoiceLineService.java index 147d37f8a..00365e887 100644 --- a/src/main/java/org/folio/services/invoice/InvoiceLineService.java +++ b/src/main/java/org/folio/services/invoice/InvoiceLineService.java @@ -1,6 +1,7 @@ package org.folio.services.invoice; import static java.util.stream.Collectors.toList; +import static org.folio.invoices.utils.ErrorCodes.ERROR_CREATING_INVOICE_LINE; import static org.folio.invoices.utils.ErrorCodes.INVOICE_LINE_NOT_FOUND; import static org.folio.invoices.utils.HelperUtils.INVOICE_ID; import static org.folio.invoices.utils.ResourcePathResolver.INVOICE_LINES; @@ -10,14 +11,16 @@ import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; +import io.vertx.core.json.JsonObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.invoices.rest.exceptions.HttpException; import org.folio.invoices.utils.HelperUtils; -import org.folio.okapi.common.GenericCompositeFuture; 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.Error; import org.folio.rest.jaxrs.model.Invoice; import org.folio.rest.jaxrs.model.InvoiceLine; import org.folio.rest.jaxrs.model.InvoiceLineCollection; @@ -28,6 +31,8 @@ public class InvoiceLineService { + private static final Logger log = LogManager.getLogger(); + private static final String INVOICE_LINES_ENDPOINT = resourcesPath(INVOICE_LINES); private static final String INVOICE_LINE_BY_ID_ENDPOINT = INVOICE_LINES_ENDPOINT + "/{id}"; private static final String INVOICE_LINE_NUMBER_ENDPOINT = resourcesPath(INVOICE_LINE_NUMBER) + "?" + INVOICE_ID + "="; @@ -75,12 +80,19 @@ public Future> getInvoiceLinesRelatedForOrder(List ord .filter(invoiceLine -> orderPoLineIds.contains(invoiceLine.getPoLineId())).collect(toList())); } - public Future persistInvoiceLines(List lines, RequestContext requestContext) { - var futures = lines.stream() - .map(invoiceLine -> persistInvoiceLine(invoiceLine, requestContext)) - .collect(Collectors.toList()); - return GenericCompositeFuture.join(futures).mapEmpty(); + public Future> createInvoiceLines(List invoiceLines, RequestContext requestContext) { + return HelperUtils.executeWithSemaphores(invoiceLines, + invoiceLine -> createInvoiceLine(invoiceLine, requestContext), + requestContext); + } + + public Future updateInvoiceLines(List invoiceLines, RequestContext requestContext) { + return HelperUtils.executeWithSemaphores(invoiceLines, + invoiceLine -> updateInvoiceLine(invoiceLine, requestContext), + requestContext) + .mapEmpty(); } + public Future updateInvoiceLine(InvoiceLine invoiceLine, RequestContext requestContext) { return restClient.put(resourceByIdPath(INVOICE_LINES, invoiceLine.getId()), invoiceLine, requestContext); } @@ -94,14 +106,16 @@ public Future> getInvoiceLinesWithTotals(Invoice invoice, Req }); } - private Future persistInvoiceLine(InvoiceLine invoiceLine, RequestContext requestContext) { - RequestEntry requestEntry = new RequestEntry(INVOICE_LINE_BY_ID_ENDPOINT).withId(invoiceLine.getId()); - return restClient.put(requestEntry, invoiceLine, requestContext); - } - public Future createInvoiceLine(InvoiceLine invoiceLine, RequestContext requestContext) { RequestEntry requestEntry = new RequestEntry(INVOICE_LINES_ENDPOINT); - return restClient.post(requestEntry, invoiceLine, InvoiceLine.class, requestContext); + return restClient.post(requestEntry, invoiceLine, InvoiceLine.class, requestContext) + .recover(throwable -> { + Parameter p1 = new Parameter().withKey("invoiceId").withValue(invoiceLine.getInvoiceId()); + Parameter p2 = new Parameter().withKey("invoiceLineNumber").withValue(invoiceLine.getInvoiceLineNumber()); + Error error = ERROR_CREATING_INVOICE_LINE.toError().withParameters(List.of(p1, p2)); + log.error(JsonObject.mapFrom(error)); + throw new HttpException(500, error); + }); } public Future deleteInvoiceLine(String lineId, RequestContext requestContext) { diff --git a/src/main/java/org/folio/services/order/OrderLineService.java b/src/main/java/org/folio/services/order/OrderLineService.java index dd8e68268..becb51931 100644 --- a/src/main/java/org/folio/services/order/OrderLineService.java +++ b/src/main/java/org/folio/services/order/OrderLineService.java @@ -1,14 +1,21 @@ package org.folio.services.order; +import static java.util.stream.Collectors.toList; +import static one.util.streamex.StreamEx.ofSubLists; import static org.folio.invoices.utils.ErrorCodes.PO_LINE_NOT_FOUND; import static org.folio.invoices.utils.ErrorCodes.PO_LINE_UPDATE_FAILURE; +import static org.folio.invoices.utils.HelperUtils.collectResultsOnSuccess; +import static org.folio.invoices.utils.HelperUtils.convertIdsToCqlQuery; import static org.folio.invoices.utils.ResourcePathResolver.ORDER_LINES; import static org.folio.invoices.utils.ResourcePathResolver.resourcesPath; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.invoices.rest.exceptions.HttpException; import org.folio.okapi.common.GenericCompositeFuture; import org.folio.rest.acq.model.orders.CompositePoLine; @@ -22,6 +29,8 @@ import io.vertx.core.Future; public class OrderLineService { + private static final Logger log = LogManager.getLogger(); + private static final int MAX_IDS_FOR_GET_RQ = 15; private static final String ORDER_LINES_ENDPOINT = resourcesPath(ORDER_LINES); private static final String ORDER_LINES_BY_ID_ENDPOINT = ORDER_LINES_ENDPOINT + "/{id}"; @@ -37,9 +46,18 @@ public Future> getPoLines(String query, RequestContext requestConte .withOffset(0) .withLimit(Integer.MAX_VALUE); return restClient.get(requestEntry, PoLineCollection.class, requestContext) + .onFailure(t -> log.error("Error getting order lines by query, query={}", query, t)) .map(PoLineCollection::getPoLines); } + public Future> getPoLinesByIds(List poLineIds, RequestContext requestContext) { + return collectResultsOnSuccess(ofSubLists(poLineIds, MAX_IDS_FOR_GET_RQ) + .map(ids -> getPoLines(convertIdsToCqlQuery(ids), requestContext)).toList()) + .map(lists -> lists.stream() + .flatMap(Collection::stream) + .collect(toList())); + } + public Future getPoLine(String poLineId, RequestContext requestContext) { RequestEntry requestEntry = new RequestEntry(ORDER_LINES_BY_ID_ENDPOINT).withId(poLineId); return restClient.get(requestEntry, CompositePoLine.class, requestContext) diff --git a/src/main/java/org/folio/services/order/OrderService.java b/src/main/java/org/folio/services/order/OrderService.java index 8151bb6f3..b078ba344 100644 --- a/src/main/java/org/folio/services/order/OrderService.java +++ b/src/main/java/org/folio/services/order/OrderService.java @@ -1,20 +1,24 @@ package org.folio.services.order; import static io.vertx.core.Future.succeededFuture; +import static java.util.stream.Collectors.toList; +import static one.util.streamex.StreamEx.ofSubLists; import static org.folio.invoices.utils.ErrorCodes.CANNOT_DELETE_INVOICE_LINE; import static org.folio.invoices.utils.HelperUtils.collectResultsOnSuccess; -import static org.folio.invoices.utils.ResourcePathResolver.COMPOSITE_ORDER; +import static org.folio.invoices.utils.HelperUtils.convertIdsToCqlQuery; +import static org.folio.invoices.utils.ResourcePathResolver.COMPOSITE_ORDERS; import static org.folio.invoices.utils.ResourcePathResolver.ORDER_INVOICE_RELATIONSHIP; import static org.folio.invoices.utils.ResourcePathResolver.resourcesPath; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.invoices.rest.exceptions.HttpException; +import org.folio.okapi.common.GenericCompositeFuture; import org.folio.rest.acq.model.orders.CompositePoLine; import org.folio.rest.acq.model.orders.CompositePurchaseOrder; import org.folio.rest.acq.model.orders.OrderInvoiceRelationship; @@ -34,9 +38,10 @@ public class OrderService { private static final Logger log = LogManager.getLogger(OrderService.class); + private static final int MAX_IDS_FOR_GET_RQ = 15; private static final String ORDER_INVOICE_RELATIONSHIP_QUERY = "purchaseOrderId==%s and invoiceId==%s"; private static final String ORDER_INVOICE_RELATIONSHIP_BY_INVOICE_ID_QUERY = "invoiceId==%s"; - private static final String ORDERS_ENDPOINT = resourcesPath(COMPOSITE_ORDER); + private static final String ORDERS_ENDPOINT = resourcesPath(COMPOSITE_ORDERS); private static final String ORDERS_BY_ID_ENDPOINT = ORDERS_ENDPOINT + "/{id}"; private static final String ORDER_INVOICE_RELATIONSHIPS_ENDPOINT = resourcesPath(ORDER_INVOICE_RELATIONSHIP); private static final String ORDER_INVOICE_RELATIONSHIPS_BY_ID_ENDPOINT = ORDER_INVOICE_RELATIONSHIPS_ENDPOINT + "/{id}"; @@ -65,7 +70,16 @@ public Future> getOrders(String query, RequestContext reques .withOffset(0) .withLimit(Integer.MAX_VALUE); return restClient.get(requestEntry, PurchaseOrderCollection.class, requestContext) - .map(PurchaseOrderCollection::getPurchaseOrders); + .map(PurchaseOrderCollection::getPurchaseOrders) + .onFailure(t -> log.error("Error getting orders by query, query={}", query, t)); + } + + public Future> getOrdersByIds(List poIds, RequestContext requestContext) { + return collectResultsOnSuccess(ofSubLists(poIds, MAX_IDS_FOR_GET_RQ) + .map(ids -> getOrders(convertIdsToCqlQuery(ids), requestContext)).toList()) + .map(lists -> lists.stream() + .flatMap(Collection::stream) + .collect(toList())); } public Future getOrder(String orderId, RequestContext requestContext) { @@ -91,6 +105,24 @@ public Future createInvoiceOrderRelation(InvoiceLine invoiceLine, RequestC })); } + public Future createInvoiceOrderRelations(String invoiceId, List poIds, RequestContext requestContext) { + return getOrderInvoiceRelationshipByInvoiceId(invoiceId, requestContext) + .compose(relationshipCollection -> { + List relationships = relationshipCollection.getOrderInvoiceRelationships(); + List newRelationships = poIds.stream() + .filter(poId -> relationships.stream().noneMatch(r -> r.getInvoiceId().equals(invoiceId) && + r.getPurchaseOrderId().equals(poId))) + .map(poId -> new OrderInvoiceRelationship() + .withInvoiceId(invoiceId) + .withPurchaseOrderId(poId)) + .collect(toList()); + if (newRelationships.isEmpty()) + return succeededFuture(null); + return createOrderInvoiceRelationships(newRelationships, requestContext); + }) + .onFailure(t -> log.error("Error creating invoice-order relations, invoiceId={}", invoiceId, t)); + } + public Future getOrderInvoiceRelationshipByOrderIdAndInvoiceId(String orderId, String invoiceId, RequestContext requestContext) { String query = String.format(ORDER_INVOICE_RELATIONSHIP_QUERY, orderId, invoiceId); RequestEntry requestEntry = new RequestEntry(ORDER_INVOICE_RELATIONSHIPS_ENDPOINT) @@ -109,8 +141,16 @@ public Future getOrderInvoiceRelationshipByI return restClient.get(requestEntry, OrderInvoiceRelationshipCollection.class, requestContext); } + public Future createOrderInvoiceRelationships(List relationships, + RequestContext requestContext) { + List> futures = relationships.stream() + .map(r -> createOrderInvoiceRelationship(r, requestContext)) + .collect(toList()); + return GenericCompositeFuture.join(futures).mapEmpty(); + } + public Future createOrderInvoiceRelationship(OrderInvoiceRelationship relationship, - RequestContext requestContext) { + RequestContext requestContext) { RequestEntry requestEntry = new RequestEntry(ORDER_INVOICE_RELATIONSHIPS_ENDPOINT); return restClient.post(requestEntry, relationship, OrderInvoiceRelationship.class, requestContext); } @@ -135,7 +175,7 @@ public Future deleteOrderInvoiceRelationshipByInvoiceId(String invoiceId, return getOrderInvoiceRelationshipByInvoiceId(invoiceId, requestContext) .compose(relation -> { if (relation.getTotalRecords() > 0) { - List ids = relation.getOrderInvoiceRelationships().stream().map(OrderInvoiceRelationship::getId).collect(Collectors.toList()); + List ids = relation.getOrderInvoiceRelationships().stream().map(OrderInvoiceRelationship::getId).collect(toList()); return deleteOrderInvoiceRelations(ids, requestContext); } return succeededFuture(null); @@ -147,7 +187,7 @@ public Future isInvoiceLineLastForOrder(InvoiceLine invoiceLine, Reques .map(CompositePoLine::getPurchaseOrderId) .compose(orderId -> getOrderPoLines(orderId, requestContext) .map(compositePoLines -> compositePoLines.stream() - .map(CompositePoLine::getId).collect(Collectors.toList()))) + .map(CompositePoLine::getId).collect(toList()))) .compose(poLineIds -> invoiceLineService.getInvoiceLinesRelatedForOrder(poLineIds, invoiceLine.getInvoiceId(), requestContext)) .map(invoiceLines -> invoiceLines.size() == 1); } diff --git a/src/test/java/org/folio/dataimport/handlers/actions/CreateInvoiceEventHandlerTest.java b/src/test/java/org/folio/dataimport/handlers/actions/CreateInvoiceEventHandlerTest.java index c7c4eaf1c..85d94ac83 100644 --- a/src/test/java/org/folio/dataimport/handlers/actions/CreateInvoiceEventHandlerTest.java +++ b/src/test/java/org/folio/dataimport/handlers/actions/CreateInvoiceEventHandlerTest.java @@ -136,7 +136,9 @@ public class CreateInvoiceEventHandlerTest extends ApiTestBase { new MappingRule().withPath("invoice.invoiceLines[].subTotal") .withValue("MOA+203[2]"), new MappingRule().withPath("invoice.invoiceLines[].quantity") - .withValue("QTY+47[2]") + .withValue("QTY+47[2]"), + new MappingRule().withPath("invoice.invoiceLines[].description") + .withValue("{POL_title}; else IMD+L+050+[4]") ))))))); private MappingProfile mappingProfileWithPoLineSyntax = new MappingProfile() @@ -157,6 +159,8 @@ public class CreateInvoiceEventHandlerTest extends ApiTestBase { new MappingRule().withPath("invoice.invoiceLines[].poLineId") .withName("poLineId") .withValue("RFF+LI[2]; else {POL_NUMBER}"), + new MappingRule().withPath("invoice.invoiceLines[].quantity") + .withValue("QTY+47[2]"), new MappingRule().withPath("invoice.invoiceLines[].referenceNumbers[]") .withRepeatableFieldAction(MappingRule.RepeatableFieldAction.EXTEND_EXISTING) .withName("referenceNumbers") @@ -193,6 +197,10 @@ public class CreateInvoiceEventHandlerTest extends ApiTestBase { new MappingRule().withPath("invoice.invoiceLines[].poLineId") .withName("poLineId") .withValue("RFF+LI[2]; else {POL_NUMBER}"), + new MappingRule().withPath("invoice.invoiceLines[].quantity") + .withValue("QTY+47[2]"), + new MappingRule().withPath("invoice.invoiceLines[].description") + .withValue("{POL_title}; else IMD+L+050+[4]"), new MappingRule().withPath("invoice.invoiceLines[].fundDistributions[]") .withRepeatableFieldAction(MappingRule.RepeatableFieldAction.EXTEND_EXISTING) .withValue("{POL_FUND_DISTRIBUTIONS}"), @@ -219,6 +227,10 @@ public class CreateInvoiceEventHandlerTest extends ApiTestBase { .withValue("RFF+LI[2]; else {POL_NUMBER}"), new MappingRule().withPath("invoice.invoiceLines[].subTotal") .withValue("MOA+203[2]"), + new MappingRule().withPath("invoice.invoiceLines[].quantity") + .withValue("QTY+47[2]"), + new MappingRule().withPath("invoice.invoiceLines[].description") + .withValue("{POL_title}; else IMD+L+050+[4]"), new MappingRule().withPath("invoice.invoiceLines[].fundDistributions[]") .withRepeatableFieldAction(MappingRule.RepeatableFieldAction.EXTEND_EXISTING) .withSubfields(List.of(new RepeatableSubfieldMapping() diff --git a/src/test/java/org/folio/rest/impl/MockServer.java b/src/test/java/org/folio/rest/impl/MockServer.java index 7f7e9d328..3b90ffc47 100644 --- a/src/test/java/org/folio/rest/impl/MockServer.java +++ b/src/test/java/org/folio/rest/impl/MockServer.java @@ -20,7 +20,7 @@ import static org.folio.invoices.utils.ResourcePathResolver.BATCH_VOUCHER_STORAGE; import static org.folio.invoices.utils.ResourcePathResolver.BUDGETS; import static org.folio.invoices.utils.ResourcePathResolver.BUDGET_EXPENSE_CLASSES; -import static org.folio.invoices.utils.ResourcePathResolver.COMPOSITE_ORDER; +import static org.folio.invoices.utils.ResourcePathResolver.COMPOSITE_ORDERS; import static org.folio.invoices.utils.ResourcePathResolver.CURRENT_BUDGET; import static org.folio.invoices.utils.ResourcePathResolver.EXPENSE_CLASSES_URL; import static org.folio.invoices.utils.ResourcePathResolver.FINANCE_CREDITS; @@ -133,6 +133,10 @@ import org.folio.rest.acq.model.orders.CompositePoLine; import org.folio.rest.acq.model.orders.CompositePurchaseOrder; import org.folio.rest.acq.model.orders.OrderInvoiceRelationshipCollection; +import org.folio.rest.acq.model.orders.PoLine; +import org.folio.rest.acq.model.orders.PoLineCollection; +import org.folio.rest.acq.model.orders.PurchaseOrder; +import org.folio.rest.acq.model.orders.PurchaseOrderCollection; import org.folio.rest.acq.model.units.AcquisitionsUnit; import org.folio.rest.acq.model.units.AcquisitionsUnitCollection; import org.folio.rest.acq.model.units.AcquisitionsUnitMembershipCollection; @@ -205,6 +209,8 @@ public class MockServer { private static final String VOUCHER_LINES_COLLECTION = BASE_MOCK_DATA_PATH + "voucherLines/voucher_lines.json"; public static final String BUDGETS_PATH = BASE_MOCK_DATA_PATH + "budgets/budgets.json"; public static final String LEDGERS_PATH = BASE_MOCK_DATA_PATH + "ledgers/ledgers.json"; + public static final String POLINES_PATH = BASE_MOCK_DATA_PATH + "poLines/po_lines.json"; + public static final String COMPOSITE_ORDERS_PATH = BASE_MOCK_DATA_PATH + "compositeOrders/composite_orders.json"; private static final String ACQUISITIONS_UNITS_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "acquisitionUnits"; static final String ACQUISITIONS_UNITS_COLLECTION = ACQUISITIONS_UNITS_MOCK_DATA_PATH + "/units.json"; static final String ACQUISITIONS_MEMBERSHIPS_COLLECTION = ACQUISITIONS_UNITS_MOCK_DATA_PATH + "/memberships.json"; @@ -406,8 +412,10 @@ private Router defineRoutes() { router.route(HttpMethod.GET, resourcesPath(INVOICE_LINE_NUMBER)).handler(this::handleGetInvoiceLineNumber); router.route(HttpMethod.GET, resourceByIdPath(VOUCHER_LINES)).handler(this::handleGetVoucherLineById); router.route(HttpMethod.GET, resourceByIdPath(VOUCHERS_STORAGE)).handler(this::handleGetVoucherById); - router.route(HttpMethod.GET, resourceByIdPath(COMPOSITE_ORDER)).handler(this::handleGetOrderById); + router.route(HttpMethod.GET, resourceByIdPath(COMPOSITE_ORDERS)).handler(this::handleGetOrderById); + router.route(HttpMethod.GET, resourcesPath(COMPOSITE_ORDERS)).handler(this::handleGetOrderByQuery); router.route(HttpMethod.GET, resourceByIdPath(ORDER_LINES)).handler(this::handleGetPoLineById); + router.route(HttpMethod.GET, resourcesPath(ORDER_LINES)).handler(this::handleGetPoLineByQuery); router.route(HttpMethod.GET, resourcesPath(ORDER_INVOICE_RELATIONSHIP)).handler(this::handleGetOrderInvoiceRelations); router.route(HttpMethod.GET, resourcesPath(VOUCHER_NUMBER_START)).handler(this::handleGetSequence); router.route(HttpMethod.GET, resourcesPath(VOUCHERS_STORAGE)).handler(this::handleGetVouchers); @@ -1076,7 +1084,7 @@ private void handlePost(RoutingContext ctx, Class tClass, String entryNam serverResponse(ctx, 500, TEXT_PLAIN, INTERNAL_SERVER_ERROR.getReasonPhrase()); } else { JsonObject body = ctx.body().asJsonObject(); - if (generateId) { + if (generateId && body.getString(AbstractHelper.ID) == null) { String id = UUID.randomUUID().toString(); body.put(AbstractHelper.ID, id); } @@ -1275,7 +1283,7 @@ private void handleGetOrderById(RoutingContext ctx) { String id = ctx.request().getParam(AbstractHelper.ID); logger.info("id: " + id); - addServerRqRsData(HttpMethod.GET, COMPOSITE_ORDER, new JsonObject().put(AbstractHelper.ID, id)); + addServerRqRsData(HttpMethod.GET, COMPOSITE_ORDERS, new JsonObject().put(AbstractHelper.ID, id)); Supplier getFromFile = () -> { String filePath = String.format(MOCK_DATA_PATH_PATTERN, ORDER_MOCK_DATA_PATH, id); @@ -1286,7 +1294,7 @@ private void handleGetOrderById(RoutingContext ctx) { } }; - JsonObject order = getMockEntry(COMPOSITE_ORDER, id).orElseGet(getFromFile); + JsonObject order = getMockEntry(COMPOSITE_ORDERS, id).orElseGet(getFromFile); if (order == null) { ctx.response().setStatusCode(404).end(id); } else { @@ -1299,6 +1307,36 @@ private void handleGetOrderById(RoutingContext ctx) { } } + private void handleGetOrderByQuery(RoutingContext ctx) { + String query = StringUtils.trimToEmpty(ctx.request().getParam(QUERY)); + addServerRqQuery(COMPOSITE_ORDERS, query); + if (query.contains("id==")) { + List ids = extractIdsFromQuery("id", "==", query); + PurchaseOrderCollection orderCollection = getOrdersByIds(ids); + JsonObject collection = JsonObject.mapFrom(orderCollection); + addServerRqRsData(HttpMethod.GET, COMPOSITE_ORDERS, collection); + ctx.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(collection.encodePrettily()); + } + } + + private PurchaseOrderCollection getOrdersByIds(List ids) { + Supplier> getFromFile = () -> { + try { + return new JsonObject(getMockData(COMPOSITE_ORDERS_PATH)).mapTo(PurchaseOrderCollection.class).getPurchaseOrders(); + } catch (IOException e) { + return Collections.emptyList(); + } + }; + List orders = getMockEntries(COMPOSITE_ORDERS, PurchaseOrder.class).orElseGet(getFromFile); + if (!ids.isEmpty()) { + orders.removeIf(item -> !ids.contains(item.getId())); + } + return new PurchaseOrderCollection().withPurchaseOrders(orders).withTotalRecords(orders.size()); + } + private void handleGetPoLineById(RoutingContext ctx) { logger.info("got: " + ctx.request().path()); String id = ctx.request().getParam(AbstractHelper.ID); @@ -1337,6 +1375,36 @@ private void handleGetPoLineById(RoutingContext ctx) { } } + private void handleGetPoLineByQuery(RoutingContext ctx) { + String query = StringUtils.trimToEmpty(ctx.request().getParam(QUERY)); + addServerRqQuery(ORDER_LINES, query); + if (query.contains("id==")) { + List ids = extractIdsFromQuery("id", "==", query); + PoLineCollection poLineCollection = getPoLinesByIds(ids); + JsonObject collection = JsonObject.mapFrom(poLineCollection); + addServerRqRsData(HttpMethod.GET, ORDER_LINES, collection); + ctx.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(collection.encodePrettily()); + } + } + + private PoLineCollection getPoLinesByIds(List ids) { + Supplier> getFromFile = () -> { + try { + return new JsonObject(getMockData(POLINES_PATH)).mapTo(PoLineCollection.class).getPoLines(); + } catch (IOException e) { + return Collections.emptyList(); + } + }; + List poLines = getMockEntries(ORDER_LINES, PoLine.class).orElseGet(getFromFile); + if (!ids.isEmpty()) { + poLines.removeIf(item -> !ids.contains(item.getId())); + } + return new PoLineCollection().withPoLines(poLines).withTotalRecords(poLines.size()); + } + private void serverResponse(RoutingContext ctx, int statusCode, String contentType, String body) { ctx.response().setStatusCode(statusCode).putHeader(HttpHeaders.CONTENT_TYPE, contentType).end(body); } diff --git a/src/test/resources/mockdata/compositeOrders/composite_orders.json b/src/test/resources/mockdata/compositeOrders/composite_orders.json new file mode 100644 index 000000000..c15847de1 --- /dev/null +++ b/src/test/resources/mockdata/compositeOrders/composite_orders.json @@ -0,0 +1,30 @@ +{ + "purchaseOrders": [ + { + "id": "0cb6741d-4a00-47e5-a902-5678eb24478d", + "approved": true, + "assignedTo": "ab18897b-0e40-4f31-896b-9c9adc979a88", + "manualPo": false, + "notes": [ + "ABCDEFGHIJKLMNO", + "ABCDEFGHIJKLMNOPQRST", + "ABCDEFGHIJKLMNOPQRSTUV" + ], + "poNumber": "228D126", + "orderType": "One-Time", + "reEncumber": false, + "vendor": "d0fb5aa0-cdf1-11e8-a8d5-f2801f1b9fd1", + "workflowStatus": "Pending", + "acqUnitIds": [ + + ], + "metadata": { + "createdDate": "2020-03-25T16:46:02.584+0000", + "createdByUserId": "440c89e3-7f6c-578a-9ea8-310dad23605e", + "updatedDate": "2020-03-25T16:46:02.584+0000", + "updatedByUserId": "440c89e3-7f6c-578a-9ea8-310dad23605e" + } + } + ], + "totalRecords": 1 +} diff --git a/src/test/resources/mockdata/poLines/po_lines.json b/src/test/resources/mockdata/poLines/po_lines.json new file mode 100644 index 000000000..7083cf1e3 --- /dev/null +++ b/src/test/resources/mockdata/poLines/po_lines.json @@ -0,0 +1,84 @@ +{ + "poLines": [ + { + "id": "0000edd1-b463-41ba-bf64-1b1d9f9d0001", + "purchaseOrderId" : "0cb6741d-4a00-47e5-a902-5678eb24478d", + "checkinItems": false, + "alerts": [], + "claims": [], + "collection": false, + "contributors": [], + "fundDistribution": [ + { + "code": "USHIST", + "fundId": "388c0f7b-9fff-451c-b300-6509e443bef5", + "distributionType": "percentage", + "value": 80.0, + "encumbrance": "eb506834-6c70-4239-8d1a-6414a5b08ac3" + } + ], + "isPackage": false, + "locations": [], + "paymentStatus": "Pending", + "poLineNumber": "010170-01", + "receiptStatus": "Pending", + "reportingCodes": [], + "rush": false, + "titleOrPackage": "polTitle-1", + "vendorDetail": { + "instructions": "ABCDEFG", + "referenceNumbers": [ + { + "refNumber": "C6546362", + "refNumberType": "Vendor title number" + } + ] + } + }, + { + "id": "0000edd1-b463-41ba-bf64-1b1d9f9d0003", + "purchaseOrderId" : "0cb6741d-4a00-47e5-a902-5678eb24478d", + "checkinItems": false, + "alerts": [], + "claims": [], + "collection": false, + "contributors": [], + "fundDistribution": [ + { + "code": "USHIST", + "fundId": "388c0f7b-9fff-451c-b300-6509e443bef5", + "distributionType": "percentage", + "value": 70.0, + "encumbrance": "eb506834-6c70-4239-8d1a-6414a5b08ac3", + "expenseClassId" : "5b5ebe3a-cf8b-4f16-a880-46873ef21388" + }, + { + "code": "EUHIST", + "fundId": "55f48dc6-efa7-4cfe-bc7c-4786efe493e3", + "distributionType": "amount", + "value": 12.34, + "encumbrance": "fb506834-6c70-4239-8d1a-6414a5b08ac3", + "expenseClassId" : "6e6ed3c9-d959-4c91-a436-6076fb373816" + } + ], + "isPackage": false, + "locations": [], + "paymentStatus": "Pending", + "poLineNumber": "010170-03", + "receiptStatus": "Pending", + "reportingCodes": [], + "rush": false, + "titleOrPackage": "polTitle-3", + "vendorDetail": { + "instructions": "ABCDEFG", + "referenceNumbers": [ + { + "refNumber": "E9498296", + "refNumberType": "Vendor title number" + } + ] + } + } + ], + "totalRecords": 2 +}