From 1c825077073ed23634727d7168678ed36de76bc2 Mon Sep 17 00:00:00 2001 From: "mark.vituska" Date: Thu, 24 Jul 2025 15:25:27 +0200 Subject: [PATCH] FINERACT-2317: Add new endpoints to indirectly modify loan approved amount --- .../service/CommandWrapperBuilder.java | 9 + .../src/test/resources/features/Loan.feature | 2 +- ...ementAmountModificationCommandHandler.java | 42 ++++ .../LoanApprovedAmountValidator.java | 2 + ...oanApprovedAmountWritePlatformService.java | 2 + ...pprovedAmountWritePlatformServiceImpl.java | 42 ++++ .../loanaccount/api/LoansApiResource.java | 37 +++ .../api/LoansApiResourceSwagger.java | 47 ++++ .../LoanApprovedAmountValidatorImpl.java | 57 ++++- .../BaseLoanIntegrationTest.java | 8 + ...java => LoanModifyApprovedAmountTest.java} | 222 +++++++++++++++++- 11 files changed, 467 insertions(+), 3 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanAvailableDisbursementAmountModificationCommandHandler.java rename integration-tests/src/test/java/org/apache/fineract/integrationtests/{LoanModifyApprovedAmountTests.java => LoanModifyApprovedAmountTest.java} (67%) diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index f0321581b..fd1c61cf8 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -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; + } } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index a5edee50d..5a7a65cdc 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -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 diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanAvailableDisbursementAmountModificationCommandHandler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanAvailableDisbursementAmountModificationCommandHandler.java new file mode 100644 index 000000000..99e545aae --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanAvailableDisbursementAmountModificationCommandHandler.java @@ -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); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidator.java index 0d61d0805..f929056ec 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidator.java @@ -23,4 +23,6 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; public interface LoanApprovedAmountValidator { void validateLoanApprovedAmountModification(JsonCommand command); + + void validateLoanAvailableDisbursementAmountModification(JsonCommand command); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformService.java index 5fb262172..5c0c45a8f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformService.java @@ -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); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformServiceImpl.java index 49b01a7f3..f8fe4cb32 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformServiceImpl.java @@ -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 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(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index b1e74ea0c..f8612e6ec 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -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); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index 6c2252f1e..b609596bc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -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; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidatorImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidatorImpl.java index 9ea5b444b..bfd35595c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidatorImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidatorImpl.java @@ -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 supportedParameters = new HashSet<>( + Arrays.asList(LoanApiConstants.amountParameterName, LoanApiConstants.localeParameterName)); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final Type typeOfMap = new TypeToken>() {}.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"); + } + }); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 788b5ea2c..2f8703e70 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -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()); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTest.java similarity index 67% rename from integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTests.java rename to integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTest.java index 93bb79708..01a8ece0e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTest.java @@ -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 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)); + }); + } }