[FINERACT-167] Loan foreclosure implementation and integration test cases for the same.
This commit is contained in:
@@ -1303,6 +1303,14 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>loans/{loanId}/transactions?command=foreclosure</td>
|
||||
<td><a href="#loans_transaction_foreclosure">Foreclose an Active Loan</a></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="#loans_charges">Loan Charges</a></td>
|
||||
@@ -10141,6 +10149,13 @@ No Request Body:
|
||||
"total" "amount" is set
|
||||
to the total amount paid in advance.
|
||||
</dd>
|
||||
<dd>
|
||||
<b>'foreclosure'</b><br> "transaction date" is set to the current date by default<br>
|
||||
"transaction amount" is set
|
||||
to the sum of total loan outstanding principal
|
||||
and total Interest/ Fee/ Charges / Penalties
|
||||
till foreclosure date.
|
||||
</dd>
|
||||
</dl>
|
||||
<p>Example Request:</p>
|
||||
<div class=apiClick>loans/1/transactions/template?command=repayment</div>
|
||||
@@ -10266,6 +10281,42 @@ Request Body:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a id="loans_transaction_foreclosure" name="loans_transaction_foreclosure" class="old-syle-anchor"> </a>
|
||||
<div class="method-section">
|
||||
<div class="method-description">
|
||||
<h4>Foreclosure of an Active Loan</h4>
|
||||
</div>
|
||||
<div class="method-example">
|
||||
<code class="method-declaration">
|
||||
POST https://DomainName/api/v1/loans/{loanId}/transactions?command=foreclosure
|
||||
</code>
|
||||
<code class="method-request">
|
||||
POST loans/5/transactions?command=foreclosure
|
||||
Content-Type: application/json
|
||||
Request Body:
|
||||
{
|
||||
"dateFormat": "dd MMMM yyyy",
|
||||
"locale": "en",
|
||||
"transactionDate": "10 December 2012",
|
||||
"note": "Customer Death"
|
||||
}
|
||||
</code>
|
||||
<code class="method-response">
|
||||
{
|
||||
"changes": {
|
||||
"note": "Foreclosure Made!!!",
|
||||
"eventAmount": 7573.76,
|
||||
"transactionDate": [2012,
|
||||
12,
|
||||
10],
|
||||
"transactions": [15818]
|
||||
},
|
||||
"loanId": 5
|
||||
}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<a id="loans_transaction_waiveinterest" name="loans_transaction_waiveinterest" class="old-syle-anchor"> </a>
|
||||
<div class="method-section">
|
||||
|
||||
+53
@@ -4956,6 +4956,59 @@ public class ClientLoanIntegrationTest {
|
||||
this.validateIfValuesAreNotOverridden(loanID, loanProductID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case to verify Loan Foreclosure.
|
||||
*/
|
||||
@Test
|
||||
public void testLoanForeclosure() {
|
||||
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
|
||||
|
||||
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
|
||||
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
|
||||
final Integer loanProductID = createLoanProduct(false, NONE);
|
||||
|
||||
List<HashMap> charges = new ArrayList<>();
|
||||
|
||||
Integer flatAmountChargeOne = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false));
|
||||
addCharges(charges, flatAmountChargeOne, "50", "01 October 2011");
|
||||
Integer flatAmountChargeTwo = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "100", true));
|
||||
addCharges(charges, flatAmountChargeTwo, "100", "15 December 2011");
|
||||
|
||||
final Integer loanID = applyForLoanApplication(clientID, loanProductID, charges, null, "10,000.00");
|
||||
Assert.assertNotNull(loanID);
|
||||
|
||||
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
|
||||
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
|
||||
|
||||
System.out.println("----------------------------------- APPROVE LOAN -----------------------------------------");
|
||||
loanStatusHashMap = this.loanTransactionHelper.approveLoan("20 September 2011", loanID);
|
||||
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
|
||||
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
|
||||
|
||||
System.out.println("----------------------------------- DISBURSE LOAN ----------------------------------------");
|
||||
loanStatusHashMap = this.loanTransactionHelper.disburseLoan("20 September 2011", loanID, "10,000.00");
|
||||
System.out.println("DISBURSE " + loanStatusHashMap);
|
||||
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
|
||||
|
||||
System.out.println("---------------------------------- Make repayment 1 --------------------------------------");
|
||||
this.loanTransactionHelper.makeRepayment("20 October 2011", Float.valueOf("2676.24"), loanID);
|
||||
|
||||
System.out.println("---------------------------------- FORECLOSE LOAN ----------------------------------------");
|
||||
this.loanTransactionHelper.forecloseLoan("08 November 2011", loanID);
|
||||
|
||||
// retrieving the loan status
|
||||
loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
|
||||
// verifying the loan status is closed
|
||||
LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap);
|
||||
// retrieving the loan sub-status
|
||||
loanStatusHashMap = LoanStatusChecker.getSubStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
|
||||
// verifying the loan sub-status is foreclosed
|
||||
LoanStatusChecker.verifyLoanAccountForeclosed(loanStatusHashMap);
|
||||
|
||||
}
|
||||
|
||||
private void validateIfValuesAreNotOverridden(Integer loanID, Integer loanProductID) {
|
||||
String loanProductDetails = this.loanTransactionHelper.getLoanProductDetails(this.requestSpec, this.responseSpec, loanProductID);
|
||||
String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
|
||||
|
||||
+14
@@ -20,6 +20,7 @@ package org.apache.fineract.integrationtests.common.loans;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
@@ -59,14 +60,27 @@ public class LoanStatusChecker {
|
||||
assertTrue(getStatus(loanStatusHashMap, "overpaid"));
|
||||
}
|
||||
|
||||
public static void verifyLoanAccountForeclosed(final HashMap loanSubStatusHashMap) {
|
||||
assertEquals("Foreclosed", getSubStatus(loanSubStatusHashMap, "value"));
|
||||
}
|
||||
|
||||
public static HashMap<String, Object> getStatusOfLoan(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
|
||||
final Integer loanID) {
|
||||
final String url = "/fineract-provider/api/v1/loans/" + loanID + "?" + Utils.TENANT_IDENTIFIER;
|
||||
return Utils.performServerGet(requestSpec, responseSpec, url, "status");
|
||||
}
|
||||
|
||||
public static HashMap<String, Object> getSubStatusOfLoan(final RequestSpecification requestSpec,
|
||||
final ResponseSpecification responseSpec, final Integer loanID) {
|
||||
final String url = "/fineract-provider/api/v1/loans/" + loanID + "?" + Utils.TENANT_IDENTIFIER;
|
||||
return Utils.performServerGet(requestSpec, responseSpec, url, "subStatus");
|
||||
}
|
||||
|
||||
private static boolean getStatus(final HashMap loanStatusMap, final String nameOfLoanStatusString) {
|
||||
return (Boolean) loanStatusMap.get(nameOfLoanStatusString);
|
||||
}
|
||||
|
||||
private static String getSubStatus(final HashMap loanStatusMap, final String nameOfLoanStatusString) {
|
||||
return (String) loanStatusMap.get(nameOfLoanStatusString);
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -52,6 +52,7 @@ public class LoanTransactionHelper {
|
||||
private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant";
|
||||
private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees";
|
||||
private static final String MAKE_REFUND_BY_CASH_COMMAND = "refundByCash";
|
||||
private static final String FORECLOSURE_COMMAND = "foreclosure";
|
||||
|
||||
public LoanTransactionHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
|
||||
this.requestSpec = requestSpec;
|
||||
@@ -217,6 +218,11 @@ public class LoanTransactionHelper {
|
||||
getRepaymentBodyAsJSON(date, amountToBePaid), "");
|
||||
}
|
||||
|
||||
public HashMap forecloseLoan(final String transactionDate, final Integer loanID) {
|
||||
return (HashMap) performLoanTransaction(createLoanTransactionURL(FORECLOSURE_COMMAND, loanID),
|
||||
getForeclosureBodyAsJSON(transactionDate, loanID), "");
|
||||
}
|
||||
|
||||
public HashMap withdrawLoanApplicationByClient(final String date, final Integer loanID) {
|
||||
return performLoanTransaction(createLoanOperationURL(WITHDRAW_LOAN_APPLICATION_COMMAND, loanID),
|
||||
getWithdrawLoanApplicationBodyAsJSON(date));
|
||||
@@ -326,6 +332,17 @@ public class LoanTransactionHelper {
|
||||
return new Gson().toJson(map);
|
||||
}
|
||||
|
||||
private String getForeclosureBodyAsJSON(final String transactionDate, final Integer loanId) {
|
||||
final HashMap<String, Object> map = new HashMap<>();
|
||||
map.put("locale", "en");
|
||||
map.put("dateFormat", "dd MMMM yyyy");
|
||||
map.put("transactionDate", transactionDate);
|
||||
map.put("note", "Foreclosure Made!!!");
|
||||
String json = new Gson().toJson(map);
|
||||
System.out.println(json);
|
||||
return json;
|
||||
}
|
||||
|
||||
private String getWriteOffBodyAsJSON(final String transactionDate) {
|
||||
final HashMap<String, String> map = new HashMap<>();
|
||||
map.put("dateFormat", "dd MMMM yyyy");
|
||||
|
||||
+9
@@ -723,6 +723,15 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder loanForeclosure(final Long loanId) {
|
||||
this.actionName = "FORECLOSURE";
|
||||
this.entityName = "LOAN";
|
||||
this.entityId = null;
|
||||
this.loanId = loanId;
|
||||
this.href = "/loans/" + loanId + "/transactions?command=foreclosure";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createLoanApplication() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "LOAN";
|
||||
|
||||
+4
@@ -113,4 +113,8 @@ public interface LoanApiConstants {
|
||||
//loan write off
|
||||
public static final String WRITEOFFREASONS = "WriteOffReasons";
|
||||
|
||||
// fore closure constants
|
||||
public static final String transactionDateParamName = "transactionDate";
|
||||
public static final String noteParamName = "note";
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -129,6 +129,14 @@ public class LoanTransactionsApiResource {
|
||||
transactionData = this.loanReadPlatformService.retrieveRefundByCashTemplate(loanId);
|
||||
} else if (is(commandParam, "refundbytransfer")) {
|
||||
transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, true);
|
||||
} else if (is(commandParam, "foreclosure")) {
|
||||
LocalDate transactionDate = null;
|
||||
if (transactionDateParam == null) {
|
||||
transactionDate = DateUtils.getLocalDateOfTenant();
|
||||
} else {
|
||||
transactionDate = LocalDate.fromDateFields(transactionDateParam.getDate("transactionDate", dateFormat, locale));
|
||||
}
|
||||
transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(loanId, transactionDate);
|
||||
} else {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||
}
|
||||
@@ -189,6 +197,9 @@ public class LoanTransactionsApiResource {
|
||||
} else if (is(commandParam, "refundByCash")) {
|
||||
final CommandWrapper commandRequest = builder.refundLoanTransactionByCash(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "foreclosure")) {
|
||||
final CommandWrapper commandRequest = builder.loanForeclosure(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
|
||||
if (result == null) { throw new UnrecognizedQueryParamException("command", commandParam); }
|
||||
|
||||
+24
-16
@@ -39,6 +39,7 @@ import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
|
||||
import org.apache.fineract.portfolio.fund.data.FundData;
|
||||
import org.apache.fineract.portfolio.group.data.GroupGeneralData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.guarantor.data.GuarantorData;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData;
|
||||
@@ -47,6 +48,7 @@ import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrat
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductConfigurableAttributes;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType;
|
||||
import org.apache.fineract.portfolio.note.data.NoteData;
|
||||
import org.hibernate.dialect.Sybase11Dialect;
|
||||
import org.joda.time.LocalDate;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -65,6 +67,7 @@ public class LoanAccountData {
|
||||
|
||||
// status
|
||||
private final LoanStatusEnumData status;
|
||||
private final EnumOptionData subStatus;
|
||||
|
||||
// related to
|
||||
private final Long clientId;
|
||||
@@ -214,6 +217,7 @@ public class LoanAccountData {
|
||||
final Long id = null;
|
||||
final String accountNo = null;
|
||||
final LoanStatusEnumData status = null;
|
||||
final EnumOptionData subStatus = null;
|
||||
final String externalId = null;
|
||||
final Long clientId = null;
|
||||
final String clientName = null;
|
||||
@@ -335,7 +339,7 @@ public class LoanAccountData {
|
||||
maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
|
||||
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule,
|
||||
createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap,
|
||||
maximumGap);
|
||||
maximumGap, subStatus);
|
||||
|
||||
}
|
||||
|
||||
@@ -348,6 +352,7 @@ public class LoanAccountData {
|
||||
final Long id = null;
|
||||
final String accountNo = null;
|
||||
final LoanStatusEnumData status = null;
|
||||
final EnumOptionData subStatus = null;
|
||||
final String externalId = null;
|
||||
final GroupGeneralData group = null;
|
||||
final EnumOptionData loanType = null;
|
||||
@@ -468,7 +473,7 @@ public class LoanAccountData {
|
||||
maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
|
||||
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule,
|
||||
createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap,
|
||||
maximumGap);
|
||||
maximumGap, subStatus);
|
||||
|
||||
}
|
||||
|
||||
@@ -497,7 +502,7 @@ public class LoanAccountData {
|
||||
acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType,
|
||||
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
|
||||
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap);
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,6 +514,7 @@ public class LoanAccountData {
|
||||
final Long id = null;
|
||||
final String accountNo = null;
|
||||
final LoanStatusEnumData status = null;
|
||||
final EnumOptionData subStatus = null;
|
||||
final String externalId = null;
|
||||
final Long clientId = null;
|
||||
final String clientAccountNo = null;
|
||||
@@ -631,7 +637,7 @@ public class LoanAccountData {
|
||||
maxOutstandingBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
|
||||
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule,
|
||||
createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap,
|
||||
maximumGap);
|
||||
maximumGap, subStatus);
|
||||
|
||||
}
|
||||
|
||||
@@ -660,7 +666,7 @@ public class LoanAccountData {
|
||||
acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType,
|
||||
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
|
||||
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap);
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus);
|
||||
|
||||
}
|
||||
|
||||
@@ -678,6 +684,7 @@ public class LoanAccountData {
|
||||
final Long id = null;
|
||||
final String accountNo = null;
|
||||
final LoanStatusEnumData status = null;
|
||||
final EnumOptionData subStatus = null;
|
||||
final String externalId = null;
|
||||
final Long clientId = null;
|
||||
final String clientAccountNo = null;
|
||||
@@ -808,7 +815,7 @@ public class LoanAccountData {
|
||||
product.getDaysInYearType(), product.isInterestRecalculationEnabled(), product.toLoanInterestRecalculationData(),
|
||||
originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods,
|
||||
product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(),
|
||||
product.getMaximumGapBetweenInstallments());
|
||||
product.getMaximumGapBetweenInstallments(), subStatus);
|
||||
}
|
||||
|
||||
public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) {
|
||||
@@ -867,7 +874,7 @@ public class LoanAccountData {
|
||||
product.getDaysInMonthType(), product.getDaysInYearType(), product.isInterestRecalculationEnabled(),
|
||||
product.toLoanInterestRecalculationData(), acc.originalSchedule, acc.createStandingInstructionAtDisbursement,
|
||||
paidInAdvance, acc.interestRatesPeriods, product.isVariableInstallmentsAllowed(),
|
||||
product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments());
|
||||
product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments(), acc.subStatus);
|
||||
|
||||
}
|
||||
|
||||
@@ -897,7 +904,7 @@ public class LoanAccountData {
|
||||
final Integer graceOnArrearsAgeing, final Boolean isNPA, final EnumOptionData daysInMonthType,
|
||||
final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled,
|
||||
final LoanInterestRecalculationData interestRecalculationData, final Boolean createStandingInstructionAtDisbursement,
|
||||
final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap) {
|
||||
final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap, final EnumOptionData subStatus) {
|
||||
|
||||
final LoanScheduleData repaymentSchedule = null;
|
||||
final Collection<LoanTransactionData> transactions = null;
|
||||
@@ -952,7 +959,7 @@ public class LoanAccountData {
|
||||
outstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
|
||||
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule,
|
||||
createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap,
|
||||
maximumGap);
|
||||
maximumGap, subStatus);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1003,7 +1010,7 @@ public class LoanAccountData {
|
||||
acc.graceOnArrearsAgeing, overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType,
|
||||
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
|
||||
acc.createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, acc.isVariableInstallmentsAllowed,
|
||||
acc.minimumGap, acc.maximumGap);
|
||||
acc.minimumGap, acc.maximumGap, acc.subStatus);
|
||||
}
|
||||
|
||||
public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions,
|
||||
@@ -1044,7 +1051,7 @@ public class LoanAccountData {
|
||||
acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType,
|
||||
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
|
||||
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap);
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus);
|
||||
}
|
||||
|
||||
public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map<Long, Integer> memberLoanCycle) {
|
||||
@@ -1108,7 +1115,7 @@ public class LoanAccountData {
|
||||
acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType,
|
||||
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule,
|
||||
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap);
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus);
|
||||
|
||||
}
|
||||
|
||||
@@ -1141,7 +1148,7 @@ public class LoanAccountData {
|
||||
acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType,
|
||||
acc.isInterestRecalculationEnabled, interestRecalculationData, acc.originalSchedule,
|
||||
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap);
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus);
|
||||
}
|
||||
|
||||
public static LoanAccountData withLoanCalendarData(final LoanAccountData acc, final CalendarData calendarData) {
|
||||
@@ -1168,7 +1175,7 @@ public class LoanAccountData {
|
||||
acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges,
|
||||
acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData,
|
||||
acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap);
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus);
|
||||
}
|
||||
|
||||
public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) {
|
||||
@@ -1196,7 +1203,7 @@ public class LoanAccountData {
|
||||
acc.graceOnArrearsAgeing, acc.overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType,
|
||||
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, originalSchedule,
|
||||
acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods,
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap);
|
||||
acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus);
|
||||
}
|
||||
|
||||
private LoanAccountData(
|
||||
@@ -1263,11 +1270,12 @@ public class LoanAccountData {
|
||||
final LoanInterestRecalculationData interestRecalculationData, final LoanScheduleData originalSchedule,
|
||||
final Boolean createStandingInstructionAtDisbursement, final PaidInAdvanceData paidInAdvance,
|
||||
final Collection<InterestRatePeriodData> interestRatesPeriods, final Boolean isVariableInstallmentsAllowed,
|
||||
final Integer minimumGap, final Integer maximumGap) {
|
||||
final Integer minimumGap, final Integer maximumGap, final EnumOptionData subStatus) {
|
||||
|
||||
this.id = id;
|
||||
this.accountNo = accountNo;
|
||||
this.status = status;
|
||||
this.subStatus = subStatus;
|
||||
this.externalId = externalId;
|
||||
this.clientId = clientId;
|
||||
this.clientAccountNo = clientAccountNo;
|
||||
|
||||
+253
-6
@@ -107,6 +107,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactio
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException;
|
||||
@@ -132,6 +133,7 @@ import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.hibernate.annotations.LazyCollection;
|
||||
import org.hibernate.annotations.LazyCollectionOption;
|
||||
import org.joda.time.Days;
|
||||
import org.joda.time.LocalDate;
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.joda.time.format.DateTimeFormat;
|
||||
@@ -395,6 +397,9 @@ public class Loan extends AbstractPersistable<Long> {
|
||||
@JoinColumn(name = "writeoff_reason_cv_id", nullable = true)
|
||||
private CodeValue writeOffReason;
|
||||
|
||||
@Column(name = "loan_sub_status_id", nullable = true)
|
||||
private Integer loanSubStatus;
|
||||
|
||||
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType,
|
||||
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
|
||||
final LoanTransactionProcessingStrategy transactionProcessingStrategy,
|
||||
@@ -4814,6 +4819,14 @@ public class Loan extends AbstractPersistable<Long> {
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
break;
|
||||
case LOAN_FORECLOSURE:
|
||||
if (!isOpen()) {
|
||||
final String defaultUserMessage = "Loan foreclosure is not allowed. Loan Account is not active.";
|
||||
final ApiParameterError error = ApiParameterError.generalError(
|
||||
"error.msg.loan.foreclosure.account.is.not.active", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -5982,12 +5995,246 @@ public class Loan extends AbstractPersistable<Long> {
|
||||
this.writeOffReason = writeOffReason;
|
||||
}
|
||||
|
||||
public Group getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public LoanProduct getLoanProduct() {
|
||||
return loanProduct;
|
||||
}
|
||||
public Group getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public LoanProduct getLoanProduct() {
|
||||
return loanProduct;
|
||||
}
|
||||
|
||||
public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) {
|
||||
Money totalPrincipal = Money.of(getCurrency(), this.getSummary().getTotalPrincipalOutstanding());
|
||||
Money[] receivables = retriveIncomeOutstandingTillDate(closureDate);
|
||||
final List<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
|
||||
final LocalDate currentDate = DateUtils.getLocalDateOfTenant();
|
||||
return new LoanRepaymentScheduleInstallment(null, 0, currentDate, currentDate, totalPrincipal.getAmount(),
|
||||
receivables[0].getAmount(), receivables[1].getAmount(), receivables[2].getAmount(), false, compoundingDetails);
|
||||
}
|
||||
|
||||
public Money[] retriveIncomeOutstandingTillDate(final LocalDate paymentDate) {
|
||||
Money[] balances = new Money[3];
|
||||
final MonetaryCurrency currency = getCurrency();
|
||||
Money interest = Money.zero(currency);
|
||||
Money fee = Money.zero(currency);
|
||||
Money penalty = Money.zero(currency);
|
||||
boolean isArrearsPresent = false;
|
||||
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
||||
if (installment.isNotFullyPaidOff()) {
|
||||
if (!isArrearsPresent || !installment.getDueDate().isAfter(paymentDate)) {
|
||||
interest = interest.plus(installment.getInterestOutstanding(currency));
|
||||
fee = fee.plus(installment.getFeeChargesOutstanding(currency));
|
||||
penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency));
|
||||
isArrearsPresent = true;
|
||||
} else if (installment.getFromDate().isBefore(paymentDate)) {
|
||||
Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenalty(paymentDate, currency, installment);
|
||||
interest = interest.plus(balancesForCurrentPeroid[0]);
|
||||
fee = fee.plus(balancesForCurrentPeroid[1]);
|
||||
penalty = penalty.plus(balancesForCurrentPeroid[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
balances[0] = interest;
|
||||
balances[1] = fee;
|
||||
balances[2] = penalty;
|
||||
return balances;
|
||||
}
|
||||
|
||||
private Money[] fetchInterestFeeAndPenalty(final LocalDate paymentDate, final MonetaryCurrency currency, final LoanRepaymentScheduleInstallment installment) {
|
||||
Money penaltyForCurrentPeriod = Money.zero(getCurrency());
|
||||
Money feeForCurrentPeriod = Money.zero(getCurrency());
|
||||
int totalPeriodDays = Days.daysBetween(installment.getFromDate(), installment.getDueDate()).getDays();
|
||||
int tillDays = Days.daysBetween(installment.getFromDate(), paymentDate).getDays();
|
||||
Money interestForCurrentPeriod = Money.of(getCurrency(),BigDecimal.valueOf(calculateInterestForDays(totalPeriodDays, installment.getInterestOutstanding(getCurrency())
|
||||
.getAmount(), tillDays)));
|
||||
for (LoanCharge loanCharge : this.charges) {
|
||||
if (loanCharge.isActive()
|
||||
&& loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate)) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyForCurrentPeriod = loanCharge.getAmountOutstanding(getCurrency());
|
||||
} else {
|
||||
feeForCurrentPeriod = loanCharge.getAmountOutstanding(currency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Money[] balances = new Money[3];
|
||||
balances[0] = interestForCurrentPeriod;
|
||||
balances[1] = feeForCurrentPeriod;
|
||||
balances[2] = penaltyForCurrentPeriod;
|
||||
|
||||
return balances;
|
||||
}
|
||||
|
||||
|
||||
public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate) {
|
||||
Money[] balances = new Money[3];
|
||||
final MonetaryCurrency currency = getCurrency();
|
||||
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
||||
if (installment.getDueDate().isEqual(paymentDate)){
|
||||
Money interest = installment.getInterestOutstanding(currency);
|
||||
Money fee = installment.getFeeChargesOutstanding(currency);
|
||||
Money penalty = installment.getPenaltyChargesOutstanding(currency);
|
||||
balances[0] = interest;
|
||||
balances[1] = fee;
|
||||
balances[2] = penalty;
|
||||
break;
|
||||
}else if(installment.getDueDate().isAfter(paymentDate) && installment.getFromDate().isBefore(paymentDate)){
|
||||
balances = fetchInterestFeeAndPenalty(paymentDate, currency, installment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return balances;
|
||||
}
|
||||
private double calculateInterestForDays(int daysInPeriod, BigDecimal interest, int days) {
|
||||
if (interest.doubleValue() == 0) { return 0; }
|
||||
return ((interest.doubleValue()) / daysInPeriod) * days;
|
||||
}
|
||||
|
||||
public Money[] getReceivableIncome(final LocalDate tillDate) {
|
||||
MonetaryCurrency currency = getCurrency();
|
||||
Money receivableInterest = Money.zero(currency);
|
||||
Money receivableFee = Money.zero(currency);
|
||||
Money receivablePenalty = Money.zero(currency);
|
||||
Money[] receivables = new Money[3];
|
||||
for (final LoanTransaction transaction : this.loanTransactions) {
|
||||
if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement()
|
||||
&& !transaction.getTransactionDate().isAfter(tillDate)) {
|
||||
if (transaction.isAccrual()) {
|
||||
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency));
|
||||
receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency));
|
||||
receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency));
|
||||
} else if (transaction.isRepayment() || transaction.isChargePayment()) {
|
||||
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency));
|
||||
receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency));
|
||||
receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency));
|
||||
}
|
||||
}
|
||||
if (receivableInterest.isLessThanZero()) {
|
||||
receivableInterest = receivableInterest.zero();
|
||||
}
|
||||
if (receivableFee.isLessThanZero()) {
|
||||
receivableFee = receivableFee.zero();
|
||||
}
|
||||
if (receivablePenalty.isLessThanZero()) {
|
||||
receivablePenalty = receivablePenalty.zero();
|
||||
}
|
||||
}
|
||||
receivables[0] = receivableInterest;
|
||||
receivables[1] = receivableFee;
|
||||
receivables[2] = receivablePenalty;
|
||||
return receivables;
|
||||
}
|
||||
|
||||
public void handleForeClosureTransactions(final List<LoanTransaction> repaymentTransaction,
|
||||
final LocalDate foreClosureDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
|
||||
LoanEvent event = LoanEvent.LOAN_FORECLOSURE;
|
||||
|
||||
validateAccountStatus(event);
|
||||
|
||||
MonetaryCurrency currency = getCurrency();
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
|
||||
.determineProcessor(this.transactionProcessingStrategy);
|
||||
|
||||
loanRepaymentScheduleTransactionProcessor.processTransactionsFromDerivedFields(repaymentTransaction, currency,
|
||||
this.repaymentScheduleInstallments, charges());
|
||||
this.loanTransactions.addAll(repaymentTransaction);
|
||||
this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue();
|
||||
updateLoanSummaryDerivedFields();
|
||||
doPostLoanTransactionChecks(foreClosureDate, loanLifecycleStateMachine);
|
||||
}
|
||||
|
||||
public Money retrieveAccruedAmountAfterDate(final LocalDate tillDate) {
|
||||
Money totalAmountAccrued = Money.zero(getCurrency());
|
||||
Money actualAmountTobeAccrued = Money.zero(getCurrency());
|
||||
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
||||
totalAmountAccrued = totalAmountAccrued.plus(installment.getInterestAccrued(getCurrency()));
|
||||
|
||||
if (tillDate.isAfter(installment.getFromDate()) && tillDate.isBefore(installment.getDueDate())) {
|
||||
int daysInPeriod = Days.daysBetween(installment.getFromDate(), installment.getDueDate()).getDays();
|
||||
int tillDays = Days.daysBetween(installment.getFromDate(), tillDate).getDays();
|
||||
double interest = calculateInterestForDays(daysInPeriod, installment.getInterestCharged(getCurrency()).getAmount(),
|
||||
tillDays);
|
||||
actualAmountTobeAccrued = actualAmountTobeAccrued.plus(interest);
|
||||
} else if ((tillDate.isAfter(installment.getFromDate()) && tillDate.isEqual(installment.getDueDate()))
|
||||
|| (tillDate.isEqual(installment.getFromDate()) && tillDate.isEqual(installment.getDueDate()))
|
||||
|| (tillDate.isAfter(installment.getFromDate()) && tillDate.isAfter(installment.getDueDate()))) {
|
||||
actualAmountTobeAccrued = actualAmountTobeAccrued.plus(installment.getInterestAccrued(getCurrency()));
|
||||
}
|
||||
}
|
||||
Money accredAmountAfterDate = totalAmountAccrued.minus(actualAmountTobeAccrued);
|
||||
if (accredAmountAfterDate.isLessThanZero()) {
|
||||
accredAmountAfterDate = Money.zero(getCurrency());
|
||||
}
|
||||
return accredAmountAfterDate;
|
||||
}
|
||||
|
||||
public void validateForForeclosure(final LocalDate transactionDate) {
|
||||
|
||||
if (isInterestRecalculationEnabledForProduct()) {
|
||||
final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed.";
|
||||
throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage,
|
||||
getId());
|
||||
}
|
||||
|
||||
LocalDate lastUserTransactionDate = getLastUserTransactionDate();
|
||||
|
||||
if (DateUtils.isDateInTheFuture(transactionDate)) {
|
||||
final String defaultUserMessage = "The transactionDate cannot be in the future.";
|
||||
throw new LoanForeclosureException("loan.foreclosure.transaction.date.is.in.future", defaultUserMessage, transactionDate);
|
||||
}
|
||||
|
||||
if (lastUserTransactionDate.isAfter(transactionDate)) {
|
||||
final String defaultUserMessage = "The transactionDate cannot be in the future.";
|
||||
throw new LoanForeclosureException("loan.foreclosure.transaction.date.cannot.before.the.last.transaction.date",
|
||||
defaultUserMessage, transactionDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateInstallmentsPostDate(LocalDate transactionDate) {
|
||||
List<LoanRepaymentScheduleInstallment> newInstallments = new ArrayList<>(this.repaymentScheduleInstallments);
|
||||
final MonetaryCurrency currency = getCurrency();
|
||||
Money totalPrincipal = Money.zero(currency);
|
||||
Money [] balances = retriveIncomeForOverlappingPeriod(transactionDate);
|
||||
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
||||
if (!installment.getDueDate().isBefore(transactionDate)) {
|
||||
totalPrincipal = totalPrincipal.plus(installment.getPrincipalOutstanding(currency));
|
||||
newInstallments.remove(installment);
|
||||
}
|
||||
}
|
||||
|
||||
LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(null, newInstallments.size() + 1,
|
||||
newInstallments.get((newInstallments.size() - 1)).getDueDate(), transactionDate, totalPrincipal.getAmount(),
|
||||
balances[0].getAmount(), balances[1].getAmount(), balances[2].getAmount(), true, null);
|
||||
newInstallment.updateInstallmentNumber(newInstallments.size() + 1);
|
||||
newInstallments.add(newInstallment);
|
||||
updateLoanScheduleOnForeclosure(newInstallments);
|
||||
|
||||
Set<LoanCharge> charges = this.charges();
|
||||
int penaltyWaitPeriod = 0;
|
||||
for (LoanCharge loanCharge : charges) {
|
||||
if (loanCharge.getDueLocalDate() != null
|
||||
&& (loanCharge.getDueLocalDate().isAfter(transactionDate))) {
|
||||
loanCharge.setActive(false);
|
||||
} else if (loanCharge.getDueLocalDate() == null) {
|
||||
recalculateLoanCharge(loanCharge, penaltyWaitPeriod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLoanScheduleOnForeclosure(final Collection<LoanRepaymentScheduleInstallment> installments) {
|
||||
this.repaymentScheduleInstallments.clear();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
addRepaymentScheduleInstallment(installment);
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getLoanSubStatus() {
|
||||
return this.loanSubStatus;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.portfolio.loanaccount.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
@@ -56,4 +57,5 @@ public interface LoanAccountDomainService {
|
||||
|
||||
void saveLoanWithDataIntegrityViolationChecks(Loan loan);
|
||||
|
||||
Map<String, Object> foreCloseLoan(final Loan loan, final LocalDate foreClourseDate, String noteText);
|
||||
}
|
||||
|
||||
+111
-1
@@ -19,11 +19,12 @@
|
||||
package org.apache.fineract.portfolio.loanaccount.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -574,9 +575,118 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
return newRefundTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText) {
|
||||
MonetaryCurrency currency = loan.getCurrency();
|
||||
LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
List<LoanTransaction> newTransactions = new ArrayList<>();
|
||||
|
||||
final List<Long> existingTransactionIds = new ArrayList<>();
|
||||
final List<Long> existingReversedTransactionIds = new ArrayList<>();
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
|
||||
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
|
||||
&& (loan.getAccruedTill() == null || !foreClosureDate.isEqual(loan.getAccruedTill()))) {
|
||||
Money[] accruedReceivables = loan.getReceivableIncome(foreClosureDate);
|
||||
Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus(accruedReceivables[0]);
|
||||
Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus(accruedReceivables[1]);
|
||||
Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus(accruedReceivables[2]);
|
||||
Money total = interestPortion.plus(feePortion).plus(penaltyPortion);
|
||||
if (total.isGreaterThanZero()) {
|
||||
LoanTransaction accrualTransaction = LoanTransaction.accrual(loan, loan.getOffice(), total, interestPortion, feePortion,
|
||||
penaltyPortion, foreClosureDate);
|
||||
LocalDate fromDate = loan.getDisbursementDate();
|
||||
if (loan.getAccruedTill() != null) {
|
||||
fromDate = loan.getAccruedTill();
|
||||
}
|
||||
LoanRepaymentScheduleInstallment installment = fetchLoanRepaymentScheduleInstallment(fromDate, loan);
|
||||
installment.updateAccrualPortion(interestPortion, feePortion, penaltyPortion);
|
||||
accrualTransaction.updateCreatedDate(createdDate.toDate());
|
||||
createdDate = createdDate.plusSeconds(1);
|
||||
newTransactions.add(accrualTransaction);
|
||||
Set<LoanChargePaidBy> accrualCharges = accrualTransaction.getLoanChargesPaid();
|
||||
for (LoanCharge loanCharge : loan.charges()) {
|
||||
if (loanCharge.isActive()
|
||||
&& !loanCharge.isPaid()
|
||||
&& (loanCharge.isDueForCollectionFromAndUpToAndIncluding(fromDate, foreClosureDate) || loanCharge
|
||||
.isInstalmentFee())) {
|
||||
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge, loanCharge
|
||||
.getAmountOutstanding(currency).getAmount(), null);
|
||||
accrualCharges.add(loanChargePaidBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Money interestPayable = foreCloseDetail.getInterestCharged(currency);
|
||||
|
||||
Money feePayable = foreCloseDetail.getFeeChargesCharged(currency);
|
||||
Money penaltyPayable = foreCloseDetail.getPenaltyChargesCharged(currency);
|
||||
|
||||
Money payPrincipal = foreCloseDetail.getPrincipal(currency);
|
||||
|
||||
LoanTransaction payment = null;
|
||||
if (payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable).isGreaterThanZero()) {
|
||||
AppUser appUser = null;
|
||||
final PaymentDetail paymentDetail = null;
|
||||
String externalId = null;
|
||||
final LocalDateTime currentDateTime = DateUtils.getLocalDateTimeOfTenant();
|
||||
payment = LoanTransaction.repayment(loan.getOffice(), payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable),
|
||||
paymentDetail, foreClosureDate, externalId, currentDateTime, appUser);
|
||||
createdDate = createdDate.plusSeconds(1);
|
||||
payment.updateCreatedDate(createdDate.toDate());
|
||||
payment.updateComponents(payPrincipal, interestPayable, feePayable, penaltyPayable);
|
||||
payment.updateLoan(loan);
|
||||
newTransactions.add(payment);
|
||||
}
|
||||
List<Long> transactionIds = new ArrayList<>();
|
||||
loan.handleForeClosureTransactions(newTransactions, foreClosureDate, defaultLoanLifecycleStateMachine());
|
||||
for (LoanTransaction newTransaction : newTransactions) {
|
||||
saveLoanTransactionWithDataIntegrityViolationChecks(newTransaction);
|
||||
transactionIds.add(newTransaction.getId());
|
||||
}
|
||||
changes.put("transactions", transactionIds);
|
||||
changes.put("eventAmount", payPrincipal.getAmount().negate());
|
||||
|
||||
/***
|
||||
* TODO Vishwas Batch save is giving me a
|
||||
* HibernateOptimisticLockingFailureException, looping and saving for
|
||||
* the time being, not a major issue for now as this loop is entered
|
||||
* only in edge cases (when a payment is made before the latest payment
|
||||
* recorded against the loan)
|
||||
***/
|
||||
|
||||
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
if (StringUtils.isNotBlank(noteText)) {
|
||||
changes.put("note", noteText);
|
||||
final Note note = Note.loanNote(loan, noteText);
|
||||
this.noteRepository.save(note);
|
||||
}
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
|
||||
|
||||
return changes;
|
||||
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate fromDate, final Loan loan) {
|
||||
LoanRepaymentScheduleInstallment installment = null;
|
||||
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) {
|
||||
if (fromDate.equals(loanRepaymentScheduleInstallment.getFromDate())) {
|
||||
installment = loanRepaymentScheduleInstallment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return installment;
|
||||
}
|
||||
|
||||
private Map<BUSINESS_ENTITY, Object> constructEntityMap(final BUSINESS_ENTITY entityEvent, Object entity) {
|
||||
Map<BUSINESS_ENTITY, Object> map = new HashMap<>(1);
|
||||
map.put(entityEvent, entity);
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-1
@@ -42,5 +42,6 @@ public enum LoanEvent {
|
||||
LOAN_CHARGE_PAYMENT, //
|
||||
LOAN_CLOSED, //
|
||||
LOAN_EDIT_MULTI_DISBURSE_DATE, //
|
||||
LOAN_REFUND;
|
||||
LOAN_REFUND, //
|
||||
LOAN_FORECLOSURE;
|
||||
}
|
||||
|
||||
+6
@@ -785,4 +785,10 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
|
||||
public List<LoanInterestRecalcualtionAdditionalDetails> getLoanCompoundingDetails() {
|
||||
return this.loanCompoundingDetails;
|
||||
}
|
||||
|
||||
public Money getAccruedInterestOutstanding(final MonetaryCurrency currency) {
|
||||
final Money interestAccountedFor = getInterestPaid(currency).plus(getInterestWaived(currency))
|
||||
.plus(getInterestWrittenOff(currency));
|
||||
return getInterestAccrued(currency).minus(interestAccountedFor);
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
|
||||
public enum LoanSubStatus {
|
||||
INVALID(0, "loanSubStatusType.invalid"), //
|
||||
FORECLOSED(100, "loanSubStatusType.foreclosed");
|
||||
|
||||
private final Integer value;
|
||||
private final String code;
|
||||
|
||||
public static LoanSubStatus fromInt(final Integer statusValue) {
|
||||
|
||||
LoanSubStatus enumeration = LoanSubStatus.INVALID;
|
||||
switch (statusValue) {
|
||||
case 100:
|
||||
enumeration = LoanSubStatus.FORECLOSED;
|
||||
break;
|
||||
}
|
||||
return enumeration;
|
||||
}
|
||||
|
||||
private LoanSubStatus(final Integer value, final String code) {
|
||||
this.value = value;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public boolean hasStateOf(final LoanSubStatus state) {
|
||||
return this.value.equals(state.getValue());
|
||||
}
|
||||
|
||||
public Integer getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public boolean isForeclosed() {
|
||||
return this.value.equals(LoanSubStatus.FORECLOSED.getValue());
|
||||
}
|
||||
|
||||
public static EnumOptionData loanSubStatus(final int id) {
|
||||
return loanSubStatusEnum(LoanSubStatus.fromInt(id));
|
||||
}
|
||||
|
||||
public static EnumOptionData loanSubStatusEnum(final LoanSubStatus type) {
|
||||
final String codePrefix = "loanSubStatus.";
|
||||
EnumOptionData optionData = null;
|
||||
switch (type) {
|
||||
case FORECLOSED:
|
||||
optionData = new EnumOptionData(LoanSubStatus.FORECLOSED.getValue().longValue(), codePrefix
|
||||
+ LoanSubStatus.FORECLOSED.getCode(), "Foreclosed");
|
||||
break;
|
||||
default:
|
||||
optionData = new EnumOptionData(LoanSubStatus.INVALID.getValue().longValue(), LoanSubStatus.INVALID.getCode(), "Invalid");
|
||||
break;
|
||||
}
|
||||
return optionData;
|
||||
}
|
||||
}
|
||||
+66
-51
@@ -18,6 +18,27 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
@@ -36,26 +57,6 @@ import org.joda.time.LocalDate;
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.springframework.data.jpa.domain.AbstractPersistable;
|
||||
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* All monetary transactions against a loan are modelled through this entity.
|
||||
* Disbursements, Repayments, Waivers, Write-off etc
|
||||
@@ -116,7 +117,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "created_date", nullable = false)
|
||||
private final Date createdDate;
|
||||
private Date createdDate;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "appuser_id", nullable = true)
|
||||
@@ -133,8 +134,8 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
|
||||
private boolean manuallyAdjustedOrReversed;
|
||||
|
||||
@LazyCollection(LazyCollectionOption.FALSE)
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@JoinColumn(name = "loan_transaction_id", referencedColumnName= "id" , nullable = false)
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@JoinColumn(name = "loan_transaction_id", referencedColumnName = "id", nullable = false)
|
||||
private Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = new HashSet<>();
|
||||
|
||||
protected LoanTransaction() {
|
||||
@@ -146,19 +147,20 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
|
||||
this.appUser = null;
|
||||
}
|
||||
|
||||
public static LoanTransaction incomePosting(final Loan loan, final Office office, final Date dateOf,
|
||||
final BigDecimal amount, final BigDecimal interestPortion, final BigDecimal feeChargesPortion,
|
||||
final BigDecimal penaltyChargesPortion, final AppUser appUser){
|
||||
final Integer typeOf = LoanTransactionType.INCOME_POSTING.getValue();
|
||||
final BigDecimal principalPortion = BigDecimal.ZERO;
|
||||
final BigDecimal overPaymentPortion = BigDecimal.ZERO;
|
||||
final boolean reversed = false;
|
||||
final PaymentDetail paymentDetail = null;
|
||||
final String externalId = null;
|
||||
final LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
return new LoanTransaction(loan, office, typeOf, dateOf, amount, principalPortion, interestPortion, feeChargesPortion,
|
||||
public static LoanTransaction incomePosting(final Loan loan, final Office office, final Date dateOf, final BigDecimal amount,
|
||||
final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion,
|
||||
final AppUser appUser) {
|
||||
final Integer typeOf = LoanTransactionType.INCOME_POSTING.getValue();
|
||||
final BigDecimal principalPortion = BigDecimal.ZERO;
|
||||
final BigDecimal overPaymentPortion = BigDecimal.ZERO;
|
||||
final boolean reversed = false;
|
||||
final PaymentDetail paymentDetail = null;
|
||||
final String externalId = null;
|
||||
final LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
return new LoanTransaction(loan, office, typeOf, dateOf, amount, principalPortion, interestPortion, feeChargesPortion,
|
||||
penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId, createdDate, appUser);
|
||||
}
|
||||
|
||||
public static LoanTransaction disbursement(final Office office, final Money amount, final PaymentDetail paymentDetail,
|
||||
final LocalDate disbursementDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) {
|
||||
return new LoanTransaction(null, office, LoanTransactionType.DISBURSEMENT, paymentDetail, amount.getAmount(), disbursementDate,
|
||||
@@ -212,17 +214,25 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
|
||||
principalPortion, interestPortion, feesPortion, penaltiesPortion, overPaymentPortion, reversed, paymentDetail, externalId,
|
||||
createdDate, appUser);
|
||||
}
|
||||
|
||||
public static LoanTransaction accrual(final Loan loan, final Office office, final Money amount, final Money interest,
|
||||
final Money feeCharges, final Money penaltyCharges, final LocalDate transactionDate) {
|
||||
final AppUser appUser = null;
|
||||
return accrueTransaction(loan, office, transactionDate, amount.getAmount(), interest.getAmount(), feeCharges.getAmount(),
|
||||
penaltyCharges.getAmount(), appUser);
|
||||
}
|
||||
|
||||
public static LoanTransaction accrueTransaction(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount,
|
||||
final BigDecimal interestPortion, final BigDecimal feeChargesPortion,
|
||||
final BigDecimal penaltyChargesPortion, final AppUser appUser) {
|
||||
final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion,
|
||||
final AppUser appUser) {
|
||||
BigDecimal principalPortion = null;
|
||||
BigDecimal overPaymentPortion = null;
|
||||
boolean reversed = false;
|
||||
PaymentDetail paymentDetail = null;
|
||||
String externalId = null;
|
||||
LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf.toDate(), amount,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId,
|
||||
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf.toDate(), amount, principalPortion,
|
||||
interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId,
|
||||
createdDate, appUser);
|
||||
}
|
||||
|
||||
@@ -664,15 +674,14 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
|
||||
public boolean isAccrual() {
|
||||
return LoanTransactionType.ACCRUAL.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isNonMonetaryTransaction(){
|
||||
return isNotReversed()
|
||||
&& (LoanTransactionType.CONTRA.equals(getTypeOf())
|
||||
|| LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf())
|
||||
|| LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf())
|
||||
|| LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf())
|
||||
|| LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf())
|
||||
|| LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf()));
|
||||
|
||||
public boolean isNonMonetaryTransaction() {
|
||||
return isNotReversed()
|
||||
&& (LoanTransactionType.CONTRA.equals(getTypeOf()) || LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf())
|
||||
|| LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf())
|
||||
|| LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf())
|
||||
|| LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || LoanTransactionType.WITHDRAW_TRANSFER
|
||||
.equals(getTypeOf()));
|
||||
}
|
||||
|
||||
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
|
||||
@@ -756,13 +765,19 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
|
||||
return isMappingUpdated;
|
||||
}
|
||||
|
||||
|
||||
public Set<LoanTransactionToRepaymentScheduleMapping> getLoanTransactionToRepaymentScheduleMappings() {
|
||||
return this.loanTransactionToRepaymentScheduleMappings;
|
||||
}
|
||||
|
||||
public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo(){
|
||||
return isDisbursement() || isAccrual() || isRepaymentAtDisbursement();
|
||||
|
||||
public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() {
|
||||
return isDisbursement() || isAccrual() || isRepaymentAtDisbursement();
|
||||
}
|
||||
|
||||
public void updateCreatedDate(Date createdDate) {
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public boolean isAccrualTransaction() {
|
||||
return isAccrual();
|
||||
}
|
||||
}
|
||||
+86
@@ -627,4 +627,90 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
return latestPaidCharge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTransactionsFromDerivedFields(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
|
||||
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
|
||||
if (!loanTransaction.isAccrualTransaction()) {
|
||||
processTransactionFromDerivedFields(loanTransaction, currency, installments, charges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processTransactionFromDerivedFields(final LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
|
||||
Money principal = loanTransaction.getPrincipalPortion(currency);
|
||||
Money interest = loanTransaction.getInterestPortion(currency);
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
interest = loanTransaction.getAmount(currency);
|
||||
}
|
||||
Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
|
||||
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
if (principal.isGreaterThanZero() || interest.isGreaterThanZero() || feeCharges.isGreaterThanZero()
|
||||
|| penaltyCharges.isGreaterThanZero()) {
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
if (currentInstallment.isNotFullyPaidOff()) {
|
||||
if (penaltyCharges.isGreaterThanZero()) {
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
if (loanTransaction.isWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate, penaltyCharges);
|
||||
} else {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, penaltyCharges);
|
||||
}
|
||||
penaltyCharges = penaltyCharges.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
if (feeCharges.isGreaterThanZero()) {
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
if (loanTransaction.isWaiver()) {
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate, feeCharges);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, feeCharges);
|
||||
}
|
||||
feeCharges = feeCharges.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (interest.isGreaterThanZero()) {
|
||||
Money interestPortion = Money.zero(currency);
|
||||
if (loanTransaction.isWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, interest);
|
||||
} else {
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, interest);
|
||||
}
|
||||
interest = interest.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (principal.isGreaterThanZero()) {
|
||||
Money principalPortion = currentInstallment.payPrincipalComponent(transactionDate, principal);
|
||||
principal = principal.minus(principalPortion);
|
||||
}
|
||||
}
|
||||
if (!(principal.isGreaterThanZero() || interest.isGreaterThanZero() || feeCharges.isGreaterThanZero() || penaltyCharges
|
||||
.isGreaterThanZero())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
|
||||
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
|
||||
Integer installmentNumber = null;
|
||||
if (loanTransaction.isChargePayment() && installments.size() == 1) {
|
||||
installmentNumber = installments.get(0).getInstallmentNumber();
|
||||
}
|
||||
|
||||
if (loanTransaction.isNotWaiver()) {
|
||||
feeCharges = loanTransaction.getFeeChargesPortion(currency);
|
||||
penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
|
||||
if (feeCharges.isGreaterThanZero()) {
|
||||
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
|
||||
}
|
||||
|
||||
if (penaltyCharges.isGreaterThanZero()) {
|
||||
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+3
@@ -51,5 +51,8 @@ public interface LoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||
final Set<LoanCharge> charges);
|
||||
|
||||
void processTransactionsFromDerivedFields(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges);
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class LoanForeclosureException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public LoanForeclosureException(final String errorCode, final String errorMessage, final Object... defaultUserMessageArgs) {
|
||||
super(errorCode, errorMessage, defaultUserMessageArgs);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 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.LoanWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "LOAN", action = "FORECLOSURE")
|
||||
public class ForeClosureCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final LoanWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public ForeClosureCommandHandler(final LoanWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return writePlatformService.forecloseLoan(command.getLoanId(), command);
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -447,4 +447,26 @@ public final class LoanEventApiJsonValidator {
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
public void validateLoanForeclosure(final String json) {
|
||||
|
||||
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
|
||||
|
||||
final Set<String> foreclosureParameters = new HashSet<>(Arrays.asList("transactionDate", "note", "locale", "dateFormat"));
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, foreclosureParameters);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
|
||||
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
|
||||
|
||||
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
|
||||
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
|
||||
|
||||
validatePaymentDetails(baseDataValidator, element);
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
}
|
||||
+3
@@ -126,5 +126,8 @@ public interface LoanReadPlatformService {
|
||||
LoanTransactionData retrieveRefundByCashTemplate(Long loanId);
|
||||
|
||||
Collection<InterestRatePeriodData> retrieveLoanInterestRatePeriodData(Long loanId);
|
||||
|
||||
Collection<Long> retrieveLoanIdsWithPendingIncomePostingTransactions();
|
||||
|
||||
LoanTransactionData retrieveLoanForeclosureTemplate(final Long loanId, final LocalDate transactionDate);
|
||||
}
|
||||
+40
-2
@@ -93,6 +93,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
@@ -592,6 +593,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
||||
+ " l.total_overpaid_derived as totalOverpaid,"
|
||||
+ " l.fixed_emi_amount as fixedEmiAmount,"
|
||||
+ " l.max_outstanding_loan_balance as outstandingLoanBalance,"
|
||||
+ " l.loan_sub_status_id as loanSubStatusId,"
|
||||
+ " la.principal_overdue_derived as principalOverdue,"
|
||||
+ " la.interest_overdue_derived as interestOverdue,"
|
||||
+ " la.fee_charges_overdue_derived as feeChargesOverdue,"
|
||||
@@ -783,6 +785,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
||||
final Integer lifeCycleStatusId = JdbcSupport.getInteger(rs, "lifeCycleStatusId");
|
||||
final LoanStatusEnumData status = LoanEnumerations.status(lifeCycleStatusId);
|
||||
|
||||
final Integer loanSubStatusId = JdbcSupport.getInteger(rs, "loanSubStatusId");
|
||||
EnumOptionData loanSubStatus = null;
|
||||
if (loanSubStatusId != null) {
|
||||
loanSubStatus = LoanSubStatus.loanSubStatus(loanSubStatusId);
|
||||
}
|
||||
|
||||
// settings
|
||||
final LocalDate expectedFirstRepaymentOnDate = JdbcSupport.getLocalDate(rs, "expectedFirstRepaymentOnDate");
|
||||
final LocalDate interestChargedFromDate = JdbcSupport.getLocalDate(rs, "interestChargedFromDate");
|
||||
@@ -936,7 +944,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
||||
loanProductCounter, multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmount, outstandingLoanBalance, inArrears,
|
||||
graceOnArrearsAgeing, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
|
||||
interestRecalculationData, createStandingInstructionAtDisbursement, isvariableInstallmentsAllowed, minimumGap,
|
||||
maximumGap);
|
||||
maximumGap, loanSubStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2028,7 +2036,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
||||
|
||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN);
|
||||
final Collection<PaymentTypeData> paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||
|
||||
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate,
|
||||
retrieveTotalPaidInAdvance(loan.getId()).getPaidInAdvance(), null, null, null, null, null, null, paymentOptions, null,
|
||||
null, null, null, false);
|
||||
@@ -2100,4 +2107,35 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanTransactionData retrieveLoanForeclosureTemplate(final Long loanId, final LocalDate transactionDate) {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final Loan loan = this.loanRepository.findOne(loanId);
|
||||
if (loan == null) { throw new LoanNotFoundException(loanId); }
|
||||
loan.validateForForeclosure(transactionDate);
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
|
||||
|
||||
final CurrencyData currencyData = applicationCurrency.toData();
|
||||
|
||||
final LocalDate earliestUnpaidInstallmentDate = DateUtils.getLocalDateOfTenant();
|
||||
|
||||
final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(transactionDate);
|
||||
BigDecimal unrecognizedIncomePortion = null;
|
||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
|
||||
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||
final BigDecimal outstandingLoanBalance = loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount();
|
||||
final Boolean isReversed = false;
|
||||
|
||||
final Money outStandingAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency);
|
||||
|
||||
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate,
|
||||
outStandingAmount.getAmount(), loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount(),
|
||||
loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(), loanRepaymentScheduleInstallment
|
||||
.getFeeChargesOutstanding(currency).getAmount(), loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(
|
||||
currency).getAmount(), null, unrecognizedIncomePortion, paymentTypeOptions, null, null, null,
|
||||
outstandingLoanBalance, isReversed);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -104,6 +104,8 @@ public interface LoanWritePlatformService {
|
||||
|
||||
void recalculateInterest(long loanId);
|
||||
|
||||
CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand command);
|
||||
CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult forecloseLoan(final Long loanId, JsonCommand command);
|
||||
|
||||
}
|
||||
+34
@@ -137,6 +137,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
@@ -145,6 +146,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentException;
|
||||
@@ -822,6 +824,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
this.loanEventApiJsonValidator.validateTransaction(command.json());
|
||||
|
||||
final Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
if(loan.status().isClosed() && loan.getLoanSubStatus().equals(LoanSubStatus.FORECLOSED.getValue())) {
|
||||
final String defaultUserMessage = "The loan cannot reopend as it is foreclosed.";
|
||||
throw new LoanForeclosureException("loan.cannot.be.reopened.as.it.is.foreclosured", defaultUserMessage,
|
||||
loanId);
|
||||
}
|
||||
checkClientOrGroupActive(loan);
|
||||
final LoanTransaction transactionToAdjust = this.loanTransactionRepository.findOne(transactionId);
|
||||
if (transactionToAdjust == null) { throw new LoanTransactionNotFoundException(transactionId); }
|
||||
@@ -2883,6 +2890,33 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult forecloseLoan(final Long loanId, final JsonCommand command) {
|
||||
final String json = command.json();
|
||||
final JsonElement element = fromApiJsonHelper.parse(json);
|
||||
final Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.transactionDateParamName, element);
|
||||
this.loanEventApiJsonValidator.validateLoanForeclosure(command.json());
|
||||
loan.validateForForeclosure(transactionDate);
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
changes.put("transactionDate", transactionDate);
|
||||
|
||||
String noteText = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.noteParamName, element);
|
||||
LoanRescheduleRequest loanRescheduleRequest = null;
|
||||
this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(loan.getRepaymentScheduleInstallments(),
|
||||
loan, loanRescheduleRequest);
|
||||
loan.updateInstallmentsPostDate(transactionDate);
|
||||
|
||||
final Map<String, Object> modifications = this.loanAccountDomainService.foreCloseLoan(loan, transactionDate, noteText);
|
||||
changes.putAll(modifications);
|
||||
|
||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
||||
return commandProcessingResultBuilder.withLoanId(loanId) //
|
||||
.with(changes) //
|
||||
.build();
|
||||
}
|
||||
|
||||
private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) {
|
||||
if (!loan.isMultiDisburmentLoan()) {
|
||||
final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal";
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES
|
||||
('portfolio', 'FORECLOSURE_LOAN', 'LOAN', 'FORECLOSURE', 0),
|
||||
('portfolio', 'FORECLOSURE_LOAN_CHECKER', 'LOAN', 'FORECLOSURE_CHECKER', 0);
|
||||
|
||||
|
||||
ALTER TABLE `m_loan`
|
||||
ADD COLUMN `loan_sub_status_id` SMALLINT(5) NULL DEFAULT NULL;
|
||||
Reference in New Issue
Block a user