Skip to content

Commit

Permalink
[MODFIN-382] - Add dry-run mode for bulk FY finance updates
Browse files Browse the repository at this point in the history
  • Loading branch information
azizbekxm committed Dec 20, 2024
1 parent dc7121b commit 2a1c987
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 35 deletions.
2 changes: 1 addition & 1 deletion ramls/acq-models
11 changes: 10 additions & 1 deletion src/main/java/org/folio/rest/impl/FinanceDataApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,7 +43,13 @@ public void getFinanceFinanceData(String query, String totalRecords, int offset,
@Validate
public void putFinanceFinanceData(FyFinanceDataCollection entity, Map<String, String> okapiHeaders, Handler<AsyncResult<Response>> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,17 +91,23 @@ private Future<FyFinanceDataCollection> getFinanceData(String query, int offset,
* @param requestContext request context
* @return future with void result
*/
public Future<Void> putFinanceData(FyFinanceDataCollection financeDataCollection, RequestContext requestContext) {
public Future<FyFinanceDataCollection> 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));
}
Expand All @@ -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<Void> processAllocationTransaction(FyFinanceDataCollection fyFinanceDataCollection,
RequestContext requestContext) {
return fiscalYearService.getFiscalYearById(getFiscalYearId(fyFinanceDataCollection), requestContext)
Expand Down
46 changes: 30 additions & 16 deletions src/test/java/org/folio/rest/impl/FinanceDataApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand All @@ -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<Void> failedFuture = failedFuture(new HttpException(500, INTERNAL_SERVER_ERROR.getReasonPhrase()));
var financeDataCollection = getFinanceDataCollection();
Future<FyFinanceDataCollection> failedFuture = failedFuture(new HttpException(500, INTERNAL_SERVER_ERROR.getReasonPhrase()));

when(financeDataService.putFinanceData(any(FyFinanceDataCollection.class), any(RequestContext.class)))
.thenReturn(failedFuture);
Expand All @@ -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);

Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
Expand All @@ -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));
Expand All @@ -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());
Expand All @@ -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));
Expand All @@ -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"));
Expand All @@ -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");

Expand All @@ -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,
Expand All @@ -215,18 +222,73 @@ 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,
() -> financeDataService.putFinanceData(collection, new RequestContext(Vertx.vertx().getOrCreateContext(), new HashMap<>())));
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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
"transactionDescription": "End of year adjustment",
"transactionTag": {
"tagList": ["Urgent", "Review"]
},
"updateType": "Commit"
}
},
{
"fiscalYearId": "123e4567-e89b-12d3-a456-426614174005",
Expand Down Expand Up @@ -59,9 +58,9 @@
"transactionDescription": "Mid-year adjustment",
"transactionTag": {
"tagList": ["Urgent", "Review"]
},
"updateType": "Preview"
}
}
],
"updateType": "Commit",
"totalRecords": 2
}

0 comments on commit 2a1c987

Please sign in to comment.