diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 7b82478eb..1724ed100 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -11,7 +11,7 @@ "pathPattern": "/invoice/invoices", "permissionsRequired": ["invoice.invoices.collection.get"], "permissionsDesired": [ - "invoices.bypass-acquisition-units" + "invoices.acquisition-units.bypass.execute" ], "modulePermissions": [ "invoice-storage.invoices.collection.get", @@ -30,7 +30,8 @@ "invoice-storage.invoice-number.get", "acquisitions-units-storage.units.collection.get", "acquisitions-units-storage.memberships.collection.get", - "organizations-storage.organizations.item.get" + "organizations-storage.organizations.item.get", + "finance.ledgers.current-fiscal-year.item.get" ] }, { @@ -40,7 +41,7 @@ "invoice.invoices.item.get" ], "permissionsDesired": [ - "invoices.bypass-acquisition-units" + "invoices.acquisition-units.bypass.execute" ], "modulePermissions": [ "invoice-storage.invoices.item.get", @@ -55,11 +56,11 @@ "permissionsRequired": ["invoice.invoices.item.put"], "permissionsDesired": [ "invoices.acquisitions-units-assignments.manage", - "invoices.fiscal-year.update", - "invoice.item.approve", - "invoice.item.pay", - "invoice.item.cancel", - "invoices.bypass-acquisition-units" + "invoices.fiscal-year.update.execute", + "invoice.item.approve.execute", + "invoice.item.pay.execute", + "invoice.item.cancel.execute", + "invoices.acquisition-units.bypass.execute" ], "modulePermissions": [ "configuration.entries.collection.get", @@ -68,7 +69,7 @@ "invoice-storage.invoices.item.get", "invoice-storage.invoice-lines.item.put", "invoice-storage.invoice-lines.collection.get", - "orders.bypass-acquisition-units", + "orders.acquisition-units.bypass.execute", "orders.collection.get", "orders.po-lines.collection.get", "orders.po-lines.item.put", @@ -82,7 +83,7 @@ "voucher-storage.voucher-number.get", "acquisitions-units-storage.units.collection.get", "acquisitions-units-storage.memberships.collection.get", - "finance.transactions.batch", + "finance.transactions.batch.execute", "finance.transactions.collection.get", "finance.funds.item.get", "finance.funds.collection.get", @@ -118,7 +119,7 @@ "pathPattern": "/invoice/invoice-lines", "permissionsRequired": ["invoice.invoice-lines.collection.get"], "permissionsDesired": [ - "invoices.bypass-acquisition-units" + "invoices.acquisition-units.bypass.execute" ], "modulePermissions": [ "invoice-storage.invoice-lines.collection.get", @@ -160,7 +161,7 @@ "pathPattern": "/invoice/invoice-lines/{id}", "permissionsRequired": ["invoice.invoice-lines.item.get"], "permissionsDesired": [ - "invoices.bypass-acquisition-units" + "invoices.acquisition-units.bypass.execute" ], "modulePermissions": [ "invoice-storage.invoice-lines.item.get", @@ -178,7 +179,7 @@ "pathPattern": "/invoice/invoice-lines/{id}", "permissionsRequired": ["invoice.invoice-lines.item.put"], "permissionsDesired": [ - "invoices.bypass-acquisition-units" + "invoices.acquisition-units.bypass.execute" ], "modulePermissions": [ "invoice-storage.invoice-lines.item.put", @@ -716,19 +717,22 @@ "description": "Delete an existing Invoice" }, { - "permissionName": "invoice.item.approve", + "permissionName": "invoice.item.approve.execute", "displayName": "Invoice - approve an existing Invoice", - "description": "Approve an existing Invoice" + "description": "Approve an existing Invoice", + "replaces": ["invoice.item.approve"] }, { - "permissionName": "invoice.item.pay", + "permissionName": "invoice.item.pay.execute", "displayName": "Invoice - pay an existing Invoice", - "description": "Pay an existing Invoice" + "description": "Pay an existing Invoice", + "replaces": ["invoice.item.pay"] }, { - "permissionName": "invoice.item.cancel", + "permissionName": "invoice.item.cancel.execute", "displayName": "Invoice - cancel an existing Invoice", - "description": "Cancel an existing Invoice" + "description": "Cancel an existing Invoice", + "replaces": ["invoice.item.cancel"] }, { "permissionName": "invoice.invoice-lines.collection.get", @@ -917,9 +921,10 @@ ] }, { - "permissionName": "invoices.bypass-acquisition-units", + "permissionName": "invoices.acquisition-units.bypass.execute", "displayName": "Bypass acquisition units checks", - "description": "Backend internal permission to bypass invoice acquisition units checks" + "description": "Backend internal permission to bypass invoice acquisition units checks", + "replaces": ["invoices.bypass-acquisition-units"] }, { "permissionName": "invoices.acquisitions-units-assignments.assign", @@ -941,9 +946,10 @@ ] }, { - "permissionName": "invoices.fiscal-year.update", + "permissionName": "invoices.fiscal-year.update.execute", "displayName": "Invoice - update fiscal year", - "description": "Update a fiscal year" + "description": "Update a fiscal year", + "replaces": ["invoices.fiscal-year.update"] }, { "permissionName": "batch-groups.collection.get", @@ -1057,9 +1063,9 @@ "invoice.invoices.item.post", "invoice.invoices.item.put", "invoice.invoices.item.delete", - "invoice.item.approve", - "invoice.item.pay", - "invoice.item.cancel", + "invoice.item.approve.execute", + "invoice.item.pay.execute", + "invoice.item.cancel.execute", "invoice.invoice-lines.collection.get", "invoice.invoice-lines.item.get", "invoice.invoice-lines.item.post", diff --git a/pom.xml b/pom.xml index 4cf7864be..b7278f2fd 100644 --- a/pom.xml +++ b/pom.xml @@ -24,10 +24,11 @@ 35.2.0 2.3.3 - 4.5.4 + 4.5.9 5.4.0 5.10.0 - 6.1.5 + 2.13.2 + 6.1.12 2.1.0 3.25.3 1.9.21.1 @@ -89,17 +90,14 @@ io.vertx vertx-core - ${vertx.version} io.vertx vertx-web - ${vertx.version} io.vertx vertx-unit - ${vertx.version} test @@ -110,24 +108,22 @@ org.apache.sshd sshd-sftp - 2.9.3 + ${sshd.version} org.apache.sshd sshd-spring-sftp - 2.8.0 + ${sshd.version} io.vertx vertx-rx-java - ${vertx.version} provided io.vertx vertx-junit5 - ${vertx.version} test @@ -151,13 +147,11 @@ org.springframework spring-test - ${spring.version} test org.springframework spring-context - ${spring.version} org.jvnet.jaxb2_commons @@ -232,25 +226,21 @@ org.testcontainers postgresql - ${testcontainers.version} test org.testcontainers kafka - ${testcontainers.version} test org.testcontainers testcontainers - ${testcontainers.version} test org.testcontainers junit-jupiter - 1.19.0 test @@ -778,6 +768,27 @@ import pom + + org.springframework + spring-framework-bom + ${spring.version} + import + pom + + + io.vertx + vertx-stack-depchain + ${vertx.version} + import + pom + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + import + pom + diff --git a/src/main/java/org/folio/invoices/utils/AcqDesiredPermissions.java b/src/main/java/org/folio/invoices/utils/AcqDesiredPermissions.java index caa1d25cf..604cf20c8 100644 --- a/src/main/java/org/folio/invoices/utils/AcqDesiredPermissions.java +++ b/src/main/java/org/folio/invoices/utils/AcqDesiredPermissions.java @@ -8,11 +8,11 @@ public enum AcqDesiredPermissions { ASSIGN("invoices.acquisitions-units-assignments.assign"), MANAGE("invoices.acquisitions-units-assignments.manage"), - APPROVE("invoice.item.approve"), - PAY("invoice.item.pay"), - CANCEL("invoice.item.cancel"), - FISCAL_YEAR_UPDATE("invoices.fiscal-year.update"), - BYPASS_ACQ_UNITS("invoices.bypass-acquisition-units"); + APPROVE("invoice.item.approve.execute"), + PAY("invoice.item.pay.execute"), + CANCEL("invoice.item.cancel.execute"), + FISCAL_YEAR_UPDATE("invoices.fiscal-year.update.execute"), + BYPASS_ACQ_UNITS("invoices.acquisition-units.bypass.execute"); private String permission; private static final List values; diff --git a/src/main/java/org/folio/services/adjusment/AdjustmentsService.java b/src/main/java/org/folio/services/adjusment/AdjustmentsService.java index 8a30538f2..2d6f94d14 100644 --- a/src/main/java/org/folio/services/adjusment/AdjustmentsService.java +++ b/src/main/java/org/folio/services/adjusment/AdjustmentsService.java @@ -31,6 +31,7 @@ import io.vertx.core.json.JsonObject; public class AdjustmentsService { + private final Logger logger = LogManager.getLogger(this.getClass()); public static final Predicate NOT_PRORATED_ADJUSTMENTS_PREDICATE = adj -> adj.getProrate() == NOT_PRORATED; public static final Predicate PRORATED_ADJUSTMENTS_PREDICATE = NOT_PRORATED_ADJUSTMENTS_PREDICATE.negate(); @@ -65,7 +66,7 @@ public List applyProratedAdjustments(List lines, Invoi updatedLines.addAll(applyProratedAdjustmentByLines(adjustment, lines, currencyUnit)); break; case BY_AMOUNT: - updatedLines.addAll(applyProratedAdjustmentByAmount(adjustment, lines, currencyUnit)); + updatedLines.addAll(applyAdjustmentsAndUpdateLines(adjustment, lines, currencyUnit)); break; case BY_QUANTITY: updatedLines.addAll(applyProratedAdjustmentByQuantity(adjustment, lines, currencyUnit)); @@ -80,6 +81,15 @@ public List applyProratedAdjustments(List lines, Invoi .collect(toList()); } + private List applyAdjustmentsAndUpdateLines(Adjustment adjustment, List lines, + CurrencyUnit currencyUnit) { + if (adjustment.getRelationToTotal() == Adjustment.RelationToTotal.INCLUDED_IN) { + return applyProratedAmountTypeIncludedInAdjustments(adjustment, lines, currencyUnit); + } else { + return applyProratedAdjustmentByAmount(adjustment, lines, currencyUnit); + } + } + public void processProratedAdjustments(List lines, Invoice invoice) { List proratedAdjustments = getProratedAdjustments(invoice); @@ -88,7 +98,6 @@ public void processProratedAdjustments(List lines, Invoice invoice) // Apply prorated adjustments to each invoice line applyProratedAdjustments(lines, invoice); - } /** @@ -99,10 +108,10 @@ public void processProratedAdjustments(List lines, Invoice invoice) void filterDeletedAdjustments(List proratedAdjustments, List invoiceLines) { List adjIds = proratedAdjustments.stream() .map(Adjustment::getId) - .collect(toList()); + .toList(); invoiceLines.forEach(line -> line.getAdjustments() - .removeIf(adj -> Objects.nonNull(adj.getAdjustmentId()) && !adjIds.contains(adj.getAdjustmentId()))); + .removeIf(adj -> Objects.nonNull(adj.getAdjustmentId()) && !adjIds.contains(adj.getAdjustmentId()))); } /** @@ -179,7 +188,7 @@ private List applyAmountTypeProratedAdjustments(Adjustment adjustme int remainderSignum = remainder.signum(); MonetaryAmount smallestUnit = getSmallestUnit(expectedAdjustmentTotal, remainderSignum); - for (ListIterator iterator = getIterator(lines, remainderSignum); isIteratorHasNext(iterator, remainderSignum);) { + for (ListIterator iterator = getIterator(lines, remainderSignum); isIteratorHasNext(iterator, remainderSignum); ) { final InvoiceLine line = iteratorNext(iterator, remainderSignum); MonetaryAmount amount = lineIdAdjustmentValueMap.get(line.getId()); @@ -190,8 +199,7 @@ private List applyAmountTypeProratedAdjustments(Adjustment adjustme } Adjustment proratedAdjustment = prepareAdjustmentForLine(adjustment); - proratedAdjustment.setValue(amount.getNumber() - .doubleValue()); + proratedAdjustment.setValue(amount.getNumber().doubleValue()); if (addAdjustmentToLine(line, proratedAdjustment)) { updatedLines.add(line); } @@ -200,13 +208,40 @@ private List applyAmountTypeProratedAdjustments(Adjustment adjustme return updatedLines; } + private List applyProratedAmountTypeIncludedInAdjustments(Adjustment adjustment, List lines, + CurrencyUnit currencyUnit) { + List updatedLines = new ArrayList<>(); + for (InvoiceLine line : lines) { + if (invoiceLineWasAdjustedById(adjustment, line)) { + continue; + } + MonetaryAmount lineSubtotal = Money.of(line.getSubTotal(), currencyUnit); + MonetaryAmount amountAdjustmentValue = lineSubtotal.multiply(adjustment.getValue()) + .divide(Money.of(100, currencyUnit).add(Money.of(adjustment.getValue(), currencyUnit)).getNumber().doubleValue()) + .with(Monetary.getDefaultRounding()); + Adjustment preparedAdjustment = prepareAdjustmentForLine(adjustment.withType(Adjustment.Type.AMOUNT)) + .withValue(amountAdjustmentValue.getNumber().doubleValue()); + line.withSubTotal(lineSubtotal.subtract(amountAdjustmentValue).getNumber().doubleValue()); + if (addAdjustmentToLine(line, preparedAdjustment)) { + updatedLines.add(line); + } + } + return updatedLines; + } + + private static boolean invoiceLineWasAdjustedById(Adjustment adjustment, InvoiceLine line) { + return Objects.nonNull(adjustment.getId()) && line.getAdjustments().stream() + .map(Adjustment::getAdjustmentId) + .filter(Objects::nonNull) + .anyMatch(lineAdjustmentId -> lineAdjustmentId.equals(adjustment.getId())); + } + /** * Each invoiceLine gets a portion of the amount proportionate to the invoiceLine's contribution to the invoice subTotal. * Prorated percentage adjustments of this type aren't split but rather each invoiceLine gets an adjustment of that percentage */ private List applyProratedAdjustmentByAmount(Adjustment adjustment, List lines, CurrencyUnit currencyUnit) { - if (adjustment.getType() == Adjustment.Type.PERCENTAGE) { adjustment = convertToAmountAdjustment(adjustment, lines, currencyUnit); } @@ -232,7 +267,6 @@ private BiFunction prorateByAmountF */ private List applyProratedAdjustmentByQuantity(Adjustment adjustment, List lines, CurrencyUnit currencyUnit) { - if (adjustment.getType() == Adjustment.Type.PERCENTAGE) { return applyPercentageAdjustmentsByQuantity(adjustment, lines, currencyUnit); } @@ -285,5 +319,4 @@ private InvoiceLine iteratorNext(ListIterator iterator, int remaind private BiFunction prorateByLines(List lines) { return (amount, line) -> amount.divide(lines.size()).with(Monetary.getDefaultRounding()); } - } diff --git a/src/main/java/org/folio/services/invoice/InvoicePaymentService.java b/src/main/java/org/folio/services/invoice/InvoicePaymentService.java index f5aa438a9..9ac84dc93 100644 --- a/src/main/java/org/folio/services/invoice/InvoicePaymentService.java +++ b/src/main/java/org/folio/services/invoice/InvoicePaymentService.java @@ -10,19 +10,15 @@ import java.util.List; import java.util.Set; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.folio.InvoiceWorkflowDataHolderBuilder; -import org.folio.invoices.rest.exceptions.HttpException; import org.folio.rest.acq.model.orders.CompositePoLine; 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.services.finance.fiscalyear.CurrentFiscalYearService; import org.folio.services.finance.transaction.PaymentCreditWorkflowService; import org.folio.services.order.OrderLineService; import org.folio.services.voucher.VoucherService; @@ -30,7 +26,6 @@ import com.google.common.annotations.VisibleForTesting; -import io.vertx.core.CompositeFuture; import io.vertx.core.Future; public class InvoicePaymentService { @@ -43,10 +38,7 @@ public class InvoicePaymentService { private VoucherService voucherService; @Autowired private OrderLineService orderLineService; - @Autowired - private CurrentFiscalYearService currentFiscalYearService; - public static final String INVOICE_LINE_MUST_HAVE_FUND = "The invoice line must contain the fund for payment"; protected static final Set PO_LINE_PAYMENT_IGNORED_STATUSES = EnumSet.of(ONGOING, PAYMENT_NOT_REQUIRED); @@ -62,41 +54,17 @@ public Future payInvoice(Invoice invoice, List invoiceLines, invoice.setPaymentDate(invoice.getMetadata().getUpdatedDate()); return holderBuilder.buildCompleteHolders(invoice, invoiceLines, requestContext) .compose(holders -> paymentCreditWorkflowService.handlePaymentsAndCreditsCreation(holders, requestContext)) - .compose(vVoid -> CompositeFuture.join(updatePoLinesStatus(invoice, invoiceLines, requestContext), voucherService.payInvoiceVoucher(invoice.getId(), requestContext))) + .compose(v -> Future.join(payPoLines(invoiceLines, requestContext), voucherService.payInvoiceVoucher(invoice.getId(), requestContext))) .mapEmpty(); } - private Future updatePoLinesStatus(Invoice invoice, List invoiceLines, RequestContext requestContext) { - if (StringUtils.isBlank(invoice.getFiscalYearId())) { - return updatePoLinesToPaidStatus(invoiceLines, requestContext); - } - - String fundID = invoiceLines.stream() - .flatMap(invoiceLine -> invoiceLine.getFundDistributions().stream()) - .map(FundDistribution::getFundId) - .findFirst() - .orElse(null); - - if (StringUtils.isNotBlank(fundID)) { - return currentFiscalYearService.getCurrentFiscalYearByFund(fundID, requestContext) - .compose(currentFiscalYear -> { - if (Objects.equals(currentFiscalYear.getId(), invoice.getFiscalYearId())) { - return updatePoLinesToPaidStatus(invoiceLines, requestContext); - } else { - return Future.succeededFuture(); - } - }); - } - return Future.failedFuture(new HttpException(400, INVOICE_LINE_MUST_HAVE_FUND)); - } - /** * Updates payment status of the associated PO Lines. * * @param invoiceLines the invoice lines to be paid * @return CompletableFuture that indicates when transition is completed */ - private Future updatePoLinesToPaidStatus(List invoiceLines, RequestContext requestContext) { + private Future payPoLines(List invoiceLines, RequestContext requestContext) { Map> poLineIdInvoiceLinesMap = groupInvoiceLinesByPoLineId(invoiceLines); return fetchPoLines(poLineIdInvoiceLinesMap, requestContext) .map(this::updatePoLinesPaymentStatus) diff --git a/src/test/java/org/folio/rest/impl/ApiTestBase.java b/src/test/java/org/folio/rest/impl/ApiTestBase.java index 82fca1871..3807c2cc1 100644 --- a/src/test/java/org/folio/rest/impl/ApiTestBase.java +++ b/src/test/java/org/folio/rest/impl/ApiTestBase.java @@ -98,12 +98,12 @@ public class ApiTestBase { private static boolean runningOnOwn; public static final List permissionsList = Arrays.asList( - "invoice.item.approve", - "invoice.item.pay", + "invoice.item.approve.execute", + "invoice.item.pay.execute", "invoice.invoices.item.put", "invoices.acquisitions-units-assignments.manage", "invoices.acquisitions-units-assignments.assign", - "invoices.fiscal-year.update" + "invoices.fiscal-year.update.execute" ); public static final JsonArray permissionsArray = new JsonArray(permissionsList); @@ -114,7 +114,7 @@ public class ApiTestBase { "invoice.invoices.item.put", "invoices.acquisitions-units-assignments.manage", "invoices.acquisitions-units-assignments.assign", - "invoices.fiscal-year.update" + "invoices.fiscal-year.update.execute" ); public static final JsonArray permissionsWithoutApproveAndPayArray = new JsonArray(permissionsWithoutApproveAndPayList); @@ -122,11 +122,11 @@ public class ApiTestBase { public static final Header X_OKAPI_PERMISSION_WITHOUT_PAY_APPROVE = new Header(UserPermissionsUtil.OKAPI_HEADER_PERMISSIONS, permissionsWithoutApproveAndPayJsonArrayString); public static final List permissionsWithoutPaidList = Arrays.asList( - "invoice.item.approve", + "invoice.item.approve.execute", "invoice.invoices.item.put", "invoices.acquisitions-units-assignments.manage", "invoices.acquisitions-units-assignments.assign", - "invoices.fiscal-year.update" + "invoices.fiscal-year.update.execute" ); public static final JsonArray permissionsWithoutPaidArray = new JsonArray(permissionsWithoutPaidList); diff --git a/src/test/java/org/folio/rest/impl/InvoiceLinesProratedAdjustmentsTest.java b/src/test/java/org/folio/rest/impl/InvoiceLinesProratedAdjustmentsTest.java index fafaa0397..95011347a 100644 --- a/src/test/java/org/folio/rest/impl/InvoiceLinesProratedAdjustmentsTest.java +++ b/src/test/java/org/folio/rest/impl/InvoiceLinesProratedAdjustmentsTest.java @@ -11,6 +11,9 @@ import static org.folio.rest.impl.MockServer.getInvoiceLineCreations; import static org.folio.rest.impl.MockServer.getInvoiceLineUpdates; import static org.folio.rest.impl.MockServer.getInvoiceUpdates; +import static org.folio.rest.jaxrs.model.Adjustment.Prorate.BY_AMOUNT; +import static org.folio.rest.jaxrs.model.Adjustment.RelationToTotal.INCLUDED_IN; +import static org.folio.rest.jaxrs.model.Adjustment.Type.PERCENTAGE; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -19,6 +22,7 @@ import io.vertx.junit5.VertxExtension; import java.util.Collections; +import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -27,6 +31,7 @@ import org.folio.rest.jaxrs.model.InvoiceLine; import org.folio.rest.jaxrs.model.InvoiceLineCollection; import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -182,6 +187,82 @@ public void testDeleteLineForInvoiceWithOneAdj(Adjustment.Prorate prorate, Adjus assertThat(lineAdjustment.getValue(), is(expectedAdjValue)); } + @Test + public void testCreateInvoiceWithOnePercentageTypeByAmountProrateIncludedByTotalAdjustment() { + logger.info("=== Creating invoice with one adjustment by amount prorate included by total ==="); + + // Prepare data "from storage" + Invoice invoice = getMockAsJson(OPEN_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withId(randomUUID().toString()); + Adjustment invoiceAdjustment = new Adjustment() + .withId(UUID.randomUUID().toString()) + .withDescription("VAT") + .withProrate(BY_AMOUNT) + .withType(PERCENTAGE) + .withRelationToTotal(INCLUDED_IN) + .withValue(7d); + invoice.withAdjustments(Collections.singletonList(invoiceAdjustment)); + addMockEntry(INVOICES, invoice); + + // Prepare request body + InvoiceLine invoiceLineBody = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withQuantity(1); + + // Send create request + InvoiceLine invoiceLine = verifySuccessPost(INVOICE_LINES_PATH, invoiceLineBody).as(InvoiceLine.class); + + // Verification + assertThat(getInvoiceLineUpdates(), Matchers.hasSize(0)); + assertThat(getInvoiceUpdates(), Matchers.hasSize(1)); + compareRecordWithSentToStorage(invoiceLine); + + assertThat(invoiceLine.getAdjustments(), hasSize(1)); + assertThat(invoiceLine.getAdjustmentsTotal(), is(0d)); + assertThat(invoiceLine.getSubTotal(), is(28.04d)); + + Adjustment lineAdjustment = invoiceLine.getAdjustments().get(0); + verifyInvoiceLineAdjustmentCommon(invoiceAdjustment, lineAdjustment); + assertThat(lineAdjustment.getValue(), is(1.96d)); + } + + @Test + public void testDeleteInvoiceWithOnePercentageTypeByAmountProrateIncludedByTotalAdjustment() { + logger.info("=== Deleting invoice with one adjustment by amount prorate included by total ==="); + + // Prepare data "from storage" + Invoice invoice = getMockAsJson(OPEN_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withId(randomUUID().toString()); + Adjustment invoiceAdjustment = new Adjustment() + .withId(UUID.randomUUID().toString()) + .withDescription("VAT") + .withProrate(BY_AMOUNT) + .withType(PERCENTAGE) + .withRelationToTotal(INCLUDED_IN) + .withValue(7d); + invoice.withAdjustments(Collections.singletonList(invoiceAdjustment)); + addMockEntry(INVOICES, invoice); + + InvoiceLine line1 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withQuantity(1); + addMockEntry(INVOICE_LINES, line1); + InvoiceLine line2 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withQuantity(1); + addMockEntry(INVOICE_LINES, line2); + + // Send delete request + verifyDeleteResponse(String.format(INVOICE_LINE_ID_PATH, line2.getId()), "", 204); + + // Verification + assertThat(getInvoiceLineUpdates(), Matchers.hasSize(1)); + assertThat(getInvoiceUpdates(), Matchers.hasSize(1)); + + InvoiceLine lineToStorage = getLineToStorageById(line1.getId()); + assertThat(lineToStorage.getAdjustments(), hasSize(1)); + + assertThat(lineToStorage.getAdjustments(), hasSize(1)); + assertThat(lineToStorage.getAdjustmentsTotal(), is(0d)); + assertThat(lineToStorage.getSubTotal(), is(28.04)); + + Adjustment lineAdjustment = lineToStorage.getAdjustments().get(0); + verifyInvoiceLineAdjustmentCommon(invoiceAdjustment, lineAdjustment); + assertThat(lineAdjustment.getValue(), is(1.96d)); + } + private InvoiceLine getLineToStorageById(String invoiceLineId) { return getInvoiceLineUpdates().stream() .filter(line -> invoiceLineId.equals(line.getString("id"))) diff --git a/src/test/java/org/folio/rest/impl/InvoicesApiTest.java b/src/test/java/org/folio/rest/impl/InvoicesApiTest.java index a19f03c2c..f008c9aad 100644 --- a/src/test/java/org/folio/rest/impl/InvoicesApiTest.java +++ b/src/test/java/org/folio/rest/impl/InvoicesApiTest.java @@ -2279,22 +2279,6 @@ void testUpdateInvoiceTransitionToPaidPoLineIdNotSpecified() { assertThatVoucherPaid(); } - @Test - void testShouldFailInvoiceLineDoesNotHaveFund() { - logger.info("=== Test should fail because invoice line doesn't have fund ==="); - - Invoice reqData = getMockAsJson(APPROVED_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withStatus(Invoice.Status.PAID); - String id = reqData.getId(); - - prepareMockVoucher(id); - InvoiceLine invoiceLine = getMinimalContentInvoiceLine(id); - addMockEntry(INVOICE_LINES, JsonObject.mapFrom(invoiceLine)); - - String jsonBody = JsonObject.mapFrom(reqData).encode(); - Headers headers = prepareHeaders(X_OKAPI_URL, X_OKAPI_TENANT, X_OKAPI_TOKEN, X_OKAPI_USER_ID); - verifyPut(String.format(INVOICE_ID_PATH, id), jsonBody, headers, "", 400); - } - @Test void testUpdateInvoiceTransitionToPaidNoVoucherUpdate() { logger.info("=== Test transition invoice to paid - voucher already paid ==="); @@ -2565,8 +2549,7 @@ void testUpdateInvoiceTransitionToPaidTransactionsAlreadyExists() { verifyPut(String.format(INVOICE_ID_PATH, id), jsonBody, headers, "", 204); assertThat(getRqRsEntries(HttpMethod.GET, FINANCE_TRANSACTIONS), hasSize(2)); - assertThat(getRqRsEntries(HttpMethod.GET, FUNDS), hasSize(2)); - assertThat(getRqRsEntries(HttpMethod.GET, CURRENT_FISCAL_YEAR), hasSize(1)); + assertThat(getRqRsEntries(HttpMethod.GET, FUNDS), hasSize(1)); assertThat(getRqRsEntries(HttpMethod.POST, FINANCE_BATCH_TRANSACTIONS), hasSize(0)); } @@ -3170,7 +3153,7 @@ private void checkCreditsPayments(Invoice invoice, List invoiceLine assertThat(getRqRsEntries(HttpMethod.GET, FINANCE_TRANSACTIONS), hasSize(2)); - assertThat(getRqRsEntries(HttpMethod.GET, FUNDS), hasSize(2)); + assertThat(getRqRsEntries(HttpMethod.GET, FUNDS), hasSize(1)); var transactions = getRqRsEntries(HttpMethod.POST, FINANCE_BATCH_TRANSACTIONS).stream() .map(entries -> entries.mapTo(Batch.class)) diff --git a/src/test/java/org/folio/rest/impl/InvoicesProratedAdjustmentsTest.java b/src/test/java/org/folio/rest/impl/InvoicesProratedAdjustmentsTest.java index 58a2a4bc9..534b81c3a 100644 --- a/src/test/java/org/folio/rest/impl/InvoicesProratedAdjustmentsTest.java +++ b/src/test/java/org/folio/rest/impl/InvoicesProratedAdjustmentsTest.java @@ -13,6 +13,7 @@ import static org.folio.rest.jaxrs.model.Adjustment.Prorate.BY_LINE; import static org.folio.rest.jaxrs.model.Adjustment.Prorate.BY_QUANTITY; import static org.folio.rest.jaxrs.model.Adjustment.Prorate.NOT_PRORATED; +import static org.folio.rest.jaxrs.model.Adjustment.RelationToTotal.INCLUDED_IN; import static org.folio.rest.jaxrs.model.Adjustment.Type.AMOUNT; import static org.folio.rest.jaxrs.model.Adjustment.Type.PERCENTAGE; import static org.hamcrest.MatcherAssert.assertThat; @@ -30,6 +31,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.UUID; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; @@ -1015,6 +1017,111 @@ public void testUpdateInvoiceWithThreeLinesAddingPercentageAdjustmentByLines() { }); } + @Test + public void testUpdateInvoiceWithOneLinePercentageTypeByAmountProrateIncludedByTotalAdjustment() { + logger.info("=== Updating invoice with one line adding 7% adjustment by amount prorate included by total ==="); + + // Prepare data "from storage" + Invoice invoice = getMockAsJson(OPEN_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withId(randomUUID().toString()); + invoice.getAdjustments().clear(); + addMockEntry(INVOICES, invoice); + + InvoiceLine invoiceLine = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withInvoiceLineNumber("n-1"); + addMockEntry(INVOICE_LINES, invoiceLine); + + // Prepare request body + Invoice invoiceBody = copyObject(invoice); + Adjustment adjustment = new Adjustment() + .withId(UUID.randomUUID().toString()) + .withDescription("VAT") + .withProrate(BY_AMOUNT) + .withType(PERCENTAGE) + .withRelationToTotal(INCLUDED_IN) + .withValue(7d); + invoiceBody.getAdjustments().add(adjustment); + + // Send update request + verifyPut(String.format(INVOICE_ID_PATH, invoice.getId()), invoiceBody, "", 204); + + // Verification + assertThat(getInvoiceUpdates(), hasSize(1)); + assertThat(getInvoiceLineUpdates(), hasSize(1)); + + Invoice invoiceToStorage = getInvoiceUpdates().get(0).mapTo(Invoice.class); + assertThat(invoiceToStorage.getAdjustments(), hasSize(1)); + assertThat(invoiceToStorage.getAdjustmentsTotal(), is(0.0)); + Adjustment invoiceAdjustment = invoiceToStorage.getAdjustments().get(0); + assertThat(invoiceAdjustment.getId(), not(is(emptyOrNullString()))); + + Stream.of(invoiceLine.getId()) + .forEach(id -> { + InvoiceLine lineToStorage = getLineToStorageById(id); + assertThat(lineToStorage.getAdjustments(), hasSize(1)); + assertThat(lineToStorage.getAdjustmentsTotal(), is(0d)); + assertThat(lineToStorage.getSubTotal(), is(28.04d)); + + Adjustment lineAdjustment = lineToStorage.getAdjustments().get(0); + verifyInvoiceLineAdjustmentCommon(invoiceAdjustment, lineAdjustment); + assertThat(lineAdjustment.getValue(), is(1.96d)); + }); + } + + @Test + public void testUpdateInvoiceWithThreeLinesPercentageTypeByAmountProrateIncludedByTotalAdjustment() { + logger.info("=== Updating invoice with three lines adding 7% adjustment by amount prorate included by total ==="); + + // Prepare data "from storage" + Invoice invoice = getMockAsJson(OPEN_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withId(randomUUID().toString()); + invoice.getAdjustments().clear(); + addMockEntry(INVOICES, invoice); + + InvoiceLine invoiceLine1 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withInvoiceLineNumber("n-1"); + addMockEntry(INVOICE_LINES, invoiceLine1); + + InvoiceLine invoiceLine2 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withInvoiceLineNumber("n-2"); + addMockEntry(INVOICE_LINES, invoiceLine2); + + InvoiceLine invoiceLine3 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withInvoiceLineNumber("n-3"); + addMockEntry(INVOICE_LINES, invoiceLine3); + + // Prepare request body + Invoice invoiceBody = copyObject(invoice); + Adjustment adjustment = new Adjustment() + .withId(UUID.randomUUID().toString()) + .withDescription("VAT") + .withProrate(BY_AMOUNT) + .withType(PERCENTAGE) + .withRelationToTotal(INCLUDED_IN) + .withValue(7d); + invoiceBody.getAdjustments().add(adjustment); + + // Send update request + verifyPut(String.format(INVOICE_ID_PATH, invoice.getId()), invoiceBody, "", 204); + + // Verification + assertThat(getInvoiceUpdates(), hasSize(1)); + assertThat(getInvoiceLineUpdates(), hasSize(3)); + + Invoice invoiceToStorage = getInvoiceUpdates().get(0).mapTo(Invoice.class); + assertThat(invoiceToStorage.getAdjustments(), hasSize(1)); + assertThat(invoiceToStorage.getAdjustmentsTotal(), is(0.0)); + Adjustment invoiceAdjustment = invoiceToStorage.getAdjustments().get(0); + assertThat(invoiceAdjustment.getId(), not(is(emptyOrNullString()))); + + Stream.of(invoiceLine1.getId(), invoiceLine2.getId(), invoiceLine3.getId()) + .forEach(id -> { + InvoiceLine lineToStorage = getLineToStorageById(id); + assertThat(lineToStorage.getAdjustments(), hasSize(1)); + assertThat(lineToStorage.getAdjustmentsTotal(), is(0d)); + assertThat(lineToStorage.getSubTotal(), is(28.04d)); + + Adjustment lineAdjustment = lineToStorage.getAdjustments().get(0); + verifyInvoiceLineAdjustmentCommon(invoiceAdjustment, lineAdjustment); + assertThat(lineAdjustment.getValue(), is(1.96d)); + }); + } + + private InvoiceLine getLineToStorageById(String invoiceLineId) { return getInvoiceLineUpdates().stream() .filter(line -> invoiceLineId.equals(line.getString("id"))) diff --git a/src/test/java/org/folio/utils/UserPermissionsUtilTest.java b/src/test/java/org/folio/utils/UserPermissionsUtilTest.java index b4253d8a8..e3a8afb8a 100644 --- a/src/test/java/org/folio/utils/UserPermissionsUtilTest.java +++ b/src/test/java/org/folio/utils/UserPermissionsUtilTest.java @@ -24,7 +24,7 @@ public class UserPermissionsUtilTest { @DisplayName("Should not throw exception when approve permission is in position") void shouldNotThrowExceptionWhenApprovePermissionIsInPosition() { List permissionsList = List.of( - "invoice.item.approve" + "invoice.item.approve.execute" ); String permissionsJsonArrayString = new JsonArray(permissionsList).encode(); @@ -44,7 +44,7 @@ void shouldThrowExceptionWhenApprovePermissionIsAbsent() { List permissionsList = Arrays.asList( "invoice.invoices.item.put", "invoices.acquisitions-units-assignments.manage", - "invoices.fiscal-year.update" + "invoices.fiscal-year.update.execute" ); String permissionsJsonArrayString = new JsonArray(permissionsList).encode(); @@ -63,7 +63,7 @@ void shouldThrowExceptionWhenApprovePermissionIsAbsent() { @DisplayName("Should not throw exception when pay permission is in position") void shouldNotThrowExceptionWhenPayPermissionIsInPosition() { List permissionsList = List.of( - "invoice.item.pay" + "invoice.item.pay.execute" ); String permissionsJsonArrayString = new JsonArray(permissionsList).encode(); @@ -83,7 +83,7 @@ void shouldThrowCorrectErrorCodeWhenPayPermissionIsAbsent() { List permissionsList = Arrays.asList( "invoice.invoices.item.put", "invoices.acquisitions-units-assignments.manage", - "invoices.fiscal-year.update" + "invoices.fiscal-year.update.execute" ); String permissionsJsonArrayString = new JsonArray(permissionsList).encode(); @@ -103,7 +103,7 @@ void shouldThrowCorrectErrorCodeWhenPayPermissionIsAbsent() { @DisplayName("Should not throw exception when cancel permission is in position") void shouldNotThrowExceptionWhenCancelPermissionIsInPosition() { List permissionsList = List.of( - "invoice.item.cancel" + "invoice.item.cancel.execute" ); String permissionsJsonArrayString = new JsonArray(permissionsList).encode(); @@ -121,8 +121,8 @@ void shouldNotThrowExceptionWhenCancelPermissionIsInPosition() { @DisplayName("Should throw correct error code when cancel permission absent") void shouldThrowCorrectErrorCodeWhenCancelPermissionIsAbsent() { List permissionsList = Arrays.asList( - "invoice.item.approve", - "invoice.item.pay" + "invoice.item.approve.execute", + "invoice.item.pay.execute" ); String permissionsJsonArrayString = new JsonArray(permissionsList).encode(); @@ -144,7 +144,7 @@ void shouldThrowCorrectErrorCodeWhenAssignPermissionIsAbsent() { List permissionsList = Arrays.asList( "invoice.invoices.item.put", "invoices.acquisitions-units-assignments.manage", - "invoices.fiscal-year.update" + "invoices.fiscal-year.update.execute" ); String permissionsJsonArrayString = new JsonArray(permissionsList).encode(); @@ -170,7 +170,7 @@ void shouldNotThrowExceptionWhenAssignPermissionIsAssigned() { List permissionsList = Arrays.asList( "invoice.invoices.item.put", "invoices.acquisitions-units-assignments.manage", - "invoices.fiscal-year.update", + "invoices.fiscal-year.update.execute", "invoices.acquisitions-units-assignments.assign" ); @@ -260,7 +260,7 @@ void shouldThrowExceptionWhenFiscalYearUpdatePermissionIsAbsent() { void shouldNotThrowExceptionWhenFiscalYearUpdatePermissionIsAssigned() { // Create a list of permissions List permissionsList = Arrays.asList( - "invoices.fiscal-year.update", + "invoices.fiscal-year.update.execute", "invoices.acquisitions-units-assignments.assign" );