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
@@ -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");
}
});
}
}