Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MODFIN-382] - Add dry-run mode for bulk FY finance updates #268

Merged
merged 4 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 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,10 @@
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.folio.rest.annotations.Validate;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.jaxrs.model.FyFinanceDataCollection;
Expand Down Expand Up @@ -40,7 +42,7 @@ 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 -> 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 -> financeDataCollection)
.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
61 changes: 40 additions & 21 deletions src/test/java/org/folio/rest/impl/FinanceDataApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import static java.util.Collections.emptyList;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.Status.OK;
import static org.folio.rest.util.ErrorCodes.GENERIC_ERROR_CODE;
import static org.folio.rest.util.RestTestUtils.verifyGet;
Expand Down Expand Up @@ -138,25 +137,21 @@ 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(financeDataCollection));

verifyPut(FINANCE_DATA_ENDPOINT, financeDataCollection, "", NO_CONTENT.getStatusCode());
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));
}

@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 +166,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,26 +178,52 @@ 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(entity));

verifyPut(FINANCE_DATA_ENDPOINT, entity, "", NO_CONTENT.getStatusCode());
var response = verifyPut(FINANCE_DATA_ENDPOINT, entity, APPLICATION_JSON, OK.getStatusCode())
.as(FyFinanceDataCollection.class);

assertEquals(entity, response);
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() {
@Bean
public FinanceDataService financeDataService() {
return mock(FinanceDataService.class);
}

@Bean AcqUnitsService acqUnitsService() {
@Bean
AcqUnitsService acqUnitsService() {
return mock(AcqUnitsService.class);
}
}
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
}
Loading