FINERACT-2317: Add new endpoints to indirectly modify loan approved amount

This commit is contained in:
mark.vituska
2025-08-06 12:46:28 +02:00
committed by Adam Saghy
parent 2cc0cda1a6
commit 1c82507707
11 changed files with 467 additions and 3 deletions
@@ -3894,4 +3894,13 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId + "/transactions/" + transactionId + "?command=interest-refund";
return this;
}
public CommandWrapperBuilder updateLoanAvailableDisbursementAmount(final Long loanId) {
this.actionName = "UPDATE";
this.entityName = "LOAN_AVAILABLE_DISBURSEMENT_AMOUNT";
this.entityId = loanId;
this.loanId = loanId;
this.href = "/loans/" + loanId;
return this;
}
}
@@ -7451,7 +7451,7 @@ Feature: Loan
# --- disbursement - 1 January, 2025 ---
When Admin successfully disburse the loan on "01 January 2025" with "300" EUR transaction amount
When Admin sets the business date to "03 January 2025"
Then Update loan approved amount with new amount "800" value
Then Update loan approved amount is forbidden with amount "800" due to higher principal amount on loan
@TestRailId:C3868
Scenario: Verify approved amount change with greater value for progressive multidisbursal loan that expects two tranches - UC7_2
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.service.LoanApprovedAmountWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@CommandType(entity = "LOAN_AVAILABLE_DISBURSEMENT_AMOUNT", action = "UPDATE")
public class LoanAvailableDisbursementAmountModificationCommandHandler implements NewCommandSourceHandler {
private final LoanApprovedAmountWritePlatformService loanApprovedAmountWritePlatformService;
@Override
@Transactional
public CommandProcessingResult processCommand(JsonCommand command) {
return loanApprovedAmountWritePlatformService.modifyLoanAvailableDisbursementAmount(command.getLoanId(), command);
}
}
@@ -23,4 +23,6 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
public interface LoanApprovedAmountValidator {
void validateLoanApprovedAmountModification(JsonCommand command);
void validateLoanAvailableDisbursementAmountModification(JsonCommand command);
}
@@ -24,4 +24,6 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface LoanApprovedAmountWritePlatformService {
CommandProcessingResult modifyLoanApprovedAmount(Long loanId, JsonCommand command);
CommandProcessingResult modifyLoanAvailableDisbursementAmount(Long loanId, JsonCommand command);
}
@@ -31,6 +31,7 @@ import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistoryRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApprovedAmountValidator;
import org.springframework.stereotype.Service;
@@ -73,4 +74,45 @@ public class LoanApprovedAmountWritePlatformServiceImpl implements LoanApprovedA
.with(changes) //
.build();
}
@Override
public CommandProcessingResult modifyLoanAvailableDisbursementAmount(Long loanId, JsonCommand command) {
// API rule validations
this.loanApprovedAmountValidator.validateLoanAvailableDisbursementAmountModification(command);
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("newAvailableDisbursementAmount", command.stringValueOfParameterNamed(LoanApiConstants.amountParameterName));
changes.put("locale", command.locale());
Loan loan = this.loanAssembler.assembleFrom(loanId);
changes.put("oldApprovedAmount", loan.getApprovedPrincipal());
BigDecimal expectedDisbursementAmount = loan.getDisbursementDetails().stream().filter(t -> t.actualDisbursementDate() == null)
.map(LoanDisbursementDetails::principal).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal oldAvailableDisbursement = loan.getApprovedPrincipal().subtract(loan.getSummary().getTotalPrincipal())
.subtract(expectedDisbursementAmount);
changes.put("oldAvailableDisbursementAmount", oldAvailableDisbursement);
BigDecimal newAvailableDisbursementAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.amountParameterName);
BigDecimal newApprovedAmount = loan.getSummary().getTotalPrincipal().add(expectedDisbursementAmount)
.add(newAvailableDisbursementAmount);
changes.put("newApprovedAmount", newApprovedAmount);
LoanApprovedAmountHistory loanApprovedAmountHistory = new LoanApprovedAmountHistory(loan.getId(), newApprovedAmount,
loan.getApprovedPrincipal());
loan.setApprovedPrincipal(newApprovedAmount);
loanApprovedAmountHistoryRepository.saveAndFlush(loanApprovedAmountHistory);
businessEventNotifierService.notifyPostBusinessEvent(new LoanApprovedAmountChangedBusinessEvent(loan));
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withEntityExternalId(loan.getExternalId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.with(changes) //
.build();
}
}
@@ -924,6 +924,34 @@ public class LoansApiResource {
return getLoanApprovedAmountHistory(null, ExternalIdFactory.produce(loanExternalId));
}
@PUT
@Path("{loanId}/available-disbursement-amount")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Modifies the available disbursement amount of the loan", description = "Modifies the available disbursement amount of the loan, this indirectly modifies the approved amount that can be disbursed on the loan")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansAvailableDisbursementAmountRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansAvailableDisbursementAmountResponse.class))) })
public CommandProcessingResult modifyLoanAvailableDisbursementAmount(
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, @Context final UriInfo uriInfo,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
return modifyLoanAvailableDisbursementAmount(loanId, ExternalId.empty(), apiRequestBodyAsJson);
}
@PUT
@Path("external-id/{loanExternalId}/available-disbursement-amount")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Modifies the available disbursement amount of the loan", description = "Modifies the available disbursement amount of the loan, this indirectly modifies the approved amount that can be disbursed on the loan")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansAvailableDisbursementAmountRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansAvailableDisbursementAmountResponse.class))) })
public CommandProcessingResult modifyLoanAvailableDisbursementAmount(
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
@Context final UriInfo uriInfo, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return modifyLoanAvailableDisbursementAmount(null, ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
}
private String retrieveApprovalTemplate(final Long loanId, final String loanExternalIdStr, final String templateType,
final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
@@ -1350,4 +1378,13 @@ public class LoansApiResource {
return loanApprovedAmountHistoryRepository.findAllByLoanId(resolvedLoanId, sortedByCreationDate);
}
private CommandProcessingResult modifyLoanAvailableDisbursementAmount(Long loanId, ExternalId loanExternalId,
String apiRequestBodyAsJson) {
Long resolvedLoanId = loanId == null ? loanReadPlatformService.getResolvedLoanId(loanExternalId) : loanId;
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandWrapper commandRequest = builder.updateLoanAvailableDisbursementAmount(resolvedLoanId).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
}
@@ -1814,4 +1814,51 @@ final class LoansApiResourceSwagger {
@Schema(description = "PutLoansApprovedAmountChanges")
public PutLoansApprovedAmountChanges changes;
}
@Schema(description = "PutLoansAvailableDisbursementAmountRequest")
public static final class PutLoansAvailableDisbursementAmountRequest {
private PutLoansAvailableDisbursementAmountRequest() {}
@Schema(example = "1000")
public BigDecimal amount;
@Schema(example = "en")
public String locale;
}
@Schema(description = "PutLoansAvailableDisbursementAmountResponse")
public static final class PutLoansAvailableDisbursementAmountResponse {
private PutLoansAvailableDisbursementAmountResponse() {}
static final class PutLoansAvailableDisbursementAmountChanges {
private PutLoansAvailableDisbursementAmountChanges() {}
@Schema(example = "1000")
public BigDecimal oldApprovedAmount;
@Schema(example = "1000")
public BigDecimal newApprovedAmount;
@Schema(example = "1000")
public BigDecimal oldAvailableDisbursementAmount;
@Schema(example = "1000")
public BigDecimal newAvailableDisbursementAmount;
@Schema(example = "en_GB")
public String locale;
}
@Schema(example = "3")
public Long resourceId;
@Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
public String resourceExternalId;
@Schema(example = "2")
public Long officeId;
@Schema(example = "6")
public Long clientId;
@Schema(example = "10")
public Long groupId;
@Schema(description = "PutLoansAvailableDisbursementAmountChanges")
public PutLoansAvailableDisbursementAmountChanges changes;
}
}
@@ -35,6 +35,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.portfolio.common.service.Validator;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
@@ -95,10 +96,64 @@ public final class LoanApprovedAmountValidatorImpl implements LoanApprovedAmount
}
BigDecimal totalPrincipalOnLoan = loan.getSummary().getTotalPrincipal();
if (MathUtil.isLessThan(newApprovedAmount, totalPrincipalOnLoan)) {
BigDecimal totalExpectedPrincipal = loan.getDisbursementDetails().stream().filter(t -> t.actualDisbursementDate() == null)
.map(LoanDisbursementDetails::principal).reduce(BigDecimal.ZERO, BigDecimal::add);
if (MathUtil.isLessThan(newApprovedAmount, totalPrincipalOnLoan.add(totalExpectedPrincipal))) {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
.failWithCode("less.than.disbursed.principal.and.capitalized.income");
}
});
}
@Override
public void validateLoanAvailableDisbursementAmountModification(JsonCommand command) {
String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Set<String> supportedParameters = new HashSet<>(
Arrays.asList(LoanApiConstants.amountParameterName, LoanApiConstants.localeParameterName));
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
final BigDecimal newAvailableDisbursementAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.amountParameterName, element);
Validator.validateOrThrow("loan.available.disbursement.amount", baseDataValidator -> {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName).value(newAvailableDisbursementAmount).notNull();
});
Validator.validateOrThrowDomainViolation("loan.available.disbursement.amount", baseDataValidator -> {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName).value(newAvailableDisbursementAmount)
.zeroOrPositiveAmount();
final Long loanId = command.getLoanId();
Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
if (INVALID_LOAN_STATUSES_FOR_APPROVED_AMOUNT_MODIFICATION.contains(loan.getStatus())) {
baseDataValidator.reset()
.failWithCodeNoParameterAddedToErrorCode("loan.status.not.valid.for.available.disbursement.amount.modification");
}
BigDecimal maximumThresholdForApprovedAmount;
if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
maximumThresholdForApprovedAmount = loanApplicationValidator.getOverAppliedMax(loan);
} else {
maximumThresholdForApprovedAmount = loan.getProposedPrincipal();
}
BigDecimal expectedDisbursementAmount = loan.getDisbursementDetails().stream().filter(t -> t.actualDisbursementDate() == null)
.map(LoanDisbursementDetails::principal).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal maximumAvailableDisbursementThreshold = maximumThresholdForApprovedAmount
.subtract(loan.getSummary().getTotalPrincipal()).subtract(expectedDisbursementAmount);
if (MathUtil.isGreaterThan(newAvailableDisbursementAmount, maximumAvailableDisbursementThreshold)) {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
.failWithCode("can't.be.greater.than.maximum.available.disbursement.amount.calculation");
}
});
}
}
@@ -84,6 +84,8 @@ import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
import org.apache.fineract.client.models.PutLoansApprovedAmountRequest;
import org.apache.fineract.client.models.PutLoansApprovedAmountResponse;
import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountRequest;
import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountResponse;
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.client.models.RetrieveLoansPointInTimeRequest;
import org.apache.fineract.client.util.CallFailedRuntimeException;
@@ -789,6 +791,12 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
return Calls.ok(fineractClient().loans.getLoanApprovedAmountHistory(loanId));
}
protected PutLoansAvailableDisbursementAmountResponse modifyLoanAvailableDisbursementAmount(Long loanId, BigDecimal approvedAmount) {
PutLoansAvailableDisbursementAmountRequest request = new PutLoansAvailableDisbursementAmountRequest().amount(approvedAmount)
.locale("en");
return Calls.ok(fineractClient().loans.modifyLoanAvailableDisbursementAmount(loanId, request));
}
protected void verifyOutstanding(LoanPointInTimeData loan, OutstandingAmounts outstanding) {
assertThat(BigDecimal.valueOf(outstanding.principalOutstanding))
.isEqualByComparingTo(loan.getPrincipal().getPrincipalOutstanding());
@@ -36,13 +36,14 @@ import org.apache.fineract.client.models.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PutLoansApprovedAmountResponse;
import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountResponse;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class LoanModifyApprovedAmountTests extends BaseLoanIntegrationTest {
public class LoanModifyApprovedAmountTest extends BaseLoanIntegrationTest {
@Test
public void testValidLoanApprovedAmountModification() {
@@ -478,4 +479,223 @@ public class LoanModifyApprovedAmountTests extends BaseLoanIntegrationTest {
assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"));
});
}
@Test
public void testValidLoanAvailableDisbursementAmountModification() {
BigDecimal sixHundred = BigDecimal.valueOf(600.0);
BigDecimal sevenHundred = BigDecimal.valueOf(700.0);
BigDecimal nineHundred = BigDecimal.valueOf(900.0);
BigDecimal thousand = BigDecimal.valueOf(1000.0);
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
1000.0, 10.0, 4, null);
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
PutLoansAvailableDisbursementAmountResponse putLoansAvailableDisbursementAmountResponse = modifyLoanAvailableDisbursementAmount(
loanId, sixHundred);
Assertions.assertEquals(loanId, putLoansAvailableDisbursementAmountResponse.getResourceId());
Assertions.assertNotNull(putLoansAvailableDisbursementAmountResponse.getChanges());
Assertions.assertNotNull(putLoansAvailableDisbursementAmountResponse.getChanges().getNewApprovedAmount());
Assertions.assertNotNull(putLoansAvailableDisbursementAmountResponse.getChanges().getOldApprovedAmount());
Assertions.assertNotNull(putLoansAvailableDisbursementAmountResponse.getChanges().getOldAvailableDisbursementAmount());
Assertions.assertNotNull(putLoansAvailableDisbursementAmountResponse.getChanges().getNewAvailableDisbursementAmount());
Assertions.assertEquals(sevenHundred,
putLoansAvailableDisbursementAmountResponse.getChanges().getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(thousand,
putLoansAvailableDisbursementAmountResponse.getChanges().getOldApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(sixHundred, putLoansAvailableDisbursementAmountResponse.getChanges().getNewAvailableDisbursementAmount()
.setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(nineHundred, putLoansAvailableDisbursementAmountResponse.getChanges()
.getOldAvailableDisbursementAmount().setScale(1, RoundingMode.HALF_UP));
});
}
@Test
public void testLoanAvailableDisbursementAmountModificationEvent() {
externalEventHelper.enableBusinessEvent("LoanApprovedAmountChangedBusinessEvent");
BigDecimal sixHundred = BigDecimal.valueOf(600.0);
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
1000.0, 10.0, 4, null);
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
deleteAllExternalEvents();
modifyLoanAvailableDisbursementAmount(loanId, sixHundred);
verifyBusinessEvents(new LoanBusinessEvent("LoanApprovedAmountChangedBusinessEvent", "01 January 2024", 300, 100.0, 100.0));
});
}
@Test
public void testValidLoanAvailableDisbursementAmountModificationInvalidRequest() {
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
1000.0, 10.0, 4, null);
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> modifyLoanAvailableDisbursementAmount(loanId, null));
assertEquals(400, exception.getResponse().code());
assertTrue(exception.getMessage().contains("validation.msg.loan.available.disbursement.amount.amount.cannot.be.blank"));
});
}
@Test
public void testValidLoanAvailableDisbursementAmountModificationInvalidLoanStatus() {
BigDecimal sixHundred = BigDecimal.valueOf(600.0);
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
runAt("1 January 2024", () -> {
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
loanProductsResponse.getResourceId(), "1 January 2024", 1000.0, 10.0, 4, null));
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> modifyLoanAvailableDisbursementAmount(postLoansResponse.getResourceId(), sixHundred));
assertEquals(403, exception.getResponse().code());
assertTrue(exception.getMessage().contains(
"validation.msg.loan.available.disbursement.amount.loan.status.not.valid.for.available.disbursement.amount.modification"));
});
}
@Test
public void testModifyLoanAvailableDisbursementAmountHigherThanApprovedAmount() {
BigDecimal twoThousand = BigDecimal.valueOf(2000.0);
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
1000.0, 10.0, 4, null);
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> modifyLoanAvailableDisbursementAmount(loanId, twoThousand));
assertEquals(403, exception.getResponse().code());
assertTrue(exception.getMessage().contains(
"validation.msg.loan.available.disbursement.amount.amount.can't.be.greater.than.maximum.available.disbursement.amount.calculation"));
});
}
@Test
public void testModifyLoanAvailableDisbursementAmountWithNegativeAmount() {
BigDecimal sixHundred = BigDecimal.valueOf(600.0);
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
1000.0, 10.0, 4, null);
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> modifyLoanAvailableDisbursementAmount(loanId, sixHundred.negate()));
assertEquals(403, exception.getResponse().code());
assertTrue(exception.getMessage().contains("validation.msg.loan.available.disbursement.amount.amount.not.zero.or.greater"));
});
}
@Test
public void testModifyLoanAvailableDisbursementAmountCapitalizedIncomeCountsAsPrincipal() {
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressiveWithCapitalizedIncome());
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
1000.0, 10.0, 4, null);
disburseLoan(loanId, BigDecimal.valueOf(500), "1 January 2024");
PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId,
"1 January 2024", 500.0);
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> modifyLoanAvailableDisbursementAmount(loanId, BigDecimal.valueOf(600.0)));
assertEquals(403, exception.getResponse().code());
assertTrue(exception.getMessage().contains(
"validation.msg.loan.available.disbursement.amount.amount.can't.be.greater.than.maximum.available.disbursement.amount.calculation"));
loanTransactionHelper.reverseLoanTransaction(capitalizedIncomeResponse.getLoanId(), capitalizedIncomeResponse.getResourceId(),
"1 January 2024");
Assertions.assertDoesNotThrow(() -> modifyLoanAvailableDisbursementAmount(loanId, BigDecimal.valueOf(600.0)));
});
}
@Test
public void testModifyLoanAvailableDisbursementAmountFutureExpectedDisbursementsCountAsPrincipal() {
BigDecimal twoHundred = BigDecimal.valueOf(200.0);
BigDecimal eightHundred = BigDecimal.valueOf(800.0);
BigDecimal thousand = BigDecimal.valueOf(1000.0);
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressive().disallowExpectedDisbursements(false).allowApprovedDisbursedAmountsOverApplied(null)
.overAppliedCalculationType(null).overAppliedNumber(null));
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
1000.0, 7.0, 6, (request) -> request.disbursementData(List.of(new PostLoansDisbursementData()
.expectedDisbursementDate("1 January 2024").principal(BigDecimal.valueOf(800.0)))));
PutLoansAvailableDisbursementAmountResponse putLoansAvailableDisbursementAmountResponse = modifyLoanAvailableDisbursementAmount(
loanId, BigDecimal.ZERO);
Assertions.assertEquals(eightHundred,
putLoansAvailableDisbursementAmountResponse.getChanges().getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(thousand,
putLoansAvailableDisbursementAmountResponse.getChanges().getOldApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(BigDecimal.ZERO,
putLoansAvailableDisbursementAmountResponse.getChanges().getNewAvailableDisbursementAmount());
Assertions.assertEquals(twoHundred, putLoansAvailableDisbursementAmountResponse.getChanges().getOldAvailableDisbursementAmount()
.setScale(1, RoundingMode.HALF_UP));
});
}
@Test
public void testModifyLoanAvailableDisbursementAmountCreatesHistoryEntries() {
BigDecimal fourHundred = BigDecimal.valueOf(400.0);
BigDecimal sixHundred = BigDecimal.valueOf(600.0);
BigDecimal eightHundred = BigDecimal.valueOf(800.0);
BigDecimal thousand = BigDecimal.valueOf(1000.0);
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
1000.0, 10.0, 4, null);
modifyLoanAvailableDisbursementAmount(loanId, BigDecimal.valueOf(800.0));
modifyLoanAvailableDisbursementAmount(loanId, BigDecimal.valueOf(600.0));
modifyLoanAvailableDisbursementAmount(loanId, BigDecimal.valueOf(400.0));
List<LoanApprovedAmountHistoryData> loanApprovedAmountHistory = getLoanApprovedAmountHistory(loanId);
Assertions.assertNotNull(loanApprovedAmountHistory);
Assertions.assertEquals(3, loanApprovedAmountHistory.size());
Assertions.assertEquals(thousand, loanApprovedAmountHistory.get(0).getOldApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(eightHundred,
loanApprovedAmountHistory.get(0).getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(eightHundred,
loanApprovedAmountHistory.get(1).getOldApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(sixHundred, loanApprovedAmountHistory.get(1).getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(sixHundred, loanApprovedAmountHistory.get(2).getOldApprovedAmount().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(fourHundred, loanApprovedAmountHistory.get(2).getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
});
}
}