From 2a1c987b122f6cbe4073dad964069169ad248745 Mon Sep 17 00:00:00 2001 From: Azizbek Khushvakov Date: Fri, 20 Dec 2024 13:37:26 +0500 Subject: [PATCH] [MODFIN-382] - Add dry-run mode for bulk FY finance updates --- ramls/acq-models | 2 +- .../org/folio/rest/impl/FinanceDataApi.java | 11 ++- .../financedata/FinanceDataService.java | 20 ++++- .../folio/rest/impl/FinanceDataApiTest.java | 46 ++++++---- .../financedata/FinanceDataServiceTest.java | 84 ++++++++++++++++--- .../fy_finance_data_collection_put.json | 7 +- 6 files changed, 135 insertions(+), 35 deletions(-) diff --git a/ramls/acq-models b/ramls/acq-models index 98e3a991..e3660f6c 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit 98e3a991ab754d3fa4e85beec7df0a2aab779d93 +Subproject commit e3660f6ce83a4f27ebf7c5ca618ecc1a587d4240 diff --git a/src/main/java/org/folio/rest/impl/FinanceDataApi.java b/src/main/java/org/folio/rest/impl/FinanceDataApi.java index 405c6aec..7167d2a3 100644 --- a/src/main/java/org/folio/rest/impl/FinanceDataApi.java +++ b/src/main/java/org/folio/rest/impl/FinanceDataApi.java @@ -6,8 +6,11 @@ import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; + import java.util.Map; import javax.ws.rs.core.Response; + +import org.apache.commons.collections4.CollectionUtils; import org.folio.rest.annotations.Validate; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.FyFinanceDataCollection; @@ -40,7 +43,13 @@ public void getFinanceFinanceData(String query, String totalRecords, int offset, @Validate public void putFinanceFinanceData(FyFinanceDataCollection entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { financeDataService.putFinanceData(entity, new RequestContext(vertxContext, okapiHeaders)) - .onSuccess(v -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse()))) + .onSuccess(financeDataCollection -> { + if (CollectionUtils.isEmpty(financeDataCollection.getFyFinanceData())) { + asyncResultHandler.handle(succeededFuture(buildNoContentResponse())); + } else { + asyncResultHandler.handle(succeededFuture(buildOkResponse(financeDataCollection))); + } + }) .onFailure(fail -> handleErrorResponse(asyncResultHandler, fail)); } } diff --git a/src/main/java/org/folio/services/financedata/FinanceDataService.java b/src/main/java/org/folio/services/financedata/FinanceDataService.java index 7c60d8c2..2755aede 100644 --- a/src/main/java/org/folio/services/financedata/FinanceDataService.java +++ b/src/main/java/org/folio/services/financedata/FinanceDataService.java @@ -7,6 +7,7 @@ import static org.folio.rest.util.ResourcePathResolver.FINANCE_DATA_STORAGE; import static org.folio.rest.util.ResourcePathResolver.resourcesPath; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -90,17 +91,23 @@ private Future getFinanceData(String query, int offset, * @param requestContext request context * @return future with void result */ - public Future putFinanceData(FyFinanceDataCollection financeDataCollection, RequestContext requestContext) { + public Future putFinanceData(FyFinanceDataCollection financeDataCollection, RequestContext requestContext) { log.debug("Trying to update finance data collection with size: {}", financeDataCollection.getTotalRecords()); if (CollectionUtils.isEmpty(financeDataCollection.getFyFinanceData())) { log.info("putFinanceData:: Finance data collection is empty, nothing to update"); - return succeededFuture(); + return succeededFuture(financeDataCollection); } validateFinanceDataCollection(financeDataCollection, getFiscalYearId(financeDataCollection)); + calculateAfterAllocation(financeDataCollection); + if (financeDataCollection.getUpdateType().equals(FyFinanceDataCollection.UpdateType.PREVIEW)) { + log.info("putFinanceData:: Running dry-run mode finance data collection"); + return succeededFuture(financeDataCollection); + } return processAllocationTransaction(financeDataCollection, requestContext) .compose(v -> updateFinanceData(financeDataCollection, requestContext)) + .map(v -> new FyFinanceDataCollection()) .onSuccess(asyncResult -> processLogs(financeDataCollection, requestContext, COMPLETED)) .onFailure(asyncResult -> processLogs(financeDataCollection, requestContext, ERROR)); } @@ -121,6 +128,15 @@ private void validateFinanceDataCollection(FyFinanceDataCollection financeDataCo } } + private void calculateAfterAllocation(FyFinanceDataCollection financeDataCollection) { + financeDataCollection.getFyFinanceData().forEach(financeData -> { + var allocationChange = BigDecimal.valueOf(financeData.getBudgetAllocationChange()); + var initialAllocation = BigDecimal.valueOf(financeData.getBudgetInitialAllocation()); + var afterAllocation = initialAllocation.add(allocationChange); + financeData.setBudgetAfterAllocation(afterAllocation.doubleValue()); + }); + } + private Future processAllocationTransaction(FyFinanceDataCollection fyFinanceDataCollection, RequestContext requestContext) { return fiscalYearService.getFiscalYearById(getFiscalYearId(fyFinanceDataCollection), requestContext) diff --git a/src/test/java/org/folio/rest/impl/FinanceDataApiTest.java b/src/test/java/org/folio/rest/impl/FinanceDataApiTest.java index 533f90a6..181d8663 100644 --- a/src/test/java/org/folio/rest/impl/FinanceDataApiTest.java +++ b/src/test/java/org/folio/rest/impl/FinanceDataApiTest.java @@ -138,12 +138,9 @@ void negative_testGetFinanceFinanceDataFailure() { @Test void positive_testPutFinanceFinanceDataSuccess() throws IOException { - var jsonData = getMockData("mockdata/finance-data/fy_finance_data_collection_put.json"); - var jsonObject = new JsonObject(jsonData); - var financeDataCollection = jsonObject.mapTo(FyFinanceDataCollection.class); - + var financeDataCollection = getFinanceDataCollection(); when(financeDataService.putFinanceData(any(FyFinanceDataCollection.class), any(RequestContext.class))) - .thenReturn(succeededFuture(null)); + .thenReturn(succeededFuture(new FyFinanceDataCollection())); verifyPut(FINANCE_DATA_ENDPOINT, financeDataCollection, "", NO_CONTENT.getStatusCode()); @@ -152,11 +149,8 @@ void positive_testPutFinanceFinanceDataSuccess() throws IOException { @Test void negative_testPutFinanceFinanceDataFailure() throws IOException { - var jsonData = getMockData("mockdata/finance-data/fy_finance_data_collection_put.json"); - var jsonObject = new JsonObject(jsonData); - var financeDataCollection = jsonObject.mapTo(FyFinanceDataCollection.class); - - Future failedFuture = failedFuture(new HttpException(500, INTERNAL_SERVER_ERROR.getReasonPhrase())); + var financeDataCollection = getFinanceDataCollection(); + Future failedFuture = failedFuture(new HttpException(500, INTERNAL_SERVER_ERROR.getReasonPhrase())); when(financeDataService.putFinanceData(any(FyFinanceDataCollection.class), any(RequestContext.class))) .thenReturn(failedFuture); @@ -171,9 +165,7 @@ void negative_testPutFinanceFinanceDataFailure() throws IOException { @Test void negative_testPutFinanceFinanceDataBadRequest() throws IOException { - var jsonData = getMockData("mockdata/finance-data/fy_finance_data_collection_put.json"); - var jsonObject = new JsonObject(jsonData); - var financeDataCollection = jsonObject.mapTo(FyFinanceDataCollection.class); + var financeDataCollection = getFinanceDataCollection(); // Modify one field to make it invalid financeDataCollection.getFyFinanceData().get(0).setFiscalYearId(null); @@ -185,19 +177,41 @@ void negative_testPutFinanceFinanceDataBadRequest() throws IOException { } @Test - void testPutFinanceFinanceDataWithEmptyCollection() { - FyFinanceDataCollection entity = new FyFinanceDataCollection() + void positive_testPutFinanceFinanceDataWithEmptyCollection() { + var entity = new FyFinanceDataCollection() .withFyFinanceData(emptyList()) + .withUpdateType(FyFinanceDataCollection.UpdateType.COMMIT) .withTotalRecords(0); when(financeDataService.putFinanceData(any(FyFinanceDataCollection.class), any(RequestContext.class))) - .thenReturn(succeededFuture(null)); + .thenReturn(succeededFuture(new FyFinanceDataCollection())); verifyPut(FINANCE_DATA_ENDPOINT, entity, "", NO_CONTENT.getStatusCode()); verify(financeDataService).putFinanceData(eq(entity), any(RequestContext.class)); } + @Test + void positive_testPutFinanceFinanceDataPreviewMode() throws IOException { + var financeDataCollection = getFinanceDataCollection(); + financeDataCollection.setUpdateType(FyFinanceDataCollection.UpdateType.PREVIEW); + + when(financeDataService.putFinanceData(any(FyFinanceDataCollection.class), any(RequestContext.class))) + .thenReturn(succeededFuture(financeDataCollection)); + + var response = verifyPut(FINANCE_DATA_ENDPOINT, financeDataCollection, APPLICATION_JSON, OK.getStatusCode()) + .as(FyFinanceDataCollection.class); + + assertEquals(financeDataCollection, response); + verify(financeDataService).putFinanceData(eq(financeDataCollection), any(RequestContext.class)); + } + + private FyFinanceDataCollection getFinanceDataCollection() throws IOException { + var jsonData = getMockData("mockdata/finance-data/fy_finance_data_collection_put.json"); + var jsonObject = new JsonObject(jsonData); + return jsonObject.mapTo(FyFinanceDataCollection.class); + } + static class ContextConfiguration { @Bean public FinanceDataService financeDataService() { diff --git a/src/test/java/org/folio/services/financedata/FinanceDataServiceTest.java b/src/test/java/org/folio/services/financedata/FinanceDataServiceTest.java index de150a9e..43ae3d08 100644 --- a/src/test/java/org/folio/services/financedata/FinanceDataServiceTest.java +++ b/src/test/java/org/folio/services/financedata/FinanceDataServiceTest.java @@ -12,6 +12,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -89,7 +90,7 @@ void positive_shouldGetFinanceDataWithAcqUnitsRestriction(VertxTestContext vertx String expectedQuery = "(" + acqUnitIdsQuery + ") and (" + query + ")"; int offset = 0; int limit = 10; - FyFinanceDataCollection fyFinanceDataCollection = new FyFinanceDataCollection(); + var fyFinanceDataCollection = new FyFinanceDataCollection(); when(acqUnitsService.buildAcqUnitsCqlClauseForFinanceData(any())).thenReturn(succeededFuture(acqUnitIdsQuery)); when(restClient.get(anyString(), eq(FyFinanceDataCollection.class), any())).thenReturn(succeededFuture(fyFinanceDataCollection)); @@ -110,7 +111,7 @@ void negative_shouldReturnEmptyCollectionWhenFinanceDataNotFound(VertxTestContex String expectedQuery = "(" + noFdUnitAssignedCql + ") and (" + query + ")"; int offset = 0; int limit = 10; - FyFinanceDataCollection emptyCollection = new FyFinanceDataCollection().withTotalRecords(0); + var emptyCollection = new FyFinanceDataCollection().withTotalRecords(0); when(acqUnitsService.buildAcqUnitsCqlClauseForFinanceData(any())).thenReturn(succeededFuture(noFdUnitAssignedCql)); when(restClient.get(anyString(), eq(FyFinanceDataCollection.class), any())).thenReturn(succeededFuture(emptyCollection)); @@ -127,7 +128,9 @@ void negative_shouldReturnEmptyCollectionWhenFinanceDataNotFound(VertxTestContex @Test void positive_testPutFinanceData_PutFinanceDataSuccessfully(VertxTestContext vertxTestContext) { - var financeDataCollection = new FyFinanceDataCollection().withFyFinanceData(List.of(createValidFyFinanceData())); + var financeDataCollection = new FyFinanceDataCollection() + .withFyFinanceData(List.of(createValidFyFinanceData())) + .withUpdateType(FyFinanceDataCollection.UpdateType.COMMIT); var fiscalYear = new FiscalYear().withCurrency("USD"); when(restClient.put(anyString(), any(), any())).thenReturn(succeededFuture()); @@ -148,7 +151,9 @@ void positive_testPutFinanceData_PutFinanceDataSuccessfully(VertxTestContext ver @Test void negative_testPutFinanceData_LogErrorWhenPutFinanceDataFails(VertxTestContext vertxTestContext) { - var financeDataCollection = new FyFinanceDataCollection().withFyFinanceData(List.of(createValidFyFinanceData())); + var financeDataCollection = new FyFinanceDataCollection() + .withFyFinanceData(List.of(createValidFyFinanceData())) + .withUpdateType(FyFinanceDataCollection.UpdateType.COMMIT); var fiscalYear = new FiscalYear().withCurrency("USD"); when(fiscalYearService.getFiscalYearById(any(), any())).thenReturn(succeededFuture(fiscalYear)); @@ -167,10 +172,11 @@ void negative_testPutFinanceData_LogErrorWhenPutFinanceDataFails(VertxTestContex @Test void negative_testPutFinanceData_FailureInProcessAllocationTransaction(VertxTestContext vertxTestContext) { - FyFinanceDataCollection financeData = new FyFinanceDataCollection(); - FyFinanceData data = createValidFyFinanceData(); - financeData.setFyFinanceData(singletonList(data)); - FiscalYear fiscalYear = new FiscalYear().withCurrency("USD"); + var data = createValidFyFinanceData(); + var financeData = new FyFinanceDataCollection() + .withFyFinanceData(singletonList(data)) + .withUpdateType(FyFinanceDataCollection.UpdateType.COMMIT); + var fiscalYear = new FiscalYear().withCurrency("USD"); when(fiscalYearService.getFiscalYearById(any(), any())).thenReturn(succeededFuture(fiscalYear)); when(transactionApiService.processBatch(any(), any())).thenReturn(failedFuture("Process failed")); @@ -185,7 +191,7 @@ void negative_testPutFinanceData_FailureInProcessAllocationTransaction(VertxTest } @Test - void testCreateAllocationTransactionUsingReflection() throws Exception { + void negative_testCreateAllocationTransactionUsingReflection() throws Exception { var data = createValidFyFinanceData(); var fiscalYear = new FiscalYear().withCurrency("USD"); @@ -201,12 +207,13 @@ void testCreateAllocationTransactionUsingReflection() throws Exception { } @Test - void testPutFinanceData_InvalidAllocationChange() { + void negative_testPutFinanceData_InvalidAllocationChange() { var financeData = createValidFyFinanceData(); financeData.setBudgetInitialAllocation(100.0); financeData.setBudgetAllocationChange(-150.0); var collection = new FyFinanceDataCollection() .withFyFinanceData(Collections.singletonList(financeData)) + .withUpdateType(FyFinanceDataCollection.UpdateType.COMMIT) .withTotalRecords(1); var exception = assertThrows(HttpException.class, @@ -215,11 +222,12 @@ void testPutFinanceData_InvalidAllocationChange() { } @Test - void testPutFinanceData_MissingRequiredField() { + void negative_testPutFinanceData_MissingRequiredField() { var financeData = createValidFyFinanceData(); financeData.setBudgetInitialAllocation(null); var collection = new FyFinanceDataCollection() .withFyFinanceData(Collections.singletonList(financeData)) + .withUpdateType(FyFinanceDataCollection.UpdateType.COMMIT) .withTotalRecords(1); var exception = assertThrows(HttpException.class, @@ -227,6 +235,60 @@ void testPutFinanceData_MissingRequiredField() { assertEquals("Budget initial allocation is required", exception.getErrors().getErrors().get(0).getMessage()); } + @Test + void positive_testPutFinanceData_PreviewMode(VertxTestContext vertxTestContext) { + var financeDataCollection = new FyFinanceDataCollection() + .withFyFinanceData(List.of(createValidFyFinanceData())) + .withUpdateType(FyFinanceDataCollection.UpdateType.PREVIEW); + + var future = financeDataService.putFinanceData(financeDataCollection, requestContextMock); + vertxTestContext.assertComplete(future) + .onComplete(result -> { + assertTrue(result.succeeded()); + assertEquals(financeDataCollection, result.result()); + result.result().getFyFinanceData().forEach(financeData -> + assertEquals( + financeData.getBudgetInitialAllocation() + financeData.getBudgetAllocationChange(), + financeData.getBudgetAfterAllocation())); + verify(restClient, never()).put(anyString(), any(), any()); + verify(transactionApiService, never()).processBatch(any(), any()); + verify(fundUpdateLogService, never()).createFundUpdateLog(any(), any()); + vertxTestContext.completeNow(); + }); + } + + @Test + void positive_testPutFinanceData_PreviewModeWithEmptyData(VertxTestContext vertxTestContext) { + var financeDataCollection = new FyFinanceDataCollection() + .withFyFinanceData(Collections.emptyList()) + .withUpdateType(FyFinanceDataCollection.UpdateType.PREVIEW); + + var future = financeDataService.putFinanceData(financeDataCollection, requestContextMock); + vertxTestContext.assertComplete(future) + .onComplete(result -> { + assertTrue(result.succeeded()); + assertEquals(financeDataCollection, result.result()); + verify(restClient, never()).put(anyString(), any(), any()); + verify(transactionApiService, never()).processBatch(any(), any()); + verify(fundUpdateLogService, never()).createFundUpdateLog(any(), any()); + vertxTestContext.completeNow(); + }); + } + + @Test + void negative_testPutFinanceData_PreviewMode_MissingRequiredField(VertxTestContext vertxTestContext) { + var financeData = createValidFyFinanceData(); + financeData.setBudgetInitialAllocation(null); + var financeDataCollection = new FyFinanceDataCollection() + .withFyFinanceData(Collections.singletonList(financeData)) + .withUpdateType(FyFinanceDataCollection.UpdateType.PREVIEW); + + var exception = assertThrows(HttpException.class, + () -> financeDataService.putFinanceData(financeDataCollection, requestContextMock)); + assertEquals("Budget initial allocation is required", exception.getErrors().getErrors().get(0).getMessage()); + vertxTestContext.completeNow(); + } + private FyFinanceData createValidFyFinanceData() { return new FyFinanceData() .withFundId(UUID.randomUUID().toString()) diff --git a/src/test/resources/mockdata/finance-data/fy_finance_data_collection_put.json b/src/test/resources/mockdata/finance-data/fy_finance_data_collection_put.json index da6a564c..7c43b958 100644 --- a/src/test/resources/mockdata/finance-data/fy_finance_data_collection_put.json +++ b/src/test/resources/mockdata/finance-data/fy_finance_data_collection_put.json @@ -28,8 +28,7 @@ "transactionDescription": "End of year adjustment", "transactionTag": { "tagList": ["Urgent", "Review"] - }, - "updateType": "Commit" + } }, { "fiscalYearId": "123e4567-e89b-12d3-a456-426614174005", @@ -59,9 +58,9 @@ "transactionDescription": "Mid-year adjustment", "transactionTag": { "tagList": ["Urgent", "Review"] - }, - "updateType": "Preview" + } } ], + "updateType": "Commit", "totalRecords": 2 }