From ca706627f6952ffc6215860b9e000209ba61c253 Mon Sep 17 00:00:00 2001 From: Abdulkhakimov <89521577+Abdulkhakimov@users.noreply.github.com> Date: Wed, 1 Nov 2023 18:08:16 +0500 Subject: [PATCH] [MODINVOICE-509] - Invoice cancellation allowed against closed budget (#444) --- .../InvoiceWorkflowDataHolderBuilder.java | 14 ++ .../folio/config/ServicesConfiguration.java | 5 +- .../org/folio/rest/impl/InvoiceHelper.java | 16 +- .../folio/rest/impl/InvoiceLineHelper.java | 22 +- .../invoice/InvoiceCancelService.java | 32 ++- .../invoice/InvoicePaymentService.java | 15 +- .../java/org/folio/TestMockDataConstants.java | 4 +- .../invoice/InvoiceCancelServiceTest.java | 203 +++++++++--------- .../resources/mockdata/budgets/budget.json | 23 ++ 9 files changed, 186 insertions(+), 148 deletions(-) create mode 100644 src/test/resources/mockdata/budgets/budget.json diff --git a/src/main/java/org/folio/InvoiceWorkflowDataHolderBuilder.java b/src/main/java/org/folio/InvoiceWorkflowDataHolderBuilder.java index ab8f14cc6..95e670f2e 100644 --- a/src/main/java/org/folio/InvoiceWorkflowDataHolderBuilder.java +++ b/src/main/java/org/folio/InvoiceWorkflowDataHolderBuilder.java @@ -72,6 +72,20 @@ public InvoiceWorkflowDataHolderBuilder(ExchangeRateProviderResolver exchangeRat this.expenseClassRetrieveService = expenseClassRetrieveService; } + public Future> buildCompleteHolders(Invoice invoice, + List invoiceLines, + RequestContext requestContext) { + List dataHolders = buildHoldersSkeleton(invoiceLines, invoice); + return withFunds(dataHolders, requestContext) + .compose(holders -> withLedgers(holders, requestContext)) + .compose(holders -> withBudgets(holders, requestContext)) + .map(this::checkMultipleFiscalYears) + .compose(holders -> withFiscalYear(holders, requestContext)) + .compose(holders -> withEncumbrances(holders, requestContext)) + .compose(holders -> withExpenseClasses(holders, requestContext)) + .compose(holders -> withExchangeRate(holders, requestContext)); + } + public List buildHoldersSkeleton(List lines, Invoice invoice) { List holders = lines.stream() .flatMap(invoiceLine -> invoiceLine.getFundDistributions().stream() diff --git a/src/main/java/org/folio/config/ServicesConfiguration.java b/src/main/java/org/folio/config/ServicesConfiguration.java index 331fca6fd..02f37d0ff 100644 --- a/src/main/java/org/folio/config/ServicesConfiguration.java +++ b/src/main/java/org/folio/config/ServicesConfiguration.java @@ -213,9 +213,10 @@ InvoiceCancelService invoiceCancelService(BaseTransactionService baseTransaction InvoiceTransactionSummaryService invoiceTransactionSummaryService, VoucherService voucherService, OrderLineService orderLineService, - OrderService orderService) { + OrderService orderService, + InvoiceWorkflowDataHolderBuilder invoiceWorkflowDataHolderBuilder) { return new InvoiceCancelService(baseTransactionService, encumbranceService, invoiceTransactionSummaryService, - voucherService, orderLineService, orderService); + voucherService, orderLineService, orderService, invoiceWorkflowDataHolderBuilder); } @Bean BatchVoucherService batchVoucherService(RestClient restClient) { diff --git a/src/main/java/org/folio/rest/impl/InvoiceHelper.java b/src/main/java/org/folio/rest/impl/InvoiceHelper.java index ff7d3d921..628f20194 100644 --- a/src/main/java/org/folio/rest/impl/InvoiceHelper.java +++ b/src/main/java/org/folio/rest/impl/InvoiceHelper.java @@ -325,7 +325,7 @@ public Future getFiscalYearsByInvoiceId(String invoiceId) private Future handleExchangeRateChange(Invoice invoice, List invoiceLines) { - return getInvoiceWorkflowDataHolders(invoice, invoiceLines, requestContext) + return holderBuilder.buildCompleteHolders(invoice, invoiceLines, requestContext) .compose(holders -> holderBuilder.withExistingTransactions(holders, requestContext)) .compose(holders -> pendingPaymentWorkflowService.handlePendingPaymentsUpdate(holders, requestContext)) .compose(aVoid -> updateVoucher(invoice, invoiceLines)); @@ -502,7 +502,7 @@ private Future approveInvoice(Invoice invoice, List lines) { validateBeforeApproval(organization, invoice, lines); return null; }) - .compose(v -> getInvoiceWorkflowDataHolders(invoice, lines, requestContext)) + .compose(v -> holderBuilder.buildCompleteHolders(invoice, lines, requestContext)) .compose(holders -> encumbranceService.updateInvoiceLinesEncumbranceLinks(holders, holders.get(0).getFiscalYear().getId(), requestContext) .compose(linesToUpdate -> invoiceLineService.persistInvoiceLines(linesToUpdate, requestContext)) @@ -529,18 +529,6 @@ private void validateBeforeApproval(Organization organization, Invoice invoice, validator.validateBeforeApproval(invoice, lines); } - private Future> getInvoiceWorkflowDataHolders(Invoice invoice, List lines, RequestContext requestContext) { - List dataHolders = holderBuilder.buildHoldersSkeleton(lines, invoice); - return holderBuilder.withFunds(dataHolders, requestContext) - .compose(holders -> holderBuilder.withLedgers(holders, requestContext)) - .compose(holders -> holderBuilder.withBudgets(holders, requestContext)) - .map(holderBuilder::checkMultipleFiscalYears) - .compose(holders -> holderBuilder.withFiscalYear(holders, requestContext)) - .compose(holders -> holderBuilder.withEncumbrances(holders, requestContext)) - .compose(holders -> holderBuilder.withExpenseClasses(holders, requestContext)) - .compose(holders -> holderBuilder.withExchangeRate(holders, requestContext)); - } - private Future updateVoucherWithSystemCurrency(Voucher voucher, List lines) { if (!CollectionUtils.isEmpty(lines) && !CollectionUtils.isEmpty(lines.get(0).getFundDistributions())) { String fundId = lines.get(0).getFundDistributions().get(0).getFundId(); diff --git a/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java b/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java index 3417fdff1..db0c2ee33 100644 --- a/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java +++ b/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java @@ -205,7 +205,8 @@ public Future updateInvoiceLine(InvoiceLine invoiceLine, RequestContext re ilProcessing.setInvoice(invoice); return null; }) - .compose(invoice -> getInvoiceWorkflowDataHolders(ilProcessing, requestContext)) + .compose(invoice -> holderBuilder.buildCompleteHolders(ilProcessing.getInvoice(), + Collections.singletonList(ilProcessing.getInvoiceLine()), requestContext)) .compose(holders -> budgetExpenseClassService.checkExpenseClasses(holders, requestContext)) .map(holders -> updateInvoiceFiscalYear(holders, ilProcessing)) .map(holders -> { @@ -353,7 +354,8 @@ public Future createInvoiceLine(InvoiceLine invoiceLine) { }) .compose(v -> protectionHelper.isOperationRestricted(ilProcessing.getInvoice().getAcqUnitIds(), ProtectedOperationType.CREATE)) - .compose(v -> getInvoiceWorkflowDataHolders(ilProcessing, requestContext) + .compose(invoice -> holderBuilder.buildCompleteHolders(ilProcessing.getInvoice(), + Collections.singletonList(ilProcessing.getInvoiceLine()), requestContext) .compose(holders -> budgetExpenseClassService.checkExpenseClasses(holders, requestContext)) .compose(holders -> generateNewInvoiceLineNumber(holders, ilProcessing, requestContext)) .map(holders -> updateInvoiceFiscalYear(holders, ilProcessing)) @@ -619,22 +621,6 @@ private List addPoNumberToList(List numbers, String newNumber) { return newNumbers; } - private Future> getInvoiceWorkflowDataHolders(ILProcessing ilProcessing, - RequestContext requestContext) { - List lines = new ArrayList<>(); - lines.add(ilProcessing.getInvoiceLine()); - - List dataHolders = holderBuilder.buildHoldersSkeleton(lines, ilProcessing.getInvoice()); - return holderBuilder.withFunds(dataHolders, requestContext) - .compose(holders -> holderBuilder.withLedgers(holders, requestContext)) - .compose(holders -> holderBuilder.withBudgets(holders, requestContext)) - .map(holderBuilder::checkMultipleFiscalYears) - .compose(holders -> holderBuilder.withFiscalYear(holders, requestContext)) - .compose(holders -> holderBuilder.withEncumbrances(holders, requestContext)) - .compose(holders -> holderBuilder.withExpenseClasses(holders, requestContext)) - .compose(holders -> holderBuilder.withExchangeRate(holders, requestContext)); - } - private List updateInvoiceFiscalYear(List holders, ILProcessing ilProcessing) { diff --git a/src/main/java/org/folio/services/invoice/InvoiceCancelService.java b/src/main/java/org/folio/services/invoice/InvoiceCancelService.java index 35486f6a6..a4d7ee87d 100644 --- a/src/main/java/org/folio/services/invoice/InvoiceCancelService.java +++ b/src/main/java/org/folio/services/invoice/InvoiceCancelService.java @@ -7,6 +7,7 @@ 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; +import static org.folio.invoices.utils.HelperUtils.INVOICE_ID; import static org.folio.invoices.utils.HelperUtils.collectResultsOnSuccess; import static org.folio.invoices.utils.HelperUtils.convertIdsToCqlQuery; import static org.folio.rest.RestConstants.MAX_IDS_FOR_GET_RQ; @@ -22,7 +23,9 @@ import one.util.streamex.StreamEx; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.folio.InvoiceWorkflowDataHolderBuilder; import org.folio.invoices.rest.exceptions.HttpException; +import org.folio.models.InvoiceWorkflowDataHolder; import org.folio.rest.acq.model.finance.InvoiceTransactionSummary; import org.folio.rest.acq.model.finance.Transaction; import org.folio.rest.acq.model.finance.Transaction.TransactionType; @@ -56,19 +59,22 @@ public class InvoiceCancelService { private final VoucherService voucherService; private final OrderLineService orderLineService; private final OrderService orderService; + private final InvoiceWorkflowDataHolderBuilder holderBuilder; public InvoiceCancelService(BaseTransactionService baseTransactionService, EncumbranceService encumbranceService, InvoiceTransactionSummaryService invoiceTransactionSummaryService, VoucherService voucherService, OrderLineService orderLineService, - OrderService orderService) { + OrderService orderService, + InvoiceWorkflowDataHolderBuilder holderBuilder) { this.baseTransactionService = baseTransactionService; this.encumbranceService = encumbranceService; this.invoiceTransactionSummaryService = invoiceTransactionSummaryService; this.voucherService = voucherService; this.orderLineService = orderLineService; this.orderService = orderService; + this.holderBuilder = holderBuilder; } /** @@ -90,6 +96,7 @@ public Future cancelInvoice(Invoice invoiceFromStorage, List validateCancelInvoice(invoiceFromStorage); return null; }) + .compose(v -> validateBudgetsStatus(invoiceFromStorage, lines, requestContext)) .compose(v -> getTransactions(invoiceId, requestContext)) .compose(transactions -> cancelTransactions(invoiceId, transactions, requestContext)) .map(v -> { @@ -104,13 +111,32 @@ private void validateCancelInvoice(Invoice invoiceFromStorage) { List cancellable = List.of(Invoice.Status.APPROVED, Invoice.Status.PAID); if (!cancellable.contains(invoiceFromStorage.getStatus())) { List parameters = Collections.singletonList( - new Parameter().withKey("invoiceId").withValue(invoiceFromStorage.getId())); + new Parameter().withKey(INVOICE_ID).withValue(invoiceFromStorage.getId())); Error error = CANNOT_CANCEL_INVOICE.toError() .withParameters(parameters); throw new HttpException(422, error); } } + /** + * Performs validation of budget statuses associated with an invoice. + * Associated budgets should have {@link org.folio.rest.acq.model.finance.Budget.BudgetStatus#ACTIVE} status + * to pass validation successfully. + * + * @param invoice The invoice.This parameter is necessary to extract the associated budgets. + * @param lines The list of invoice lines. This parameter is necessary to extract the associated budgets. + * @param requestContext The request context providing additional information. + * @return A `Future` of type `Void`, representing the result of the validation. If the future succeeds, + * it indicates that the validation has been successfully completed, and active budgets have been extracted + * @throws HttpException If no active budgets are found, an exception is thrown. + */ + private Future validateBudgetsStatus(Invoice invoice, List lines, RequestContext requestContext) { + List 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)) + .mapEmpty(); + } + private Future> getTransactions(String invoiceId, RequestContext requestContext) { String query = String.format("sourceInvoiceId==%s", invoiceId); List relevantTransactionTypes = List.of(PENDING_PAYMENT, PAYMENT, CREDIT); @@ -131,7 +157,7 @@ private Future cancelTransactions(String invoiceId, List tran .recover(t -> { logger.error("Failed to cancel transactions for invoice with id {}", invoiceId, t); List parameters = Collections.singletonList( - new Parameter().withKey("invoiceId").withValue(invoiceId)); + new Parameter().withKey(INVOICE_ID).withValue(invoiceId)); throw new HttpException(500, CANCEL_TRANSACTIONS_ERROR.toError().withParameters(parameters)); }); } diff --git a/src/main/java/org/folio/services/invoice/InvoicePaymentService.java b/src/main/java/org/folio/services/invoice/InvoicePaymentService.java index 21c263995..98754ea70 100644 --- a/src/main/java/org/folio/services/invoice/InvoicePaymentService.java +++ b/src/main/java/org/folio/services/invoice/InvoicePaymentService.java @@ -13,7 +13,6 @@ import org.apache.commons.lang3.StringUtils; import org.folio.InvoiceWorkflowDataHolderBuilder; import org.folio.invoices.rest.exceptions.HttpException; -import org.folio.models.InvoiceWorkflowDataHolder; import org.folio.rest.acq.model.orders.CompositePoLine; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.FundDistribution; @@ -55,7 +54,7 @@ public class InvoicePaymentService { public Future payInvoice(Invoice invoice, List invoiceLines, RequestContext requestContext) { // Set payment date, when the invoice is being paid. invoice.setPaymentDate(invoice.getMetadata().getUpdatedDate()); - return getInvoiceWorkflowDataHolders(invoice, invoiceLines, requestContext) + 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))) .mapEmpty(); @@ -85,18 +84,6 @@ private Future updatePoLinesStatus(Invoice invoice, List invo return Future.failedFuture(new HttpException(400, INVOICE_LINE_MUST_HAVE_FUND)); } - private Future> getInvoiceWorkflowDataHolders(Invoice invoice, List lines, RequestContext requestContext) { - List dataHolders = holderBuilder.buildHoldersSkeleton(lines, invoice); - return holderBuilder.withFunds(dataHolders, requestContext) - .compose(holders -> holderBuilder.withLedgers(holders, requestContext)) - .compose(holders -> holderBuilder.withBudgets(holders, requestContext)) - .map(holderBuilder::checkMultipleFiscalYears) - .compose(holders -> holderBuilder.withFiscalYear(holders, requestContext)) - .compose(holders -> holderBuilder.withEncumbrances(holders, requestContext)) - .compose(holders -> holderBuilder.withExpenseClasses(holders, requestContext)) - .compose(holders -> holderBuilder.withExchangeRate(holders, requestContext)); - } - /** * Updates payment status of the associated PO Lines. * diff --git a/src/test/java/org/folio/TestMockDataConstants.java b/src/test/java/org/folio/TestMockDataConstants.java index 53f027111..b4673ff94 100644 --- a/src/test/java/org/folio/TestMockDataConstants.java +++ b/src/test/java/org/folio/TestMockDataConstants.java @@ -4,9 +4,11 @@ public final class TestMockDataConstants { private TestMockDataConstants() { } public static final String BASE_MOCK_DATA_PATH = "mockdata/"; + public static final String BASE_MOCK_BUDGETS_BASE_PATH = BASE_MOCK_DATA_PATH + "budgets/"; public static final String BASE_MOCK_TRANSACTIONS_BASE_PATH = BASE_MOCK_DATA_PATH + "transactions/"; - public static final String MOCK_TRANSACTIONS_LIST = BASE_MOCK_TRANSACTIONS_BASE_PATH + "transactions.json"; public static final String MOCK_CREDITS_LIST = BASE_MOCK_TRANSACTIONS_BASE_PATH + "credits.json"; + public static final String MOCK_BUDGET_ITEM = BASE_MOCK_BUDGETS_BASE_PATH + "budget.json"; + public static final String MOCK_BUDGETS_LIST = BASE_MOCK_BUDGETS_BASE_PATH + "budgets.json"; public static final String MOCK_ENCUMBRANCES_LIST = BASE_MOCK_TRANSACTIONS_BASE_PATH + "encumbrances.json"; public static final String MOCK_PAYMENTS_LIST = BASE_MOCK_TRANSACTIONS_BASE_PATH + "payments.json"; public static final String MOCK_PENDING_PAYMENTS_LIST = BASE_MOCK_TRANSACTIONS_BASE_PATH + "pending-payments.json"; diff --git a/src/test/java/org/folio/services/invoice/InvoiceCancelServiceTest.java b/src/test/java/org/folio/services/invoice/InvoiceCancelServiceTest.java index decb0ffc1..30362a071 100644 --- a/src/test/java/org/folio/services/invoice/InvoiceCancelServiceTest.java +++ b/src/test/java/org/folio/services/invoice/InvoiceCancelServiceTest.java @@ -3,22 +3,22 @@ import static io.vertx.core.Future.failedFuture; import static io.vertx.core.Future.succeededFuture; import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; import static org.folio.ApiTestSuite.mockPort; -import static org.folio.TestMockDataConstants.INVOICE_LINES_LIST_PATH; import static org.folio.TestMockDataConstants.INVOICE_MOCK_DATA_PATH; -import static org.folio.TestMockDataConstants.MOCK_CREDITS_LIST; +import static org.folio.TestMockDataConstants.VOUCHER_MOCK_DATA_PATH; +import static org.folio.TestMockDataConstants.INVOICE_LINES_LIST_PATH; import static org.folio.TestMockDataConstants.MOCK_ENCUMBRANCES_LIST; -import static org.folio.TestMockDataConstants.MOCK_PAYMENTS_LIST; import static org.folio.TestMockDataConstants.MOCK_PENDING_PAYMENTS_LIST; -import static org.folio.TestMockDataConstants.VOUCHER_MOCK_DATA_PATH; +import static org.folio.TestMockDataConstants.MOCK_CREDITS_LIST; +import static org.folio.TestMockDataConstants.MOCK_PAYMENTS_LIST; +import static org.folio.TestMockDataConstants.MOCK_BUDGET_ITEM; +import static org.folio.TestMockDataConstants.MOCK_BUDGETS_LIST; +import static org.folio.invoices.utils.ErrorCodes.BUDGET_NOT_FOUND; import static org.folio.invoices.utils.ErrorCodes.CANNOT_CANCEL_INVOICE; import static org.folio.invoices.utils.ErrorCodes.ERROR_UNRELEASING_ENCUMBRANCES; +import static org.folio.invoices.utils.ErrorCodes.BUDGET_NOT_FOUND_USING_FISCAL_YEAR_ID; import static org.folio.invoices.utils.ResourcePathResolver.FINANCE_TRANSACTIONS; import static org.folio.invoices.utils.ResourcePathResolver.INVOICE_TRANSACTION_SUMMARIES; -import static org.folio.invoices.utils.ResourcePathResolver.ORDER_TRANSACTION_SUMMARIES; -import static org.folio.invoices.utils.ResourcePathResolver.VOUCHERS_STORAGE; import static org.folio.invoices.utils.ResourcePathResolver.resourcesPath; import static org.folio.rest.RestConstants.OKAPI_URL; import static org.folio.rest.acq.model.finance.Encumbrance.Status.PENDING; @@ -28,13 +28,12 @@ import static org.folio.rest.impl.ApiTestBase.X_OKAPI_USER_ID; import static org.folio.services.finance.transaction.BaseTransactionServiceTest.X_OKAPI_TENANT; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; import java.io.IOException; -import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; @@ -42,13 +41,16 @@ import java.util.Map; import java.util.UUID; +import org.folio.InvoiceWorkflowDataHolderBuilder; import org.folio.invoices.rest.exceptions.HttpException; +import org.folio.rest.acq.model.finance.Budget; +import org.folio.rest.acq.model.finance.Transaction; import org.folio.rest.acq.model.finance.Encumbrance; -import org.folio.rest.acq.model.finance.InvoiceTransactionSummary; +import org.folio.rest.acq.model.finance.BudgetCollection; +import org.folio.rest.acq.model.finance.TransactionCollection; import org.folio.rest.acq.model.finance.OrderTransactionSummary; -import org.folio.rest.acq.model.finance.Transaction; +import org.folio.rest.acq.model.finance.InvoiceTransactionSummary; import org.folio.rest.acq.model.finance.Transaction.TransactionType; -import org.folio.rest.acq.model.finance.TransactionCollection; import org.folio.rest.acq.model.orders.PoLine; import org.folio.rest.acq.model.orders.PoLineCollection; import org.folio.rest.acq.model.orders.PurchaseOrder; @@ -62,6 +64,12 @@ import org.folio.rest.jaxrs.model.InvoiceLineCollection; import org.folio.rest.jaxrs.model.Voucher; import org.folio.rest.jaxrs.model.VoucherCollection; +import org.folio.services.exchange.ExchangeRateProviderResolver; +import org.folio.services.finance.FundService; +import org.folio.services.finance.LedgerService; +import org.folio.services.finance.budget.BudgetService; +import org.folio.services.finance.expence.ExpenseClassRetrieveService; +import org.folio.services.finance.fiscalyear.FiscalYearService; import org.folio.services.finance.transaction.BaseTransactionService; import org.folio.services.finance.transaction.EncumbranceService; import org.folio.services.finance.transaction.InvoiceTransactionSummaryService; @@ -75,7 +83,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import io.vertx.core.Future; @@ -95,11 +102,8 @@ public class InvoiceCancelServiceTest { private static final String OPENED_INVOICE_SAMPLE_PATH = INVOICE_MOCK_DATA_PATH + OPENED_INVOICE_ID + ".json"; private static final String TRANSACTIONS_ENDPOINT = resourcesPath(FINANCE_TRANSACTIONS); private static final String INVOICE_TRANSACTION_SUMMARIES_BY_ID_ENDPOINT = resourcesPath(INVOICE_TRANSACTION_SUMMARIES) + "/{id}"; - private static final String VOUCHER_ENDPOINT = resourcesPath(VOUCHERS_STORAGE); private static final String EXISTING_VOUCHER_ID = "a9b99f8a-7100-47f2-9903-6293d44a9905"; private static final String VOUCHER_SAMPLE_PATH = VOUCHER_MOCK_DATA_PATH + EXISTING_VOUCHER_ID + ".json"; - private static final String VOUCHER_BY_ID_ENDPOINT = resourcesPath(VOUCHERS_STORAGE) + "/{id}"; - private static final String ORDER_TRANSACTION_SUMMARIES_BY_ID_ENDPOINT = resourcesPath(ORDER_TRANSACTION_SUMMARIES) + "/{id}"; private InvoiceCancelService cancelService; @@ -126,9 +130,19 @@ public void initMocks() { OrderLineService orderLineService = new OrderLineService(restClient); InvoiceLineService invoiceLineService = new InvoiceLineService(restClient); OrderService orderService = new OrderService(restClient, invoiceLineService, orderLineService); + + ExchangeRateProviderResolver exchangeRateProviderResolver = new ExchangeRateProviderResolver(); + FiscalYearService fiscalYearService = new FiscalYearService(restClient); + FundService fundService = new FundService(restClient); + LedgerService ledgerService = new LedgerService(restClient); + BudgetService budgetService = new BudgetService(restClient); + ExpenseClassRetrieveService expenseClassRetrieveService = new ExpenseClassRetrieveService(restClient); + InvoiceWorkflowDataHolderBuilder holderBuilder = new InvoiceWorkflowDataHolderBuilder(exchangeRateProviderResolver, + fiscalYearService, fundService, ledgerService, baseTransactionService, budgetService, expenseClassRetrieveService); + cancelService = new InvoiceCancelService(baseTransactionService, encumbranceService, invoiceTransactionSummaryService, voucherService, orderLineService, - orderService); + orderService, holderBuilder); } @Test @@ -136,15 +150,12 @@ public void cancelApprovedInvoiceTest(VertxTestContext vertxTestContext) throws Invoice invoice = getMockAs(APPROVED_INVOICE_SAMPLE_PATH, Invoice.class); List invoiceLines = getMockAs(INVOICE_LINES_LIST_PATH, InvoiceLineCollection.class).getInvoiceLines(); - setupRestCalls(invoice, false); + setupRestCalls(invoice); Future future = cancelService.cancelInvoice(invoice, invoiceLines, requestContext); vertxTestContext.assertComplete(future) - .onSuccess(result -> { - vertxTestContext.completeNow(); - }) + .onSuccess(result -> vertxTestContext.completeNow()) .onFailure(vertxTestContext::failNow); - } @Test @@ -152,13 +163,11 @@ public void cancelPaidInvoiceTest(VertxTestContext vertxTestContext) throws IOEx Invoice invoice = getMockAs(PAID_INVOICE_SAMPLE_PATH, Invoice.class); List invoiceLines = getMockAs(INVOICE_LINES_LIST_PATH, InvoiceLineCollection.class).getInvoiceLines(); - setupRestCalls(invoice, false); + setupRestCalls(invoice); Future future = cancelService.cancelInvoice(invoice, invoiceLines, requestContext); vertxTestContext.assertComplete(future) - .onSuccess(result -> { - vertxTestContext.completeNow(); - }) + .onSuccess(result -> vertxTestContext.completeNow()) .onFailure(vertxTestContext::failNow); } @@ -175,7 +184,41 @@ public void validateCancelInvoiceTest(VertxTestContext vertxTestContext) throws assertEquals(CANNOT_CANCEL_INVOICE.getDescription(), exception.getMessage()); vertxTestContext.completeNow(); }); + } + + @Test + public void validateBudgetWhenCancelInvoiceAndDefinedFiscalYearTest(VertxTestContext vertxTestContext) throws IOException { + Invoice invoice = getMockAs(PAID_INVOICE_SAMPLE_PATH, Invoice.class); + invoice.withFiscalYearId(UUID.randomUUID().toString()); + List invoiceLines = getMockAs(INVOICE_LINES_LIST_PATH, InvoiceLineCollection.class).getInvoiceLines(); + + setupRestCalls(invoice, true, false); + + Future future = cancelService.cancelInvoice(invoice, invoiceLines, requestContext); + vertxTestContext.assertFailure(future) + .onComplete(result -> { + var exception = (HttpException) result.cause(); + assertEquals(404, exception.getCode()); + assertEquals(BUDGET_NOT_FOUND_USING_FISCAL_YEAR_ID.getDescription(), exception.getMessage()); + vertxTestContext.completeNow(); + }); + } + + @Test + public void validateBudgetWhenCancelInvoiceAndUndefinedFiscalYearTest(VertxTestContext vertxTestContext) throws IOException { + Invoice invoice = getMockAs(PAID_INVOICE_SAMPLE_PATH, Invoice.class); + List invoiceLines = getMockAs(INVOICE_LINES_LIST_PATH, InvoiceLineCollection.class).getInvoiceLines(); + + setupRestCalls(invoice, true, false); + Future future = cancelService.cancelInvoice(invoice, invoiceLines, requestContext); + vertxTestContext.assertFailure(future) + .onComplete(result -> { + var exception = (HttpException) result.cause(); + assertEquals(404, exception.getCode()); + assertEquals(BUDGET_NOT_FOUND.getDescription(), exception.getMessage()); + vertxTestContext.completeNow(); + }); } @Test @@ -183,7 +226,7 @@ public void errorUnreleasingEncumbrances(VertxTestContext vertxTestContext) thro Invoice invoice = getMockAs(APPROVED_INVOICE_SAMPLE_PATH, Invoice.class); List invoiceLines = getMockAs(INVOICE_LINES_LIST_PATH, InvoiceLineCollection.class).getInvoiceLines(); - setupRestCalls(invoice, true); + setupRestCalls(invoice, false, true); Future future = cancelService.cancelInvoice(invoice, invoiceLines, requestContext); vertxTestContext.assertFailure(future) @@ -193,15 +236,19 @@ public void errorUnreleasingEncumbrances(VertxTestContext vertxTestContext) thro assertEquals(ERROR_UNRELEASING_ENCUMBRANCES.getDescription(), httpException.getMessage()); vertxTestContext.completeNow(); }); + } + private void setupRestCalls(Invoice invoice) throws IOException { + setupRestCalls(invoice, false, false); } - private void setupRestCalls(Invoice invoice, boolean withError) throws IOException { + private void setupRestCalls(Invoice invoice, boolean withEmptyBudgets, boolean withEncumbranceError) throws IOException { setupGetTransactions(invoice); setupUpdateTransactionSummary(invoice); - setupUpdateTransactions(); - setupUpdateVoucher(invoice); - setupUnreleaseEncumbrances(withError); + setupGetBudget(withEmptyBudgets); + setupGetBudgets(withEmptyBudgets); + setupUpdateVoucher(); + setupUnreleaseEncumbrances(withEncumbranceError); } private void setupGetTransactions(Invoice invoice) throws IOException { @@ -219,8 +266,31 @@ private void setupGetTransactions(Invoice invoice) throws IOException { doReturn(succeededFuture(trCollection)).when(restClient).get(requestEntry, TransactionCollection.class, requestContext); } - private boolean sameRequestEntry(RequestEntry entry1, RequestEntry entry2) { - return entry1.buildEndpoint().equals(entry2.buildEndpoint()); + private void setupGetBudget(boolean withEmptyBudget) { + when(restClient.get(any(RequestEntry.class), eq(Budget.class), eq(requestContext))) + .thenAnswer((Answer>) invocation -> { + if (withEmptyBudget) { + return failedFuture(new HttpException(404, "Current budget doesn't exist")); + } else { + Budget budget = getMockAs(MOCK_BUDGET_ITEM, Budget.class); + return succeededFuture(budget.withFundId(UUID.randomUUID().toString())); + } + }); + } + + private void setupGetBudgets(boolean withEmptyBudgets) { + when(restClient.get(any(RequestEntry.class), eq(BudgetCollection.class), eq(requestContext))) + .thenAnswer((Answer>) invocation -> { + BudgetCollection budgetCollection; + if (withEmptyBudgets) { + budgetCollection = new BudgetCollection(); + } else { + budgetCollection = getMockAs(MOCK_BUDGETS_LIST, BudgetCollection.class); + budgetCollection.getBudgets().forEach(budget -> + budget.setFundId(UUID.randomUUID().toString())); + } + return succeededFuture(budgetCollection); + }); } private TransactionCollection mergeCollections(List collections) { @@ -243,26 +313,14 @@ private void setupUpdateTransactionSummary(Invoice invoice) { doReturn(succeededFuture(null)).when(restClient).put(requestEntry, summary, requestContext); } - private void setupUpdateTransactions() { - List matchedTypes = List.of(TransactionType.PAYMENT, TransactionType.PENDING_PAYMENT, - TransactionType.CREDIT); - // doReturn(succeededFuture(null)).when(restClient).put(any(RequestEntry.class), any(Transaction.class), eq(requestContext)); - } - - private void setupUpdateVoucher(Invoice invoice) throws IOException { + private void setupUpdateVoucher() throws IOException { // GET Voucher Voucher voucher = getMockAs(VOUCHER_SAMPLE_PATH, Voucher.class); VoucherCollection voucherCollection = new VoucherCollection() .withVouchers(singletonList(voucher)) .withTotalRecords(1); - RequestEntry getRequestEntry = new RequestEntry(VOUCHER_ENDPOINT) - .withQuery(String.format("invoiceId==%s", invoice.getId())) - .withLimit(1) - .withOffset(0); doReturn(succeededFuture(voucherCollection)).when(restClient).get(any(RequestEntry.class), eq(VoucherCollection.class), eq(requestContext)); // PUT Voucher - RequestEntry putRequestEntry = new RequestEntry(VOUCHER_BY_ID_ENDPOINT) - .withId(voucher.getId()); doReturn(succeededFuture(null)).when(restClient).put(any(RequestEntry.class), any(Voucher.class), eq(requestContext)); } @@ -291,7 +349,7 @@ private void setupUnreleaseEncumbrances(boolean withError) { ); setupPoLineQuery(poLines); setupOrderQuery(orders); - setupEncumbranceQuery(orders, poLines, transactions); + setupEncumbranceQuery(transactions); setupUpdateOrderTransactionSummary(orders.get(1)); if (withError) setupEncumbrancePutWithError(transactions.get(1)); @@ -300,70 +358,37 @@ private void setupUnreleaseEncumbrances(boolean withError) { } private void setupPoLineQuery(List poLines) { - String poLineIdsQuery = poLines.stream() - .map(PoLine::getId) - .collect(joining(" or ")); - String poLineQuery = "paymentStatus==(\"Awaiting Payment\" OR \"Partially Paid\" OR \"Fully Paid\" OR \"Ongoing\") AND id==(" + - poLineIdsQuery + ")"; PoLineCollection poLineCollection = new PoLineCollection() .withPoLines(poLines) .withTotalRecords(poLines.size()); - RequestEntry requestEntry = new RequestEntry("/orders/order-lines") - .withQuery(poLineQuery) - .withOffset(0) - .withLimit(Integer.MAX_VALUE); doReturn(succeededFuture(poLineCollection)).when(restClient).get(any(RequestEntry.class), eq(PoLineCollection.class), eq( requestContext)); } private void setupOrderQuery(List orders) { - List orderIds = orders.stream().map(PurchaseOrder::getId).collect(toList()); List openOrders = List.of(orders.get(1)); - String orderIdsQuery = String.join(" or ", orderIds); - String orderQuery = "workflowStatus==\"Open\" AND id==(" + orderIdsQuery + ")"; PurchaseOrderCollection orderCollection = new PurchaseOrderCollection() .withPurchaseOrders(openOrders) .withTotalRecords(openOrders.size()); - RequestEntry requestEntry = new RequestEntry("/orders/composite-orders") - .withQuery(orderQuery) - .withOffset(0) - .withLimit(Integer.MAX_VALUE); doReturn(succeededFuture(orderCollection)).when(restClient).get(any(RequestEntry.class), eq(PurchaseOrderCollection.class), eq( requestContext)); } - private void setupEncumbranceQuery(List orders, List poLines, - List transactions) { - List selectedPoLines = poLines.stream() - .filter(line -> orders.stream().anyMatch(order -> order.getId().equals(line.getPurchaseOrderId()) && - order.getWorkflowStatus().equals(WorkflowStatus.OPEN))) - .collect(toList()); - String poLineIdsQuery = selectedPoLines.stream() - .map(PoLine::getId) - .collect(joining(" or ")); - String transactionQuery = "transactionType==Encumbrance and encumbrance.sourcePoLineId==(" + poLineIdsQuery + ")"; + private void setupEncumbranceQuery(List transactions) { TransactionCollection transactionCollection = new TransactionCollection() .withTransactions(transactions) .withTotalRecords(transactions.size()); - RequestEntry requestEntry = new RequestEntry(TRANSACTIONS_ENDPOINT) - .withQuery(transactionQuery) - .withOffset(0) - .withLimit(Integer.MAX_VALUE); doReturn(succeededFuture(transactionCollection)).when(restClient).get(any(RequestEntry.class), eq(TransactionCollection.class), eq( requestContext)); } private void setupUpdateOrderTransactionSummary(PurchaseOrder order) { - RequestEntry requestEntry = new RequestEntry(ORDER_TRANSACTION_SUMMARIES_BY_ID_ENDPOINT) - .withPathParameter("id", order.getId()); OrderTransactionSummary summary = new OrderTransactionSummary().withId(order.getId()) .withNumTransactions(1); doReturn(succeededFuture(null)).when(restClient).put(any(RequestEntry.class), eq(summary), eq(requestContext)); } private void setupEncumbrancePut(Transaction transaction) { - RequestEntry requestEntry = new RequestEntry("/finance/encumbrances/{id}") - .withPathParameter("id", transaction.getId()); Transaction updatedTransaction = JsonObject.mapFrom(transaction).mapTo(Transaction.class); updatedTransaction.getEncumbrance().setStatus(UNRELEASED); doReturn(succeededFuture(null)).when(restClient).put(any(RequestEntry.class), eq(updatedTransaction), any(RequestContext.class)); @@ -378,22 +403,8 @@ private void setupEncumbrancePutWithError(Transaction transaction) { doReturn(failedFuture(ex)).when(restClient).put(requestEntry, updatedTransaction, requestContext); } - private void checkInvoiceLines(List invoiceLines) { - assertTrue(invoiceLines.stream().allMatch(line -> line.getInvoiceLineStatus() == InvoiceLine.InvoiceLineStatus.CANCELLED)); - } - private T getMockAs(String path, Class c) throws IOException { String contents = new String(Files.readAllBytes(Paths.get(RESOURCES_PATH, path))); return new JsonObject(contents).mapTo(c); } - - private static class RuntimeExceptionAnswer implements Answer { - // This is useful to check a null result with a given stub. - // NOTE: doReturn() must be used with this. - public Object answer(InvocationOnMock invocation) { - Method method = invocation.getMethod(); - throw new RuntimeException(method.getDeclaringClass().getName() + "." + method.getName() + - "() invocation was not stubbed"); - } - } } diff --git a/src/test/resources/mockdata/budgets/budget.json b/src/test/resources/mockdata/budgets/budget.json new file mode 100644 index 000000000..b3e09de4c --- /dev/null +++ b/src/test/resources/mockdata/budgets/budget.json @@ -0,0 +1,23 @@ +{ + "id": "55f48dc6-efa7-4cfe-bc7c-4786efe493e2", + "name": "GRANT1-SUBN-FY2019", + "budgetStatus": "Active", + "allowableEncumbrance": 100.0, + "allowableExpenditure": 100.0, + "allocated": 70000.0, + "awaitingPayment": 2310.0, + "available": 61040.0, + "encumbered": 2870.0, + "expenditures": 3780.0, + "unavailable": 0.0, + "overEncumbrance": 0.0, + "overExpended": 0.0, + "totalFunding": 70000.0, + "fundId": "55f48dc6-efa7-4cfe-bc7c-4786efe493e3", + "fiscalYearId": "78110b4e-2f8e-4eef-81ee-3058c0c7a9ee", + "acqUnitIds": [], + "metadata": { + "createdDate": "2020-01-30T01:28:17.666+0000", + "updatedDate": "2020-01-30T01:28:17.666+0000" + } +}