FINERACT-2317: Add support for modifying loan approved amounts with validation and history tracking
This commit is contained in:
+9
@@ -3876,4 +3876,13 @@ public class CommandWrapperBuilder {
|
||||
this.href = "/loans/" + loanId + "/transactions/template?command=buyDownFee";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder updateLoanApprovedAmount(final Long loanId) {
|
||||
this.actionName = "UPDATE";
|
||||
this.entityName = "LOAN_APPROVED_AMOUNT";
|
||||
this.entityId = loanId;
|
||||
this.loanId = loanId;
|
||||
this.href = "/loans/" + loanId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-4
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
|
||||
public final class Validator {
|
||||
@@ -30,14 +31,28 @@ public final class Validator {
|
||||
private Validator() {}
|
||||
|
||||
public static void validateOrThrow(String resource, Consumer<DataValidatorBuilder> baseDataValidator) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource(resource);
|
||||
|
||||
baseDataValidator.accept(dataValidatorBuilder);
|
||||
final List<ApiParameterError> dataValidationErrors = getApiParameterErrors(resource, baseDataValidator);
|
||||
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateOrThrowDomainViolation(String resource, Consumer<DataValidatorBuilder> baseDataValidator) {
|
||||
final List<ApiParameterError> dataValidationErrors = getApiParameterErrors(resource, baseDataValidator);
|
||||
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new GeneralPlatformDomainRuleException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors.toArray(new Object[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ApiParameterError> getApiParameterErrors(String resource, Consumer<DataValidatorBuilder> baseDataValidator) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource(resource);
|
||||
|
||||
baseDataValidator.accept(dataValidatorBuilder);
|
||||
return dataValidationErrors;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -65,7 +65,7 @@ public final class ErrorMessageHelper {
|
||||
}
|
||||
|
||||
public static String addDisbursementExceedApprovedAmountFailure() {
|
||||
return "Loan can't be disbursed,disburse amount is exceeding approved principal ";
|
||||
return "Loan can't be disbursed, disburse amount is exceeding approved principal.";
|
||||
}
|
||||
|
||||
public static String addDisbursementExceedMaxAppliedAmountFailure(String totalDisbAmount, String maxDisbursalAmount) {
|
||||
@@ -980,4 +980,12 @@ public final class ErrorMessageHelper {
|
||||
public static String addInstallmentFeePrincipalPercentageChargeFailure() {
|
||||
return "Failed data validation due to: installment.loancharge.with.calculation.type.principal.not.allowed.";
|
||||
}
|
||||
|
||||
public static String updateApprovedLoanExceedPrincipalFailure() {
|
||||
return "Failed data validation due to: less.than.disbursed.principal.and.capitalized.income.";
|
||||
}
|
||||
|
||||
public static String updateApprovedLoanLessMinAllowedAmountFailure() {
|
||||
return "The parameter `amount` must be greater than 0.";
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -61,5 +61,14 @@ public class ErrorResponse {
|
||||
public static class Error {
|
||||
|
||||
private String developerMessage;
|
||||
private List<ErrorMessageArg> args;
|
||||
}
|
||||
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ErrorMessageArg {
|
||||
|
||||
private Object value;
|
||||
}
|
||||
}
|
||||
|
||||
+98
@@ -24,6 +24,7 @@ import static org.apache.fineract.test.data.loanproduct.DefaultLoanProduct.LP2_A
|
||||
import static org.apache.fineract.test.data.loanproduct.DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR;
|
||||
import static org.apache.fineract.test.data.loanproduct.DefaultLoanProduct.LP2_ADV_PYMNT_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR;
|
||||
import static org.apache.fineract.test.factory.LoanProductsRequestFactory.CHARGE_OFF_REASONS;
|
||||
import static org.apache.fineract.test.factory.LoanProductsRequestFactory.LOCALE_EN;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -98,6 +99,8 @@ import org.apache.fineract.client.models.PostLoansRequestChargeData;
|
||||
import org.apache.fineract.client.models.PostLoansResponse;
|
||||
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
||||
import org.apache.fineract.client.models.PutLoanProductsProductIdResponse;
|
||||
import org.apache.fineract.client.models.PutLoansApprovedAmountRequest;
|
||||
import org.apache.fineract.client.models.PutLoansApprovedAmountResponse;
|
||||
import org.apache.fineract.client.models.PutLoansLoanIdRequest;
|
||||
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.services.BusinessDateManagementApi;
|
||||
@@ -574,6 +577,12 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
createFullyCustomizedLoanWithExpectedTrancheDisbursementsDetails(data.get(1));
|
||||
}
|
||||
|
||||
@When("Admin creates a fully customized loan with three expected disbursements details and following data:")
|
||||
public void createFullyCustomizedLoanWithThreeDisbursementsDetails(final DataTable table) throws IOException {
|
||||
final List<List<String>> data = table.asLists();
|
||||
createFullyCustomizedLoanWithThreeExpectedTrancheDisbursementsDetails(data.get(1));
|
||||
}
|
||||
|
||||
@When("Admin creates a fully customized loan with forced disabled downpayment with the following data:")
|
||||
public void createFullyCustomizedLoanWithForcedDisabledDownpayment(DataTable table) throws IOException {
|
||||
List<List<String>> data = table.asLists();
|
||||
@@ -3688,6 +3697,27 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
createFullyCustomizedLoanExpectsTrancheDisbursementDetails(loanData, disbursementDetail);
|
||||
}
|
||||
|
||||
public void createFullyCustomizedLoanWithThreeExpectedTrancheDisbursementsDetails(final List<String> loanData) throws IOException {
|
||||
final String expectedDisbursementDateFirstDisbursal = loanData.get(16);
|
||||
final Double disbursementPrincipalAmountFirstDisbursal = Double.valueOf(loanData.get(17));
|
||||
|
||||
final String expectedDisbursementDateSecondDisbursal = loanData.get(18);
|
||||
final Double disbursementPrincipalAmountSecondDisbursal = Double.valueOf(loanData.get(19));
|
||||
|
||||
final String expectedDisbursementDateThirdDisbursal = loanData.get(20);
|
||||
final Double disbursementPrincipalAmountThirdDisbursal = Double.valueOf(loanData.get(21));
|
||||
|
||||
List<PostLoansDisbursementData> disbursementDetail = new ArrayList<>();
|
||||
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateFirstDisbursal)
|
||||
.principal(BigDecimal.valueOf(disbursementPrincipalAmountFirstDisbursal)));
|
||||
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateSecondDisbursal)
|
||||
.principal(BigDecimal.valueOf(disbursementPrincipalAmountSecondDisbursal)));
|
||||
disbursementDetail.add(new PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateThirdDisbursal)
|
||||
.principal(BigDecimal.valueOf(disbursementPrincipalAmountThirdDisbursal)));
|
||||
|
||||
createFullyCustomizedLoanExpectsTrancheDisbursementDetails(loanData, disbursementDetail);
|
||||
}
|
||||
|
||||
public void createFullyCustomizedLoanExpectsTrancheDisbursementDetails(final List<String> loanData,
|
||||
List<PostLoansDisbursementData> disbursementDetail) throws IOException {
|
||||
final String loanProduct = loanData.get(0);
|
||||
@@ -4859,4 +4889,72 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
log.debug("BuyDown Fee Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId());
|
||||
}
|
||||
|
||||
@Then("Update loan approved amount with new amount {string} value")
|
||||
public void updateLoanApprovedAmount(final String amount) throws IOException {
|
||||
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||
final long loanId = loanResponse.body().getLoanId();
|
||||
final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
|
||||
|
||||
final PutLoansApprovedAmountRequest modifyLoanApprovedAmountRequest = new PutLoansApprovedAmountRequest().locale(LOCALE_EN)
|
||||
.amount(new BigDecimal(amount));
|
||||
|
||||
final Response<PutLoansApprovedAmountResponse> modifyLoanApprovedAmountResponse = loansApi
|
||||
.modifyLoanApprovedAmount(loanId, modifyLoanApprovedAmountRequest).execute();
|
||||
|
||||
ErrorHelper.checkSuccessfulApiCall(modifyLoanApprovedAmountResponse);
|
||||
|
||||
}
|
||||
|
||||
@Then("Update loan approved amount is forbidden with amount {string} due to exceed applied amount")
|
||||
public void updateLoanApprovedAmountForbiddenExceedAppliedAmount(final String amount) throws IOException {
|
||||
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||
final long loanId = loanResponse.body().getLoanId();
|
||||
final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
|
||||
|
||||
final PutLoansApprovedAmountRequest modifyLoanApprovedAmountRequest = new PutLoansApprovedAmountRequest().locale(LOCALE_EN)
|
||||
.amount(new BigDecimal(amount));
|
||||
|
||||
final Response<PutLoansApprovedAmountResponse> modifyLoanApprovedAmountResponse = loansApi
|
||||
.modifyLoanApprovedAmount(loanId, modifyLoanApprovedAmountRequest).execute();
|
||||
|
||||
ErrorResponse errorDetails = ErrorResponse.from(modifyLoanApprovedAmountResponse);
|
||||
assertThat(errorDetails.getHttpStatusCode()).isEqualTo(403);
|
||||
|
||||
Object errorArgs = errorDetails.getErrors().getFirst().getArgs().getFirst().getValue();
|
||||
String developerMessage;
|
||||
if (errorArgs instanceof Map errorArgsMap) {
|
||||
developerMessage = (String) errorArgsMap.get("developerMessage");
|
||||
} else {
|
||||
developerMessage = errorDetails.getDeveloperMessage();
|
||||
}
|
||||
assertThat(developerMessage).isEqualTo(ErrorMessageHelper.updateApprovedLoanExceedPrincipalFailure());
|
||||
}
|
||||
|
||||
@Then("Update loan approved amount is forbidden with amount {string} due to min allowed amount")
|
||||
public void updateLoanApprovedAmountForbiddenMinAllowedAmount(final String amount) throws IOException {
|
||||
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||
final long loanId = loanResponse.body().getLoanId();
|
||||
final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
|
||||
|
||||
final PutLoansApprovedAmountRequest modifyLoanApprovedAmountRequest = new PutLoansApprovedAmountRequest().locale(LOCALE_EN)
|
||||
.amount(new BigDecimal(amount));
|
||||
|
||||
final Response<PutLoansApprovedAmountResponse> modifyLoanApprovedAmountResponse = loansApi
|
||||
.modifyLoanApprovedAmount(loanId, modifyLoanApprovedAmountRequest).execute();
|
||||
|
||||
ErrorResponse errorDetails = ErrorResponse.from(modifyLoanApprovedAmountResponse);
|
||||
assertThat(errorDetails.getHttpStatusCode()).isEqualTo(400);
|
||||
|
||||
Object errorArgs = errorDetails.getErrors().getFirst().getArgs().getFirst().getValue();
|
||||
String developerMessage;
|
||||
if (errorArgs instanceof Map errorArgsMap) {
|
||||
developerMessage = (String) errorArgsMap.get("developerMessage");
|
||||
} else {
|
||||
developerMessage = errorDetails.getDeveloperMessage();
|
||||
}
|
||||
assertThat(developerMessage).isEqualTo(ErrorMessageHelper.updateApprovedLoanLessMinAllowedAmountFailure());
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.infrastructure.event.business.domain.loan;
|
||||
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
|
||||
public class LoanApprovedAmountChangedBusinessEvent extends LoanBusinessEvent {
|
||||
|
||||
private static final String TYPE = "LoanApprovedAmountChangedBusinessEvent";
|
||||
|
||||
public LoanApprovedAmountChangedBusinessEvent(Loan value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
|
||||
/**
|
||||
* Immutable object representing an Approved Amount change operation on a Loan
|
||||
*
|
||||
* Note: no getter/setters required as google-gson will produce json from fields of object.
|
||||
*/
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
public class LoanApprovedAmountHistoryData implements Serializable {
|
||||
|
||||
private Long loanId;
|
||||
private ExternalId externalLoanId;
|
||||
private BigDecimal newApprovedAmount;
|
||||
private BigDecimal oldApprovedAmount;
|
||||
private OffsetDateTime dateOfChange;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_loan_approved_amount_history")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoanApprovedAmountHistory extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "loan_id", nullable = false)
|
||||
private Long loanId;
|
||||
|
||||
@Column(name = "new_approved_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal newApprovedAmount;
|
||||
|
||||
@Column(name = "old_approved_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal oldApprovedAmount;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
public interface LoanApprovedAmountHistoryRepository
|
||||
extends JpaRepository<LoanApprovedAmountHistory, Long>, JpaSpecificationExecutor<LoanApprovedAmountHistory> {
|
||||
|
||||
@Query("""
|
||||
SELECT NEW org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData(
|
||||
laah.loanId, l.externalId, laah.newApprovedAmount, laah.oldApprovedAmount, laah.createdDate
|
||||
)
|
||||
FROM LoanApprovedAmountHistory laah JOIN Loan l on laah.loanId = l.id
|
||||
WHERE laah.loanId = :loanId
|
||||
""")
|
||||
List<LoanApprovedAmountHistoryData> findAllByLoanId(Long loanId, Pageable pageable);
|
||||
}
|
||||
+42
@@ -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_APPROVED_AMOUNT", action = "UPDATE")
|
||||
public class LoanApprovedAmountModificationCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final LoanApprovedAmountWritePlatformService loanApprovedAmountWritePlatformService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult processCommand(JsonCommand command) {
|
||||
return loanApprovedAmountWritePlatformService.modifyLoanApprovedAmount(command.getLoanId(), command);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.serialization;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
|
||||
public interface LoanApprovedAmountValidator {
|
||||
|
||||
void validateLoanApprovedAmountModification(JsonCommand command);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface LoanApprovedAmountWritePlatformService {
|
||||
|
||||
CommandProcessingResult modifyLoanApprovedAmount(Long loanId, JsonCommand command);
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApprovedAmountChangedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
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.serialization.LoanApprovedAmountValidator;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoanApprovedAmountWritePlatformServiceImpl implements LoanApprovedAmountWritePlatformService {
|
||||
|
||||
private final LoanAssembler loanAssembler;
|
||||
private final LoanApprovedAmountValidator loanApprovedAmountValidator;
|
||||
private final LoanApprovedAmountHistoryRepository loanApprovedAmountHistoryRepository;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult modifyLoanApprovedAmount(final Long loanId, final JsonCommand command) {
|
||||
// API rule validations
|
||||
this.loanApprovedAmountValidator.validateLoanApprovedAmountModification(command);
|
||||
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
changes.put("newApprovedAmount", command.stringValueOfParameterNamed(LoanApiConstants.amountParameterName));
|
||||
changes.put("locale", command.locale());
|
||||
|
||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
changes.put("oldApprovedAmount", loan.getApprovedPrincipal());
|
||||
|
||||
BigDecimal newApprovedAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.amountParameterName);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
+1
@@ -86,6 +86,7 @@
|
||||
<class>org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.Loan</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistory</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCharge</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement</class>
|
||||
|
||||
+69
@@ -127,12 +127,14 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.GlimRepaymentTemplate;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistoryRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
|
||||
@@ -176,6 +178,8 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -303,6 +307,7 @@ public class LoansApiResource {
|
||||
private final LoanTermVariationsRepository loanTermVariationsRepository;
|
||||
private final LoanSummaryProviderDelegate loanSummaryProviderDelegate;
|
||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||
private final LoanApprovedAmountHistoryRepository loanApprovedAmountHistoryRepository;
|
||||
|
||||
/*
|
||||
* This template API is used for loan approval, ideally this should be invoked on loan that are pending for
|
||||
@@ -872,6 +877,55 @@ public class LoansApiResource {
|
||||
return createLoanDelinquencyAction(null, ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{loanId}/approved-amount")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Modifies the approved amount of the loan", description = "")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansApprovedAmountRequest.class)))
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansApprovedAmountResponse.class))) })
|
||||
public CommandProcessingResult modifyLoanApprovedAmount(
|
||||
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, @Context final UriInfo uriInfo,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
return modifyLoanApprovedAmount(loanId, ExternalId.empty(), apiRequestBodyAsJson);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("external-id/{loanExternalId}/approved-amount")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Modifies the approved amount of the loan", description = "")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansApprovedAmountRequest.class)))
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansApprovedAmountResponse.class))) })
|
||||
public CommandProcessingResult modifyLoanApprovedAmount(
|
||||
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
|
||||
@Context final UriInfo uriInfo, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
return modifyLoanApprovedAmount(null, ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{loanId}/approved-amount")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Collects and returns the approved amount modification history for a given loan", description = "")
|
||||
public List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(
|
||||
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, @Context final UriInfo uriInfo) {
|
||||
return getLoanApprovedAmountHistory(loanId, ExternalId.empty());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("external-id/{loanExternalId}/approved-amount")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Collects and returns the approved amount modification history for a given loan", description = "")
|
||||
public List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(
|
||||
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
return getLoanApprovedAmountHistory(null, ExternalIdFactory.produce(loanExternalId));
|
||||
}
|
||||
|
||||
private String retrieveApprovalTemplate(final Long loanId, final String loanExternalIdStr, final String templateType,
|
||||
final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
@@ -1294,4 +1348,19 @@ public class LoansApiResource {
|
||||
return delinquencyActionSerializer.serialize(result);
|
||||
}
|
||||
|
||||
private CommandProcessingResult modifyLoanApprovedAmount(Long loanId, ExternalId loanExternalId, String apiRequestBodyAsJson) {
|
||||
Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
|
||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||
CommandWrapper commandRequest = builder.updateLoanApprovedAmount(resolvedLoanId).build();
|
||||
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
|
||||
private List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(Long loanId, ExternalId loanExternalId) {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
|
||||
Pageable sortedByCreationDate = Pageable.unpaged(Sort.by("createdDate").ascending());
|
||||
return loanApprovedAmountHistoryRepository.findAllByLoanId(resolvedLoanId, sortedByCreationDate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+43
@@ -1771,4 +1771,47 @@ final class LoansApiResourceSwagger {
|
||||
@Schema(description = "PostLoansLoanIdChanges")
|
||||
public PostLoansLoanIdChanges changes;
|
||||
}
|
||||
|
||||
@Schema(description = "PutLoansApprovedAmountRequest")
|
||||
public static final class PutLoansApprovedAmountRequest {
|
||||
|
||||
private PutLoansApprovedAmountRequest() {}
|
||||
|
||||
@Schema(example = "1000")
|
||||
public BigDecimal amount;
|
||||
@Schema(example = "en")
|
||||
public String locale;
|
||||
}
|
||||
|
||||
@Schema(description = "PutLoansApprovedAmountResponse")
|
||||
public static final class PutLoansApprovedAmountResponse {
|
||||
|
||||
private PutLoansApprovedAmountResponse() {}
|
||||
|
||||
static final class PutLoansApprovedAmountChanges {
|
||||
|
||||
private PutLoansApprovedAmountChanges() {}
|
||||
|
||||
@Schema(example = "1000")
|
||||
public BigDecimal oldApprovedAmount;
|
||||
@Schema(example = "1000")
|
||||
public BigDecimal newApprovedAmount;
|
||||
@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 = "PutLoansApprovedAmountChanges")
|
||||
public PutLoansApprovedAmountChanges changes;
|
||||
}
|
||||
}
|
||||
|
||||
+23
-2
@@ -2174,12 +2174,33 @@ public final class LoanApplicationValidator {
|
||||
|
||||
public BigDecimal getOverAppliedMax(Loan loan) {
|
||||
LoanProduct loanProduct = loan.getLoanProduct();
|
||||
|
||||
// Check if overapplied calculation type and number are properly configured
|
||||
if (loanProduct.getOverAppliedCalculationType() == null || loanProduct.getOverAppliedNumber() == null) {
|
||||
// If overapplied calculation is not configured, return proposed principal (original behavior)
|
||||
return loan.getProposedPrincipal();
|
||||
}
|
||||
|
||||
// For loans with approved amount modifications, use proposed principal as base to allow
|
||||
// disbursement up to the originally requested amount regardless of the reduced approved amount
|
||||
boolean hasApprovedAmountModification = loan.getApprovedPrincipal() != null && loan.getProposedPrincipal() != null
|
||||
&& loan.getApprovedPrincipal().compareTo(loan.getProposedPrincipal()) != 0;
|
||||
|
||||
BigDecimal basePrincipal;
|
||||
if (hasApprovedAmountModification) {
|
||||
// Use proposed principal for loans with approved amount modifications
|
||||
basePrincipal = loan.getProposedPrincipal();
|
||||
} else {
|
||||
// Use approved principal for normal loans
|
||||
basePrincipal = loan.getApprovedPrincipal() != null ? loan.getApprovedPrincipal() : loan.getProposedPrincipal();
|
||||
}
|
||||
|
||||
if ("percentage".equals(loanProduct.getOverAppliedCalculationType())) {
|
||||
BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber());
|
||||
BigDecimal totalPercentage = BigDecimal.valueOf(1).add(overAppliedNumber.divide(BigDecimal.valueOf(100)));
|
||||
return loan.getProposedPrincipal().multiply(totalPercentage);
|
||||
return basePrincipal.multiply(totalPercentage);
|
||||
} else {
|
||||
return loan.getProposedPrincipal().add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber()));
|
||||
return basePrincipal.add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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.serialization;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
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.LoanRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class LoanApprovedAmountValidatorImpl implements LoanApprovedAmountValidator {
|
||||
|
||||
private static final Set<LoanStatus> INVALID_LOAN_STATUSES_FOR_APPROVED_AMOUNT_MODIFICATION = Set.of(LoanStatus.INVALID,
|
||||
LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.REJECTED);
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final LoanRepository loanRepository;
|
||||
private final LoanApplicationValidator loanApplicationValidator;
|
||||
|
||||
@Override
|
||||
public void validateLoanApprovedAmountModification(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 newApprovedAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.amountParameterName,
|
||||
element);
|
||||
|
||||
Validator.validateOrThrow("loan.approved.amount", baseDataValidator -> {
|
||||
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName).value(newApprovedAmount).notNull();
|
||||
});
|
||||
|
||||
Validator.validateOrThrowDomainViolation("loan.approved.amount", baseDataValidator -> {
|
||||
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName).value(newApprovedAmount).positiveAmount();
|
||||
|
||||
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.approved.amount.modification");
|
||||
}
|
||||
|
||||
BigDecimal maximumThresholdForApprovedAmount;
|
||||
if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
|
||||
maximumThresholdForApprovedAmount = loanApplicationValidator.getOverAppliedMax(loan);
|
||||
} else {
|
||||
maximumThresholdForApprovedAmount = loan.getProposedPrincipal();
|
||||
}
|
||||
|
||||
if (MathUtil.isGreaterThan(newApprovedAmount, maximumThresholdForApprovedAmount)) {
|
||||
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
|
||||
.failWithCode("can't.be.greater.than.maximum.applied.loan.amount.calculation");
|
||||
}
|
||||
|
||||
BigDecimal totalPrincipalOnLoan = loan.getSummary().getTotalPrincipal();
|
||||
if (MathUtil.isLessThan(newApprovedAmount, totalPrincipalOnLoan)) {
|
||||
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
|
||||
.failWithCode("less.than.disbursed.principal.and.capitalized.income");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -44,7 +44,7 @@ public final class LoanDisbursementValidator {
|
||||
} else {
|
||||
if ((totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0)
|
||||
|| (totalDisbursed.add(capitalizedIncome).compareTo(loan.getApprovedPrincipal()) > 0)) {
|
||||
final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved principal ";
|
||||
final String errorMsg = "Loan can't be disbursed, disburse amount is exceeding approved principal.";
|
||||
throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed,
|
||||
loan.getApprovedPrincipal());
|
||||
}
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ public final class LoanTransactionValidatorImpl implements LoanTransactionValida
|
||||
validateLoanClientIsActive(loan);
|
||||
validateLoanGroupIsActive(loan);
|
||||
|
||||
final BigDecimal disbursedAmount = loan.getDisbursedAmount();
|
||||
final BigDecimal disbursedAmount = loan.getSummary().getTotalPrincipalDisbursed();
|
||||
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, principal, disbursedAmount);
|
||||
|
||||
if (loan.isChargedOff()) {
|
||||
|
||||
+1
@@ -161,6 +161,7 @@ public class LoanDisbursementService {
|
||||
} else {
|
||||
loan.getLoanRepaymentScheduleDetail()
|
||||
.setPrincipal(loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(diff).getAmount());
|
||||
totalAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount();
|
||||
}
|
||||
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount);
|
||||
}
|
||||
|
||||
+1
-1
@@ -432,7 +432,7 @@ public class LoanAccountConfiguration {
|
||||
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService,
|
||||
LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper,
|
||||
LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService,
|
||||
LoanTransactionService loanTransactionService, BuyDownFeePlatformService buyDownFeePlatformService) {
|
||||
LoanTransactionService loanTransactionService) {
|
||||
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
|
||||
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
|
||||
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
|
||||
|
||||
@@ -209,4 +209,6 @@
|
||||
<include file="parts/0188_create_loan_buy_down_fee_balance.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0189_add_loan_buydown_fee_event.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0192_create_loan_approved_amount_history.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet id="1" author="fineract">
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanApprovedAmountChangedBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1" context="postgresql">
|
||||
<createTable tableName="m_loan_approved_amount_history">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_m_loan_approved_amount_history"/>
|
||||
</column>
|
||||
<column name="loan_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="new_approved_amount" type="DECIMAL(19, 6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="old_approved_amount" type="DECIMAL(19, 6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT"/>
|
||||
<column name="created_on_utc" type="timestamp with time zone"/>
|
||||
<column name="last_modified_by" type="BIGINT"/>
|
||||
<column name="last_modified_on_utc" type="timestamp with time zone"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="2" context="mysql">
|
||||
<createTable tableName="m_loan_approved_amount_history">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_m_loan_approved_amount_history"/>
|
||||
</column>
|
||||
<column name="loan_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="new_approved_amount" type="DECIMAL(19, 6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="old_approved_amount" type="DECIMAL(19, 6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT"/>
|
||||
<column name="created_on_utc" type="DATETIME"/>
|
||||
<column name="last_modified_by" type="BIGINT"/>
|
||||
<column name="last_modified_on_utc" type="DATETIME"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="3">
|
||||
<addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_loan_approved_amount_history"
|
||||
constraintName="FK_loan_approved_amount_history_created_by" deferrable="false" initiallyDeferred="false"
|
||||
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
|
||||
referencedTableName="m_appuser" validate="true"/>
|
||||
<addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_loan_approved_amount_history"
|
||||
constraintName="FK_loan_approved_amount_history_last_modified_by" deferrable="false" initiallyDeferred="false"
|
||||
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
|
||||
referencedTableName="m_appuser" validate="true"/>
|
||||
<addForeignKeyConstraint baseColumnNames="loan_id" baseTableName="m_loan_approved_amount_history"
|
||||
constraintName="FK_loan_approved_amount_history_loan_id" deferrable="false" initiallyDeferred="false"
|
||||
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
|
||||
referencedTableName="m_loan" validate="true"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+2
-2
@@ -112,7 +112,7 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
|
||||
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
|
||||
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
|
||||
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
|
||||
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
@@ -206,7 +206,7 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
|
||||
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
|
||||
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
|
||||
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
|
||||
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
|
||||
+12
@@ -67,6 +67,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdStatus;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
|
||||
import org.apache.fineract.client.models.JournalEntryTransactionItem;
|
||||
import org.apache.fineract.client.models.LoanApprovedAmountHistoryData;
|
||||
import org.apache.fineract.client.models.LoanPointInTimeData;
|
||||
import org.apache.fineract.client.models.PaymentAllocationOrder;
|
||||
import org.apache.fineract.client.models.PostChargesResponse;
|
||||
@@ -81,6 +82,8 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionI
|
||||
import org.apache.fineract.client.models.PostLoansRequest;
|
||||
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.PutLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.RetrieveLoansPointInTimeRequest;
|
||||
import org.apache.fineract.client.util.CallFailedRuntimeException;
|
||||
@@ -777,6 +780,15 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
return Calls.ok(fineractClient().loansPointInTimeApi.retrieveLoansPointInTime(request));
|
||||
}
|
||||
|
||||
protected PutLoansApprovedAmountResponse modifyLoanApprovedAmount(Long loanId, BigDecimal approvedAmount) {
|
||||
PutLoansApprovedAmountRequest request = new PutLoansApprovedAmountRequest().amount(approvedAmount).locale("en");
|
||||
return Calls.ok(fineractClient().loans.modifyLoanApprovedAmount(loanId, request));
|
||||
}
|
||||
|
||||
protected List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(Long loanId) {
|
||||
return Calls.ok(fineractClient().loans.getLoanApprovedAmountHistory(loanId));
|
||||
}
|
||||
|
||||
protected void verifyOutstanding(LoanPointInTimeData loan, OutstandingAmounts outstanding) {
|
||||
assertThat(BigDecimal.valueOf(outstanding.principalOutstanding))
|
||||
.isEqualByComparingTo(loan.getPrincipal().getPrincipalOutstanding());
|
||||
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
/**
|
||||
* 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.integrationtests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
|
||||
import org.apache.fineract.client.models.LoanApprovedAmountHistoryData;
|
||||
import org.apache.fineract.client.models.PostClientsResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansDisbursementData;
|
||||
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.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 {
|
||||
|
||||
@Test
|
||||
public void testValidLoanApprovedAmountModification() {
|
||||
BigDecimal sixHundred = BigDecimal.valueOf(600.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");
|
||||
PutLoansApprovedAmountResponse putLoansApprovedAmountResponse = modifyLoanApprovedAmount(loanId, sixHundred);
|
||||
|
||||
Assertions.assertEquals(loanId, putLoansApprovedAmountResponse.getResourceId());
|
||||
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges());
|
||||
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges().getNewApprovedAmount());
|
||||
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges().getOldApprovedAmount());
|
||||
Assertions.assertEquals(sixHundred,
|
||||
putLoansApprovedAmountResponse.getChanges().getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
|
||||
Assertions.assertEquals(thousand,
|
||||
putLoansApprovedAmountResponse.getChanges().getOldApprovedAmount().setScale(1, RoundingMode.HALF_UP));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoanApprovedAmountModificationEvent() {
|
||||
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();
|
||||
modifyLoanApprovedAmount(loanId, sixHundred);
|
||||
|
||||
verifyBusinessEvents(new LoanBusinessEvent("LoanApprovedAmountChangedBusinessEvent", "01 January 2024", 300, 100.0, 100.0));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidLoanApprovedAmountModificationInvalidRequest() {
|
||||
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,
|
||||
() -> modifyLoanApprovedAmount(loanId, null));
|
||||
|
||||
assertEquals(400, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("validation.msg.loan.approved.amount.amount.cannot.be.blank"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidLoanApprovedAmountModificationInvalidLoanStatus() {
|
||||
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,
|
||||
() -> modifyLoanApprovedAmount(postLoansResponse.getResourceId(), sixHundred));
|
||||
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage()
|
||||
.contains("validation.msg.loan.approved.amount.loan.status.not.valid.for.approved.amount.modification"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyLoanApprovedAmountTooHigh() {
|
||||
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,
|
||||
() -> modifyLoanApprovedAmount(loanId, twoThousand));
|
||||
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage()
|
||||
.contains("validation.msg.loan.approved.amount.amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyLoanApprovedAmountHigherButInRange() {
|
||||
BigDecimal thousand = BigDecimal.valueOf(1000.0);
|
||||
BigDecimal fifteenHundred = BigDecimal.valueOf(1500.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);
|
||||
|
||||
PutLoansApprovedAmountResponse putLoansApprovedAmountResponse = modifyLoanApprovedAmount(loanId, fifteenHundred);
|
||||
|
||||
Assertions.assertEquals(loanId, putLoansApprovedAmountResponse.getResourceId());
|
||||
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges());
|
||||
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges().getNewApprovedAmount());
|
||||
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges().getOldApprovedAmount());
|
||||
Assertions.assertEquals(fifteenHundred,
|
||||
putLoansApprovedAmountResponse.getChanges().getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
|
||||
Assertions.assertEquals(thousand,
|
||||
putLoansApprovedAmountResponse.getChanges().getOldApprovedAmount().setScale(1, RoundingMode.HALF_UP));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyLoanApprovedAmountWithNegativeAmount() {
|
||||
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,
|
||||
() -> modifyLoanApprovedAmount(loanId, sixHundred.negate()));
|
||||
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("validation.msg.loan.approved.amount.amount.not.greater.than.zero"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyLoanApprovedAmountCapitalizedIncomeCountsAsPrincipal() {
|
||||
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,
|
||||
() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(500.0)));
|
||||
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage()
|
||||
.contains("validation.msg.loan.approved.amount.amount.less.than.disbursed.principal.and.capitalized.income"));
|
||||
|
||||
loanTransactionHelper.reverseLoanTransaction(capitalizedIncomeResponse.getLoanId(), capitalizedIncomeResponse.getResourceId(),
|
||||
"1 January 2024");
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(500.0)));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyLoanApprovedAmountFutureExpectedDisbursementsCountAsPrincipal() {
|
||||
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(1000.0)))));
|
||||
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(500.0)));
|
||||
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage()
|
||||
.contains("validation.msg.loan.approved.amount.amount.less.than.disbursed.principal.and.capitalized.income"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyLoanApprovedAmountCreatesHistoryEntries() {
|
||||
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);
|
||||
|
||||
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(800.0));
|
||||
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(600.0));
|
||||
modifyLoanApprovedAmount(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));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisbursementValidationAfterApprovedAmountReduction() {
|
||||
// Test that disbursement validation properly respects reduced approved amounts
|
||||
// Scenario: Reduce approved amount and verify disbursements are limited to new amount
|
||||
|
||||
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
|
||||
runAt("1 January 2024", () -> {
|
||||
// Create loan with applied amount $1000
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
|
||||
1000.0, 10.0, 4, null);
|
||||
|
||||
// Reduce approved amount to $900
|
||||
PutLoansApprovedAmountResponse modifyResponse = modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(900.0));
|
||||
assertEquals(BigDecimal.valueOf(900.0), modifyResponse.getChanges().getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
|
||||
|
||||
// Disburse $100 (should work as it's within approved amount)
|
||||
Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024"),
|
||||
"Should be able to disburse $100 after reducing approved amount to $900");
|
||||
|
||||
// Disburse additional $250 (total $350, should work as it's within proposed $1000 × 150% = $1350)
|
||||
Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, BigDecimal.valueOf(250), "1 January 2024"),
|
||||
"Should be able to disburse additional $250 (total $350) within allowed limit");
|
||||
|
||||
// Try to disburse additional $1200 (total $1550, should fail as it exceeds $1000 × 150% = $1350)
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> disburseLoan(loanId, BigDecimal.valueOf(1200), "1 January 2024"));
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"),
|
||||
"Should fail when total disbursements exceed modified approved amount × over-applied percentage");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressiveDisbursementsWithDynamicApprovedAmountChanges() {
|
||||
// Test multiple disbursements with increasing and decreasing approved amount modifications
|
||||
// Validates that each disbursement respects the current approved amount limits
|
||||
|
||||
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
|
||||
runAt("1 January 2024", () -> {
|
||||
// Create loan with $1000 applied amount
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
|
||||
1000.0, 10.0, 4, null);
|
||||
|
||||
// First disbursement: $300
|
||||
disburseLoan(loanId, BigDecimal.valueOf(300), "1 January 2024");
|
||||
|
||||
// Increase approved amount to $1200
|
||||
PutLoansApprovedAmountResponse increaseResponse = modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(1200.0));
|
||||
assertEquals(BigDecimal.valueOf(1200.0),
|
||||
increaseResponse.getChanges().getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
|
||||
|
||||
// Second disbursement: $400 (total $700, within $1200)
|
||||
Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, BigDecimal.valueOf(400), "1 January 2024"));
|
||||
|
||||
// Reduce approved amount to $800
|
||||
PutLoansApprovedAmountResponse reduceResponse = modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(800.0));
|
||||
assertEquals(BigDecimal.valueOf(800.0), reduceResponse.getChanges().getNewApprovedAmount().setScale(1, RoundingMode.HALF_UP));
|
||||
|
||||
// Third disbursement: $100 (total $800, within proposed $1000 × 150% = $1500)
|
||||
Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024"));
|
||||
|
||||
// Fourth disbursement: $800 (total $1600, should fail as it exceeds $1000 × 150% = $1500)
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> disburseLoan(loanId, BigDecimal.valueOf(800), "1 January 2024"));
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApprovedAmountModificationWithCapitalizedIncomeScenario() {
|
||||
// Test approved amount modification interaction with capitalized income
|
||||
|
||||
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
|
||||
.createLoanProduct(create4IProgressiveWithCapitalizedIncome());
|
||||
runAt("1 January 2024", () -> {
|
||||
// Create loan with $1000 applied amount
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
|
||||
1000.0, 10.0, 4, null);
|
||||
|
||||
// Disburse $300
|
||||
disburseLoan(loanId, BigDecimal.valueOf(300), "1 January 2024");
|
||||
|
||||
// Add capitalized income of $200 (total disbursed equivalent: $500)
|
||||
loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 200.0);
|
||||
|
||||
// Try to reduce approved amount to $400 (should fail as disbursed + capitalized = $500)
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(400.0)));
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage()
|
||||
.contains("validation.msg.loan.approved.amount.amount.less.than.disbursed.principal.and.capitalized.income"));
|
||||
|
||||
// Should succeed with $500 (exactly matching disbursed + capitalized)
|
||||
Assertions.assertDoesNotThrow(() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(500.0)));
|
||||
|
||||
// Should succeed with $600 (above disbursed + capitalized)
|
||||
Assertions.assertDoesNotThrow(() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(600.0)));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUndoDisbursementAfterApprovedAmountReduction() {
|
||||
// Test undo disbursement functionality after approved amount reduction
|
||||
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);
|
||||
|
||||
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(800.0));
|
||||
disburseLoan(loanId, BigDecimal.valueOf(600), "1 January 2024");
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||
if (loanDetails.getSummary() != null && loanDetails.getSummary().getPrincipalDisbursed() != null) {
|
||||
assertEquals(BigDecimal.valueOf(600.0), loanDetails.getSummary().getPrincipalDisbursed().setScale(1, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
PostLoansLoanIdRequest undoRequest = new PostLoansLoanIdRequest().note("Undo disbursement for testing");
|
||||
Assertions.assertDoesNotThrow(() -> loanTransactionHelper.undoDisbursalLoan(loanId, undoRequest));
|
||||
|
||||
GetLoansLoanIdResponse loanDetailsAfterUndo = loanTransactionHelper.getLoanDetails(loanId);
|
||||
BigDecimal activeDisbursedAmount = BigDecimal.ZERO;
|
||||
if (loanDetailsAfterUndo.getTransactions() != null && !loanDetailsAfterUndo.getTransactions().isEmpty()) {
|
||||
activeDisbursedAmount = loanDetailsAfterUndo.getTransactions().stream()
|
||||
.filter(transaction -> transaction.getType() != null && "Disbursement".equals(transaction.getType().getValue()))
|
||||
.filter(transaction -> !Boolean.TRUE.equals(transaction.getManuallyReversed()))
|
||||
.map(GetLoansLoanIdTransactions::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
assertEquals(0, BigDecimal.ZERO.compareTo(activeDisbursedAmount));
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(400.0)));
|
||||
|
||||
GetLoansLoanIdResponse finalLoanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||
assertEquals(BigDecimal.valueOf(400.0), finalLoanDetails.getApprovedPrincipal().setScale(1, RoundingMode.HALF_UP));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUndoLastDisbursementWithMultipleDisbursements() {
|
||||
// Test undo last disbursement in multi-disbursement scenario with approved amount modifications
|
||||
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(300), "1 January 2024");
|
||||
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(1200.0));
|
||||
disburseLoan(loanId, BigDecimal.valueOf(400), "1 January 2024");
|
||||
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(800.0));
|
||||
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||
if (loanDetails.getSummary() != null && loanDetails.getSummary().getPrincipalDisbursed() != null) {
|
||||
assertEquals(BigDecimal.valueOf(800.0), loanDetails.getSummary().getPrincipalDisbursed().setScale(1, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
PostLoansLoanIdRequest undoLastRequest = new PostLoansLoanIdRequest().note("Undo last disbursement");
|
||||
Assertions.assertDoesNotThrow(() -> loanTransactionHelper.undoLastDisbursalLoan(loanId, undoLastRequest));
|
||||
|
||||
GetLoansLoanIdResponse loanDetailsAfterUndo = loanTransactionHelper.getLoanDetails(loanId);
|
||||
BigDecimal activeDisbursedAmount = BigDecimal.ZERO;
|
||||
if (loanDetailsAfterUndo.getTransactions() != null && !loanDetailsAfterUndo.getTransactions().isEmpty()) {
|
||||
activeDisbursedAmount = loanDetailsAfterUndo.getTransactions().stream()
|
||||
.filter(transaction -> transaction.getType() != null && "Disbursement".equals(transaction.getType().getValue()))
|
||||
.filter(transaction -> !Boolean.TRUE.equals(transaction.getManuallyReversed()))
|
||||
.map(GetLoansLoanIdTransactions::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
assertEquals(BigDecimal.valueOf(700.0), activeDisbursedAmount.setScale(1, RoundingMode.HALF_UP));
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(700.0)));
|
||||
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(600.0)));
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage()
|
||||
.contains("validation.msg.loan.approved.amount.amount.less.than.disbursed.principal.and.capitalized.income"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisbursementValidationAfterUndoWithReducedApprovedAmount() {
|
||||
// Test disbursement validation after undo disbursement with reduced approved amount
|
||||
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);
|
||||
|
||||
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(600.0));
|
||||
disburseLoan(loanId, BigDecimal.valueOf(500), "1 January 2024");
|
||||
|
||||
PostLoansLoanIdRequest undoRequest = new PostLoansLoanIdRequest().note("Undo for testing validation");
|
||||
loanTransactionHelper.undoDisbursalLoan(loanId, undoRequest);
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, BigDecimal.valueOf(700), "1 January 2024"));
|
||||
|
||||
loanTransactionHelper.undoDisbursalLoan(loanId, undoRequest);
|
||||
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> disburseLoan(loanId, BigDecimal.valueOf(1600), "1 January 2024"));
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"));
|
||||
});
|
||||
}
|
||||
}
|
||||
+5
@@ -657,6 +657,11 @@ public class ExternalEventConfigurationHelper {
|
||||
loanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent);
|
||||
|
||||
Map<String, Object> loanApprovedAmountChangedBusinessEvent = new HashMap<>();
|
||||
loanApprovedAmountChangedBusinessEvent.put("type", "LoanApprovedAmountChangedBusinessEvent");
|
||||
loanApprovedAmountChangedBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanApprovedAmountChangedBusinessEvent);
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -29,7 +29,9 @@ import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||
import org.apache.fineract.client.models.PutLoansApprovedAmountRequest;
|
||||
import org.apache.fineract.client.util.Calls;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
|
||||
import org.apache.fineract.integrationtests.common.FineractClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
@@ -52,6 +54,13 @@ public class LoanTestLifecycleExtension implements AfterEachCallback {
|
||||
loanIds.forEach(loanId -> {
|
||||
GetLoansLoanIdResponse loanResponse = Calls
|
||||
.ok(FineractClientHelper.getFineractClient().loans.retrieveLoan((long) loanId, null, "all", null, null));
|
||||
if (MathUtil.isLessThan(loanResponse.getApprovedPrincipal(), loanResponse.getProposedPrincipal())) {
|
||||
// reset approved principal in case it's less than proposed principal so all expected disbursements
|
||||
// can be properly disbursed
|
||||
PutLoansApprovedAmountRequest request = new PutLoansApprovedAmountRequest().amount(loanResponse.getProposedPrincipal())
|
||||
.locale("en");
|
||||
Calls.ok(FineractClientHelper.getFineractClient().loans.modifyLoanApprovedAmount(loanId, request));
|
||||
}
|
||||
loanResponse.getDisbursementDetails().forEach(disbursementDetail -> {
|
||||
if (disbursementDetail.getActualDisbursementDate() == null) {
|
||||
loanTransactionHelper.disburseLoan((long) loanId,
|
||||
|
||||
Reference in New Issue
Block a user