FINERACT-2279: Contract Termination - Undo

This commit is contained in:
mariiaKraievska
2025-06-18 16:19:33 +02:00
committed by Adam Saghy
parent 11cecb0d45
commit ee29ce23ac
23 changed files with 412 additions and 3 deletions
@@ -3849,4 +3849,13 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId;
return this;
}
public CommandWrapperBuilder undoContractTermination(final Long loanId) {
this.actionName = "CONTRACT_TERMINATION_UNDO";
this.entityName = "LOAN";
this.entityId = loanId;
this.loanId = loanId;
this.href = "/loans/" + loanId;
return this;
}
}
@@ -305,6 +305,10 @@ public class LoanRequestFactory {
.locale(DEFAULT_LOCALE).note("Capitalized Income");
}
public static PostLoansLoanIdRequest defaultContractTerminationUndoRequest() {
return new PostLoansLoanIdRequest().note("Contract Termination Undo");
}
public static PostLoansLoanIdRequest defaultLoanContractTerminationRequest() {
return new PostLoansLoanIdRequest().dateFormat(DATE_FORMAT).locale(DEFAULT_LOCALE).note("Contract Termination");
}
@@ -79,6 +79,7 @@ import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransaction
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionMakeRepaymentPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionMerchantIssuedRefundPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionPayoutRefundPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanUndoContractTerminationBusinessEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import retrofit2.Response;
@@ -273,6 +274,10 @@ public class EventCheckHelper {
eventAssertionBuilder.extractingData(LoanTransactionAdjustmentDataV1::getNewTransactionDetail).isEqualTo(null);
}
public void loanUndoContractTerminationEventCheck(final GetLoansLoanIdTransactions transaction) {
eventAssertion.assertEventRaised(LoanUndoContractTerminationBusinessEvent.class, transaction.getId());
}
private boolean areBigDecimalValuesEqual(BigDecimal actual, BigDecimal expected) {
log.debug("--- Checking BigDecimal values.... ---");
log.debug("Actual: {}", actual);
@@ -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.test.messaging.event.loan.transaction;
public class LoanUndoContractTerminationBusinessEvent extends AbstractLoanTransactionEvent {
@Override
public String getEventName() {
return "LoanUndoContractTerminationBusinessEvent";
}
}
@@ -4471,6 +4471,35 @@ public class LoanStepDef extends AbstractStepDef {
.isEqualTo(transactionId);
}
@And("Admin successfully undoes loan contract termination")
public void undoLoanContractTermination() throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
assert loanResponse.body() != null;
final Response<PostLoansLoanIdResponse> loanContractTerminationResponse = testContext()
.get(TestContextKey.LOAN_CONTRACT_TERMINATION_RESPONSE);
assert loanContractTerminationResponse.body() != null;
final Long loanId = loanResponse.body().getLoanId();
final List<GetLoansLoanIdTransactions> transactions = Objects
.requireNonNull(loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute().body()).getTransactions();
assert transactions != null;
final GetLoansLoanIdTransactions targetTransaction = transactions.stream().filter(t -> {
assert t.getType() != null;
return Boolean.TRUE.equals(t.getType().getContractTermination());
}).findFirst().orElse(null);
final PostLoansLoanIdRequest request = LoanRequestFactory.defaultContractTerminationUndoRequest();
final Response<PostLoansLoanIdResponse> response = loansApi.stateTransitions(loanId, request, "undoContractTermination").execute();
testContext().set(TestContextKey.LOAN_UNDO_CONTRACT_TERMINATION_RESPONSE, response);
ErrorHelper.checkSuccessfulApiCall(response);
assert targetTransaction != null;
eventCheckHelper.checkTransactionWithLoanTransactionAdjustmentBizEvent(targetTransaction);
eventCheckHelper.loanUndoContractTerminationEventCheck(targetTransaction);
eventCheckHelper.loanBalanceChangedEventCheck(loanId);
}
@Then("LoanTransactionContractTerminationPostBusinessEvent is raised on {string}")
public void checkLoanTransactionContractTerminationPostBusinessEvent(final String date) throws IOException {
final Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
@@ -228,4 +228,5 @@ public abstract class TestContextKey {
public static final String LOAN_CAPITALIZED_INCOME_ADJUSTMENT_RESPONSE = "loanCapitalizedIncomeAdjustmentResponse";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationContractTermination";
public static final String LOAN_CONTRACT_TERMINATION_RESPONSE = "loanContractTerminationResponse";
public static final String LOAN_UNDO_CONTRACT_TERMINATION_RESPONSE = "loanUndoContractTerminationResponse";
}
@@ -1047,4 +1047,82 @@ Feature: Contract Termination
| 31 March 2024 | Accrual | 1.72 | 0.0 | 1.72 | 0.0 | 0.0 | 0.0 | false | false |
| 31 March 2024 | Accrual Adjustment | 0.57 | 0.0 | 0.57 | 0.0 | 0.0 | 0.0 | false | false |
| 31 March 2024 | Contract Termination | 31.15 | 31.07 | 0.08 | 0.0 | 0.0 | 0.0 | false | true |
And Global configuration "is-principal-compounding-disabled-for-overdue-loans" is disabled
And Global configuration "is-principal-compounding-disabled-for-overdue-loans" is disabled
@TestRailId:C3738
Scenario: Verify contract termination undo when interest recalculation is enabled - case when contract termination occurs on due date
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
When Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 17.01 | 0.0 | 0.0 | 85.04 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
When Admin sets the business date to "1 March 2024"
And Admin successfully terminates loan contract
Then Loan Repayment schedule has 2 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | | 0.0 | 83.57 | 0.49 | 0.0 | 0.0 | 84.06 | 0.0 | 0.0 | 0.0 | 84.06 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 1.07 | 0.0 | 0.0 | 101.07 | 17.01 | 0.0 | 0.0 | 84.06 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false | false |
| 01 March 2024 | Contract Termination | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | false |
When Admin successfully undoes loan contract termination
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 17.01 | 0.0 | 0.0 | 85.04 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false | false |
| 01 March 2024 | Contract Termination | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | true | false |
@@ -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.transaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class LoanUndoContractTerminationBusinessEvent extends LoanTransactionBusinessEvent {
private static final String TYPE = "LoanUndoContractTerminationBusinessEvent";
public LoanUndoContractTerminationBusinessEvent(LoanTransaction value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -180,6 +180,7 @@ public interface LoanApiConstants {
String CAPITALIZED_INCOME_TRANSACTION_COMMAND = "capitalizedIncome";
String CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND = "capitalizedIncomeAdjustment";
String CONTRACT_TERMINATION_COMMAND = "contractTermination";
String UNDO_CONTRACT_TERMINATION_COMMAND = "undoContractTermination";
// Data Validator names
String LOAN_FRAUD_DATAVALIDATOR_PREFIX = "loans.fraud";
@@ -71,6 +71,7 @@ public class LoanTransactionEnumData implements Serializable {
private final boolean capitalizedIncomeAmortization;
private final boolean capitalizedIncomeAdjustment;
private final boolean capitalizedIncomeAmortizationAdjustment;
private final boolean contractTermination;
public LoanTransactionEnumData(final Long id, final String code, final String value) {
this.id = id;
@@ -112,6 +113,7 @@ public class LoanTransactionEnumData implements Serializable {
this.capitalizedIncomeAdjustment = Long.valueOf(LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT.getValue()).equals(this.id);
this.capitalizedIncomeAmortizationAdjustment = Long
.valueOf(LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getValue()).equals(this.id);
this.contractTermination = Long.valueOf(LoanTransactionType.CONTRACT_TERMINATION.getValue()).equals(this.id);
}
public boolean isRepaymentType() {
@@ -1789,6 +1789,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return getLoanTransaction(e -> e.isNotReversed() && e.isChargeOff());
}
public LoanTransaction findContractTerminationTransaction() {
return getLoanTransaction(e -> e.isNotReversed() && e.isContractTermination());
}
public void handleMaturityDateActivate() {
if (this.expectedMaturityDate != null && !this.expectedMaturityDate.equals(this.actualMaturityDate)) {
this.actualMaturityDate = this.expectedMaturityDate;
@@ -92,4 +92,6 @@ public interface LoanTransactionValidator {
void validateNote(DataValidatorBuilder baseDataValidator, JsonElement element);
void validateExternalId(DataValidatorBuilder baseDataValidator, JsonElement element);
void validateReversalExternalId(DataValidatorBuilder baseDataValidator, JsonElement element);
}
@@ -52,4 +52,5 @@
<include relativeToChangelogFile="true" file="parts/1027_add_capitalized_income_transaction_type.xml"/>
<include relativeToChangelogFile="true" file="parts/1028_add_missing_indexes.xml"/>
<include relativeToChangelogFile="true" file="parts/1029_add_installment_amount_in_multiples_of_to_loan.xml"/>
<include relativeToChangelogFile="true" file="parts/1030_add_loan_undo_contract_termination_event.xml"/>
</databaseChangeLog>
@@ -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="LoanUndoContractTerminationBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -26,4 +26,6 @@ public interface ProgressiveLoanTransactionValidator extends LoanTransactionVali
void validateCapitalizedIncome(JsonCommand command, Long loanId);
void validateCapitalizedIncomeAdjustment(JsonCommand command, Long loanId, Long capitalizedIncomeTransactionId);
void validateContractTerminationUndo(JsonCommand command, Long loanId);
}
@@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -219,6 +220,47 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
});
}
@Override
public void validateContractTerminationUndo(final JsonCommand command, final Long loanId) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getContractTerminationUndoParameters());
Validator.validateOrThrow("loan.contract.termination.undo", baseDataValidator -> {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
validateLoanClientIsActive(loan);
validateLoanGroupIsActive(loan);
if (!loan.isOpen()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.active",
"Loan: " + loanId + " Undo Contract Termination is not allowed. Loan Account is not Active", loanId);
}
if (!loan.isContractTermination()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.contract.terminated",
"Loan: " + loanId + " is not contract terminated", loanId);
}
final LoanTransaction contractTerminationTransaction = loan.findContractTerminationTransaction();
if (contractTerminationTransaction == null) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.contract.termination.transaction.not.found",
"Loan: " + loanId + " contract termination transaction was not found", loanId);
}
if (!contractTerminationTransaction.equals(loan.getLastUserTransaction())) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.contract.termination.is.not.the.last.user.transaction",
"Loan: " + loanId
+ " contract termination cannot be undone. User transaction was found after contract termination!",
loanId);
}
validateNote(baseDataValidator, element);
validateReversalExternalId(baseDataValidator, element);
});
}
// Delegates
@Override
public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) {
@@ -368,6 +410,11 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
loanTransactionValidator.validateExternalId(baseDataValidator, element);
}
@Override
public void validateReversalExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
loanTransactionValidator.validateReversalExternalId(baseDataValidator, element);
}
private Set<String> getCapitalizedIncomeParameters() {
return new HashSet<>(
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
@@ -377,4 +424,8 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
return new HashSet<>(
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
}
private Set<String> getContractTerminationUndoParameters() {
return new HashSet<>(Arrays.asList("note", "reversalExternalId"));
}
}
@@ -1234,6 +1234,8 @@ public class LoansApiResource {
commandRequest = builder.assignDelinquency(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CONTRACT_TERMINATION_COMMAND)) {
commandRequest = builder.applyContractTermination(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.UNDO_CONTRACT_TERMINATION_COMMAND)) {
commandRequest = builder.undoContractTermination(resolvedLoanId).build();
}
if (commandRequest == null) {
@@ -733,6 +733,8 @@ final class LoansApiResourceSwagger {
public boolean capitalizedIncomeAmortization;
@Schema(example = "false")
public boolean capitalizedIncomeAdjustment;
@Schema(example = "false")
public boolean contractTermination;
}
static final class GetLoansLoanIdPaymentDetailData {
@@ -0,0 +1,40 @@
/**
* 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.loan.contracttermination;
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.contracttermination.LoanContractTerminationServiceImpl;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@CommandType(entity = "LOAN", action = "CONTRACT_TERMINATION_UNDO")
public class UndoLoanContractTerminationCommandHandler implements NewCommandSourceHandler {
private final LoanContractTerminationServiceImpl loanContractTerminationService;
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return loanContractTerminationService.undoContractTermination(command);
}
}
@@ -1081,4 +1081,12 @@ public final class LoanTransactionValidatorImpl implements LoanTransactionValida
baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100);
}
}
@Override
public void validateReversalExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
final String reversalExternalId = this.fromApiJsonHelper.extractStringNamed("reversalExternalId", element);
if (StringUtils.isNotBlank(reversalExternalId)) {
baseDataValidator.reset().parameter("reversalExternalId").value(reversalExternalId).notExceedingLengthOf(100);
}
}
}
@@ -32,19 +32,25 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionContractTerminationPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoContractTerminationBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
@@ -65,6 +71,9 @@ public class LoanContractTerminationServiceImpl {
private final ExternalIdFactory externalIdFactory;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionService loanTransactionService;
private final LoanScheduleService loanScheduleService;
private final LoanChargeValidator loanChargeValidator;
private final ProgressiveLoanTransactionValidator loanTransactionValidator;
public CommandProcessingResult applyContractTermination(final JsonCommand command) {
final Loan loan = loanAssembler.assembleFrom(command.getLoanId());
@@ -116,6 +125,67 @@ public class LoanContractTerminationServiceImpl {
.with(changes).build();
}
public CommandProcessingResult undoContractTermination(final JsonCommand command) {
final Long loanId = command.getLoanId();
loanTransactionValidator.validateContractTerminationUndo(command, loanId);
final Loan loan = loanAssembler.assembleFrom(loanId);
final LoanTransaction contractTerminationTransaction = loan.findContractTerminationTransaction();
businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoContractTerminationBusinessEvent(contractTerminationTransaction));
businessEventNotifierService.notifyPreBusinessEvent(
new LoanAdjustTransactionBusinessEvent(new LoanAdjustTransactionBusinessEvent.Data(contractTerminationTransaction)));
// check if reversalExternalId is provided
final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME);
final ExternalId reversalTxnExternalId = ExternalIdFactory.produce(reversalExternalId);
final Map<String, Object> changes = new LinkedHashMap<>();
// Add note if provided
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanTransactionNote(loan, contractTerminationTransaction, noteText);
noteRepository.save(note);
}
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(contractTerminationTransaction.getLoan(),
contractTerminationTransaction, "reversed");
contractTerminationTransaction.reverse(reversalTxnExternalId);
contractTerminationTransaction.manuallyAdjustedOrReversed();
loan.liftContractTerminationSubStatus();
changes.put(LoanApiConstants.subStatusAttributeName, loan.getLoanSubStatus());
loanTransactionRepository.saveAndFlush(contractTerminationTransaction);
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule()) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
reprocessLoanTransactionsService.reprocessTransactions(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoContractTerminationBusinessEvent(contractTerminationTransaction));
final LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(
contractTerminationTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.withEntityId(contractTerminationTransaction.getId()) //
.withEntityExternalId(contractTerminationTransaction.getExternalId()) //
.with(changes) //
.build();
}
public void validateContractTermination(final Loan loan) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
@@ -109,7 +109,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -199,7 +199,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -621,6 +621,11 @@ public class ExternalEventConfigurationHelper {
loanCapitalizedIncomeTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanCapitalizedIncomeTransactionCreatedBusinessEvent);
Map<String, Object> loanTransactionUndoContractTerminationBusinessEvent = new HashMap<>();
loanTransactionUndoContractTerminationBusinessEvent.put("type", "LoanUndoContractTerminationBusinessEvent");
loanTransactionUndoContractTerminationBusinessEvent.put("enabled", false);
defaults.add(loanTransactionUndoContractTerminationBusinessEvent);
return defaults;
}