[FINERACT-167] Loan foreclosure implementation and integration test cases for the same.

This commit is contained in:
sachinkulkarni12
2016-06-10 18:59:26 +05:30
parent b43d3ef064
commit b9b345a563
25 changed files with 974 additions and 78 deletions
+51
View File
@@ -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">&nbsp;</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">&nbsp;</a>
<div class="method-section">
@@ -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);
@@ -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);
}
}
@@ -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");
@@ -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";
@@ -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";
}
@@ -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); }
@@ -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;
@@ -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;
}
}
@@ -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);
}
@@ -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;
}
}
@@ -42,5 +42,6 @@ public enum LoanEvent {
LOAN_CHARGE_PAYMENT, //
LOAN_CLOSED, //
LOAN_EDIT_MULTI_DISBURSE_DATE, //
LOAN_REFUND;
LOAN_REFUND, //
LOAN_FORECLOSURE;
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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);
}
}
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
@@ -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";
@@ -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;