MIFOSX-1613 : changes for overdue status and npa

This commit is contained in:
Pramod Nuthakki
2014-12-31 18:28:43 +05:30
parent d310eb308d
commit 4b6d794a47
30 changed files with 1198 additions and 374 deletions
+24 -2
View File
@@ -11295,6 +11295,13 @@ An enumeration that indicates the type of transaction processing strategy to be
be overdue before being classified as a NPA (non performing asset)
</td>
</tr>
<tr class=alt>
<td>accountMovesOutOfNPAOnlyOnArrearsCompletion</td>
</tr>
<tr>
<td class=fielddesc>Optional: Boolean - if provided as true, Loan Account moves out of NPA state only when all arrears are cleared
</td>
</tr>
<tr class=alt>
<td>accountingRule</td>
</tr>
@@ -11356,6 +11363,12 @@ An enumeration that indicates the type of transaction processing strategy to be
<tr>
<td class=fielddesc>Specifies rest frequency start date for interest recalculation.<br>
</tr>
<tr class=alt>
<td>isArrearsBasedOnOriginalSchedule</td>
</tr>
<tr>
<td class=fielddesc>If Specified as true, arrears will be identified based on original schedule.<br>
</tr>
</table>
</div>
</div>
@@ -11470,7 +11483,8 @@ GET https://DomainName/api/v1/loanproducts
"id":1,
"code":"interestRecalculationFrequencyType.same.as.repayment.period",
"value":"Same as repayment period"
}
},
"isArrearsBasedOnOriginalSchedule" : true
},
"accountingRule": {
"id": 2,
@@ -12598,7 +12612,7 @@ GET https://DomainName/api/v1/loanproducts/{productId}
numberOfRepaymentVariationsForBorrowerCycle,
interestRateVariationsForBorrowerCycle,
multiDisburseLoan,maxTrancheCount,
outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds
outstandingLoanBalance,overdueDaysForNPA,accountMovesOutOfNPAOnlyOnArrearsCompletion,holdGuaranteeFunds
</td>
</tr>
</table>
@@ -12631,6 +12645,14 @@ GET https://DomainName/api/v1/loanproducts/{productId}
<td>interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType</td>
</tr>
</table>
<table class=matrixHeading>
<tr class="matrixHeadingBG">
<td><div class="mifosXHeading2">Additional Optional Fields if interest recalculation is enabled(true) </div></td>
</tr>
<tr class=alt>
<td>isArrearsBasedOnOriginalSchedule</td>
</tr>
</table>
<table class=matrixHeading>
<tr class="matrixHeadingBG">
<td><div class="mifosXHeading2">Additional Optional Fields if interest recalculation is enabled(true) and recalculationRestFrequencyType is not same as repayment period</div></td>
@@ -3584,7 +3584,7 @@ public class ClientLoanIntegrationTest {
final Integer loanProductID = createLoanProductWithInterestRecalculation(LoanProductTestBuilder.MIFOS_STANDARD_STRATEGY,
LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_INTEREST_AND_FEE,
LoanProductTestBuilder.RECALCULATION_STRATEGY_RESCHEDULE_NEXT_REPAYMENTS,
LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "1", REST_START_DATE, null, overdueFeeChargeId.toString());
LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "1", REST_START_DATE, null, overdueFeeChargeId.toString(), false);
final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, LOAN_DISBURSEMENT_DATE,
REST_START_DATE, LoanApplicationTestBuilder.MIFOS_STANDARD_STRATEGY, null);
@@ -3766,6 +3766,156 @@ public class ClientLoanIntegrationTest {
}
@Test
public void testLoanScheduleWithInterestRecalculation_WITH_CURRENT_REPAYMENT_BASED_ARREARS_AGEING() {
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
Calendar todaysDate = Calendar.getInstance();
todaysDate.add(Calendar.DAY_OF_MONTH, -14);
final String LOAN_DISBURSEMENT_DATE = dateFormat.format(todaysDate.getTime());
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
final Integer loanProductID = createLoanProductWithInterestRecalculation(LoanProductTestBuilder.RBI_INDIA_STRATEGY,
LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_INTEREST,
LoanProductTestBuilder.RECALCULATION_STRATEGY_RESCHEDULE_NEXT_REPAYMENTS,
LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "1", LOAN_DISBURSEMENT_DATE, null);
final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, LOAN_DISBURSEMENT_DATE,
LOAN_DISBURSEMENT_DATE, LoanApplicationTestBuilder.RBI_INDIA_STRATEGY, new ArrayList<HashMap>(0));
Assert.assertNotNull(loanID);
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
List<Map<String, Object>> expectedvalues = new ArrayList<>();
todaysDate = Calendar.getInstance();
addRepaymentValues(expectedvalues, todaysDate, -1, "2482.76", "46.15", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2494.22", "34.69", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2505.73", "23.18", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2517.29", "11.62", "0.0", "0.0");
verifyLoanRepaymentSchedule(loanSchedule, expectedvalues);
System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.approveLoan(LOAN_DISBURSEMENT_DATE, loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.disburseLoan(LOAN_DISBURSEMENT_DATE, loanID);
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
expectedvalues = new ArrayList<>();
todaysDate = Calendar.getInstance();
addRepaymentValues(expectedvalues, todaysDate, -1, "2482.76", "46.15", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2482.55", "46.36", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2505.67", "23.24", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2529.02", "11.67", "0.0", "0.0");
verifyLoanRepaymentSchedule(loanSchedule, expectedvalues);
todaysDate = Calendar.getInstance();
todaysDate.add(Calendar.DAY_OF_MONTH, -7);
HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID);
List dates = (List) loanSummary.get("overdueSinceDate");
assertEquals(todaysDate.get(Calendar.YEAR), dates.get(0));
assertEquals(todaysDate.get(Calendar.MONTH) + 1, dates.get(1));
assertEquals(todaysDate.get(Calendar.DAY_OF_MONTH), dates.get(2));
todaysDate = Calendar.getInstance();
todaysDate.add(Calendar.DAY_OF_MONTH, -8);
final String LOAN_FIRST_REPAYMENT_DATE = dateFormat.format(todaysDate.getTime());
Float totalDueForCurrentPeriod = (Float) loanSchedule.get(1).get("totalDueForPeriod");
this.loanTransactionHelper.makeRepayment(LOAN_FIRST_REPAYMENT_DATE, totalDueForCurrentPeriod, loanID);
todaysDate = Calendar.getInstance();
todaysDate.add(Calendar.DAY_OF_MONTH, -7);
loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID);
dates = (List) loanSummary.get("overdueSinceDate");
assertEquals(todaysDate.get(Calendar.YEAR), dates.get(0));
assertEquals(todaysDate.get(Calendar.MONTH) + 1, dates.get(1));
assertEquals(todaysDate.get(Calendar.DAY_OF_MONTH), dates.get(2));
}
@Test
public void testLoanScheduleWithInterestRecalculation_WITH_ORIGINAL_REPAYMENT_BASED_ARREARS_AGEING() {
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
Calendar todaysDate = Calendar.getInstance();
todaysDate.add(Calendar.DAY_OF_MONTH, -14);
final String LOAN_DISBURSEMENT_DATE = dateFormat.format(todaysDate.getTime());
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
final Integer loanProductID = createLoanProductWithInterestRecalculation(LoanProductTestBuilder.RBI_INDIA_STRATEGY,
LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_INTEREST,
LoanProductTestBuilder.RECALCULATION_STRATEGY_RESCHEDULE_NEXT_REPAYMENTS,
LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "1", LOAN_DISBURSEMENT_DATE, null, null, true);
final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, LOAN_DISBURSEMENT_DATE,
LOAN_DISBURSEMENT_DATE, LoanApplicationTestBuilder.RBI_INDIA_STRATEGY, new ArrayList<HashMap>(0));
Assert.assertNotNull(loanID);
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
List<Map<String, Object>> expectedvalues = new ArrayList<>();
todaysDate = Calendar.getInstance();
addRepaymentValues(expectedvalues, todaysDate, -1, "2482.76", "46.15", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2494.22", "34.69", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2505.73", "23.18", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2517.29", "11.62", "0.0", "0.0");
verifyLoanRepaymentSchedule(loanSchedule, expectedvalues);
System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.approveLoan(LOAN_DISBURSEMENT_DATE, loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.disburseLoan(LOAN_DISBURSEMENT_DATE, loanID);
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
expectedvalues = new ArrayList<>();
todaysDate = Calendar.getInstance();
addRepaymentValues(expectedvalues, todaysDate, -1, "2482.76", "46.15", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2482.55", "46.36", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2505.67", "23.24", "0.0", "0.0");
addRepaymentValues(expectedvalues, todaysDate, 1, "2529.02", "11.67", "0.0", "0.0");
verifyLoanRepaymentSchedule(loanSchedule, expectedvalues);
todaysDate = Calendar.getInstance();
todaysDate.add(Calendar.DAY_OF_MONTH, -7);
HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID);
List dates = (List) loanSummary.get("overdueSinceDate");
assertEquals(todaysDate.get(Calendar.YEAR), dates.get(0));
assertEquals(todaysDate.get(Calendar.MONTH) + 1, dates.get(1));
assertEquals(todaysDate.get(Calendar.DAY_OF_MONTH), dates.get(2));
todaysDate = Calendar.getInstance();
todaysDate.add(Calendar.DAY_OF_MONTH, -8);
final String LOAN_FIRST_REPAYMENT_DATE = dateFormat.format(todaysDate.getTime());
Float totalDueForCurrentPeriod = (Float) loanSchedule.get(1).get("totalDueForPeriod");
this.loanTransactionHelper.makeRepayment(LOAN_FIRST_REPAYMENT_DATE, totalDueForCurrentPeriod, loanID);
todaysDate = Calendar.getInstance();
todaysDate.add(Calendar.DAY_OF_MONTH, -7);
loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID);
dates = (List) loanSummary.get("overdueSinceDate");
Assert.assertNull(dates);
}
private void addRepaymentValues(List<Map<String, Object>> expectedvalues, Calendar todaysDate, int addMonths, String principalDue,
String interestDue, String feeChargesDue, String penaltyChargesDue) {
Map<String, Object> values = new HashMap<>(3);
@@ -3795,13 +3945,14 @@ public class ClientLoanIntegrationTest {
final String recalculationRestFrequencyDate, final Account[] accounts) {
return createLoanProductWithInterestRecalculation(repaymentStrategy, interestRecalculationCompoundingMethod,
rescheduleStrategyMethod, recalculationRestFrequencyType, recalculationRestFrequencyInterval,
recalculationRestFrequencyDate, accounts, null);
recalculationRestFrequencyDate, accounts, null, false);
}
private Integer createLoanProductWithInterestRecalculation(final String repaymentStrategy,
final String interestRecalculationCompoundingMethod, final String rescheduleStrategyMethod,
final String recalculationRestFrequencyType, final String recalculationRestFrequencyInterval,
final String recalculationRestFrequencyDate, final Account[] accounts, final String chargeId) {
final String recalculationRestFrequencyDate, final Account[] accounts, final String chargeId,
boolean isArrearsBasedOnOriginalSchedule) {
System.out.println("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
LoanProductTestBuilder builder = new LoanProductTestBuilder()
.withPrincipal("10000000.00")
@@ -3820,6 +3971,8 @@ public class ClientLoanIntegrationTest {
builder = builder.withAccountingRulePeriodicAccrual(accounts);
}
if (isArrearsBasedOnOriginalSchedule) builder = builder.withArrearsConfiguration();
final String loanProductJSON = builder.build(chargeId);
return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
}
@@ -92,6 +92,7 @@ public class LoanProductTestBuilder {
private String mandatoryGuarantee = null;
private String minimumGuaranteeFromOwnFunds = null;
private String minimumGuaranteeFromGuarantor = null;
private String isArrearsBasedOnOriginalSchedule = null;
public String build(final String chargeId) {
final HashMap<String, Object> map = new HashMap<>();
@@ -148,6 +149,9 @@ public class LoanProductTestBuilder {
map.put("recalculationRestFrequencyType", recalculationRestFrequencyType);
map.put("recalculationRestFrequencyInterval", recalculationRestFrequencyInterval);
map.put("recalculationRestFrequencyDate", recalculationRestFrequencyDate);
if (isArrearsBasedOnOriginalSchedule != null) {
map.put("isArrearsBasedOnOriginalSchedule", isArrearsBasedOnOriginalSchedule);
}
}
if (holdGuaranteeFunds != null) {
map.put("holdGuaranteeFunds", this.holdGuaranteeFunds);
@@ -398,6 +402,11 @@ public class LoanProductTestBuilder {
return this;
}
public LoanProductTestBuilder withArrearsConfiguration() {
this.isArrearsBasedOnOriginalSchedule = "true";
return this;
}
public LoanProductTestBuilder withOnHoldFundDetails(final String mandatoryGuarantee, final String minimumGuaranteeFromGuarantor,
final String minimumGuaranteeFromOwnFunds) {
this.holdGuaranteeFunds = true;
@@ -13,7 +13,6 @@ import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstan
import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstants.transferDateParamName;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -37,7 +36,6 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidRefundDateException;
import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler;
import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService;
import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
@@ -12,8 +12,14 @@ public class BusinessEventNotificationConstants {
public static enum BUSINESS_EVENTS {
LOAN_APPROVED("loan_approved"), LOAN_UNDO_APPROVAL("loan_undo_approval"), LOAN_UNDO_DISBURSAL("loan_undo_disbursal"), LOAN_UNDO_TRANSACTION(
"loan_undo_transaction"), LOAN_MAKE_REPAYMENT("loan_repayment_transaction"), LOAN_WRITTEN_OFF("loan_writtenoff"), LOAN_UNDO_WRITTEN_OFF(
"loan_undo_writtenoff");
"loan_undo_transaction"), LOAN_ADJUST_TRANSACTION("loan_adjust_transaction"), LOAN_MAKE_REPAYMENT(
"loan_repayment_transaction"), LOAN_WRITTEN_OFF("loan_writtenoff"), LOAN_UNDO_WRITTEN_OFF("loan_undo_writtenoff"), LOAN_DISBURSAL(
"loan_disbursal"), LOAN_WAIVE_INTEREST("loan_waive_interest"), LOAN_CLOSE("loan_close"), LOAN_CLOSE_AS_RESCHEDULE(
"loan_close_as_reschedule"), LOAN_ADD_CHARGE("loan_add_charge"), LOAN_UPDATE_CHARGE("loan_update_charge"), LOAN_WAIVE_CHARGE(
"loan_waive_charge"), LOAN_DELETE_CHARGE("loan_delete_charge"), LOAN_CHARGE_PAYMENT("loan_charge_payment"), LOAN_INITIATE_TRANSFER(
"loan_initiate_transfer"), LOAN_ACCEPT_TRANSFER("loan_accept_transfer"), LOAN_WITHDRAW_TRANSFER("loan_withdraw_transfer"), LOAN_REJECT_TRANSFER(
"loan_reject_transfer"), LOAN_REASSIGN_OFFICER("loan_reassign_officer"), LOAN_REMOVE_OFFICER("loan_remove_officer"), LOAN_APPLY_OVERDUE_CHARGE(
"loan_apply_overdue_charge"), LOAN_INTEREST_RECALCULATION("loan_interest_recalculation"), LOAN_REFUND("loan_refund");
private final String value;
@@ -36,4 +42,19 @@ public class BusinessEventNotificationConstants {
return this.value;
}
}
public static enum BUSINESS_ENTITY {
LOAN("loan"), LOAN_TRANSACTION("loan_transaction"), LOAN_CHARGE("loan_charge"), LOAN_ADJUSTED_TRANSACTION(
"loan_adjusted_transaction");
private final String value;
private BUSINESS_ENTITY(final String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
}
@@ -5,7 +5,9 @@
*/
package org.mifosplatform.portfolio.common.service;
import org.springframework.data.jpa.domain.AbstractPersistable;
import java.util.Map;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
/**
* The interface to be implemented by classes that want to be informed when a
@@ -18,11 +20,11 @@ public interface BusinessEventListner {
/**
* Implement this method for notifications before executing Business Event
*/
public void businessEventToBeExecuted(AbstractPersistable<Long> businessEventEntity);
public void businessEventToBeExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity);
/**
* Implement this method for notifications after executing Business Event
*/
public void businessEventWasExecuted(AbstractPersistable<Long> businessEventEntity);
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity);
}
@@ -6,7 +6,8 @@
package org.mifosplatform.portfolio.common.service;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS;
import org.springframework.data.jpa.domain.AbstractPersistable;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
import java.util.Map;
/**
* Implemented class is responsible for notifying the business event to
@@ -19,13 +20,13 @@ public interface BusinessEventNotifierService {
* Method should be called to notify listeners before Business event
* execution for any pre-processing of event
*/
public void notifyBusinessEventToBeExecuted(BUSINESS_EVENTS businessEvent, AbstractPersistable<Long> businessEventEntity);
public void notifyBusinessEventToBeExecuted(BUSINESS_EVENTS businessEvent, Map<BUSINESS_ENTITY, Object> businessEventEntity);
/**
* Method should be called to notify listeners after Business event
* execution for any post-processing of event
*/
public void notifyBusinessEventWasExecuted(BUSINESS_EVENTS businessEvent, AbstractPersistable<Long> businessEventEntity);
public void notifyBusinessEventWasExecuted(BUSINESS_EVENTS businessEvent, Map<BUSINESS_ENTITY, Object> businessEventEntity);
/**
* Method is to register a class as listener for pre-processing of any
@@ -10,8 +10,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS;
import org.springframework.data.jpa.domain.AbstractPersistable;
import org.springframework.stereotype.Service;
@Service
@@ -31,7 +31,7 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
* org.springframework.data.jpa.domain.AbstractPersistable)
*/
@Override
public void notifyBusinessEventToBeExecuted(BUSINESS_EVENTS businessEvent, AbstractPersistable<Long> businessEventEntity) {
public void notifyBusinessEventToBeExecuted(BUSINESS_EVENTS businessEvent, Map<BUSINESS_ENTITY, Object> businessEventEntity) {
List<BusinessEventListner> businessEventListners = this.preListners.get(businessEvent);
if (businessEventListners != null) {
for (BusinessEventListner eventListner : businessEventListners) {
@@ -51,7 +51,7 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
* org.springframework.data.jpa.domain.AbstractPersistable)
*/
@Override
public void notifyBusinessEventWasExecuted(BUSINESS_EVENTS businessEvent, AbstractPersistable<Long> businessEventEntity) {
public void notifyBusinessEventWasExecuted(BUSINESS_EVENTS businessEvent, Map<BUSINESS_ENTITY, Object> businessEventEntity) {
List<BusinessEventListner> businessEventListners = this.postListners.get(businessEvent);
if (businessEventListners != null) {
for (BusinessEventListner eventListner : businessEventListners) {
@@ -299,9 +299,6 @@ public class Loan extends AbstractPersistable<Long> {
@Embedded
private LoanSummary summary;
@OneToOne(mappedBy = "loan", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.LAZY)
private LoanSummaryArrearsAging summaryArrearsAging;
@Transient
private boolean accountNumberRequiresAutoGeneration = false;
@Transient
@@ -1146,7 +1143,6 @@ public class Loan extends AbstractPersistable<Long> {
if (isNotDisbursed()) {
this.summary.zeroFields();
this.summaryArrearsAging = null;
this.totalOverpaid = null;
} else {
final Money overpaidBy = calculateTotalOverpayment();
@@ -1158,13 +1154,6 @@ public class Loan extends AbstractPersistable<Long> {
final Money principal = this.loanRepaymentScheduleDetail.getPrincipal();
this.summary.updateSummary(loanCurrency(), principal, this.repaymentScheduleInstallments, this.loanSummaryWrapper,
isDisbursed());
if (this.summaryArrearsAging == null) {
this.summaryArrearsAging = new LoanSummaryArrearsAging(this);
}
this.summaryArrearsAging.updateSummary(loanCurrency(), this.repaymentScheduleInstallments, this.loanSummaryWrapper);
if (this.summaryArrearsAging.isNotInArrears(loanCurrency())) {
this.summaryArrearsAging = null;
}
updateLoanOutstandingBalaces();
}
}
@@ -1928,7 +1917,7 @@ public class Loan extends AbstractPersistable<Long> {
actualChanges.put(LoanApiConstants.approvedLoanAmountParameterName, approvedLoanAmount);
actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, approvedLoanAmount);
} else if(approvedLoanAmount.compareTo(this.proposedPrincipal) == 1){
} else if (approvedLoanAmount.compareTo(this.proposedPrincipal) == 1) {
final String errorMessage = "Loan approved amount can't be greater than loan amount demanded.";
throw new InvalidLoanStateTransitionException("approval", "amount.can't.be.greater.than.loan.amount.demanded",
errorMessage, this.proposedPrincipal, approvedLoanAmount);
@@ -3377,7 +3366,7 @@ public class Loan extends AbstractPersistable<Long> {
return status().isRejected();
}
private boolean isOpen() {
public boolean isOpen() {
return status().isActive();
}
@@ -5113,4 +5102,5 @@ public class Loan extends AbstractPersistable<Long> {
return lastTransactionDate == null ? now : lastTransactionDate;
}
}
@@ -9,6 +9,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -44,6 +45,7 @@ import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository;
import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.portfolio.client.domain.Client;
import org.mifosplatform.portfolio.client.exception.ClientNotActiveException;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS;
import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService;
@@ -126,6 +128,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final String txnExternalId, final boolean isRecoveryRepayment, boolean isAccountTransfer) {
AppUser currentUser = getAppUserIfPresent();
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
// TODO: Is it required to validate transaction date with meeting dates
// if repayments is synced with meeting?
@@ -221,7 +225,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, newRepaymentTransaction);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRepaymentTransaction));
builderResult.withEntityId(newRepaymentTransaction.getId()) //
.withOfficeId(loan.getOfficeId()) //
@@ -276,7 +281,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
AppUser currentUser = getAppUserIfPresent();
boolean isAccountTransfer = true;
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CHARGE_PAYMENT,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
@@ -311,6 +317,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CHARGE_PAYMENT,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newPaymentTransaction));
return newPaymentTransaction;
}
@@ -349,7 +357,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
boolean isAccountTransfer = true;
final Loan loan = this.loanAccountAssembler.assembleFrom(accountId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REFUND,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
@@ -374,7 +383,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REFUND,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRefundTransaction));
builderResult.withEntityId(newRefundTransaction.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
@@ -556,7 +566,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) {
final Loan loan = this.loanAccountAssembler.assembleFrom(accountId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REFUND,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
AppUser currentUser = getAppUserIfPresent();
@@ -584,6 +595,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REFUND,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRefundTransaction));
builderResult.withEntityId(newRefundTransaction.getId()) //
.withOfficeId(loan.getOfficeId()) //
@@ -592,4 +605,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
return newRefundTransaction;
}
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;
}
}
@@ -927,4 +927,9 @@ public class LoanCharge extends AbstractPersistable<Long> {
}
return paidChargePerInstallment;
}
public Loan getLoan() {
return this.loan;
}
}
@@ -306,4 +306,29 @@ public final class LoanSummary {
public BigDecimal getTotalPrincipalWrittenOff() {
return this.totalPrincipalWrittenOff;
}
public BigDecimal getTotalInterestWaived() {
return this.totalInterestWaived;
}
public BigDecimal getTotalFeeChargesRepaid() {
return this.totalFeeChargesRepaid;
}
public BigDecimal getTotalFeeChargesWaived() {
return this.totalFeeChargesWaived;
}
public BigDecimal getTotalPenaltyChargesRepaid() {
return this.totalPenaltyChargesRepaid;
}
public BigDecimal getTotalPenaltyChargesWaived() {
return this.totalPenaltyChargesWaived;
}
}
@@ -1,130 +0,0 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.springframework.data.domain.Persistable;
/**
* <p>
* A specialist view of arrears & aging summary details of a {@link Loan}.
* <p>
*
* <p>
* This is intentionally split from the loan summary concept and maps to its own
* table that contains only arrears and aging information. The reason is to make
* any overnight batch update of these details simpler and improve performance.
* </p>
*/
@Entity
@Table(name = "m_loan_arrears_aging")
@org.hibernate.annotations.GenericGenerator(name = "loan-primarykey", strategy = "foreign", parameters = { //
@org.hibernate.annotations.Parameter(name = "property", value = "loan") //
})
public final class LoanSummaryArrearsAging implements Persistable<Long> {
@Id
@GeneratedValue(generator = "loan-primarykey")
@Column(name = "loan_id", unique = true, nullable = false)
private Long loanId;
@Column(name = "principal_overdue_derived", scale = 6, precision = 19)
private BigDecimal totalPrincipalOverdue;
@Column(name = "interest_overdue_derived", scale = 6, precision = 19)
private BigDecimal totalInterestOverdue;
@Column(name = "fee_charges_overdue_derived", scale = 6, precision = 19)
private BigDecimal totalFeeChargesOverdue;
@Column(name = "penalty_charges_overdue_derived", scale = 6, precision = 19)
private BigDecimal totalPenaltyChargesOverdue;
@Column(name = "total_overdue_derived", scale = 6, precision = 19)
private BigDecimal totalOverdue;
@Temporal(TemporalType.DATE)
@Column(name = "overdue_since_date_derived")
private Date overdueSinceDate;
@OneToOne(optional = false)
@PrimaryKeyJoinColumn
private Loan loan;
protected LoanSummaryArrearsAging() {
//
}
public LoanSummaryArrearsAging(final Loan loan) {
this.loan = loan;
zeroFields();
}
@Override
public Long getId() {
return this.loanId;
}
@Override
public boolean isNew() {
return null == getId();
}
public void zeroFields() {
this.totalPrincipalOverdue = BigDecimal.ZERO;
this.totalInterestOverdue = BigDecimal.ZERO;
this.totalFeeChargesOverdue = BigDecimal.ZERO;
this.totalPenaltyChargesOverdue = BigDecimal.ZERO;
this.totalOverdue = BigDecimal.ZERO;
this.overdueSinceDate = null;
}
public void updateSummary(final MonetaryCurrency currency, final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments,
final LoanSummaryWrapper summaryWrapper) {
this.totalPrincipalOverdue = summaryWrapper.calculateTotalPrincipalOverdueOn(repaymentScheduleInstallments, currency,
DateUtils.getLocalDateOfTenant()).getAmount();
this.totalInterestOverdue = summaryWrapper.calculateTotalInterestOverdueOn(repaymentScheduleInstallments, currency,
DateUtils.getLocalDateOfTenant()).getAmount();
this.totalFeeChargesOverdue = summaryWrapper.calculateTotalFeeChargesOverdueOn(repaymentScheduleInstallments, currency,
DateUtils.getLocalDateOfTenant()).getAmount();
this.totalPenaltyChargesOverdue = summaryWrapper.calculateTotalPenaltyChargesOverdueOn(repaymentScheduleInstallments, currency,
DateUtils.getLocalDateOfTenant()).getAmount();
final Money totalOverdue = Money.of(currency, this.totalPrincipalOverdue).plus(this.totalInterestOverdue)
.plus(this.totalFeeChargesOverdue).plus(this.totalPenaltyChargesOverdue);
this.totalOverdue = totalOverdue.getAmount();
final LocalDate overdueSinceLocalDate = summaryWrapper.determineOverdueSinceDateFrom(repaymentScheduleInstallments, currency,
DateUtils.getLocalDateOfTenant());
if (overdueSinceLocalDate != null) {
this.overdueSinceDate = overdueSinceLocalDate.toDate();
} else {
this.overdueSinceDate = null;
}
}
public boolean isNotInArrears(final MonetaryCurrency currency) {
return Money.of(currency, this.totalOverdue).isZero();
}
}
@@ -26,6 +26,7 @@ import org.mifosplatform.portfolio.account.data.AccountTransferDTO;
import org.mifosplatform.portfolio.account.domain.AccountTransferDetails;
import org.mifosplatform.portfolio.account.domain.AccountTransferType;
import org.mifosplatform.portfolio.account.service.AccountTransfersWritePlatformService;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS;
import org.mifosplatform.portfolio.common.service.BusinessEventListner;
import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService;
@@ -46,7 +47,6 @@ import org.mifosplatform.portfolio.savings.domain.DepositAccountOnHoldTransactio
import org.mifosplatform.portfolio.savings.domain.SavingsAccount;
import org.mifosplatform.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.AbstractPersistable;
import org.springframework.stereotype.Service;
@Service
@@ -83,8 +83,8 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_UNDO_APPROVAL, new UndoAllFundTransactions());
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL,
new ReverseAllFundsOnBusinessEvent());
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_UNDO_TRANSACTION,
new ReverseFundsOnBusinessEvent());
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION,
new AdjustFundsOnBusinessEvent());
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT,
new ReleaseFundsOnBusinessEvent());
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_WRITTEN_OFF, new ReleaseAllFunds());
@@ -236,8 +236,9 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
releaseLoanIds.put(loanId, guarantorFundingDetails.getId());
try {
BigDecimal remainingAmount = guarantorFundingDetails.getAmountRemaining();
if(loan.getGuaranteeAmount().compareTo(loan.getPrincpal().getAmount()) == 1){
remainingAmount = remainingAmount.multiply(loan.getPrincpal().getAmount()).divide(loan.getGuaranteeAmount(), roundingMode);
if (loan.getGuaranteeAmount().compareTo(loan.getPrincpal().getAmount()) == 1) {
remainingAmount = remainingAmount.multiply(loan.getPrincpal().getAmount()).divide(loan.getGuaranteeAmount(),
roundingMode);
}
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, remainingAmount, fromAccountType,
toAccountType, fromAccountId, toAccountId, description, locale, fmt, paymentDetail, fromTransferType,
@@ -499,12 +500,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
private class ValidateOnBusinessEvent implements BusinessEventListner {
@Override
public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable<Long> businessEventEntity) {}
public void businessEventToBeExecuted(@SuppressWarnings("unused") Map<BUSINESS_ENTITY, Object> businessEventEntity) {}
@Override
public void businessEventWasExecuted(AbstractPersistable<Long> businessEventEntity) {
if (businessEventEntity instanceof Loan) {
Loan loan = (Loan) businessEventEntity;
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN);
if (entity instanceof Loan) {
Loan loan = (Loan) entity;
validateGuarantorBusinessRules(loan);
}
}
@@ -513,12 +515,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
private class HoldFundsOnBusinessEvent implements BusinessEventListner {
@Override
public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable<Long> businessEventEntity) {}
public void businessEventToBeExecuted(@SuppressWarnings("unused") Map<BUSINESS_ENTITY, Object> businessEventEntity) {}
@Override
public void businessEventWasExecuted(AbstractPersistable<Long> businessEventEntity) {
if (businessEventEntity instanceof Loan) {
Loan loan = (Loan) businessEventEntity;
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN);
if (entity instanceof Loan) {
Loan loan = (Loan) entity;
holdGuarantorFunds(loan);
}
}
@@ -527,12 +530,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
private class ReleaseFundsOnBusinessEvent implements BusinessEventListner {
@Override
public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable<Long> businessEventEntity) {}
public void businessEventToBeExecuted(@SuppressWarnings("unused") Map<BUSINESS_ENTITY, Object> businessEventEntity) {}
@Override
public void businessEventWasExecuted(AbstractPersistable<Long> businessEventEntity) {
if (businessEventEntity instanceof LoanTransaction) {
LoanTransaction loanTransaction = (LoanTransaction) businessEventEntity;
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION);
if (entity instanceof LoanTransaction) {
LoanTransaction loanTransaction = (LoanTransaction) entity;
if (releaseLoanIds.containsKey(loanTransaction.getLoan().getId())) {
completeGuarantorFund(loanTransaction);
} else {
@@ -545,12 +549,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
private class ReverseFundsOnBusinessEvent implements BusinessEventListner {
@Override
public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable<Long> businessEventEntity) {}
public void businessEventToBeExecuted(@SuppressWarnings("unused") Map<BUSINESS_ENTITY, Object> businessEventEntity) {}
@Override
public void businessEventWasExecuted(AbstractPersistable<Long> businessEventEntity) {
if (businessEventEntity instanceof LoanTransaction) {
LoanTransaction loanTransaction = (LoanTransaction) businessEventEntity;
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION);
if (entity instanceof LoanTransaction) {
LoanTransaction loanTransaction = (LoanTransaction) entity;
List<Long> reersedTransactions = new ArrayList<>(1);
reersedTransactions.add(loanTransaction.getId());
reverseTransaction(reersedTransactions);
@@ -558,15 +563,38 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
}
}
private class AdjustFundsOnBusinessEvent implements BusinessEventListner {
@Override
public void businessEventToBeExecuted(@SuppressWarnings("unused") Map<BUSINESS_ENTITY, Object> businessEventEntity) {}
@Override
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION);
if (entity instanceof LoanTransaction) {
LoanTransaction loanTransaction = (LoanTransaction) entity;
List<Long> reersedTransactions = new ArrayList<>(1);
reersedTransactions.add(loanTransaction.getId());
reverseTransaction(reersedTransactions);
}
Object transactionentity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION);
if (transactionentity != null && transactionentity instanceof LoanTransaction) {
LoanTransaction loanTransaction = (LoanTransaction) transactionentity;
releaseGuarantorFunds(loanTransaction);
}
}
}
private class ReverseAllFundsOnBusinessEvent implements BusinessEventListner {
@Override
public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable<Long> businessEventEntity) {}
public void businessEventToBeExecuted(@SuppressWarnings("unused") Map<BUSINESS_ENTITY, Object> businessEventEntity) {}
@Override
public void businessEventWasExecuted(AbstractPersistable<Long> businessEventEntity) {
if (businessEventEntity instanceof Loan) {
Loan loan = (Loan) businessEventEntity;
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN);
if (entity instanceof Loan) {
Loan loan = (Loan) entity;
List<Long> reersedTransactions = new ArrayList<>(1);
reersedTransactions.addAll(loan.findExistingTransactionIds());
reverseTransaction(reersedTransactions);
@@ -577,12 +605,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
private class UndoAllFundTransactions implements BusinessEventListner {
@Override
public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable<Long> businessEventEntity) {}
public void businessEventToBeExecuted(@SuppressWarnings("unused") Map<BUSINESS_ENTITY, Object> businessEventEntity) {}
@Override
public void businessEventWasExecuted(AbstractPersistable<Long> businessEventEntity) {
if (businessEventEntity instanceof Loan) {
Loan loan = (Loan) businessEventEntity;
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN);
if (entity instanceof Loan) {
Loan loan = (Loan) entity;
reverseAllFundTransaction(loan);
}
}
@@ -591,12 +620,13 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
private class ReleaseAllFunds implements BusinessEventListner {
@Override
public void businessEventToBeExecuted(@SuppressWarnings("unused") AbstractPersistable<Long> businessEventEntity) {}
public void businessEventToBeExecuted(@SuppressWarnings("unused") Map<BUSINESS_ENTITY, Object> businessEventEntity) {}
@Override
public void businessEventWasExecuted(AbstractPersistable<Long> businessEventEntity) {
if (businessEventEntity instanceof LoanTransaction) {
LoanTransaction loanTransaction = (LoanTransaction) businessEventEntity;
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object entity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION);
if (entity instanceof LoanTransaction) {
LoanTransaction loanTransaction = (LoanTransaction) entity;
releaseAllGuarantors(loanTransaction);
}
}
@@ -19,13 +19,10 @@ public class LoanSchedulePeriodData {
private final Integer period;
private final LocalDate fromDate;
private final LocalDate dueDate;
@SuppressWarnings("unused")
private final LocalDate obligationsMetOnDate;
@SuppressWarnings("unused")
private final Boolean complete;
private final Integer daysInPeriod;
private final BigDecimal principalDisbursed;
@SuppressWarnings("unused")
private final BigDecimal principalOriginalDue;
private final BigDecimal principalDue;
private final BigDecimal principalPaid;
@@ -52,21 +49,14 @@ public class LoanSchedulePeriodData {
@SuppressWarnings("unused")
private final BigDecimal totalOriginalDueForPeriod;
@SuppressWarnings("unused")
private final BigDecimal totalDueForPeriod;
@SuppressWarnings("unused")
private final BigDecimal totalPaidForPeriod;
@SuppressWarnings("unused")
private final BigDecimal totalPaidInAdvanceForPeriod;
@SuppressWarnings("unused")
private final BigDecimal totalPaidLateForPeriod;
@SuppressWarnings("unused")
private final BigDecimal totalWaivedForPeriod;
@SuppressWarnings("unused")
private final BigDecimal totalWrittenOffForPeriod;
private final BigDecimal totalOutstandingForPeriod;
private final BigDecimal totalOverdue;
@SuppressWarnings("unused")
private final BigDecimal totalActualCostOfLoanForPeriod;
public static LoanSchedulePeriodData disbursementOnlyPeriod(final LocalDate disbursementDate, final BigDecimal principalDisbursed,
@@ -108,6 +98,26 @@ public class LoanSchedulePeriodData {
totalActualCostOfLoanForPeriod);
}
public static LoanSchedulePeriodData WithPaidDetail(final LoanSchedulePeriodData loanSchedulePeriodData, final boolean complete,
final BigDecimal principalPaid, final BigDecimal interestPaid, final BigDecimal feeChargesPaid,
final BigDecimal penaltyChargesPaid) {
return new LoanSchedulePeriodData(loanSchedulePeriodData.period, loanSchedulePeriodData.fromDate, loanSchedulePeriodData.dueDate,
loanSchedulePeriodData.obligationsMetOnDate, complete, loanSchedulePeriodData.principalOriginalDue, principalPaid,
loanSchedulePeriodData.principalWrittenOff, loanSchedulePeriodData.principalOutstanding,
loanSchedulePeriodData.principalLoanBalanceOutstanding, loanSchedulePeriodData.interestDue, interestPaid,
loanSchedulePeriodData.interestWaived, loanSchedulePeriodData.interestWrittenOff,
loanSchedulePeriodData.interestOutstanding, loanSchedulePeriodData.feeChargesDue, feeChargesPaid,
loanSchedulePeriodData.feeChargesWaived, loanSchedulePeriodData.feeChargesWrittenOff,
loanSchedulePeriodData.feeChargesOutstanding, loanSchedulePeriodData.penaltyChargesDue, penaltyChargesPaid,
loanSchedulePeriodData.penaltyChargesWaived, loanSchedulePeriodData.penaltyChargesWrittenOff,
loanSchedulePeriodData.penaltyChargesOutstanding, loanSchedulePeriodData.totalDueForPeriod,
loanSchedulePeriodData.totalPaidForPeriod, loanSchedulePeriodData.totalPaidInAdvanceForPeriod,
loanSchedulePeriodData.totalPaidLateForPeriod, loanSchedulePeriodData.totalWaivedForPeriod,
loanSchedulePeriodData.totalWrittenOffForPeriod, loanSchedulePeriodData.totalOutstandingForPeriod,
loanSchedulePeriodData.totalActualCostOfLoanForPeriod);
}
/*
* constructor used for creating period on loan schedule that is only a
* disbursement (typically first period)
@@ -417,4 +427,9 @@ public class LoanSchedulePeriodData {
public BigDecimal principalLoanBalanceOutstanding() {
return this.principalLoanBalanceOutstanding;
}
public Boolean getComplete() {
return this.complete;
}
}
@@ -48,6 +48,7 @@ import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper;
import org.mifosplatform.portfolio.client.exception.ClientNotActiveException;
import org.mifosplatform.portfolio.collateral.domain.LoanCollateral;
import org.mifosplatform.portfolio.collateral.service.CollateralAssembler;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS;
import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService;
import org.mifosplatform.portfolio.fund.domain.Fund;
@@ -762,7 +763,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
this.noteRepository.save(note);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_APPROVED, loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_APPROVED,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
return new CommandProcessingResultBuilder() //
@@ -806,7 +808,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_APPROVAL, loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_APPROVAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
return new CommandProcessingResultBuilder() //
@@ -962,4 +965,10 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
return user;
}
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;
}
}
@@ -0,0 +1,18 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.service;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
public interface LoanArrearsAgingService {
void updateLoanArrearsAgeingDetails();
void updateLoanArrearsAgeingDetailsWithOriginalSchedule(Loan loan);
void updateLoanArrearsAgeingDetails(Loan loan);
}
@@ -0,0 +1,503 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.mifosplatform.infrastructure.core.domain.JdbcSupport;
import org.mifosplatform.infrastructure.core.service.RoutingDataSource;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.jobs.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.service.JobName;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS;
import org.mifosplatform.portfolio.common.service.BusinessEventListner;
import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanSummary;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.mifosplatform.scheduledjobs.service.ScheduledJobRunnerServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService, BusinessEventListner {
private final static Logger logger = LoggerFactory.getLogger(ScheduledJobRunnerServiceImpl.class);
private final BusinessEventNotifierService businessEventNotifierService;
private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd");
private final JdbcTemplate jdbcTemplate;
@Autowired
public LoanArrearsAgingServiceImpl(final RoutingDataSource dataSource, final BusinessEventNotifierService businessEventNotifierService) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.businessEventNotifierService = businessEventNotifierService;
}
@PostConstruct
public void registerForNotification() {
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_REFUND, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_WAIVE_INTEREST, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_ADD_CHARGE, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_WAIVE_CHARGE, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_CHARGE_PAYMENT, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE, this);
this.businessEventNotifierService.addBusinessEventPostListners(BUSINESS_EVENTS.LOAN_DISBURSAL, new DisbursementEventListner());
}
@Transactional
@Override
@CronTarget(jobName = JobName.UPDATE_LOAN_ARREARS_AGEING)
public void updateLoanArrearsAgeingDetails() {
this.jdbcTemplate.execute("truncate table m_loan_arrears_aging");
final StringBuilder updateSqlBuilder = new StringBuilder(900);
updateSqlBuilder
.append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`)");
updateSqlBuilder.append("select ml.id as loanId,");
updateSqlBuilder
.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) as principal_overdue_derived,");
updateSqlBuilder
.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) as interest_overdue_derived,");
updateSqlBuilder
.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) as fee_charges_overdue_derived,");
updateSqlBuilder
.append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as penalty_charges_overdue_derived,");
updateSqlBuilder.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) +");
updateSqlBuilder.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) +");
updateSqlBuilder.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) +");
updateSqlBuilder
.append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as total_overdue_derived,");
updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived ");
updateSqlBuilder.append(" FROM m_loan ml ");
updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
updateSqlBuilder.append(" left join m_product_loan_recalculation_details prd on prd.product_id = ml.product_id ");
updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); // active
updateSqlBuilder.append(" and mr.completed_derived is false ");
updateSqlBuilder.append(" and mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(ml.grace_on_arrears_ageing,0) day) ");
updateSqlBuilder.append(" and (prd.arrears_based_on_original_schedule = 0 or prd.arrears_based_on_original_schedule is null) ");
updateSqlBuilder.append(" GROUP BY ml.id");
List<String> insertStatements = updateLoanArrearsAgeingDetailsWithOriginalSchedule();
insertStatements.add(0, updateSqlBuilder.toString());
final int[] results = this.jdbcTemplate.batchUpdate(insertStatements.toArray(new String[0]));
int result = 0;
for (int i : results) {
result += i;
}
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result);
}
@Override
public void updateLoanArrearsAgeingDetailsWithOriginalSchedule(final Loan loan) {
int count = this.jdbcTemplate.queryForObject("select count(mla.loan_id) from m_loan_arrears_aging mla where mla.loan_id =?",
Integer.class, loan.getId());
List<String> updateStatement = new ArrayList<>();
OriginalScheduleExtractor originalScheduleExtractor = new OriginalScheduleExtractor(loan.getId().toString());
Map<Long, List<LoanSchedulePeriodData>> scheduleDate = this.jdbcTemplate.query(originalScheduleExtractor.schema,
originalScheduleExtractor);
if (scheduleDate.size() > 0) {
List<Map<String, Object>> transactions = getLoanSummary(loan.getId(), loan.getLoanSummary());
updateSchheduleWithPaidDetail(scheduleDate, transactions);
createInsertStatements(updateStatement, scheduleDate, count == 0);
if (updateStatement.size() == 1) {
this.jdbcTemplate.update(updateStatement.get(0));
} else {
String deletestatement = "DELETE FROM `m_loan_arrears_aging` WHERE `loan_id`=" + loan.getId();
this.jdbcTemplate.update(deletestatement);
}
}
}
@Override
public void updateLoanArrearsAgeingDetails(final Loan loan) {
int count = this.jdbcTemplate.queryForObject("select count(mla.loan_id) from m_loan_arrears_aging mla where mla.loan_id =?",
Integer.class, loan.getId());
String updateStatement = constructUpdateStatement(loan, count == 0);
if (updateStatement == null) {
String deletestatement = "DELETE FROM `m_loan_arrears_aging` WHERE `loan_id`=" + loan.getId();
this.jdbcTemplate.update(deletestatement);
} else {
this.jdbcTemplate.update(updateStatement);
}
}
private String constructUpdateStatement(final Loan loan, boolean isInsertStatement) {
String updateSql = null;
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
BigDecimal principalOverdue = BigDecimal.ZERO;
BigDecimal interestOverdue = BigDecimal.ZERO;
BigDecimal feeOverdue = BigDecimal.ZERO;
BigDecimal penaltyOverdue = BigDecimal.ZERO;
LocalDate overDueSince = LocalDate.now();
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getDueDate().isBefore(LocalDate.now())) {
principalOverdue = principalOverdue.add(installment.getPrincipalOutstanding(loan.getCurrency()).getAmount());
interestOverdue = interestOverdue.add(installment.getInterestOutstanding(loan.getCurrency()).getAmount());
feeOverdue = feeOverdue.add(installment.getFeeChargesOutstanding(loan.getCurrency()).getAmount());
penaltyOverdue = penaltyOverdue.add(installment.getPenaltyChargesOutstanding(loan.getCurrency()).getAmount());
if (installment.isNotFullyPaidOff() && overDueSince.isAfter(installment.getDueDate())) {
overDueSince = installment.getDueDate();
}
}
}
BigDecimal totalOverDue = principalOverdue.add(interestOverdue).add(feeOverdue).add(penaltyOverdue);
if (totalOverDue.compareTo(BigDecimal.ZERO) == 1) {
if (isInsertStatement) {
updateSql = constructInsertStatement(loan.getId(), principalOverdue, interestOverdue, feeOverdue, penaltyOverdue,
overDueSince);
} else {
updateSql = constructUpdateStatement(loan.getId(), principalOverdue, interestOverdue, feeOverdue, penaltyOverdue,
overDueSince);
}
}
return updateSql;
}
private List<String> updateLoanArrearsAgeingDetailsWithOriginalSchedule() {
List<String> insertStatement = new ArrayList<>();
final StringBuilder loanIdentifier = new StringBuilder();
loanIdentifier.append("select ml.id as loanId FROM m_loan ml ");
loanIdentifier.append("INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
loanIdentifier
.append("inner join m_product_loan_recalculation_details prd on prd.product_id = ml.product_id and prd.arrears_based_on_original_schedule = 1 ");
loanIdentifier
.append("WHERE ml.loan_status_id = 300 and mr.completed_derived is false and mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(ml.grace_on_arrears_ageing,0) day) group by ml.id");
List<Long> loanIds = this.jdbcTemplate.queryForList(loanIdentifier.toString(), Long.class);
if (!loanIds.isEmpty()) {
String loanIdsAsString = loanIds.toString();
loanIdsAsString = loanIdsAsString.substring(1, loanIdsAsString.length() - 1);
OriginalScheduleExtractor originalScheduleExtractor = new OriginalScheduleExtractor(loanIdsAsString);
Map<Long, List<LoanSchedulePeriodData>> scheduleDate = this.jdbcTemplate.query(originalScheduleExtractor.schema,
originalScheduleExtractor);
List<Map<String, Object>> loanSummary = getLoanSummary(loanIdsAsString);
updateSchheduleWithPaidDetail(scheduleDate, loanSummary);
createInsertStatements(insertStatement, scheduleDate, true);
}
return insertStatement;
}
private List<Map<String, Object>> getLoanSummary(final String loanIdsAsString) {
final StringBuilder transactionsSql = new StringBuilder();
transactionsSql.append("select ml.id as loanId, ");
transactionsSql
.append("ml.principal_repaid_derived as principalAmtPaid, ml.principal_writtenoff_derived as principalAmtWrittenoff, ");
transactionsSql.append(" ml.interest_repaid_derived as interestAmtPaid, ml.interest_waived_derived as interestAmtWaived, ");
transactionsSql.append("ml.fee_charges_repaid_derived as feeAmtPaid, ml.fee_charges_waived_derived as feeAmtWaived, ");
transactionsSql
.append("ml.penalty_charges_repaid_derived as penaltyAmtPaid, ml.penalty_charges_waived_derived as penaltyAmtWaived ");
transactionsSql.append("from m_loan ml ");
transactionsSql.append("where ml.id IN (").append(loanIdsAsString).append(") order by ml.id");
List<Map<String, Object>> loanSummary = this.jdbcTemplate.queryForList(transactionsSql.toString());
return loanSummary;
}
private List<Map<String, Object>> getLoanSummary(final Long loanId, final LoanSummary loanSummary) {
List<Map<String, Object>> transactionDetail = new ArrayList<>();
Map<String, Object> transactionMap = new HashMap<>();
transactionMap.put("loanId", loanId);
transactionMap.put("principalAmtPaid", loanSummary.getTotalPrincipalRepaid());
transactionMap.put("principalAmtWrittenoff", loanSummary.getTotalPrincipalWrittenOff());
transactionMap.put("interestAmtPaid", loanSummary.getTotalInterestRepaid());
transactionMap.put("interestAmtWaived", loanSummary.getTotalInterestWaived());
transactionMap.put("feeAmtPaid", loanSummary.getTotalFeeChargesRepaid());
transactionMap.put("feeAmtWaived", loanSummary.getTotalFeeChargesWaived());
transactionMap.put("penaltyAmtPaid", loanSummary.getTotalPenaltyChargesRepaid());
transactionMap.put("penaltyAmtWaived", loanSummary.getTotalPenaltyChargesWaived());
transactionDetail.add(transactionMap);
return transactionDetail;
}
private void createInsertStatements(List<String> insertStatement, Map<Long, List<LoanSchedulePeriodData>> scheduleDate,
boolean isInsertStatement) {
for (Map.Entry<Long, List<LoanSchedulePeriodData>> entry : scheduleDate.entrySet()) {
final Long loanId = entry.getKey();
BigDecimal principalOverdue = BigDecimal.ZERO;
BigDecimal interestOverdue = BigDecimal.ZERO;
BigDecimal feeOverdue = BigDecimal.ZERO;
BigDecimal penaltyOverdue = BigDecimal.ZERO;
LocalDate overDueSince = LocalDate.now();
for (LoanSchedulePeriodData loanSchedulePeriodData : entry.getValue()) {
if (!loanSchedulePeriodData.getComplete()) {
principalOverdue = principalOverdue.add(loanSchedulePeriodData.principalDue().subtract(
loanSchedulePeriodData.principalPaid()));
interestOverdue = interestOverdue.add(loanSchedulePeriodData.interestDue().subtract(
loanSchedulePeriodData.interestPaid()));
feeOverdue = feeOverdue.add(loanSchedulePeriodData.feeChargesDue().subtract(loanSchedulePeriodData.feeChargesPaid()));
penaltyOverdue = penaltyOverdue.add(loanSchedulePeriodData.penaltyChargesDue().subtract(
loanSchedulePeriodData.penaltyChargesPaid()));
if (overDueSince.isAfter(loanSchedulePeriodData.periodDueDate())
&& loanSchedulePeriodData.principalDue().subtract(loanSchedulePeriodData.principalPaid())
.compareTo(BigDecimal.ZERO) == 1) {
overDueSince = loanSchedulePeriodData.periodDueDate();
}
}
}
if (principalOverdue.compareTo(BigDecimal.ZERO) == 1) {
String sqlStatement = null;
if (isInsertStatement) {
sqlStatement = constructInsertStatement(loanId, principalOverdue, interestOverdue, feeOverdue, penaltyOverdue,
overDueSince);
} else {
sqlStatement = constructUpdateStatement(loanId, principalOverdue, interestOverdue, feeOverdue, penaltyOverdue,
overDueSince);
}
insertStatement.add(sqlStatement);
}
}
}
private String constructInsertStatement(final Long loanId, BigDecimal principalOverdue, BigDecimal interestOverdue,
BigDecimal feeOverdue, BigDecimal penaltyOverdue, LocalDate overDueSince) {
final StringBuilder insertStatementBuilder = new StringBuilder(900);
insertStatementBuilder
.append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,")
.append("`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`) VALUES(");
insertStatementBuilder.append(loanId).append(",");
insertStatementBuilder.append(principalOverdue).append(",");
insertStatementBuilder.append(interestOverdue).append(",");
insertStatementBuilder.append(feeOverdue).append(",");
insertStatementBuilder.append(penaltyOverdue).append(",");
BigDecimal totalOverDue = principalOverdue.add(interestOverdue).add(feeOverdue).add(penaltyOverdue);
insertStatementBuilder.append(totalOverDue).append(",'");
insertStatementBuilder.append(this.formatter.print(overDueSince)).append("')");
return insertStatementBuilder.toString();
}
private String constructUpdateStatement(final Long loanId, BigDecimal principalOverdue, BigDecimal interestOverdue,
BigDecimal feeOverdue, BigDecimal penaltyOverdue, LocalDate overDueSince) {
final StringBuilder insertStatementBuilder = new StringBuilder(900);
insertStatementBuilder.append("UPDATE m_loan_arrears_aging mla SET mla.principal_overdue_derived=");
insertStatementBuilder.append(principalOverdue).append(", mla.interest_overdue_derived=");
insertStatementBuilder.append(interestOverdue).append(", mla.fee_charges_overdue_derived=");
insertStatementBuilder.append(feeOverdue).append(", mla.penalty_charges_overdue_derived=");
insertStatementBuilder.append(penaltyOverdue).append(", mla.total_overdue_derived=");
BigDecimal totalOverDue = principalOverdue.add(interestOverdue).add(feeOverdue).add(penaltyOverdue);
insertStatementBuilder.append(totalOverDue).append(",mla.overdue_since_date_derived= '");
insertStatementBuilder.append(this.formatter.print(overDueSince)).append("' ");
insertStatementBuilder.append("WHERE mla.loan_id=").append(loanId);
return insertStatementBuilder.toString();
}
private void updateSchheduleWithPaidDetail(Map<Long, List<LoanSchedulePeriodData>> scheduleDate, List<Map<String, Object>> loanSummary) {
for (Map<String, Object> transactionMap : loanSummary) {
Long loanId = (Long) transactionMap.get("loanId");
BigDecimal principalAmtPaid = (BigDecimal) transactionMap.get("principalAmtPaid");
BigDecimal principalAmtWrittenoff = (BigDecimal) transactionMap.get("principalAmtWrittenoff");
BigDecimal interestAmtPaid = (BigDecimal) transactionMap.get("interestAmtPaid");
BigDecimal interestAmtWaived = (BigDecimal) transactionMap.get("interestAmtWaived");
BigDecimal feeAmtPaid = (BigDecimal) transactionMap.get("feeAmtPaid");
BigDecimal feeAmtWaived = (BigDecimal) transactionMap.get("feeAmtWaived");
BigDecimal penaltyAmtPaid = (BigDecimal) transactionMap.get("penaltyAmtPaid");
BigDecimal penaltyAmtWaived = (BigDecimal) transactionMap.get("penaltyAmtWaived");
BigDecimal principalAmt = principalAmtPaid.add(principalAmtWrittenoff);
BigDecimal interestAmt = interestAmtPaid.add(interestAmtWaived);
BigDecimal feeAmt = feeAmtPaid.add(feeAmtWaived);
BigDecimal penaltyAmt = penaltyAmtPaid.add(penaltyAmtWaived);
List<LoanSchedulePeriodData> loanSchedulePeriodDatas = scheduleDate.get(loanId);
if (loanSchedulePeriodDatas != null) {
List<LoanSchedulePeriodData> updatedPeriodData = new ArrayList<>(loanSchedulePeriodDatas.size());
for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDatas) {
BigDecimal principalPaid = null;
BigDecimal interestPaid = null;
BigDecimal feeChargesPaid = null;
BigDecimal penaltyChargesPaid = null;
Boolean isComplete = true;
if (loanSchedulePeriodData.principalDue().compareTo(principalAmt) == 1) {
principalPaid = principalAmt;
principalAmt = BigDecimal.ZERO;
isComplete = false;
} else {
principalPaid = loanSchedulePeriodData.principalDue();
principalAmt = principalAmt.subtract(loanSchedulePeriodData.principalDue());
}
if (loanSchedulePeriodData.interestDue().compareTo(interestAmt) == 1) {
interestPaid = interestAmt;
interestAmt = BigDecimal.ZERO;
isComplete = false;
} else {
interestPaid = loanSchedulePeriodData.interestDue();
interestAmt = interestAmt.subtract(loanSchedulePeriodData.interestDue());
}
if (loanSchedulePeriodData.feeChargesDue().compareTo(feeAmt) == 1) {
feeChargesPaid = feeAmt;
feeAmt = BigDecimal.ZERO;
isComplete = false;
} else {
feeChargesPaid = loanSchedulePeriodData.feeChargesDue();
feeAmt = feeAmt.subtract(loanSchedulePeriodData.feeChargesDue());
}
if (loanSchedulePeriodData.penaltyChargesDue().compareTo(penaltyAmt) == 1) {
penaltyChargesPaid = penaltyAmt;
penaltyAmt = BigDecimal.ZERO;
isComplete = false;
} else {
penaltyChargesPaid = loanSchedulePeriodData.penaltyChargesDue();
penaltyAmt = penaltyAmt.subtract(loanSchedulePeriodData.penaltyChargesDue());
}
LoanSchedulePeriodData periodData = LoanSchedulePeriodData.WithPaidDetail(loanSchedulePeriodData, isComplete,
principalPaid, interestPaid, feeChargesPaid, penaltyChargesPaid);
updatedPeriodData.add(periodData);
}
loanSchedulePeriodDatas.clear();
loanSchedulePeriodDatas.addAll(updatedPeriodData);
}
}
}
private static final class OriginalScheduleExtractor implements ResultSetExtractor<Map<Long, List<LoanSchedulePeriodData>>> {
private final String schema;
public OriginalScheduleExtractor(final String loanIdsAsString) {
final StringBuilder scheduleDetail = new StringBuilder();
scheduleDetail.append("select ml.id as loanId, mr.duedate as dueDate, mr.principal_amount as principalAmount, ");
scheduleDetail
.append("mr.interest_amount as interestAmount, mr.fee_charges_amount as feeAmount, mr.penalty_charges_amount as penaltyAmount ");
scheduleDetail.append("from m_loan ml INNER JOIN m_loan_repayment_schedule_history mr on mr.loan_id = ml.id ");
scheduleDetail.append("where mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(ml.grace_on_arrears_ageing,0) day) and ");
scheduleDetail.append("ml.id IN(").append(loanIdsAsString).append(") and mr.version = (");
scheduleDetail.append("select max(lrs.version) from m_loan_repayment_schedule_history lrs where mr.loan_id = lrs.loan_id");
scheduleDetail.append(") order by ml.id,mr.duedate");
this.schema = scheduleDetail.toString();
}
@Override
public Map<Long, List<LoanSchedulePeriodData>> extractData(ResultSet rs) throws SQLException, DataAccessException {
Map<Long, List<LoanSchedulePeriodData>> scheduleDate = new HashMap<>();
while (rs.next()) {
Long loanId = rs.getLong("loanId");
List<LoanSchedulePeriodData> periodDatas = new ArrayList<>();
LoanSchedulePeriodData loanSchedulePeriodData = fetchLoanSchedulePeriodData(rs);
periodDatas.add(loanSchedulePeriodData);
while (rs.next()) {
Long tempLoanId = rs.getLong("loanId");
if (loanId.equals(tempLoanId)) {
periodDatas.add(fetchLoanSchedulePeriodData(rs));
} else {
rs.previous();
break;
}
}
scheduleDate.put(loanId, periodDatas);
}
return scheduleDate;
}
private LoanSchedulePeriodData fetchLoanSchedulePeriodData(ResultSet rs) throws SQLException {
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
final BigDecimal principalDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAmount");
final BigDecimal interestDueOnPrincipalOutstanding = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestAmount");
final BigDecimal feeChargesDueForPeriod = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeAmount");
final BigDecimal penaltyChargesDueForPeriod = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyAmount");
final Integer periodNumber = null;
final LocalDate fromDate = null;
final BigDecimal principalOutstanding = null;
final BigDecimal totalDueForPeriod = null;
return LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, fromDate, dueDate, principalDue, principalOutstanding,
interestDueOnPrincipalOutstanding, feeChargesDueForPeriod, penaltyChargesDueForPeriod, totalDueForPeriod);
}
}
@SuppressWarnings("unused")
@Override
public void businessEventToBeExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
// TODO Auto-generated method stub
}
@Override
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Loan loan = null;
Object loanEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN);
Object loanTransactionEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_TRANSACTION);
Object loanAdjustTransactionEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION);
Object loanChargeEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN_CHARGE);
if (loanEntity != null) {
loan = (Loan) loanEntity;
} else if (loanTransactionEntity != null) {
LoanTransaction loanTransaction = (LoanTransaction) loanTransactionEntity;
loan = loanTransaction.getLoan();
} else if (loanAdjustTransactionEntity != null) {
LoanTransaction loanTransaction = (LoanTransaction) loanAdjustTransactionEntity;
loan = loanTransaction.getLoan();
} else if (loanChargeEntity != null) {
LoanCharge loanCharge = (LoanCharge) loanChargeEntity;
loan = loanCharge.getLoan();
}
if (loan != null && loan.isOpen() && loan.repaymentScheduleDetail().isInterestRecalculationEnabled()
&& loan.loanProduct().isArrearsBasedOnOriginalSchedule()) {
updateLoanArrearsAgeingDetailsWithOriginalSchedule(loan);
} else {
updateLoanArrearsAgeingDetails(loan);
}
}
private class DisbursementEventListner implements BusinessEventListner {
@SuppressWarnings("unused")
@Override
public void businessEventToBeExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
// TODO Auto-generated method stub
}
@Override
public void businessEventWasExecuted(Map<BUSINESS_ENTITY, Object> businessEventEntity) {
Object loanEntity = businessEventEntity.get(BUSINESS_ENTITY.LOAN);
if (loanEntity != null) {
Loan loan = (Loan) loanEntity;
updateLoanArrearsAgeingDetails(loan);
}
}
}
}
@@ -159,8 +159,9 @@ public class LoanAssembler {
final Long transactionProcessingStrategyId = this.fromApiJsonHelper.extractLongNamed("transactionProcessingStrategyId", element);
final Long loanPurposeId = this.fromApiJsonHelper.extractLongNamed("loanPurposeId", element);
final Boolean syncDisbursementWithMeeting = this.fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element);
final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper.extractBooleanNamed("createStandingInstructionAtDisbursement", element);
final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper.extractBooleanNamed(
"createStandingInstructionAtDisbursement", element);
final LoanProduct loanProduct = this.loanProductRepository.findOne(productId);
if (loanProduct == null) { throw new LoanProductNotFoundException(productId); }
@@ -234,14 +235,14 @@ public class LoanAssembler {
loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(),
loanProduct, fund, loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges,
collateral, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance,
collateral, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance,
createStandingInstructionAtDisbursement);
} else if (group != null) {
loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer,
loanPurpose, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral,
syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance,
syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance,
createStandingInstructionAtDisbursement);
} else if (client != null) {
@@ -264,8 +265,9 @@ public class LoanAssembler {
loanDisbursementDetails.updateLoan(loanApplication);
}
}
final LocalDate recalculationRestFrequencyDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanProductConstants.recalculationRestFrequencyDateParamName, element);
final LocalDate recalculationRestFrequencyDate = this.fromApiJsonHelper.extractLocalDateNamed(
LoanProductConstants.recalculationRestFrequencyDateParamName, element);
final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
@@ -9,7 +9,6 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -93,6 +92,7 @@ import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkDi
import org.mifosplatform.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand;
import org.mifosplatform.portfolio.collectionsheet.command.SingleDisbursalCommand;
import org.mifosplatform.portfolio.collectionsheet.command.SingleRepaymentCommand;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
import org.mifosplatform.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS;
import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.common.service.BusinessEventNotifierService;
@@ -124,7 +124,6 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidRefundDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanDisbursalException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException;
@@ -291,6 +290,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance);
}
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
@@ -405,6 +407,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
@@ -540,6 +544,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(singleLoanDisbursalCommand.getLoanId());
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
@@ -648,6 +654,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
return changes;
@@ -661,6 +669,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
removeLoanCycle(loan);
//
@@ -703,7 +713,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(applicationCurrency.toData(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL, loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
return new CommandProcessingResultBuilder() //
@@ -789,10 +800,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LoanTransaction transactionToAdjust = this.loanTransactionRepository.findOne(transactionId);
if (transactionToAdjust == null) { throw new LoanTransactionNotFoundException(transactionId); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION,
constructEntityMap(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION, transactionToAdjust));
if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { throw new PlatformServiceUnavailableException(
"error.msg.loan.transfer.transaction.update.not.allowed", "Loan transaction:" + transactionId
+ " update not allowed as it involves in account transfer", transactionId); }
@@ -914,10 +925,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_TRANSACTION, transactionToAdjust);
Map<BUSINESS_ENTITY, Object> entityMap = constructEntityMap(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION, transactionToAdjust);
if (newTransactionDetail.isRepayment() && newTransactionDetail.isGreaterThanZero(loan.getPrincpal().getCurrency())) {
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, newTransactionDetail);
entityMap.put(BUSINESS_ENTITY.LOAN_TRANSACTION, newTransactionDetail);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION, entityMap);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
@@ -964,7 +976,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
final LoanTransaction waiveInterestTransaction = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney,
transactionDate, interestComponent, unrecognizedIncome, DateUtils.getLocalDateTimeOfTenant(), currentUser);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WAIVE_INTEREST,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, waiveInterestTransaction));
CalendarInstance restCalendarInstance = null;
ApplicationCurrency applicationCurrency = null;
LocalDate calculatedRepaymentsStartingFromDate = null;
@@ -1027,6 +1040,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WAIVE_INTEREST,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, waiveInterestTransaction));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(waiveInterestTransaction.getId()) //
@@ -1052,6 +1067,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WRITTEN_OFF,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
removeLoanCycle(loan);
final List<Long> existingTransactionIds = new ArrayList<>();
@@ -1102,7 +1119,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WRITTEN_OFF, writeoff);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WRITTEN_OFF,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeoff));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(writeoff.getId()) //
@@ -1124,6 +1142,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CLOSE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
@@ -1184,6 +1204,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CLOSE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
CommandProcessingResult result = null;
if (possibleClosingTransaction != null) {
@@ -1220,6 +1242,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
removeLoanCycle(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CLOSE_AS_RESCHEDULE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
@@ -1236,7 +1260,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CLOSE_AS_RESCHEDULE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanId) //
@@ -1261,6 +1286,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId);
final LoanCharge loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ADD_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
@@ -1283,6 +1310,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ADD_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanCharge.getId()) //
@@ -1455,10 +1484,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (!loan.status().isSubmittedAndPendingApproval()) { throw new LoanChargeCannotBeUpdatedException(
LOAN_CHARGE_CANNOT_BE_UPDATED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UPDATE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
final Map<String, Object> changes = loan.updateLoanCharge(loanCharge, command);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UPDATE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanChargeId) //
@@ -1491,6 +1524,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.ALREADY_WAIVED, loanCharge.getId());
} else if (loanCharge.isPaid()) { throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.ALREADY_PAID,
loanCharge.getId()); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WAIVE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
Integer loanInstallmentNumber = null;
if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge chargePerInstallment = null;
@@ -1566,6 +1601,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WAIVE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanChargeId) //
@@ -1589,10 +1627,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// yet to be approved (are in submitted and pending status)
if (!loan.status().isSubmittedAndPendingApproval()) { throw new LoanChargeCannotBeDeletedException(
LOAN_CHARGE_CANNOT_BE_DELETED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DELETE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
loan.removeLoanCharge(loanCharge);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DELETE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanChargeId) //
@@ -1670,7 +1711,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction,
isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanChargeId) //
@@ -1787,6 +1827,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(accountId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_INITIATE_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
@@ -1800,7 +1842,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_INITIATE_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return newTransferTransaction;
}
@@ -1812,7 +1855,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(accountId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ACCEPT_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
@@ -1832,6 +1876,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ACCEPT_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return newTransferAcceptanceTransaction;
}
@@ -1843,6 +1889,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(accountId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WITHDRAW_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
@@ -1856,6 +1904,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WITHDRAW_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return newTransferAcceptanceTransaction;
}
@@ -1864,8 +1914,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public void rejectLoanTransfer(final Long accountId) {
final Loan loan = this.loanAssembler.assembleFrom(accountId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REJECT_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
loan.setLoanStatus(LoanStatus.TRANSFER_ON_HOLD.getValue());
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REJECT_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
@Transactional
@@ -1883,12 +1937,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
if (!loan.hasLoanOfficer(fromLoanOfficer)) { throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); }
loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
@@ -1918,12 +1975,16 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
for (final String loanIdString : loanIds) {
final Long loanId = Long.valueOf(loanIdString);
final Loan loan = this.loanAssembler.assembleFrom(loanId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
checkClientOrGroupActive(loan);
if (!loan.hasLoanOfficer(fromLoanOfficer)) { throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); }
loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
this.loanRepository.flush();
@@ -1946,10 +2007,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
checkClientOrGroupActive(loan);
if (loan.getLoanOfficer() == null) { throw new LoanOfficerUnassignmentException(loanId); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REMOVE_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
loan.removeLoanOfficer(dateOfLoanOfficerunAssigned);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REMOVE_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
@@ -1971,19 +2036,18 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
@Transactional
@Override
public void applyMeetingDateChanges(final Calendar calendar, final Collection<CalendarInstance> loanCalendarInstances) {
final Boolean reschedulebasedOnMeetingDates = null;
final Boolean reschedulebasedOnMeetingDates = null;
final LocalDate presentMeetingDate = null;
final LocalDate newMeetingDate = null;
applyMeetingDateChanges(calendar, loanCalendarInstances, reschedulebasedOnMeetingDates, presentMeetingDate, newMeetingDate);
}
@Transactional
@Override
@@ -2011,16 +2075,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
throw new CalendarParameterUpdateNotSupportedException("jlg.loan.recalculation", defaultUserMessage);
}
holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
if(reschedulebasedOnMeetingDates != null && reschedulebasedOnMeetingDates ){
if (reschedulebasedOnMeetingDates != null && reschedulebasedOnMeetingDates) {
loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled,
holidays, workingDays, reschedulebasedOnMeetingDates, presentMeetingDate, newMeetingDate);
}else{
} else {
loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled,
holidays, workingDays);
holidays, workingDays);
}
saveLoanWithDataIntegrityViolationChecks(loan);
}
}
@@ -2337,6 +2400,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
}
@@ -2387,7 +2453,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
installment = loan.fetchRepaymentScheduleInstallment(periodNumber);
}
if (loan != null) {
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
for (Map.Entry<Integer, LocalDate> entry : scheduleDates.entrySet()) {
final LoanCharge loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command, entry.getValue());
@@ -2425,6 +2492,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
Long overdurPenaltyWaitPeriod = null;
LocalDate lastTransactionDate = null;
LoanTransaction writeOffTransaction = loan.findWriteOffTransaction();
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeOffTransaction));
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
@@ -2460,7 +2529,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
if (writeOffTransaction != null) {
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF, writeOffTransaction);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeOffTransaction));
}
return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
@@ -2617,6 +2687,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
public void recalculateInterest(final long loanId) {
AppUser currentUser = getAppUserIfPresent();
Loan loan = this.loanAssembler.assembleFrom(loanId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_INTEREST_RECALCULATION,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
CalendarInstance restCalendarInstance = null;
ApplicationCurrency applicationCurrency = null;
LocalDate calculatedRepaymentsStartingFromDate = null;
@@ -2662,6 +2734,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_INTEREST_RECALCULATION,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
@Override
@@ -2708,7 +2782,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
return installments;
}
@Override
@Transactional
public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) {
@@ -2718,11 +2792,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
//checkRefundDateIsAfterAtLeastOneRepayment(loanId, transactionDate);
// checkRefundDateIsAfterAtLeastOneRepayment(loanId, transactionDate);
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
checkIfLoanIsPaidInAdvance(loanId, transactionAmount);
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
@@ -2748,15 +2822,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
private void checkIfLoanIsPaidInAdvance(final Long loanId,final BigDecimal transactionAmount) {
private void checkIfLoanIsPaidInAdvance(final Long loanId, final BigDecimal transactionAmount) {
BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(loanId).getPaidInAdvance();
if(overpaid == null || overpaid.equals(new BigDecimal(0)) || transactionAmount.floatValue() > overpaid.floatValue()) {
if(overpaid == null)
overpaid = BigDecimal.ZERO;
if (overpaid == null || overpaid.equals(new BigDecimal(0)) || transactionAmount.floatValue() > overpaid.floatValue()) {
if (overpaid == null) overpaid = BigDecimal.ZERO;
throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString());
}
}
}
private AppUser getAppUserIfPresent() {
AppUser user = null;
@@ -2766,4 +2839,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
return user;
}
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;
}
}
@@ -61,6 +61,7 @@ public interface LoanProductConstants {
public static final String graceOnArrearsAgeingParameterName = "graceOnArrearsAgeing";
public static final String overdueDaysForNPAParameterName = "overdueDaysForNPA";
public static final String minimumDaysBetweenDisbursalAndFirstRepayment = "minimumDaysBetweenDisbursalAndFirstRepayment";
public static final String accountMovesOutOfNPAOnlyOnArrearsCompletionParamName = "accountMovesOutOfNPAOnlyOnArrearsCompletion";
// Interest recalculation related
public static final String isInterestRecalculationEnabledParameterName = "isInterestRecalculationEnabled";
@@ -71,6 +72,7 @@ public interface LoanProductConstants {
public static final String recalculationRestFrequencyTypeParameterName = "recalculationRestFrequencyType";
public static final String recalculationRestFrequencyIntervalParameterName = "recalculationRestFrequencyInterval";
public static final String recalculationRestFrequencyDateParamName = "recalculationRestFrequencyDate";
public static final String isArrearsBasedOnOriginalScheduleParamName = "isArrearsBasedOnOriginalSchedule";
// Guarantee related
public static final String holdGuaranteeFundsParamName = "holdGuaranteeFunds";
@@ -137,6 +137,7 @@ public class LoanProductData {
private final Boolean holdGuaranteeFunds;
private final LoanProductGuaranteeData productGuaranteeData;
private final Boolean accountMovesOutOfNPAOnlyOnArrearsCompletion;
/**
* Used when returning lookup information about loan product for dropdowns.
@@ -187,6 +188,7 @@ public class LoanProductData {
final BigDecimal outstandingLoanBalance = null;
final LoanProductGuaranteeData productGuaranteeData = null;
final Boolean holdGuaranteeFunds = false;
final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = false;
final EnumOptionData daysInMonthType = null;
final EnumOptionData daysInYearType = null;
@@ -202,7 +204,8 @@ public class LoanProductData {
accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations,
interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData);
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData,
accountMovesOutOfNPAOnlyOnArrearsCompletion);
}
public static LoanProductData lookupWithCurrency(final Long id, final String name, final CurrencyData currency) {
@@ -258,6 +261,7 @@ public class LoanProductData {
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = null;
final Boolean holdGuaranteeFunds = false;
final LoanProductGuaranteeData productGuaranteeData = null;
final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -267,7 +271,8 @@ public class LoanProductData {
accountingType, includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations,
interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData);
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData,
accountMovesOutOfNPAOnlyOnArrearsCompletion);
}
public static LoanProductData sensibleDefaultsForNewLoanProductCreation() {
@@ -330,6 +335,7 @@ public class LoanProductData {
final Integer minimumDaysBetweenDisbursalAndFirstRepayment = null;
final Boolean holdGuaranteeFunds = false;
final LoanProductGuaranteeData productGuaranteeData = null;
final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -340,7 +346,7 @@ public class LoanProductData {
principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle,
multiDisburseLoan, maxTrancheCount, outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType,
daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment,
holdGuaranteeFunds, productGuaranteeData);
holdGuaranteeFunds, productGuaranteeData, accountMovesOutOfNPAOnlyOnArrearsCompletion);
}
public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings,
@@ -371,8 +377,8 @@ public class LoanProductData {
Integer maxTrancheCount, BigDecimal outstandingLoanBalance, final Integer graceOnArrearsAgeing,
final Integer overdueDaysForNPA, final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType,
final boolean isInterestRecalculationEnabled, final LoanProductInterestRecalculationData interestRecalculationData,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, boolean holdGuaranteeFunds,
LoanProductGuaranteeData loanProductGuaranteeData) {
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final boolean holdGuaranteeFunds,
final LoanProductGuaranteeData loanProductGuaranteeData, final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion) {
this.id = id;
this.name = name;
this.shortName = shortName;
@@ -445,6 +451,7 @@ public class LoanProductData {
this.interestRecalculationData = interestRecalculationData;
this.holdGuaranteeFunds = holdGuaranteeFunds;
this.productGuaranteeData = loanProductGuaranteeData;
this.accountMovesOutOfNPAOnlyOnArrearsCompletion = accountMovesOutOfNPAOnlyOnArrearsCompletion;
this.daysInMonthTypeOptions = null;
this.daysInYearTypeOptions = null;
@@ -548,6 +555,7 @@ public class LoanProductData {
this.interestRecalculationData = productData.interestRecalculationData;
this.holdGuaranteeFunds = productData.holdGuaranteeFunds;
this.productGuaranteeData = productData.productGuaranteeData;
this.accountMovesOutOfNPAOnlyOnArrearsCompletion = productData.accountMovesOutOfNPAOnlyOnArrearsCompletion;
this.daysInMonthTypeOptions = daysInMonthTypeOptions;
this.daysInYearTypeOptions = daysInYearTypeOptions;
@@ -24,11 +24,13 @@ public class LoanProductInterestRecalculationData {
private final EnumOptionData recalculationRestFrequencyType;
private final Integer recalculationRestFrequencyInterval;
private final LocalDate recalculationRestFrequencyDate;
@SuppressWarnings("unused")
private final boolean isArrearsBasedOnOriginalSchedule;
public LoanProductInterestRecalculationData(final Long id, final Long productId,
final EnumOptionData interestRecalculationCompoundingType, final EnumOptionData rescheduleStrategyType,
final EnumOptionData recalculationRestFrequencyType, final Integer recalculationRestFrequencyInterval,
final LocalDate recalculationRestFrequencyDate) {
final LocalDate recalculationRestFrequencyDate, boolean isArrearsBasedOnOriginalSchedule) {
this.id = id;
this.productId = productId;
this.interestRecalculationCompoundingType = interestRecalculationCompoundingType;
@@ -36,6 +38,7 @@ public class LoanProductInterestRecalculationData {
this.recalculationRestFrequencyType = recalculationRestFrequencyType;
this.recalculationRestFrequencyInterval = recalculationRestFrequencyInterval;
this.recalculationRestFrequencyDate = recalculationRestFrequencyDate;
this.isArrearsBasedOnOriginalSchedule = isArrearsBasedOnOriginalSchedule;
}
public static LoanProductInterestRecalculationData sensibleDefaultsForNewLoanProductCreation() {
@@ -46,8 +49,10 @@ public class LoanProductInterestRecalculationData {
final EnumOptionData recalculationRestFrequencyType = null;
final Integer recalculationRestFrequencyInterval = null;
final LocalDate recalculationRestFrequencyDate = null;
final boolean isArrearsBasedOnOriginalSchedule = false;
return new LoanProductInterestRecalculationData(id, productId, interestRecalculationCompoundingType, rescheduleStrategyType,
recalculationRestFrequencyType, recalculationRestFrequencyInterval, recalculationRestFrequencyDate);
recalculationRestFrequencyType, recalculationRestFrequencyInterval, recalculationRestFrequencyDate,
isArrearsBasedOnOriginalSchedule);
}
public EnumOptionData getInterestRecalculationCompoundingType() {
@@ -136,6 +136,9 @@ public class LoanProduct extends AbstractPersistable<Long> {
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loanProduct", optional = true, orphanRemoval = true)
private LoanProductGuaranteeDetails loanProductGuaranteeDetails;
@Column(name = "account_moves_out_of_npa_only_on_arrears_completion")
private boolean accountMovesOutOfNPAOnlyOnArrearsCompletion;
public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy,
final List<Charge> productCharges, final JsonCommand command, final AprCalculator aprCalculator) {
@@ -225,6 +228,9 @@ public class LoanProduct extends AbstractPersistable<Long> {
loanProductGuaranteeDetails = LoanProductGuaranteeDetails.createFrom(command);
}
final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName);
return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal,
maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType,
annualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentFrequencyType,
@@ -233,7 +239,7 @@ public class LoanProduct extends AbstractPersistable<Long> {
startDate, closeDate, externalId, useBorrowerCycle, loanProductBorrowerCycleVariations, multiDisburseLoan, maxTrancheCount,
outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationSettings, minimumDaysBetweenDisbursalAndFirstRepayment,
holdGuarantorFunds, loanProductGuaranteeDetails);
holdGuarantorFunds, loanProductGuaranteeDetails, accountMovesOutOfNPAOnlyOnArrearsCompletion);
}
@@ -458,7 +464,7 @@ public class LoanProduct extends AbstractPersistable<Long> {
final boolean isInterestRecalculationEnabled,
final LoanProductInterestRecalculationDetails productInterestRecalculationDetails,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final boolean holdGuarantorFunds,
final LoanProductGuaranteeDetails loanProductGuaranteeDetails) {
final LoanProductGuaranteeDetails loanProductGuaranteeDetails, final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion) {
this.fund = fund;
this.transactionProcessingStrategy = transactionProcessingStrategy;
this.name = name.trim();
@@ -510,6 +516,7 @@ public class LoanProduct extends AbstractPersistable<Long> {
this.minimumDaysBetweenDisbursalAndFirstRepayment = minimumDaysBetweenDisbursalAndFirstRepayment;
this.holdGuaranteeFunds = holdGuarantorFunds;
this.loanProductGuaranteeDetails = loanProductGuaranteeDetails;
this.accountMovesOutOfNPAOnlyOnArrearsCompletion = accountMovesOutOfNPAOnlyOnArrearsCompletion;
}
public MonetaryCurrency getCurrency() {
@@ -737,6 +744,14 @@ public class LoanProduct extends AbstractPersistable<Long> {
this.loanProductGuaranteeDetails.update(command, actualChanges);
}
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName,
this.accountMovesOutOfNPAOnlyOnArrearsCompletion)) {
final boolean newValue = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName);
actualChanges.put(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, newValue);
this.accountMovesOutOfNPAOnlyOnArrearsCompletion = newValue;
}
return actualChanges;
}
@@ -989,4 +1004,12 @@ public class LoanProduct extends AbstractPersistable<Long> {
return this.shortName;
}
public boolean isArrearsBasedOnOriginalSchedule() {
boolean isBasedOnOriginalSchedule = false;
if (getProductInterestRecalculationDetails() != null) {
isBasedOnOriginalSchedule = getProductInterestRecalculationDetails().isArrearsBasedOnOriginalSchedule();
}
return isBasedOnOriginalSchedule;
}
}
@@ -57,6 +57,9 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
@Column(name = "rest_freqency_date")
private Date restFrequencyDate;
@Column(name = "arrears_based_on_original_schedule")
private boolean isArrearsBasedOnOriginalSchedule;
protected LoanProductInterestRecalculationDetails() {
//
}
@@ -75,6 +78,8 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
.localDateValueOfParameterNamed(LoanProductConstants.recalculationRestFrequencyDateParamName);
Integer recurrenceInterval = command
.integerValueOfParameterNamed(LoanProductConstants.recalculationRestFrequencyIntervalParameterName);
final boolean isArrearsBasedOnOriginalSchedule = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName);
RecalculationFrequencyType frequencyType = RecalculationFrequencyType.fromInt(recurrenceFrequency);
Date recurrenceOnDate = null;
if (recurrenceOnLocalDate != null) {
@@ -87,17 +92,18 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
}
return new LoanProductInterestRecalculationDetails(interestRecalculationCompoundingMethod, loanRescheduleStrategyMethod,
recurrenceFrequency, recurrenceInterval, recurrenceOnDate);
recurrenceFrequency, recurrenceInterval, recurrenceOnDate, isArrearsBasedOnOriginalSchedule);
}
private LoanProductInterestRecalculationDetails(final Integer interestRecalculationCompoundingMethod,
final Integer rescheduleStrategyMethod, final Integer restFrequencyType, final Integer restInterval,
final Date restFrequencyDate) {
final Date restFrequencyDate, final boolean isArrearsBasedOnOriginalSchedule) {
this.interestRecalculationCompoundingMethod = interestRecalculationCompoundingMethod;
this.rescheduleStrategyMethod = rescheduleStrategyMethod;
this.restFrequencyType = restFrequencyType;
this.restInterval = restInterval;
this.restFrequencyDate = restFrequencyDate;
this.isArrearsBasedOnOriginalSchedule = isArrearsBasedOnOriginalSchedule;
}
public void updateProduct(final LoanProduct loanProduct) {
@@ -169,6 +175,14 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
this.restFrequencyDate = recurrenceOnDate;
}
}
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName,
this.isArrearsBasedOnOriginalSchedule)) {
final boolean newValue = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName);
actualChanges.put(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName, newValue);
this.isArrearsBasedOnOriginalSchedule = newValue;
}
}
public LocalDate getRestFrequencyLocalDate() {
@@ -186,4 +200,9 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
public Integer getRestInterval() {
return this.restInterval;
}
public boolean isArrearsBasedOnOriginalSchedule() {
return this.isArrearsBasedOnOriginalSchedule;
}
}
@@ -71,9 +71,10 @@ public final class LoanProductDataValidator {
LoanProductConstants.recalculationRestFrequencyDateParamName,
LoanProductConstants.recalculationRestFrequencyIntervalParameterName,
LoanProductConstants.recalculationRestFrequencyTypeParameterName,
LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName,
LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment, LoanProductConstants.mandatoryGuaranteeParamName,
LoanProductConstants.holdGuaranteeFundsParamName, LoanProductConstants.minimumGuaranteeFromGuarantorParamName,
LoanProductConstants.minimumGuaranteeFromOwnFundsParamName));
LoanProductConstants.minimumGuaranteeFromOwnFundsParamName, LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName));
private final FromJsonHelper fromApiJsonHelper;
@@ -299,6 +300,13 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LoanProductConstants.daysInMonthTypeParameterName).value(daysInMonthType).notNull()
.isOneOfTheseValues(1, 30);
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, element)) {
Boolean npaChangeConfig = this.fromApiJsonHelper.extractBooleanNamed(
LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, element);
baseDataValidator.reset().parameter(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName).value(npaChangeConfig)
.notNull().isOneOfTheseValues(true, false);
}
// Interest recalculation settings
final Boolean isInterestRecalculationEnabled = this.fromApiJsonHelper.extractBooleanNamed(
LoanProductConstants.isInterestRecalculationEnabledParameterName, element);
@@ -503,6 +511,12 @@ public final class LoanProductDataValidator {
}
}
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName, element)) {
final Boolean isArrearsBasedOnOriginalSchedule = this.fromApiJsonHelper.extractBooleanNamed(
LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName, element);
baseDataValidator.reset().parameter(LoanProductConstants.isArrearsBasedOnOriginalScheduleParamName)
.value(isArrearsBasedOnOriginalSchedule).notNull().isOneOfTheseValues(true, false);
}
}
public void validateForUpdate(final String json, final LoanProduct loanProduct) {
@@ -719,6 +733,13 @@ public final class LoanProductDataValidator {
.isOneOfTheseValues(1, 30);
}
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, element)) {
Boolean npaChangeConfig = this.fromApiJsonHelper.extractBooleanNamed(
LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName, element);
baseDataValidator.reset().parameter(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName).value(npaChangeConfig)
.notNull().isOneOfTheseValues(true, false);
}
// Interest recalculation settings
Boolean isInterestRecalculationEnabled = loanProduct.isInterestRecalculationEnabled();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isInterestRecalculationEnabledParameterName, element)) {
@@ -86,13 +86,13 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
String sql = "select " + rm.loanProductSchema();
// Check if branch specific products are enabled. If yes, fetch only products mapped to current user's office
String inClause = mifosEntityAccessUtil.
getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(
MifosEntityType.LOAN_PRODUCT);
if ( (inClause != null) && (!(inClause.trim().isEmpty())) ) {
sql += " where lp.id in ( " + inClause + " ) ";
}
// Check if branch specific products are enabled. If yes, fetch only
// products mapped to current user's office
String inClause = mifosEntityAccessUtil
.getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(MifosEntityType.LOAN_PRODUCT);
if ((inClause != null) && (!(inClause.trim().isEmpty()))) {
sql += " where lp.id in ( " + inClause + " ) ";
}
return this.jdbcTemplate.query(sql, rm, new Object[] {});
}
@@ -109,18 +109,18 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final LoanProductLookupMapper rm = new LoanProductLookupMapper();
String sql = "select ";
if(activeOnly) {
if (activeOnly) {
sql += rm.activeOnlySchema();
} else {
sql += rm.schema();
}
// Check if branch specific products are enabled. If yes, fetch only products mapped to current user's office
String inClause = mifosEntityAccessUtil.
getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(
MifosEntityType.LOAN_PRODUCT);
if ( (inClause != null) && (!(inClause.trim().isEmpty())) ) {
if(activeOnly) {
// Check if branch specific products are enabled. If yes, fetch only
// products mapped to current user's office
String inClause = mifosEntityAccessUtil
.getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(MifosEntityType.LOAN_PRODUCT);
if ((inClause != null) && (!(inClause.trim().isEmpty()))) {
if (activeOnly) {
sql += " and id in ( " + inClause + " )";
} else {
sql += " where id in ( " + inClause + " ) ";
@@ -162,10 +162,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lp.days_in_month_enum as daysInMonth, lp.days_in_year_enum as daysInYear, lp.interest_recalculation_enabled as isInterestRecalculationEnabled, "
+ "lpr.id as lprId, lpr.product_id as productId, lpr.compound_type_enum as compoundType, lpr.reschedule_strategy_enum as rescheduleStrategy, "
+ "lpr.rest_frequency_type_enum as restFrequencyEnum, lpr.rest_frequency_interval as restFrequencyInterval, "
+ "lpr.rest_freqency_date as restFrequencyDate, "
+ "lpr.rest_freqency_date as restFrequencyDate, lpr.arrears_based_on_original_schedule as isArrearsBasedOnOriginalSchedule, "
+ "lp.hold_guarantee_funds as holdGuaranteeFunds, "
+ "lpg.id as lpgId, lpg.mandatory_guarantee as mandatoryGuarantee, "
+ "lpg.minimum_guarantee_from_own_funds as minimumGuaranteeFromOwnFunds, lpg.minimum_guarantee_from_guarantor_funds as minimumGuaranteeFromGuarantor, "
+ "lp.account_moves_out_of_npa_only_on_arrears_completion as accountMovesOutOfNPAOnlyOnArrearsCompletion, "
+ "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, curr.display_symbol as currencyDisplaySymbol, lp.external_id as externalId "
+ " from m_product_loan lp "
+ " left join m_fund f on f.id = lp.fund_id "
@@ -290,10 +291,10 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final EnumOptionData restFrequencyType = LoanEnumerations.interestRecalculationFrequencyType(restFrequencyEnumValue);
final int restFrequencyInterval = JdbcSupport.getInteger(rs, "restFrequencyInterval");
final LocalDate restFrequencyDate = JdbcSupport.getLocalDate(rs, "restFrequencyDate");
final boolean isArrearsBasedOnOriginalSchedule = rs.getBoolean("isArrearsBasedOnOriginalSchedule");
interestRecalculationData = new LoanProductInterestRecalculationData(lprId, productId,
interestRecalculationCompoundingType, rescheduleStrategyType, restFrequencyType, restFrequencyInterval,
restFrequencyDate);
restFrequencyDate, isArrearsBasedOnOriginalSchedule);
}
final boolean holdGuaranteeFunds = rs.getBoolean("holdGuaranteeFunds");
@@ -307,6 +308,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
minimumGuaranteeFromGuarantor);
}
final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion = rs.getBoolean("accountMovesOutOfNPAOnlyOnArrearsCompletion");
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType,
@@ -316,7 +319,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
closeDate, status, externalId, principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle,
numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, loanProductGuaranteeData);
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, loanProductGuaranteeData,
accountMovesOutOfNPAOnlyOnArrearsCompletion);
}
}
@@ -390,13 +394,13 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
String sql = "select " + rm.loanProductSchema() + " where lp.currency_code='" + currencyCode + "'";
// Check if branch specific products are enabled. If yes, fetch only products mapped to current user's office
String inClause = mifosEntityAccessUtil.
getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(
MifosEntityType.LOAN_PRODUCT);
if ( (inClause != null) && (!(inClause.trim().isEmpty())) ) {
sql += " and id in ( " + inClause + " ) ";
}
// Check if branch specific products are enabled. If yes, fetch only
// products mapped to current user's office
String inClause = mifosEntityAccessUtil
.getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(MifosEntityType.LOAN_PRODUCT);
if ((inClause != null) && (!(inClause.trim().isEmpty()))) {
sql += " and id in ( " + inClause + " ) ";
}
return this.jdbcTemplate.query(sql, rm, new Object[] {});
}
@@ -410,13 +414,13 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
String sql = "Select " + rm.productMixSchema();
// Check if branch specific products are enabled. If yes, fetch only products mapped to current user's office
String inClause = mifosEntityAccessUtil.
getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(
MifosEntityType.LOAN_PRODUCT);
if ( (inClause != null) && (!(inClause.trim().isEmpty())) ) {
sql += " and lp.id in ( " + inClause + " ) ";
}
// Check if branch specific products are enabled. If yes, fetch only
// products mapped to current user's office
String inClause = mifosEntityAccessUtil
.getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(MifosEntityType.LOAN_PRODUCT);
if ((inClause != null) && (!(inClause.trim().isEmpty()))) {
sql += " and lp.id in ( " + inClause + " ) ";
}
return this.jdbcTemplate.query(sql, rm, new Object[] {});
}
@@ -429,24 +433,23 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final LoanProductLookupMapper rm = new LoanProductLookupMapper();
String sql = "Select " + rm.restrictedProductsSchema() + " where pm.product_id=? ";
// Check if branch specific products are enabled. If yes, fetch only products mapped to current user's office
String inClause1 = mifosEntityAccessUtil.
getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(
MifosEntityType.LOAN_PRODUCT);
if ( (inClause1 != null) && (!(inClause1.trim().isEmpty())) ) {
sql += " and rp.id in ( " + inClause1 + " ) ";
}
// Check if branch specific products are enabled. If yes, fetch only
// products mapped to current user's office
String inClause1 = mifosEntityAccessUtil
.getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(MifosEntityType.LOAN_PRODUCT);
if ((inClause1 != null) && (!(inClause1.trim().isEmpty()))) {
sql += " and rp.id in ( " + inClause1 + " ) ";
}
sql += " UNION Select "
+ rm.derivedRestrictedProductsSchema() + " where pm.restricted_product_id=?";
sql += " UNION Select " + rm.derivedRestrictedProductsSchema() + " where pm.restricted_product_id=?";
// Check if branch specific products are enabled. If yes, fetch only products mapped to current user's office
String inClause2 = mifosEntityAccessUtil.
getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(
MifosEntityType.LOAN_PRODUCT);
if ( (inClause2 != null) && (!(inClause2.trim().isEmpty())) ) {
sql += " and lp.id in ( " + inClause2 + " ) ";
}
// Check if branch specific products are enabled. If yes, fetch only
// products mapped to current user's office
String inClause2 = mifosEntityAccessUtil
.getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(MifosEntityType.LOAN_PRODUCT);
if ((inClause2 != null) && (!(inClause2.trim().isEmpty()))) {
sql += " and lp.id in ( " + inClause2 + " ) ";
}
return this.jdbcTemplate.query(sql, rm, new Object[] { productId, productId });
}
@@ -460,16 +463,15 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
String sql = "Select " + rm.schema() + " where ";
// Check if branch specific products are enabled. If yes, fetch only products mapped to current user's office
String inClause = mifosEntityAccessUtil.
getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(
MifosEntityType.LOAN_PRODUCT);
if ( (inClause != null) && (!(inClause.trim().isEmpty())) ) {
sql += " lp.id in ( " + inClause + " ) and ";
}
// Check if branch specific products are enabled. If yes, fetch only
// products mapped to current user's office
String inClause = mifosEntityAccessUtil
.getSQLWhereClauseForProductIDsForUserOffice_ifGlobalConfigEnabled(MifosEntityType.LOAN_PRODUCT);
if ((inClause != null) && (!(inClause.trim().isEmpty()))) {
sql += " lp.id in ( " + inClause + " ) and ";
}
sql += "lp.id not in ("
+ "Select pm.restricted_product_id from m_product_mix pm where pm.product_id=? " + "UNION "
sql += "lp.id not in (" + "Select pm.restricted_product_id from m_product_mix pm where pm.product_id=? " + "UNION "
+ "Select pm.product_id from m_product_mix pm where pm.restricted_product_id=?)";
return this.jdbcTemplate.query(sql, rm, new Object[] { productId, productId });
@@ -11,8 +11,6 @@ public interface ScheduledJobRunnerService {
void updateLoanSummaryDetails();
void updateLoanArrearsAgeingDetails();
void updateLoanPaidInAdvance();
void applyAnnualFeeForSavings();
@@ -136,46 +136,6 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result);
}
@Transactional
@Override
@CronTarget(jobName = JobName.UPDATE_LOAN_ARREARS_AGEING)
public void updateLoanArrearsAgeingDetails() {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
jdbcTemplate.execute("truncate table m_loan_arrears_aging");
final StringBuilder updateSqlBuilder = new StringBuilder(900);
updateSqlBuilder
.append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`)");
updateSqlBuilder.append("select ml.id as loanId,");
updateSqlBuilder
.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) as principal_overdue_derived,");
updateSqlBuilder
.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) as interest_overdue_derived,");
updateSqlBuilder
.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) as fee_charges_overdue_derived,");
updateSqlBuilder
.append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as penalty_charges_overdue_derived,");
updateSqlBuilder.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) +");
updateSqlBuilder.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) +");
updateSqlBuilder.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) +");
updateSqlBuilder
.append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as total_overdue_derived,");
updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived ");
updateSqlBuilder.append(" FROM m_loan ml ");
updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); // active
updateSqlBuilder.append(" and mr.completed_derived is false ");
updateSqlBuilder.append(" and mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(ml.grace_on_arrears_ageing,0) day) ");
updateSqlBuilder.append(" GROUP BY ml.id");
final int result = jdbcTemplate.update(updateSqlBuilder.toString());
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result);
}
@Transactional
@Override
@CronTarget(jobName = JobName.UPDATE_LOAN_PAID_IN_ADVANCE)
@@ -273,18 +233,28 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService
final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
jdbcTemplate.update("UPDATE m_loan ml SET ml.is_npa=0");
final StringBuilder resetNPASqlBuilder = new StringBuilder(900);
resetNPASqlBuilder.append("update m_loan loan ");
resetNPASqlBuilder.append("left join m_loan_arrears_aging laa on laa.loan_id = loan.id ");
resetNPASqlBuilder.append("inner join m_product_loan mpl on mpl.id = loan.product_id and mpl.overdue_days_for_npa is not null ");
resetNPASqlBuilder.append("set loan.is_npa = 0 ");
resetNPASqlBuilder.append("where loan.loan_status_id = 300 and mpl.account_moves_out_of_npa_only_on_arrears_completion = 0 ");
resetNPASqlBuilder
.append("or (mpl.account_moves_out_of_npa_only_on_arrears_completion = 1 and laa.overdue_since_date_derived is null)");
jdbcTemplate.update(resetNPASqlBuilder.toString());
final StringBuilder updateSqlBuilder = new StringBuilder(900);
updateSqlBuilder.append("UPDATE m_loan as ml,");
updateSqlBuilder.append(" (select loan.id from m_loan_repayment_schedule mr ");
updateSqlBuilder
.append(" INNER JOIN m_loan loan on mr.loan_id = loan.id INNER JOIN m_product_loan mpl on mpl.id = loan.product_id AND mpl.overdue_days_for_npa is not null ");
updateSqlBuilder.append("WHERE loan.loan_status_id = 300 and mr.completed_derived is false ");
updateSqlBuilder
.append(" and mr.duedate < SUBDATE(CURDATE(),INTERVAL ifnull(mpl.overdue_days_for_npa,0) day) group by loan.id) as sl ");
updateSqlBuilder.append("SET ml.is_npa=1 where ml.id=sl.id");
updateSqlBuilder.append(" (select loan.id ");
updateSqlBuilder.append("from m_loan_arrears_aging laa");
updateSqlBuilder.append(" INNER JOIN m_loan loan on laa.loan_id = loan.id ");
updateSqlBuilder.append(" INNER JOIN m_product_loan mpl on mpl.id = loan.product_id AND mpl.overdue_days_for_npa is not null ");
updateSqlBuilder.append("WHERE loan.loan_status_id = 300 and ");
updateSqlBuilder.append("laa.overdue_since_date_derived < SUBDATE(CURDATE(),INTERVAL ifnull(mpl.overdue_days_for_npa,0) day) ");
updateSqlBuilder.append("group by loan.id) as sl ");
updateSqlBuilder.append("SET ml.is_npa=1 where ml.id=sl.id ");
final int result = jdbcTemplate.update(updateSqlBuilder.toString());
@@ -0,0 +1,5 @@
ALTER TABLE `m_product_loan_recalculation_details`
ADD COLUMN `arrears_based_on_original_schedule` TINYINT(1) NOT NULL DEFAULT '0';
ALTER TABLE `m_product_loan`
ADD COLUMN `account_moves_out_of_npa_only_on_arrears_completion` TINYINT(1) NOT NULL DEFAULT '0';