MIFOSX-1421 : loan recalculation changes

This commit is contained in:
Pramod Nuthakki
2014-08-06 02:10:08 +05:30
parent 817794b26b
commit fd3a401821
28 changed files with 1415 additions and 160 deletions
@@ -0,0 +1,7 @@
INSERT INTO `job` (`name`, `display_name`, `cron_expression`, `create_time`, `task_priority`, `scheduler_group`) VALUES ('Recalculate Interest For Loans', 'Recalculate Interest For Loans', '0 1 0 1/1 * ? *', now(), 4, 3);
UPDATE `job` SET `scheduler_group`=3 WHERE `name`='Update Non Performing Assets';
UPDATE `job` SET `task_priority`=3 WHERE `name`='Add Accrual Transactions';
UPDATE `job` SET `task_priority`=2 WHERE `name`='Add Periodic Accrual Transactions';
@@ -22,7 +22,8 @@ public enum JobName {
UPDATE_NPA("Update Non Performing Assets"),
UPDATE_DEPOSITS_ACCOUNT_MATURITY_DETAILS("Update Deposit Accounts Maturity details"),
TRANSFER_INTEREST_TO_SAVINGS("Transfer Interest To Savings"),
ADD_PERIODIC_ACCRUAL_ENTRIES("Add Periodic Accrual Transactions");
ADD_PERIODIC_ACCRUAL_ENTRIES("Add Periodic Accrual Transactions"),
RECALCULATE_INTEREST_FOR_LOAN("Recalculate Interest For Loans");
private final String name;
@@ -32,8 +32,8 @@ public interface CalendarInstanceRepository extends JpaRepository<CalendarInstan
*/
CalendarInstance findByEntityIdAndEntityTypeIdAndCalendarTypeId(Long entityId, Integer entityTypeId, Integer calendarTypeId);
@Query("from CalendarInstance ci where ci.entityId = :loanId and ci.entityTypeId = :entityTypeId")
CalendarInstance findCalendarInstaneByLoanId(@Param("loanId") Long loanId, @Param("entityTypeId") Integer entityTypeId);
@Query("from CalendarInstance ci where ci.entityId = :entityId and ci.entityTypeId = :entityTypeId")
CalendarInstance findCalendarInstaneByEntityId(@Param("entityId") Long entityId, @Param("entityTypeId") Integer entityTypeId);
Collection<CalendarInstance> findByCalendarIdAndEntityTypeId(Long calendarId, Integer entityTypeId);
@@ -90,12 +90,13 @@ public class LoanTransactionsApiResource {
transactionData = this.loanReadPlatformService.retrieveNewClosureDetails();
} else if (is(commandParam, "disburse")) {
transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, true);
} else if(is(commandParam, "disburseToSavings")){
} else if (is(commandParam, "disburseToSavings")) {
transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, false);
} else if(is(commandParam, "recoverypayment")){
} else if (is(commandParam, "recoverypayment")) {
transactionData = this.loanReadPlatformService.retrieveRecoveryPaymentTemplate(loanId);
}
else {
} else if (is(commandParam, "prepayLoan")) {
transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(loanId);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
@@ -150,7 +151,7 @@ public class LoanTransactionsApiResource {
} else if (is(commandParam, "undowriteoff")) {
final CommandWrapper commandRequest = builder.undoWriteOffLoanTransaction(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}else if(is(commandParam,"recoverypayment")){
} else if (is(commandParam, "recoverypayment")) {
final CommandWrapper commandRequest = builder.loanRecoveryPaymentTransaction(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@@ -0,0 +1,33 @@
package org.mifosplatform.portfolio.loanaccount.data;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
public class LoanChargePaidDetail {
private final Money amount;
private final LoanRepaymentScheduleInstallment installment;
private final boolean isFeeCharge;
public LoanChargePaidDetail( Money amount,LoanRepaymentScheduleInstallment installment,boolean isFeeCharge){
this.amount = amount;
this.installment = installment;
this.isFeeCharge = isFeeCharge;
}
public Money getAmount() {
return this.amount;
}
public LoanRepaymentScheduleInstallment getInstallment() {
return this.installment;
}
public boolean isFeeCharge() {
return this.isFeeCharge;
}
}
@@ -60,6 +60,7 @@ import org.mifosplatform.organisation.staff.domain.Staff;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.organisation.workingdays.service.WorkingDaysUtil;
import org.mifosplatform.portfolio.accountdetails.domain.AccountType;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.portfolio.charge.domain.ChargeCalculationType;
import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeAddedException;
@@ -310,6 +311,9 @@ public class Loan extends AbstractPersistable<Long> {
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loan", optional = true, orphanRemoval = true)
private LoanInterestRecalculationDetails loanInterestRecalculationDetails;
@Column(name = "is_npa", nullable = false)
private boolean isNpa;
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanTransactionProcessingStrategy transactionProcessingStrategy,
@@ -496,8 +500,8 @@ public class Loan extends AbstractPersistable<Long> {
// NOTE: must add new loan charge to set of loan charges before
// reporcessing the repayment schedule.
if(this.charges == null) {
this.charges = new HashSet<>();
if (this.charges == null) {
this.charges = new HashSet<>();
}
this.charges.add(loanCharge);
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
@@ -1511,7 +1515,8 @@ public class Loan extends AbstractPersistable<Long> {
validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays);
/**
* Copy interest recalculation settings if interest recalculation is enabled
* Copy interest recalculation settings if interest recalculation is
* enabled
*/
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
this.loanInterestRecalculationDetails = this.loanProduct.copyInterestRecalculationSettings();
@@ -1722,7 +1727,8 @@ public class Loan extends AbstractPersistable<Long> {
final ApplicationCurrency currency, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final Map<String, Object> actualChanges, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final boolean allowTransactionsOnHoliday,
final boolean allowTransactionsOnNonWorkingDay, final boolean recalculateSchedule) {
final boolean allowTransactionsOnNonWorkingDay, final boolean recalculateSchedule,
final CalendarInstance calendarInstanceForInterestRecalculation) {
final LoanStatus statusEnum = this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED,
LoanStatus.fromInt(this.loanStatus));
@@ -1765,6 +1771,11 @@ public class Loan extends AbstractPersistable<Long> {
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
}
}
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()
&& fetchRepaymentScheduleInstallment(1).getDueDate().isBefore(LocalDate.now())) {
regenerateRepaymentScheduleWithInterestRecalculation(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate,
isHolidayEnabled, holidays, workingDays, calendarInstanceForInterestRecalculation);
}
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
updateLoanSummaryDerivedFields();
handleDisbursementTransaction(actualDisbursementDate);
@@ -2127,7 +2138,8 @@ public class Loan extends AbstractPersistable<Long> {
}
boolean isEmiAmountChanged = this.loanTermVariations.size() > 0;
updateLoanToPreDisbursalState();
if (isScheduleRegenerateRequired || isDisbueseAmtChanged || isEmiAmountChanged) {
if (isScheduleRegenerateRequired || isDisbueseAmtChanged || isEmiAmountChanged
|| this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
// clear off actual disbusrement date so schedule regeneration
// uses expected date.
@@ -2186,7 +2198,9 @@ public class Loan extends AbstractPersistable<Long> {
public ChangedTransactionDetail waiveInterest(final LoanTransaction waiveInterestTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final List<Long> existingReversedTransactionIds, final List<Holiday> holidays, final WorkingDays workingDays,
final boolean isHolidayEnabled, final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency,
final LocalDate calculatedRepaymentsStartingFromDate, final CalendarInstance calendarInstanceForInterestRecalculation) {
validateAccountStatus(LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
@@ -2197,7 +2211,8 @@ public class Loan extends AbstractPersistable<Long> {
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(waiveInterestTransaction,
loanLifecycleStateMachine, null);
loanLifecycleStateMachine, null, loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled,
holidays, workingDays, calendarInstanceForInterestRecalculation);
return changedTransactionDetail;
}
@@ -2205,7 +2220,9 @@ public class Loan extends AbstractPersistable<Long> {
public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List<Holiday> holidays,
final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay, boolean isRecoveryRepayment) {
final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay, final boolean isHolidayEnabled,
boolean isRecoveryRepayment, final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency,
final LocalDate calculatedRepaymentsStartingFromDate, final CalendarInstance calendarInstanceForInterestRecalculation) {
LoanEvent event = null;
if (isRecoveryRepayment) {
@@ -2224,7 +2241,8 @@ public class Loan extends AbstractPersistable<Long> {
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction,
loanLifecycleStateMachine, null);
loanLifecycleStateMachine, null, loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled,
holidays, workingDays, calendarInstanceForInterestRecalculation);
return changedTransactionDetail;
}
@@ -2289,7 +2307,10 @@ public class Loan extends AbstractPersistable<Long> {
}
private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) {
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction,
final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency,
final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays, final CalendarInstance calendarInstanceForInterestRecalculation) {
ChangedTransactionDetail changedTransactionDetail = null;
@@ -2353,10 +2374,36 @@ public class Loan extends AbstractPersistable<Long> {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
if (isTransactionChronologicallyLatest && adjustedTransaction == null) {
final LoanRepaymentScheduleInstallment currentInstallment = fetchLoanRepaymentScheduleInstallment(loanTransaction
.getTransactionDate());
boolean reprocess = true;
if (adjustedTransaction == null && loanTransaction.getTransactionDate().isEqual(LocalDate.now()) && currentInstallment != null) {
reprocess = false;
}
if (isTransactionChronologicallyLatest && adjustedTransaction == null
&& (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled())) {
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments,
charges());
} else {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
reprocess = true;
} else {
final LoanRepaymentScheduleInstallment nextInstallment = fetchRepaymentScheduleInstallment(currentInstallment
.getInstallmentNumber() + 1);
if (nextInstallment != null && nextInstallment.getTotalPaidInAdvance(getCurrency()).isGreaterThanZero()) {
reprocess = true;
}
}
}
}
if (reprocess) {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate,
isHolidayEnabled, holidays, workingDays, calendarInstanceForInterestRecalculation);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges());
@@ -2384,6 +2431,17 @@ public class Loan extends AbstractPersistable<Long> {
return changedTransactionDetail;
}
private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) {
LoanRepaymentScheduleInstallment installment = null;
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) {
if (dueDate.equals(loanRepaymentScheduleInstallment.getDueDate())) {
installment = loanRepaymentScheduleInstallment;
break;
}
}
return installment;
}
private List<LoanTransaction> retreiveListOfTransactionsPostDisbursement() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
@@ -2556,7 +2614,9 @@ public class Loan extends AbstractPersistable<Long> {
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction transactionForAdjustment,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final boolean allowTransactionsOnHoliday, final List<Holiday> holidays, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) {
final boolean isHolidayEnabled, final boolean allowTransactionsOnNonWorkingDay,
final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency,
final LocalDate calculatedRepaymentsStartingFromDate, final CalendarInstance calendarInstanceForInterestRecalculation) {
validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(), allowTransactionsOnHoliday, holidays);
validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay);
@@ -2589,13 +2649,17 @@ public class Loan extends AbstractPersistable<Long> {
if (newTransactionDetail.isRepayment() || newTransactionDetail.isInterestWaiver()) {
changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine,
transactionForAdjustment);
transactionForAdjustment, loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled,
holidays, workingDays, calendarInstanceForInterestRecalculation);
}
return changedTransactionDetail;
}
public ChangedTransactionDetail undoWrittenOff(final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
public ChangedTransactionDetail undoWrittenOff(final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays, final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency,
final LocalDate calculatedRepaymentsStartingFromDate, final CalendarInstance calendarInstanceForInterestRecalculation) {
validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO);
existingTransactionIds.addAll(findExistingTransactionIds());
@@ -2606,6 +2670,10 @@ public class Loan extends AbstractPersistable<Long> {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate,
isHolidayEnabled, holidays, workingDays, calendarInstanceForInterestRecalculation);
}
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments,
charges());
@@ -2672,12 +2740,13 @@ public class Loan extends AbstractPersistable<Long> {
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final AppUser currentUser, final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency,
final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays) {
final WorkingDays workingDays, final CalendarInstance calendarInstanceForInterestRecalculation) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
ChangedTransactionDetail changedTransactionDetail = closeDisbursements(loanScheduleFactory, currency,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, loanRepaymentScheduleTransactionProcessor);
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, loanRepaymentScheduleTransactionProcessor,
calendarInstanceForInterestRecalculation);
validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING);
@@ -2739,14 +2808,18 @@ public class Loan extends AbstractPersistable<Long> {
private ChangedTransactionDetail closeDisbursements(final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrency currency, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
final CalendarInstance calendarInstanceForInterestRecalculation) {
ChangedTransactionDetail changedTransactionDetail = null;
if (isDisbursementAllowed() && atleastOnceDisbursed()) {
this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount());
removeDisbursementDetail();
regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays,
workingDays);
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate,
isHolidayEnabled, holidays, workingDays, calendarInstanceForInterestRecalculation);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges());
@@ -2776,7 +2849,7 @@ public class Loan extends AbstractPersistable<Long> {
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency currency,
final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays) {
final WorkingDays workingDays, final CalendarInstance calendarInstanceForInterestRecalculation) {
validateAccountStatus(LoanEvent.LOAN_CLOSED);
@@ -2804,7 +2877,8 @@ public class Loan extends AbstractPersistable<Long> {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
ChangedTransactionDetail changedTransactionDetail = closeDisbursements(loanScheduleFactory, currency,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, loanRepaymentScheduleTransactionProcessor);
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, loanRepaymentScheduleTransactionProcessor,
calendarInstanceForInterestRecalculation);
LoanTransaction loanTransaction = null;
if (isOpen()) {
@@ -3829,7 +3903,7 @@ public class Loan extends AbstractPersistable<Long> {
final JsonCommand command, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final Map<String, Object> actualChanges, final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrency currency, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays) {
final List<Holiday> holidays, final WorkingDays workingDays, final CalendarInstance calendarInstanceForInterestRecalculation) {
validateAccountStatus(LoanEvent.LOAN_EDIT_MULTI_DISBURSE_DATE);
final LocalDate expectedDisbursementDate = command.localDateValueOfParameterNamed(LoanApiConstants.disbursementDateParameterName);
disbursementDetails.updateExpectedDisbursementDate(expectedDisbursementDate.toDate());
@@ -3842,6 +3916,11 @@ public class Loan extends AbstractPersistable<Long> {
regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays,
workingDays);
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate,
isHolidayEnabled, holidays, workingDays, calendarInstanceForInterestRecalculation);
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
@@ -3916,4 +3995,157 @@ public class Loan extends AbstractPersistable<Long> {
}
return maturityDate;
}
public ChangedTransactionDetail recalculateSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final CalendarInstance calendarInstanceForInterestRecalculation, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, calendarInstanceForInterestRecalculation);
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments,
charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
/***
* Commented since throwing exception if external id present for one of
* the transactions. for this need to save the reversed transactions
* first and then new transactions.
*/
// this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
updateLoanSummaryDerivedFields();
return changedTransactionDetail;
}
private void regenerateRepaymentScheduleWithInterestRecalculation(final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final CalendarInstance calendarInstanceForInterestRecalculation) {
if (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa) { return; }
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod);
final RoundingMode roundingMode = RoundingMode.HALF_EVEN;
final MathContext mc = new MathContext(8, roundingMode);
final Integer loanTermFrequency = this.termFrequency;
final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType);
final List<DisbursementData> disbursementData = new ArrayList<>();
for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) {
disbursementData.add(disbursementDetails.toData());
}
final List<LoanTermVariationsData> loanVariationTermsData = new ArrayList<>();
boolean isDefaultEmiAmountReq = true;
for (LoanTermVariations variationTerms : this.loanTermVariations) {
if (variationTerms.getTermType().isEMIAmountVariation()) {
if (variationTerms.getTermApplicableFrom().equals(this.getDisbursementDate().toDate())) {
isDefaultEmiAmountReq = false;
}
loanVariationTermsData.add(variationTerms.toData());
}
}
if (isDefaultEmiAmountReq) {
LoanTermVariationsData data = new LoanTermVariationsData(null,
LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), this.getDisbursementDate(), this.fixedEmiAmount);
loanVariationTermsData.add(data);
}
final LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency,
loanTermPeriodFrequencyType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(),
calculatedRepaymentsStartingFromDate, getInArrearsTolerance(), this.loanRepaymentScheduleDetail,
this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData, this.maxOutstandingLoanBalance,
loanVariationTermsData, getInterestChargedFromDate(), this.loanInterestRecalculationDetails,
calendarInstanceForInterestRecalculation);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final LoanScheduleModel loanSchedule = loanScheduleGenerator.rescheduleNextInstallments(mc, applicationCurrency,
loanApplicationTerms, charges(), isHolidayEnabled, holidays, workingDays, retreiveListOfTransactionsPostDisbursement(),
loanRepaymentScheduleTransactionProcessor);
updateLoanSchedule(loanSchedule);
LocalDate lastRepaymentDate = this.getLastRepaymentPeriodDueDate();
Set<LoanCharge> charges = this.charges();
for (LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) {
if (loanCharge.getDueLocalDate() == null || !lastRepaymentDate.isBefore(loanCharge.getDueLocalDate())) {
recalculateLoanCharge(loanCharge);
loanCharge.updateWaivedAmount(getCurrency());
} else {
loanCharge.setActive(false);
}
}
}
updateLoanSummaryDerivedFields();
}
public Money fetchPrepaymentDetail(final LoanScheduleGeneratorFactory loanScheduleFactory) {
Money amount = Money.zero(getCurrency());
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod);
final RoundingMode roundingMode = RoundingMode.HALF_EVEN;
final MathContext mc = new MathContext(8, roundingMode);
final Integer loanTermFrequency = this.termFrequency;
final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType);
final List<DisbursementData> disbursementData = new ArrayList<>();
for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) {
disbursementData.add(disbursementDetails.toData());
}
final List<LoanTermVariationsData> loanVariationTermsData = new ArrayList<>();
boolean isDefaultEmiAmountReq = true;
for (LoanTermVariations variationTerms : this.loanTermVariations) {
if (variationTerms.getTermType().isEMIAmountVariation()) {
if (variationTerms.getTermApplicableFrom().equals(this.getDisbursementDate().toDate())) {
isDefaultEmiAmountReq = false;
}
loanVariationTermsData.add(variationTerms.toData());
}
}
if (isDefaultEmiAmountReq) {
LoanTermVariationsData data = new LoanTermVariationsData(null,
LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), this.getDisbursementDate(),
this.fixedEmiAmount);
loanVariationTermsData.add(data);
}
final LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(null, loanTermFrequency,
loanTermPeriodFrequencyType, getDisbursementDate(), getExpectedFirstRepaymentOnDate(), null, getInArrearsTolerance(),
this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData,
this.maxOutstandingLoanBalance, loanVariationTermsData, getInterestChargedFromDate());
amount = loanScheduleGenerator.fetchPrepaymentAmount(this.repaymentScheduleInstallments, getCurrency(), loanApplicationTerms,
mc);
} else {
amount = this.getTotalOutstandingOnLoan();
}
return amount;
}
private Money getTotalOutstandingOnLoan() {
Money cumulativeOurstanding = Money.zero(loanCurrency());
for (final LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativeOurstanding = cumulativeOurstanding.plus(scheduledRepayment.getTotalOutstanding(getCurrency()));
}
return cumulativeOurstanding;
}
}
@@ -9,6 +9,7 @@ import java.math.BigDecimal;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
public interface LoanAccountDomainService {
@@ -27,4 +28,6 @@ public interface LoanAccountDomainService {
LoanTransaction makeDisburseTransaction(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount,
PaymentDetail paymentDetail, String noteText, String txnExternalId);
LocalDate getCalculatedRepaymentsStartingFromDate(LocalDate actualDisbursementDate, Loan loan, CalendarInstance calendarInstance);
}
@@ -30,11 +30,18 @@ import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.mifosplatform.portfolio.account.domain.AccountTransferRepository;
import org.mifosplatform.portfolio.account.domain.AccountTransferTransaction;
import org.mifosplatform.portfolio.calendar.domain.Calendar;
import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
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.group.domain.Group;
import org.mifosplatform.portfolio.group.exception.GroupNotActiveException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.note.domain.Note;
import org.mifosplatform.portfolio.note.domain.NoteRepository;
import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
@@ -57,6 +64,9 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final NoteRepository noteRepository;
private final AccountTransferRepository accountTransferRepository;
private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
private final CalendarInstanceRepository calendarInstanceRepository;
@Autowired
public LoanAccountDomainServiceJpa(final LoanAssembler loanAccountAssembler, final LoanRepository loanRepository,
@@ -65,7 +75,9 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final WorkingDaysRepositoryWrapper workingDaysRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
final JournalEntryWritePlatformService journalEntryWritePlatformService,
final AccountTransferRepository accountTransferRepository) {
final AccountTransferRepository accountTransferRepository, final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository,
final CalendarInstanceRepository calendarInstanceRepository) {
this.loanAccountAssembler = loanAccountAssembler;
this.loanRepository = loanRepository;
this.loanTransactionRepository = loanTransactionRepository;
@@ -76,6 +88,9 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.accountTransferRepository = accountTransferRepository;
this.loanScheduleFactory = loanScheduleFactory;
this.applicationCurrencyRepository = applicationCurrencyRepository;
this.calendarInstanceRepository = calendarInstanceRepository;
}
@Transactional
@@ -118,9 +133,28 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
CalendarInstance restCalendarInstance = null;
ApplicationCurrency applicationCurrency = null;
LocalDate calculatedRepaymentsStartingFromDate = null;
boolean isHolidayEnabled = false;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
final MonetaryCurrency currency = loan.getCurrency();
applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan,
calendarInstance);
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
}
final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction,
defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday,
holidays, workingDays, allowTransactionsOnNonWorkingDay, isRecoveryRepayment);
holidays, workingDays, allowTransactionsOnNonWorkingDay, isHolidayEnabled, isRecoveryRepayment, this.loanScheduleFactory,
applicationCurrency, calculatedRepaymentsStartingFromDate, restCalendarInstance);
try {
this.loanTransactionRepository.save(newRepaymentTransaction);
@@ -324,6 +358,34 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.loanTransactionRepository.save(loanTransaction);
}
@Override
public LocalDate getCalculatedRepaymentsStartingFromDate(final LocalDate actualDisbursementDate, final Loan loan,
final CalendarInstance calendarInstance) {
final Calendar calendar = calendarInstance == null ? null : calendarInstance.getCalendar();
LocalDate calculatedRepaymentsStartingFromDate = loan.getExpectedFirstRepaymentOnDate();
if (calendar != null) {// sync repayments
// TODO: AA - user provided first repayment date takes precedence
// over recalculated meeting date
if (calculatedRepaymentsStartingFromDate == null) {
// FIXME: AA - Possibility of having next meeting date
// immediately after disbursement date,
// need to have minimum number of days gap between disbursement
// and first repayment date.
final LoanProductRelatedDetail repaymentScheduleDetails = loan.repaymentScheduleDetail();
if (repaymentScheduleDetails != null) {// Not expecting to be
// null
final Integer repayEvery = repaymentScheduleDetails.getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentScheduleDetails
.getRepaymentPeriodFrequencyType());
calculatedRepaymentsStartingFromDate = CalendarUtils.getFirstRepaymentMeetingDate(calendar, actualDisbursementDate,
repayEvery, frequency);
}
}
}
return calculatedRepaymentsStartingFromDate;
}
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
if (transferTransaction != null) {
@@ -8,10 +8,12 @@ package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -41,6 +43,7 @@ import org.mifosplatform.portfolio.charge.domain.ChargePaymentMode;
import org.mifosplatform.portfolio.charge.domain.ChargeTimeType;
import org.mifosplatform.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException;
import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand;
import org.mifosplatform.portfolio.loanaccount.data.LoanChargePaidDetail;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@@ -695,7 +698,12 @@ public class LoanCharge extends AbstractPersistable<Long> {
amountPaidToDate = amountPaidToDate.plus(amountOutstanding);
this.amountPaid = amountPaidToDate.getAmount();
this.amountOutstanding = BigDecimal.ZERO;
this.paid = true;
Money waivedAmount = Money.of(processAmount.getCurrency(), this.amountWaived);
if (waivedAmount.isGreaterThanZero()) {
this.waived = true;
} else {
this.paid = true;
}
} else {
amountPaidOnThisCharge = processAmount;
@@ -800,14 +808,18 @@ public class LoanCharge extends AbstractPersistable<Long> {
return this.loanInstallmentCharge;
}
public LoanRepaymentScheduleInstallment fetchRepaymentInstallment(final Money trasferedAmount) {
public List<LoanChargePaidDetail> fetchRepaymentInstallment(final MonetaryCurrency currency) {
List<LoanChargePaidDetail> chargePaidDetails = new ArrayList<>();
for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) {
if (loanChargePerInstallment.isPending()
&& trasferedAmount.getAmount().equals(
loanChargePerInstallment.getAmountThroughChargePayment(trasferedAmount.getCurrency()).getAmount())) { return loanChargePerInstallment
.getRepaymentInstallment(); }
&& loanChargePerInstallment.getAmountThroughChargePayment(currency).isGreaterThanZero()) {
LoanChargePaidDetail chargePaidDetail = new LoanChargePaidDetail(
loanChargePerInstallment.getAmountThroughChargePayment(currency),
loanChargePerInstallment.getRepaymentInstallment(), isFeeCharge());
chargePaidDetails.add(chargePaidDetail);
}
}
return null;
return chargePaidDetails;
}
public boolean isActive() {
@@ -842,4 +854,37 @@ public class LoanCharge extends AbstractPersistable<Long> {
public void updateOverdueInstallmentCharge(LoanOverdueInstallmentCharge overdueInstallmentCharge) {
this.overdueInstallmentCharge = overdueInstallmentCharge;
}
public void updateWaivedAmount(MonetaryCurrency currency) {
if (isInstalmentFee()) {
for (final LoanInstallmentCharge chargePerInstallment : this.loanInstallmentCharge) {
final Money amountWaived = chargePerInstallment.updateWaivedAmount(currency);
if (this.amountWaived == null) {
this.amountWaived = BigDecimal.ZERO;
}
this.amountWaived = this.amountWaived.add(amountWaived.getAmount());
this.amountOutstanding = this.amountOutstanding.subtract(amountWaived.getAmount());
if (determineIfFullyPaid()) {
this.paid = false;
this.waived = true;
}
return;
}
}
Money waivedAmount = Money.of(currency, this.amountWaived);
if (waivedAmount.isGreaterThanZero()) {
if (waivedAmount.isGreaterThan(this.getAmount(currency))) {
this.amountWaived = this.getAmount(currency).getAmount();
this.amountOutstanding = BigDecimal.ZERO;
this.paid = false;
this.waived = true;
} else if (waivedAmount.isLessThan(this.getAmount(currency))) {
this.paid = false;
this.waived = false;
}
}
}
}
@@ -153,7 +153,7 @@ public class LoanInstallmentCharge extends AbstractPersistable<Long> {
Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid);
final Money amountOutstanding = Money.of(incrementBy.getCurrency(), this.amountOutstanding);
Money amountPaidPreviously = amountPaidToDate;
Money amountPaidOnThisCharge = Money.zero(incrementBy.getCurrency());
if (incrementBy.isGreaterThanOrEqualTo(amountOutstanding)) {
amountPaidOnThisCharge = amountOutstanding;
@@ -166,8 +166,20 @@ public class LoanInstallmentCharge extends AbstractPersistable<Long> {
this.amountPaid = amountPaidToDate.getAmount();
this.amountOutstanding = calculateAmountOutstanding(incrementBy.getCurrency());
}
this.amountThroughChargePayment = feeAmount.getAmount();
this.paid = determineIfFullyPaid();
Money amountFromChargePayment = Money.of(incrementBy.getCurrency(), this.amountThroughChargePayment);
if (amountPaidPreviously.isGreaterThanZero()) {
amountFromChargePayment = amountFromChargePayment.plus(feeAmount);
} else {
amountFromChargePayment = feeAmount;
}
this.amountThroughChargePayment = amountFromChargePayment.getAmount();
if (determineIfFullyPaid()) {
if (this.amountWaived.compareTo(BigDecimal.ZERO) == 1) {
this.waived = true;
} else {
this.paid = true;
}
}
return amountPaidOnThisCharge;
}
@@ -195,4 +207,20 @@ public class LoanInstallmentCharge extends AbstractPersistable<Long> {
return Money.of(currency, this.amountThroughChargePayment);
}
public Money updateWaivedAmount(final MonetaryCurrency currency) {
if (this.amountWaived.compareTo(BigDecimal.ZERO) == 1) {
int comparedValue = this.amountWaived.compareTo(this.getAmount());
if (comparedValue == 1) {
this.amountWaived = this.getAmount();
this.amountOutstanding = BigDecimal.ZERO;
this.paid = false;
this.waived = true;
} else if (comparedValue == -1) {
this.paid = false;
this.waived = false;
}
}
return getAmountWaived(currency);
}
}
@@ -26,6 +26,7 @@ import org.springframework.data.jpa.domain.AbstractPersistable;
@Table(name = "m_loan_recalculation_details")
public class LoanInterestRecalculationDetails extends AbstractPersistable<Long> {
@SuppressWarnings("unused")
@OneToOne
@JoinColumn(name = "loan_id", nullable = false)
private Loan loan;
@@ -65,4 +66,12 @@ public class LoanInterestRecalculationDetails extends AbstractPersistable<Long>
this.rescheduleStrategyMethod = rescheduleStrategyMethod;
}
public InterestRecalculationCompoundingMethod getInterestRecalculationCompoundingMethod() {
return InterestRecalculationCompoundingMethod.fromInt(this.interestRecalculationCompoundingMethod);
}
public LoanRescheduleStrategyMethod getRescheduleStrategyMethod() {
return LoanRescheduleStrategyMethod.fromInt(this.rescheduleStrategyMethod);
}
}
@@ -532,4 +532,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.fromDate = newFromDate.toDate();
}
}
public Money getTotalPaidInAdvance(final MonetaryCurrency currency) {
return Money.of(currency, this.totalPaidInAdvance);
}
public Money getTotalPaidLate(final MonetaryCurrency currency) {
return Money.of(currency, this.totalPaidLate);
}
}
@@ -527,4 +527,8 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
this.externalId = externalId;
}
public boolean isAccrual() {
return LoanTransactionType.ACCRUAL.equals(getTypeOf()) && isNotReversed();
}
}
@@ -6,13 +6,16 @@
package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.portfolio.loanaccount.data.LoanChargePaidDetail;
import org.mifosplatform.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanChargePaidBy;
@@ -69,36 +72,47 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
final List<LoanTransaction> transactionstoBeProcessed = new ArrayList<>();
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
if (loanTransaction.isChargePayment()) {
final List<LoanRepaymentScheduleInstallment> chargePaymentInstallments = new ArrayList<>();
List<LoanChargePaidDetail> chargePaidDetails = new ArrayList<>();
final Set<LoanChargePaidBy> chargePaidBies = loanTransaction.getLoanChargesPaid();
final Set<LoanCharge> transferCharges = new HashSet<>();
for (final LoanChargePaidBy chargePaidBy : chargePaidBies) {
LoanCharge loanCharge = chargePaidBy.getLoanCharge();
transferCharges.add(loanCharge);
if (loanCharge.isInstalmentFee()) {
LoanRepaymentScheduleInstallment installment = null;
if (loanCharge.isFeeCharge()) {
installment = loanCharge.fetchRepaymentInstallment(loanTransaction.getFeeChargesPortion(currency));
} else {
installment = loanCharge.fetchRepaymentInstallment(loanTransaction.getPenaltyChargesPortion(currency));
}
if (installment != null) {
chargePaymentInstallments.add(installment);
}
chargePaidDetails.addAll(loanCharge.fetchRepaymentInstallment(currency));
}
}
LocalDate startDate = disbursementDate;
for (final LoanRepaymentScheduleInstallment installment : installments) {
for (final LoanCharge loanCharge : transferCharges) {
if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate())) {
chargePaymentInstallments.add(installment);
LoanChargePaidDetail chargePaidDetail = new LoanChargePaidDetail(
installment.getFeeChargesOutstanding(currency), installment, loanCharge.isFeeCharge());
chargePaidDetails.add(chargePaidDetail);
break;
}
}
startDate = installment.getDueDate();
}
loanTransaction.resetDerivedComponents();
handleTransaction(loanTransaction, currency, chargePaymentInstallments, transferCharges);
Money unprocessed = loanTransaction.getAmount(currency);
for (LoanChargePaidDetail chargePaidDetail : chargePaidDetails) {
final List<LoanRepaymentScheduleInstallment> processInstallments = new ArrayList<>(1);
unprocessed = unprocessed.minus(chargePaidDetail.getAmount());
processInstallments.add(chargePaidDetail.getInstallment());
Money unprocessedAfterProcess = handleTransactionAndCharges(loanTransaction, currency, processInstallments,
transferCharges, chargePaidDetail.getAmount(), chargePaidDetail.isFeeCharge());
unprocessed = unprocessed.plus(unprocessedAfterProcess);
if (!unprocessed.isGreaterThanZero()) {
break;
}
}
if (unprocessed.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, unprocessed);
loanTransaction.updateOverPayments(unprocessed);
}
} else {
transactionstoBeProcessed.add(loanTransaction);
}
@@ -152,12 +166,73 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
public void handleTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
// find earliest unpaid installment for which to apply this transaction
final Money amountToProcess = null;
final boolean isChargeAmount = false;
handleTransaction(loanTransaction, currency, installments, charges, amountToProcess, isChargeAmount);
}
private void handleTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, final Money chargeAmountToProcess,
final boolean isFeeCharge) {
Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, currency, installments, charges,
chargeAmountToProcess, isFeeCharge);
if (transactionAmountUnprocessed.isGreaterThanZero()) {
if (loanTransaction.isWaiver()) {
loanTransaction.updateComponentsAndTotal(transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero(),
transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero());
} else {
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
loanTransaction.updateOverPayments(transactionAmountUnprocessed);
}
}
}
private Money handleTransactionAndCharges(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, final Money chargeAmountToProcess,
final boolean isFeeCharge) {
// to.
Money transactionAmountUnprocessed = processTransaction(loanTransaction, currency, installments, chargeAmountToProcess);
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
Integer installmentNumber = null;
if (loanTransaction.isChargePayment() && installments.size() == 1) {
installmentNumber = installments.get(0).getInstallmentNumber();
}
if (loanTransaction.isNotWaiver()) {
Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (chargeAmountToProcess != null) {
if (isFeeCharge) {
feeCharges = chargeAmountToProcess;
} else {
penaltyCharges = chargeAmountToProcess;
}
}
if (feeCharges.isGreaterThanZero()) {
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
}
if (penaltyCharges.isGreaterThanZero()) {
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
}
}
return transactionAmountUnprocessed;
}
private Money processTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, Money amountToProcess) {
int installmentIndex = 0;
final LocalDate transactionDate = loanTransaction.getTransactionDate();
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
if (amountToProcess != null) {
transactionAmountUnprocessed = amountToProcess;
}
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
@@ -182,30 +257,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
installmentIndex++;
}
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
Integer installmentNumber = null;
if (loanTransaction.isChargePayment() && installments.size() == 1) {
installmentNumber = installments.get(0).getInstallmentNumber();
}
if (loanTransaction.isNotWaiver()) {
final Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
if (feeCharges.isGreaterThanZero()) {
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
}
final Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (penaltyCharges.isGreaterThanZero()) {
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
}
}
if (transactionAmountUnprocessed.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
loanTransaction.updateOverPayments(transactionAmountUnprocessed);
}
return transactionAmountUnprocessed;
}
private Set<LoanCharge> extractFeeCharges(final Set<LoanCharge> loanCharges) {
@@ -367,4 +419,42 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// empty implementation by default.
}
@Override
public Map<LocalDate, Money> handleRecalculation(final LocalDate disbursementDate,
final List<LoanTransaction> transactionsPostDisbursement, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, LoanRepaymentScheduleInstallment currentinstallment,
final Map<LocalDate, LocalDate> recalculationDates) {
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetDerivedComponents();
currentInstallment.updateDerivedFields(currency, disbursementDate);
}
return handleRepaymentSchedule(transactionsPostDisbursement, currency, installments, currentinstallment, recalculationDates);
}
@Override
public Map<LocalDate, Money> handleRepaymentSchedule(final List<LoanTransaction> transactionsPostDisbursement,
final MonetaryCurrency currency, final List<LoanRepaymentScheduleInstallment> installments,
final LoanRepaymentScheduleInstallment installment, final Map<LocalDate, LocalDate> recalculationDates) {
Map<LocalDate, Money> unprocessedMap = new HashMap<>();
Money currentInstallmentOutstanding = installment.getTotalOutstanding(currency);
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
Money amountToProcess = null;
Money unProcessed = processTransaction(loanTransaction, currency, installments, amountToProcess);
if (loanTransaction.getTransactionDate().isAfter(installment.getFromDate())
&& recalculationDates.get(loanTransaction.getTransactionDate()).isBefore(installment.getDueDate())) {
Money earlyPayment = currentInstallmentOutstanding.minus(installment.getTotalOutstanding(currency));
if (earlyPayment.isGreaterThanZero()) {
unprocessedMap.put(loanTransaction.getTransactionDate(), earlyPayment);
}
currentInstallmentOutstanding = installment.getTotalOutstanding(currency);
}
if (unProcessed.isGreaterThanZero()) {
unprocessedMap.put(loanTransaction.getTransactionDate(), unProcessed);
}
}
return unprocessedMap;
}
}
@@ -6,10 +6,12 @@
package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -26,4 +28,10 @@ public interface LoanRepaymentScheduleTransactionProcessor {
void handleWriteOff(LoanTransaction loanTransaction, MonetaryCurrency loanCurrency,
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments);
Map<LocalDate, Money> handleRecalculation(LocalDate disbursementDate, List<LoanTransaction> transactionsPostDisbursement,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, LoanRepaymentScheduleInstallment currentinstallment, Map<LocalDate, LocalDate> recalculationDates);
Map<LocalDate, Money> handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, LoanRepaymentScheduleInstallment currentInstallment, Map<LocalDate, LocalDate> recalculationDates);
}
@@ -39,8 +39,14 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
// zero this type of transaction and ignore it for now.
transactionAmountRemaining = Money.zero(currency);
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment
.waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
@@ -64,8 +64,14 @@ public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
// zero this type of transaction and ignore it for now.
transactionAmountRemaining = Money.zero(currency);
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment
.waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
@@ -78,8 +78,14 @@ public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends Abstrac
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
// zero this type of transaction and ignore it for now.
transactionAmountRemaining = Money.zero(currency);
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment
.waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
@@ -64,8 +64,14 @@ public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
// zero this type of transaction and ignore it for now.
transactionAmountRemaining = Money.zero(currency);
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment
.waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
@@ -9,8 +9,11 @@ import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.joda.time.Days;
import org.joda.time.LocalDate;
@@ -19,8 +22,13 @@ import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementDisbursementDateException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
@@ -37,6 +45,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
public LoanScheduleModel generate(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays) {
final List<RecalculationDetail> diffAmt = null;
return generate(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled, holidays, workingDays, diffAmt);
}
private LoanScheduleModel generate(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, List<RecalculationDetail> diffAmt) {
// 1. generate list of proposed schedule due dates
final LocalDate loanEndDate = this.scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, isHolidayEnabled, holidays,
@@ -66,7 +81,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
final Collection<LoanScheduleModelPeriod> periods = createNewLoanScheduleListWithDisbursementDetails(numberOfRepayments,
loanApplicationTerms, chargesDueAtTimeOfDisbursement);
// 4. Determine the total interest owed over the full loan for FLAT interest method .
// 4. Determine the total interest owed over the full loan for FLAT
// interest method .
Money totalInterestChargedForFullLoanTerm = loanApplicationTerms.calculateTotalInterestCharged(
this.paymentPeriodsInOneYearCalculator, mc);
@@ -91,10 +107,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().zero().plus(disburseAmt));
outstandingBalance = outstandingBalance.zero().plus(disburseAmt);
}
Money reducePrincipal = totalCumulativePrincipal.zero();
Money fixedEmiAmount = totalCumulativePrincipal.zero();
while (!outstandingBalance.isZero()) {
isFirstRepayment = false;
actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms,
isFirstRepayment);
isFirstRepayment = false;
LocalDate scheduledDueDate = this.scheduledDateGenerator.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms,
isHolidayEnabled, holidays, workingDays);
final int daysInPeriod = Days.daysBetween(periodStartDate, scheduledDueDate).getDays();
@@ -127,12 +145,40 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
.calculatePortionOfRepaymentPeriodInterestChargingGrace(periodStartDateApplicableForInterest, scheduledDueDate,
loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(),
loanApplicationTerms.getRepaymentEvery());
Money balanceForcalculation = outstandingBalance;
if (reducePrincipal.isGreaterThanZero()) {
switch (loanApplicationTerms.getRescheduleStrategyMethod()) {
case REDUCE_EMI_AMOUNT:
loanApplicationTerms.setFixedEmiAmount(null);
outstandingBalance = outstandingBalance.minus(reducePrincipal);
balanceForcalculation = outstandingBalance;
totalCumulativePrincipal = totalCumulativePrincipal.plus(reducePrincipal);
reducePrincipal = reducePrincipal.zero();
break;
case REDUCE_NUMBER_OF_INSTALLMENTS:
loanApplicationTerms.setFixedEmiAmount(fixedEmiAmount.getAmount());
outstandingBalance = outstandingBalance.minus(reducePrincipal);
balanceForcalculation = outstandingBalance;
totalCumulativePrincipal = totalCumulativePrincipal.plus(reducePrincipal);
reducePrincipal = reducePrincipal.zero();
break;
case RESCHEDULE_NEXT_REPAYMENTS:
balanceForcalculation = balanceForcalculation.minus(reducePrincipal);
break;
default:
break;
}
}
if (!balanceForcalculation.isGreaterThanZero()) {
break;
}
// 5 determine principal,interest of repayment period
final PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(
this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalCumulativePrincipal,
totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace,
daysInPeriodApplicableForInterest, outstandingBalance, loanApplicationTerms, periodNumber, mc);
daysInPeriodApplicableForInterest, balanceForcalculation, loanApplicationTerms, periodNumber, mc);
if (loanApplicationTerms.getFixedEmiAmount() != null
&& loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) != 1) {
@@ -141,32 +187,102 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
loanApplicationTerms.getFixedEmiAmount());
}
// update cumulative fields for principal & interest
Money interestForThisinstallment = principalInterestForThisPeriod.interest();
totalCumulativePrincipal = totalCumulativePrincipal.plus(principalInterestForThisPeriod.principal());
totalCumulativeInterest = totalCumulativeInterest.plus(principalInterestForThisPeriod.interest());
totalCumulativeInterest = totalCumulativeInterest.plus(interestForThisinstallment);
totalOutstandingInterestPaymentDueToGrace = principalInterestForThisPeriod.interestPaymentDueToGrace();
// 6. update outstandingLoanBlance using correct 'principalDue'
outstandingBalance = outstandingBalance.minus(principalInterestForThisPeriod.principal());
Money actualOutstandingbalance = outstandingBalance;
Money feeChargesForInstallment = principalDisbursed.zero();
Money penaltyChargesForInstallment = principalDisbursed.zero();
Money principalForThisPeriod = principalInterestForThisPeriod.principal();
if (principalForThisPeriod.isGreaterThan(reducePrincipal)) {
principalForThisPeriod = principalForThisPeriod.minus(reducePrincipal);
reducePrincipal = reducePrincipal.zero();
} else {
reducePrincipal = reducePrincipal.minus(principalForThisPeriod);
principalForThisPeriod = principalForThisPeriod.zero();
}
if (periodNumber == 1) {
fixedEmiAmount = principalForThisPeriod.plus(interestForThisinstallment);
}
if (diffAmt != null && !diffAmt.isEmpty()) {
Money interestDueToLatePayment = totalCumulativeInterest.zero();
Money interestReducedDueToEarlyPayment = totalCumulativeInterest.zero();
for (RecalculationDetail detail : diffAmt) {
if (!detail.isLatePayment() && detail.getStartDate().isAfter(periodStartDate)
&& !detail.getStartDate().isAfter(scheduledDueDate)) {
reducePrincipal = reducePrincipal.plus(detail.getAmount());
int diffDays = Days.daysBetween(detail.getStartDate(), scheduledDueDate).getDays();
if (diffDays > 0) {
reducePrincipal = reducePrincipal.minus(principalForThisPeriod);
}
if (reducePrincipal.isLessThanZero()) {
reducePrincipal = reducePrincipal.zero();
} else {
Money actualOutstanding = outstandingBalance.minus(reducePrincipal);
principalForThisPeriod = reducePrincipal.plus(principalForThisPeriod);
if (actualOutstanding.isLessThanZero()) {
principalForThisPeriod = principalForThisPeriod.plus(actualOutstanding);
}
}
interestReducedDueToEarlyPayment = interestReducedDueToEarlyPayment.plus(loanApplicationTerms.interestRateFor(
this.paymentPeriodsInOneYearCalculator, mc, diffDays, detail.getAmount()));
} else if (detail.isLatePayment() && detail.isOverlapping(periodStartDate, scheduledDueDate)
&& periodStartDate.isBefore(LocalDate.now())) {
LocalDate fromDate = periodStartDate;
LocalDate toDate = scheduledDueDate;
if (!detail.getStartDate().isAfter(periodStartDate)) {
fromDate = detail.getStartDate();
}
if (!detail.getToDate().isAfter(scheduledDueDate)) {
toDate = detail.getToDate();
}
if (toDate.isAfter(LocalDate.now())) {
toDate = LocalDate.now();
}
int diffDays = Days.daysBetween(fromDate, toDate).getDays();
interestDueToLatePayment = interestDueToLatePayment.plus(loanApplicationTerms.interestRateFor(
this.paymentPeriodsInOneYearCalculator, mc, diffDays, detail.getAmount()));
}
}
if (totalOutstandingInterestPaymentDueToGrace.isGreaterThanZero()) {
totalOutstandingInterestPaymentDueToGrace = totalOutstandingInterestPaymentDueToGrace.plus(interestDueToLatePayment)
.minus(interestReducedDueToEarlyPayment);
} else {
totalCumulativeInterest = totalCumulativeInterest.plus(interestDueToLatePayment)
.minus(interestReducedDueToEarlyPayment);
interestForThisinstallment = interestForThisinstallment.plus(interestDueToLatePayment).minus(
interestReducedDueToEarlyPayment);
}
}
actualOutstandingbalance = actualOutstandingbalance.minus(reducePrincipal);
// 8. sum up real totalInstallmentDue from components
final Money totalInstallmentDue = principalInterestForThisPeriod.principal() //
.plus(principalInterestForThisPeriod.interest()) //
final Money totalInstallmentDue = principalForThisPeriod//
.plus(interestForThisinstallment) //
.plus(feeChargesForInstallment) //
.plus(penaltyChargesForInstallment);
// 9. create repayment period from parts
final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(periodNumber, periodStartDate,
scheduledDueDate, principalInterestForThisPeriod.principal(), outstandingBalance,
principalInterestForThisPeriod.interest(), feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue);
scheduledDueDate, principalForThisPeriod, actualOutstandingbalance, interestForThisinstallment,
feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue);
periods.add(installment);
// handle cumulative fields
loanTermInDays += daysInPeriod;
totalPrincipalExpected = totalPrincipalExpected.add(principalInterestForThisPeriod.principal().getAmount());
totalInterestCharged = totalInterestCharged.add(principalInterestForThisPeriod.interest().getAmount());
totalInterestCharged = totalInterestCharged.add(interestForThisinstallment.getAmount());
totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount());
totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount());
totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount());
@@ -198,6 +314,40 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
}
if (diffAmt != null && !diffAmt.isEmpty() && !periodStartDate.isAfter(LocalDate.now())) {
Map<LocalDate, RecalculationDetail> processDetails = new TreeMap<>();
for (RecalculationDetail detail : diffAmt) {
if (periodStartDate.isBefore(detail.getStartDate()) && detail.isLatePayment()) {
if (processDetails.containsKey(detail.getToDate())) {
RecalculationDetail recalculationDetail = processDetails.get(detail.getToDate());
RecalculationDetail updatedDetail = new RecalculationDetail(recalculationDetail.isLatePayment(),
recalculationDetail.getStartDate(), recalculationDetail.getToDate(), recalculationDetail.getAmount().plus(
detail.getAmount()));
processDetails.put(updatedDetail.getToDate(), updatedDetail);
} else {
processDetails.put(detail.getToDate(), detail);
}
}
}
for (RecalculationDetail detail : processDetails.values()) {
LocalDate fromDate = detail.getStartDate();
LocalDate toDate = detail.getToDate();
int diffDays = Days.daysBetween(fromDate, toDate).getDays();
Money interestDueToLatePayment = loanApplicationTerms.interestRateFor(this.paymentPeriodsInOneYearCalculator, mc, diffDays,
detail.getAmount());
totalInterestCharged = totalInterestCharged.add(interestDueToLatePayment.getAmount());
totalRepaymentExpected = totalRepaymentExpected.add(interestDueToLatePayment.getAmount());
final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, toDate,
interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment,
interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment);
periods.add(installment);
periodNumber++;
}
}
return LoanScheduleModel.from(periods, applicationCurrency, loanTermInDays, principalDisbursed, totalPrincipalExpected,
totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected,
totalOutstanding);
@@ -366,4 +516,205 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return cumulative;
}
@Override
public LoanScheduleModel rescheduleNextInstallments(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final List<LoanTransaction> transactions,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
int installmentNumber = 0;
final List<RecalculationDetail> recalculationDetails = new ArrayList<>();
LoanScheduleModel loanScheduleModel = generate(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled,
holidays, workingDays);
while (installmentNumber < loanScheduleModel.getPeriods().size() - 1) {
RecalculatedSchedule recalculatedSchedule = recalculateInterest(mc, applicationCurrency, loanApplicationTerms, loanCharges,
isHolidayEnabled, holidays, workingDays, transactions, loanRepaymentScheduleTransactionProcessor, installmentNumber,
loanScheduleModel, recalculationDetails);
installmentNumber = recalculatedSchedule.getInstallmentNumber();
loanScheduleModel = recalculatedSchedule.getLoanScheduleModel();
}
return loanScheduleModel;
}
private RecalculatedSchedule recalculateInterest(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final List<LoanTransaction> transactions,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final int installmentNumber,
LoanScheduleModel model, final List<RecalculationDetail> recalculationDetails) {
boolean processRecalculate = false;
int processedInstallmentNumber = installmentNumber;
final List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
final List<RecalculationDetail> diffAmt = new ArrayList<>();
Money unpaidPricipal = loanApplicationTerms.getPrincipal();
for (final LoanScheduleModelPeriod scheduledLoanInstallment : model.getPeriods()) {
if (scheduledLoanInstallment.isRepaymentPeriod()) {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(null,
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue());
installments.add(installment);
}
}
MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency();
final List<LoanRepaymentScheduleInstallment> processinstallmets = new ArrayList<>();
for (LoanRepaymentScheduleInstallment installment : installments) {
processinstallmets.add(installment);
unpaidPricipal = unpaidPricipal.minus(installment.getPrincipal(currency));
if (installment.getInstallmentNumber() <= installmentNumber) {
continue;
}
processedInstallmentNumber = installment.getInstallmentNumber();
if (installment.getDueDate().isAfter(LocalDate.now())) {
continue;
}
List<LoanTransaction> transactionsForInstallment = new ArrayList<>();
Map<LocalDate, LocalDate> recalculationDates = new HashMap<>();
LocalDate transactionsDate = getNextRestScheduleDate(installment.getDueDate().minusDays(1), loanApplicationTerms,
isHolidayEnabled, holidays, workingDays);
for (LoanTransaction loanTransaction : transactions) {
LocalDate loantransactionDate = loanTransaction.getTransactionDate();
if (!loantransactionDate.isAfter(transactionsDate) && !loanTransaction.isAccrual()) {
transactionsForInstallment.add(loanTransaction);
recalculationDates.put(
loantransactionDate,
getNextRestScheduleDate(loantransactionDate.minusDays(1), loanApplicationTerms, isHolidayEnabled, holidays,
workingDays));
}
}
Map<LocalDate, Money> earlyPaymentMap = loanRepaymentScheduleTransactionProcessor.handleRecalculation(
loanApplicationTerms.getExpectedDisbursementDate(), transactionsForInstallment, currency, processinstallmets,
installment, recalculationDates);
for (Map.Entry<LocalDate, Money> entry : earlyPaymentMap.entrySet()) {
LocalDate startDate = entry.getKey();
if (unpaidPricipal.isGreaterThan(entry.getValue())) {
startDate = getNextRestScheduleDate(startDate.minusDays(1), loanApplicationTerms, isHolidayEnabled, holidays,
workingDays);
}
RecalculationDetail recalculationDetail = new RecalculationDetail(false, startDate, null, entry.getValue());
if (!entry.getKey().isAfter(installment.getDueDate()) && entry.getKey().isAfter(installment.getFromDate())) {
diffAmt.add(recalculationDetail);
}
}
if (installment.getDueDate().isBefore(LocalDate.now())) {
LocalDate startDate = installment.getDueDate();
Money totalOutstanding = installment.getTotalOutstanding(currency);
while (totalOutstanding.isGreaterThanZero() && startDate.isBefore(LocalDate.now())) {
LocalDate recalculateFrom = getNextRestScheduleDate(startDate.minusDays(1), loanApplicationTerms, isHolidayEnabled,
holidays, workingDays);
LocalDate recalcualteTill = getNextRestScheduleDate(recalculateFrom, loanApplicationTerms, isHolidayEnabled, holidays,
workingDays);
applyRest(transactions, startDate, recalculateFrom, installment, loanRepaymentScheduleTransactionProcessor, currency,
loanApplicationTerms, isHolidayEnabled, holidays, workingDays);
totalOutstanding = installment.getTotalOutstanding(currency);
if (totalOutstanding.isGreaterThanZero()) {
Money latepaymentoutstanding = installment.getPrincipalOutstanding(currency);
switch (loanApplicationTerms.getInterestRecalculationCompoundingMethod()) {
case INTEREST:
latepaymentoutstanding = latepaymentoutstanding.plus(installment.getInterestOutstanding(currency));
break;
case INTEREST_AND_FEE:
latepaymentoutstanding = latepaymentoutstanding.plus(installment.getInterestOutstanding(currency))
.plus(installment.getFeeChargesOutstanding(currency))
.plus(installment.getPenaltyChargesOutstanding(currency));
break;
case FEE:
latepaymentoutstanding = latepaymentoutstanding.plus(installment.getFeeChargesOutstanding(currency)).plus(
installment.getPenaltyChargesOutstanding(currency));
break;
default:
break;
}
RecalculationDetail recalculationDetail = new RecalculationDetail(true, recalculateFrom, recalcualteTill,
latepaymentoutstanding);
diffAmt.add(recalculationDetail);
}
startDate = recalcualteTill;
}
}
if (!diffAmt.isEmpty()) {
processRecalculate = true;
break;
}
}
if (processRecalculate) {
recalculationDetails.addAll(diffAmt);
model = generate(mc, applicationCurrency, loanApplicationTerms, loanCharges, isHolidayEnabled, holidays, workingDays,
recalculationDetails);
}
return new RecalculatedSchedule(model, processedInstallmentNumber);
}
private void applyRest(List<LoanTransaction> loanTransactions, LocalDate from, LocalDate to,
LoanRepaymentScheduleInstallment installment,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, MonetaryCurrency currency,
final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled, final List<Holiday> holidays,
final WorkingDays workingDays) {
List<LoanTransaction> transactions = new ArrayList<>();
Map<LocalDate, LocalDate> recalculationDates = new HashMap<>();
for (LoanTransaction transaction : loanTransactions) {
LocalDate loantransactionDate = transaction.getTransactionDate();
if (loantransactionDate.isAfter(from) && !transaction.getTransactionDate().isAfter(to) && !transaction.isAccrual()) {
transactions.add(transaction);
recalculationDates.put(
loantransactionDate,
getNextRestScheduleDate(loantransactionDate.minusDays(1), loanApplicationTerms, isHolidayEnabled, holidays,
workingDays));
}
}
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>(1);
installments.add(installment);
loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(transactions, currency, installments, installment,
recalculationDates);
}
@SuppressWarnings("unused")
private LocalDate getNextRestScheduleDate(LocalDate startDate, LoanApplicationTerms loanApplicationTerms,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays) {
CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance();
LocalDate nextScheduleDate = CalendarUtils.getNextScheduleDate(calendarInstance.getCalendar(), startDate);
/*
* nextScheduleDate =
* this.scheduledDateGenerator.adjustRepaymentDate(nextScheduleDate,
* loanApplicationTerms, isHolidayEnabled, holidays, workingDays);
*/
return nextScheduleDate;
}
@Override
public Money fetchPrepaymentAmount(final List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
final LoanApplicationTerms applicationTerms, final MathContext mc) {
Money prepaymentAmount = Money.zero(currency);
Money amount = Money.zero(currency);
LocalDate calculateInterestFrom = LocalDate.now();
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (currentInstallment.isNotFullyPaidOff()) {
if (!currentInstallment.getDueDate().isAfter(LocalDate.now())) {
amount = amount.plus(currentInstallment.getTotalOutstanding(currency));
} else {
prepaymentAmount = prepaymentAmount.plus(currentInstallment.getPrincipalOutstanding(currency));
if (currentInstallment.getFromDate().isBefore(calculateInterestFrom)) {
calculateInterestFrom = currentInstallment.getFromDate();
}
}
}
}
int days = Days.daysBetween(calculateInterestFrom, LocalDate.now()).getDays();
Money interest = applicationTerms.interestRateFor(this.paymentPeriodsInOneYearCalculator, mc, days, prepaymentAmount);
prepaymentAmount = prepaymentAmount.plus(interest).plus(amount);
return prepaymentAmount;
}
}
@@ -16,15 +16,19 @@ import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.common.domain.DaysInMonthType;
import org.mifosplatform.portfolio.common.domain.DaysInYearType;
import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData;
import org.mifosplatform.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod;
import org.mifosplatform.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
public final class LoanApplicationTerms {
@@ -107,8 +111,14 @@ public final class LoanApplicationTerms {
private final DaysInMonthType daysInMonthType;
private final DaysInYearType daysInYearType;
private final boolean isInterestRecalculationEnabled;
private final boolean interestRecalculationEnabled;
private final LoanRescheduleStrategyMethod rescheduleStrategyMethod;
private final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod;
private final CalendarInstance restCalendarInstance;
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
@@ -124,12 +134,16 @@ public final class LoanApplicationTerms {
final List<LoanTermVariationsData> emiAmountVariations, final Integer graceOnArrearsAgeing,
final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType, final boolean isInterestRecalculationEnabled) {
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod = null;
final CalendarInstance restCalendarInstance = null;
return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery,
repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType,
annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate,
repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment,
graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount, disbursementDatas,
maxOutstandingBalance, emiAmountVariations, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled);
maxOutstandingBalance, emiAmountVariations, graceOnArrearsAgeing, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, rescheduleStrategyMethod, interestRecalculationCompoundingMethod, restCalendarInstance);
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
@@ -160,6 +174,9 @@ public final class LoanApplicationTerms {
final DaysInMonthType daysInMonthType = loanProductRelatedDetail.fetchDaysInMonthType();
final DaysInYearType daysInYearType = loanProductRelatedDetail.fetchDaysInYearType();
final boolean isInterestRecalculationEnabled = loanProductRelatedDetail.isInterestRecalculationEnabled();
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod = null;
final CalendarInstance restCalendarInstance = null;
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod,
@@ -167,7 +184,54 @@ public final class LoanApplicationTerms {
expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment,
graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount,
disbursementDatas, maxOutstandingBalance, emiAmountVariations, loanProductRelatedDetail.getGraceOnDueDate(),
daysInMonthType, daysInYearType, isInterestRecalculationEnabled);
daysInMonthType, daysInYearType, isInterestRecalculationEnabled, rescheduleStrategyMethod,
interestRecalculationCompoundingMethod, restCalendarInstance);
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final LocalDate expectedDisbursementDate,
final LocalDate repaymentsStartingFromDate, final LocalDate calculatedRepaymentsStartingFromDate,
final Money inArrearsTolerance, final LoanProductRelatedDetail loanProductRelatedDetail, final boolean multiDisburseLoan,
final BigDecimal emiAmount, final List<DisbursementData> disbursementDatas, final BigDecimal maxOutstandingBalance,
final List<LoanTermVariationsData> emiAmountVariations, final LocalDate interestChargedFromDate,
final LoanInterestRecalculationDetails interestRecalculationDetails, final CalendarInstance restCalendarInstance) {
final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments();
final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery();
final PeriodFrequencyType repaymentPeriodFrequencyType = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
final AmortizationMethod amortizationMethod = loanProductRelatedDetail.getAmortizationMethod();
final InterestMethod interestMethod = loanProductRelatedDetail.getInterestMethod();
final BigDecimal interestRatePerPeriod = loanProductRelatedDetail.getNominalInterestRatePerPeriod();
final PeriodFrequencyType interestRatePeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType();
final BigDecimal annualNominalInterestRate = loanProductRelatedDetail.getAnnualNominalInterestRate();
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = loanProductRelatedDetail
.getInterestCalculationPeriodMethod();
final Money principalMoney = loanProductRelatedDetail.getPrincipal();
//
final Integer graceOnPrincipalPayment = loanProductRelatedDetail.graceOnPrincipalPayment();
final Integer graceOnInterestPayment = loanProductRelatedDetail.graceOnInterestPayment();
final Integer graceOnInterestCharged = loanProductRelatedDetail.graceOnInterestCharged();
// Interest recalculation settings
final DaysInMonthType daysInMonthType = loanProductRelatedDetail.fetchDaysInMonthType();
final DaysInYearType daysInYearType = loanProductRelatedDetail.fetchDaysInYearType();
final boolean isInterestRecalculationEnabled = loanProductRelatedDetail.isInterestRecalculationEnabled();
LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod = null;
if (isInterestRecalculationEnabled) {
rescheduleStrategyMethod = interestRecalculationDetails.getRescheduleStrategyMethod();
interestRecalculationCompoundingMethod = interestRecalculationDetails.getInterestRecalculationCompoundingMethod();
}
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod,
interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney,
expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment,
graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount,
disbursementDatas, maxOutstandingBalance, emiAmountVariations, loanProductRelatedDetail.getGraceOnDueDate(),
daysInMonthType, daysInYearType, isInterestRecalculationEnabled, rescheduleStrategyMethod,
interestRecalculationCompoundingMethod, restCalendarInstance);
}
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
@@ -182,7 +246,8 @@ public final class LoanApplicationTerms {
final boolean multiDisburseLoan, final BigDecimal emiAmount, final List<DisbursementData> disbursementDatas,
final BigDecimal maxOutstandingBalance, final List<LoanTermVariationsData> emiAmountVariations,
final Integer graceOnArrearsAgeing, final DaysInMonthType daysInMonthType, final DaysInYearType daysInYearType,
final boolean isInterestRecalculationEnabled) {
final boolean isInterestRecalculationEnabled, final LoanRescheduleStrategyMethod rescheduleStrategyMethod,
final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod, final CalendarInstance restCalendarInstance) {
this.currency = currency;
this.loanTermFrequency = loanTermFrequency;
this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType;
@@ -216,7 +281,10 @@ public final class LoanApplicationTerms {
this.graceOnArrearsAgeing = graceOnArrearsAgeing;
this.daysInMonthType = daysInMonthType;
this.daysInYearType = daysInYearType;
this.isInterestRecalculationEnabled = isInterestRecalculationEnabled;
this.interestRecalculationEnabled = isInterestRecalculationEnabled;
this.rescheduleStrategyMethod = rescheduleStrategyMethod;
this.interestRecalculationCompoundingMethod = interestRecalculationCompoundingMethod;
this.restCalendarInstance = restCalendarInstance;
}
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -593,6 +661,18 @@ public final class LoanApplicationTerms {
return periodicInterestRate;
}
public Money interestRateFor(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc, int days,
final Money outstandingBalance) {
final long loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator);
final BigDecimal divisor = BigDecimal.valueOf(Double.valueOf("100.0"));
final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear);
final BigDecimal oneDayOfYearInterestRate = this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(
divisor, mc);
BigDecimal interestRate = oneDayOfYearInterestRate.multiply(BigDecimal.valueOf(days), mc);
return outstandingBalance.multiplyRetainScale(interestRate, mc.getRoundingMode());
}
private long calculatePeriodsInOneYear(final PaymentPeriodsInOneYearCalculator calculator) {
long periodsInOneYear = calculator.calculate(this.repaymentPeriodFrequencyType).longValue();
@@ -663,7 +743,7 @@ public final class LoanApplicationTerms {
private double paymentPerPeriod(final BigDecimal periodicInterestRate, final Money balance, final int periodsElapsed) {
if (multiDisburseLoan && fixedEmiAmount != null) { return fixedEmiAmount.doubleValue(); }
if (fixedEmiAmount != null) { return fixedEmiAmount.doubleValue(); }
final double futureValue = 0;
final double principalDouble = balance.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue();
@@ -762,7 +842,7 @@ public final class LoanApplicationTerms {
this.interestCalculationPeriodMethod, this.repaymentEvery, this.repaymentPeriodFrequencyType, this.numberOfRepayments,
this.principalGrace, this.interestPaymentGrace, this.interestChargingGrace, this.amortizationMethod,
this.inArrearsTolerance.getAmount(), this.graceOnArrearsAgeing, this.daysInMonthType.getValue(),
this.daysInYearType.getValue(), this.isInterestRecalculationEnabled);
this.daysInYearType.getValue(), this.interestRecalculationEnabled);
}
public Integer getLoanTermFrequency() {
@@ -863,4 +943,28 @@ public final class LoanApplicationTerms {
}
}
}
public void setFixedEmiAmount(BigDecimal fixedEmiAmount) {
this.fixedEmiAmount = fixedEmiAmount;
}
public LoanRescheduleStrategyMethod getLoanRescheduleStrategyMethod() {
return LoanRescheduleStrategyMethod.REDUCE_EMI_AMOUNT;
}
public boolean isInterestRecalculationEnabled() {
return this.interestRecalculationEnabled;
}
public LoanRescheduleStrategyMethod getRescheduleStrategyMethod() {
return this.rescheduleStrategyMethod;
}
public InterestRecalculationCompoundingMethod getInterestRecalculationCompoundingMethod() {
return this.interestRecalculationCompoundingMethod;
}
public CalendarInstance getRestCalendarInstance() {
return this.restCalendarInstance;
}
}
@@ -11,11 +11,24 @@ import java.util.Set;
import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.organisation.workingdays.domain.WorkingDays;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
public interface LoanScheduleGenerator {
LoanScheduleModel generate(MathContext mc, ApplicationCurrency applicationCurrency, LoanApplicationTerms loanApplicationTerms,
Set<LoanCharge> loanCharges, boolean isHolidayEnabled, List<Holiday> holidays, final WorkingDays workingDays);
LoanScheduleModel rescheduleNextInstallments(MathContext mc, ApplicationCurrency applicationCurrency,
LoanApplicationTerms loanApplicationTerms, Set<LoanCharge> loanCharges, boolean isHolidayEnabled, List<Holiday> holidays,
WorkingDays workingDays, List<LoanTransaction> transactions,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor);
Money fetchPrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
LoanApplicationTerms applicationTerms, MathContext mc);
}
@@ -0,0 +1,20 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.domain;
public class RecalculatedSchedule {
private final LoanScheduleModel loanScheduleModel;
private final Integer installmentNumber;
public RecalculatedSchedule(final LoanScheduleModel loanScheduleModel, final Integer installmentNumber) {
this.loanScheduleModel = loanScheduleModel;
this.installmentNumber = installmentNumber;
}
public LoanScheduleModel getLoanScheduleModel() {
return this.loanScheduleModel;
}
public Integer getInstallmentNumber() {
return this.installmentNumber;
}
}
@@ -0,0 +1,44 @@
package org.mifosplatform.portfolio.loanaccount.loanschedule.domain;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.domain.Money;
public class RecalculationDetail {
private boolean isLatePayment;
private LocalDate startDate;
private LocalDate toDate;
private Money amount;
public RecalculationDetail(final boolean isLatePayment, final LocalDate startDate, final LocalDate toDate, final Money amount) {
this.isLatePayment = isLatePayment;
this.startDate = startDate;
this.toDate = toDate;
this.amount = amount;
}
public boolean isLatePayment() {
return this.isLatePayment;
}
public LocalDate getStartDate() {
return this.startDate;
}
public LocalDate getToDate() {
return this.toDate;
}
public Money getAmount() {
return this.amount;
}
public boolean isOverlapping(LocalDate startDate, LocalDate endDate) {
return !this.startDate.isAfter(endDate) && !startDate.isAfter(this.toDate);
}
public void updateAmount(Money amount) {
this.amount = amount;
}
}
@@ -87,4 +87,8 @@ public interface LoanReadPlatformService {
LoanTransactionData retrieveLoanWriteoffTemplate(Long loanId);
Collection<LoanScheduleAccrualData> retrivePeriodicAccrualData(LocalDate tillDate);
Collection<Long> fetchArrearLoans();
LoanTransactionData retrieveLoanPrePaymentTemplate(Long loanId);
}
@@ -81,6 +81,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionNotFound
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanproduct.data.LoanProductData;
import org.mifosplatform.portfolio.loanproduct.data.TransactionProcessingStrategyData;
import org.mifosplatform.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
@@ -120,6 +121,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
private final LoanMapper loaanLoanMapper = new LoanMapper();
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd");
private final LoanScheduleGeneratorFactory loanScheduleFactory;
@Autowired
public LoanReadPlatformServiceImpl(final PlatformSecurityContext context, final LoanRepository loanRepository,
@@ -129,7 +131,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final GroupReadPlatformService groupReadPlatformService, final LoanDropdownReadPlatformService loanDropdownReadPlatformService,
final FundReadPlatformService fundReadPlatformService, final ChargeReadPlatformService chargeReadPlatformService,
final CodeValueReadPlatformService codeValueReadPlatformService, final RoutingDataSource dataSource,
final CalendarReadPlatformService calendarReadPlatformService, final StaffReadPlatformService staffReadPlatformService) {
final CalendarReadPlatformService calendarReadPlatformService, final StaffReadPlatformService staffReadPlatformService,
final LoanScheduleGeneratorFactory loanScheduleFactory) {
this.context = context;
this.loanRepository = loanRepository;
this.loanTransactionRepository = loanTransactionRepository;
@@ -145,6 +148,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
this.staffReadPlatformService = staffReadPlatformService;
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
this.loanScheduleFactory = loanScheduleFactory;
}
@Override
@@ -364,6 +368,29 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, paymentOptions, null, null, null);
}
@Override
public LoanTransactionData retrieveLoanPrePaymentTemplate(final Long loanId) {
this.context.authenticatedUser();
final Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) { throw new LoanNotFoundException(loanId); }
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CurrencyData currencyData = applicationCurrency.toData();
final LocalDate earliestUnpaidInstallmentDate = LocalDate.now();
final Money precloseAmount = loan.fetchPrepaymentDetail(this.loanScheduleFactory);
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
final Collection<CodeValueData> paymentOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName);
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate,
precloseAmount.getAmount(), null, null, null, null, null, paymentOptions, null, null, null);
}
@Override
public LoanTransactionData retrieveWaiveInterestDetails(final Long loanId) {
@@ -1533,4 +1560,22 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
loan.getTotalOutstandingAmount(), null, null, null, null, null, null, null, null);
}
@Override
public Collection<Long> fetchArrearLoans() {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT ml.id FROM m_loan ml ");
sqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
sqlBuilder.append(" WHERE ml.loan_status_id = ? ");
sqlBuilder.append(" and ml.interest_recalculation_enabled = 1 ");
sqlBuilder.append(" and ml.is_npa = 0 ");
sqlBuilder.append(" and mr.completed_derived is false ");
sqlBuilder.append(" and mr.duedate < ? ");
sqlBuilder.append(" group by ml.id");
try {
return this.jdbcTemplate.queryForList(sqlBuilder.toString(), Long.class, new Object[] { LoanStatus.ACTIVE.getValue(), formatter.print(LocalDate.now()) });
} catch (final EmptyResultDataAccessException e) {
return null;
}
}
}
@@ -79,4 +79,6 @@ public interface LoanWritePlatformService {
CommandProcessingResult updateDisbursementDateForTranche(Long loanId, Long disbursementId, JsonCommand command);
void recalculateInterest();
}
@@ -61,7 +61,6 @@ import org.mifosplatform.portfolio.calendar.domain.CalendarInstance;
import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository;
import org.mifosplatform.portfolio.calendar.domain.CalendarRepository;
import org.mifosplatform.portfolio.calendar.domain.CalendarType;
import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.portfolio.charge.domain.Charge;
import org.mifosplatform.portfolio.charge.domain.ChargePaymentMode;
import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper;
@@ -115,7 +114,6 @@ import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.ScheduledDate
import org.mifosplatform.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
import org.mifosplatform.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.mifosplatform.portfolio.loanproduct.data.LoanProductData;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException;
import org.mifosplatform.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.mifosplatform.portfolio.loanproduct.service.LoanProductReadPlatformService;
@@ -237,7 +235,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
checkForProductMixRestrictions(loan);
// validate actual disbursement date against meeting date
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(),
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
if (loan.isSyncDisbursementWithMeeting()) {
@@ -257,8 +255,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// Recalculate first repayment date based in actual disbursement date.
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final LocalDate calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(actualDisbursementDate, loan,
calendarInstance);
final LocalDate calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
actualDisbursementDate, loan, calendarInstance);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(),
actualDisbursementDate.toDate());
@@ -282,9 +280,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.getLoanTransactions().add(disbursementTransaction);
}
CalendarInstance restCalendarInstance = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
}
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
existingReversedTransactionIds, changes, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays,
allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule);
allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule, restCalendarInstance);
}
if (!changes.isEmpty()) {
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
@@ -333,7 +337,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
updateRecurringCalendarDatesForInterestRecalculation(loan);
return new CommandProcessingResultBuilder() //
@@ -376,33 +380,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
}
private LocalDate getCalculatedRepaymentsStartingFromDate(final LocalDate actualDisbursementDate, final Loan loan,
final CalendarInstance calendarInstance) {
final Calendar calendar = calendarInstance == null ? null : calendarInstance.getCalendar();
LocalDate calculatedRepaymentsStartingFromDate = loan.getExpectedFirstRepaymentOnDate();
if (calendar != null) {// sync repayments
// TODO: AA - user provided first repayment date takes precedence
// over recalculated meeting date
if (calculatedRepaymentsStartingFromDate == null) {
// FIXME: AA - Possibility of having next meeting date
// immediately after disbursement date,
// need to have minimum number of days gap between disbursement
// and first repayment date.
final LoanProductRelatedDetail repaymentScheduleDetails = loan.repaymentScheduleDetail();
if (repaymentScheduleDetails != null) {// Not expecting to be
// null
final Integer repayEvery = repaymentScheduleDetails.getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentScheduleDetails
.getRepaymentPeriodFrequencyType());
calculatedRepaymentsStartingFromDate = CalendarUtils.getFirstRepaymentMeetingDate(calendar, actualDisbursementDate,
repayEvery, frequency);
}
}
}
return calculatedRepaymentsStartingFromDate;
}
/****
* TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base
*
@@ -462,9 +439,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
disbursementTransaction.updateLoan(loan);
loan.getLoanTransactions().add(disbursementTransaction);
}
CalendarInstance restCalendarInstance = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(
loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
}
loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds,
existingReversedTransactionIds, changes, firstRepaymentOnDate, isHolidayEnabled, holidays, workingDays,
allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule);
allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule, restCalendarInstance);
}
if (!changes.isEmpty()) {
try {
@@ -541,11 +524,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(),
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
final LocalDate actualDisbursementDate = loan.getDisbursementDate();
final LocalDate calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(actualDisbursementDate, loan,
calendarInstance);
final LocalDate calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
actualDisbursementDate, loan, calendarInstance);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(),
@@ -699,9 +682,28 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
CalendarInstance restCalendarInstance = null;
ApplicationCurrency applicationCurrency = null;
LocalDate calculatedRepaymentsStartingFromDate = null;
boolean isHolidayEnabled = false;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
final MonetaryCurrency currency = loan.getCurrency();
applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
loan.getDisbursementDate(), loan, calendarInstance);
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
}
final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail,
defaultLoanLifecycleStateMachine(), transactionToAdjust, existingTransactionIds, existingReversedTransactionIds,
allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay);
allowTransactionsOnHoliday, holidays, workingDays, isHolidayEnabled, allowTransactionsOnNonWorkingDay,
this.loanScheduleFactory, applicationCurrency, calculatedRepaymentsStartingFromDate, restCalendarInstance);
if (newTransactionDetail.isGreaterThanZero(loan.getPrincpal().getCurrency())) {
if (paymentDetail != null) {
@@ -783,8 +785,32 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanTransaction waiveInterestTransaction = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney,
transactionDate);
final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction,
defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds);
CalendarInstance restCalendarInstance = null;
ApplicationCurrency applicationCurrency = null;
LocalDate calculatedRepaymentsStartingFromDate = null;
List<Holiday> holidays = null;
boolean isHolidayEnabled = false;
WorkingDays workingDays = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
final MonetaryCurrency currency = loan.getCurrency();
applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
loan.getDisbursementDate(), loan, calendarInstance);
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
workingDays = this.workingDaysRepository.findOne();
}
final ChangedTransactionDetail changedTransactionDetail = loan
.waiveInterest(waiveInterestTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds,
existingReversedTransactionIds, holidays, workingDays, isHolidayEnabled, this.loanScheduleFactory,
applicationCurrency, calculatedRepaymentsStartingFromDate, restCalendarInstance);
this.loanTransactionRepository.save(waiveInterestTransaction);
@@ -844,10 +870,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(),
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
final LocalDate calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan,
calendarInstance);
final LocalDate calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
loan.getDisbursementDate(), loan, calendarInstance);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan
.getDisbursementDate().toDate());
@@ -857,9 +883,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
CalendarInstance restCalendarInstance = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
}
final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine(),
changes, existingTransactionIds, existingReversedTransactionIds, currentUser, this.loanScheduleFactory,
applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays);
applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, restCalendarInstance);
LoanTransaction writeoff = changedTransactionDetail.getNewTransactionMappings().remove(0L);
this.loanTransactionRepository.save(writeoff);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -906,10 +938,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(),
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
final LocalDate calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan,
calendarInstance);
final LocalDate calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
loan.getDisbursementDate(), loan, calendarInstance);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan
.getDisbursementDate().toDate());
@@ -919,9 +951,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
CalendarInstance restCalendarInstance = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
}
ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine(), changes,
existingTransactionIds, existingReversedTransactionIds, this.loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays);
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, restCalendarInstance);
final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L);
if (possibleClosingTransaction != null) {
this.loanTransactionRepository.save(possibleClosingTransaction);
@@ -1429,9 +1467,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate);
loan.getLoanTransactions().add(newTransferAcceptanceTransaction);
if(loan.getTotalOverpaid() !=null){
if (loan.getTotalOverpaid() != null) {
loan.setLoanStatus(LoanStatus.OVERPAID.getValue());
}else{
} else {
loan.setLoanStatus(LoanStatus.ACTIVE.getValue());
}
if (loanOfficer != null) {
@@ -1953,7 +1991,31 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (!loan.isClosedWrittenOff()) { throw new PlatformServiceUnavailableException(
"error.msg.loan.status.not.written.off.update.not.allowed", "Loan :" + loanId
+ " update not allowed as loan status is not written off", loanId); }
ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(existingTransactionIds, existingReversedTransactionIds);
CalendarInstance restCalendarInstance = null;
ApplicationCurrency applicationCurrency = null;
LocalDate calculatedRepaymentsStartingFromDate = null;
List<Holiday> holidays = null;
boolean isHolidayEnabled = false;
WorkingDays workingDays = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
final MonetaryCurrency currency = loan.getCurrency();
applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
loan.getDisbursementDate(), loan, calendarInstance);
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
workingDays = this.workingDaysRepository.findOne();
}
ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(existingTransactionIds, existingReversedTransactionIds,
isHolidayEnabled, holidays, workingDays, this.loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, restCalendarInstance);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
@@ -1979,10 +2041,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
checkClientOrGroupActive(loan);
LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId);
this.loanEventApiJsonValidator.validateUpdateDisbursementDate(command.json(), loanDisbursementDetails);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(),
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
final LocalDate calculatedRepaymentsStartingFromDate = getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan,
calendarInstance);
final LocalDate calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
loan.getDisbursementDate(), loan, calendarInstance);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan
.getDisbursementDate().toDate());
@@ -1993,9 +2055,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Map<String, Object> changes = new LinkedHashMap<>();
CalendarInstance restCalendarInstance = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
}
final ChangedTransactionDetail changedTransactionDetail = loan.updateDisbursementDateForTranche(loanDisbursementDetails, command,
existingTransactionIds, existingReversedTransactionIds, changes, this.loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays);
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, restCalendarInstance);
this.loanRepository.saveAndFlush(loan);
@@ -2085,6 +2152,56 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
return transaction;
}
@Override
@CronTarget(jobName = JobName.RECALCULATE_INTEREST_FOR_LOAN)
public void recalculateInterest() {
Collection<Long> loanIds = this.loanReadPlatformService.fetchArrearLoans();
for (Long loanId : loanIds) {
recalculateInterest(loanId);
}
}
@Transactional
public void recalculateInterest(final long loanId) {
Loan loan = this.loanAssembler.assembleFrom(loanId);
CalendarInstance restCalendarInstance = null;
ApplicationCurrency applicationCurrency = null;
LocalDate calculatedRepaymentsStartingFromDate = null;
List<Holiday> holidays = null;
boolean isHolidayEnabled = false;
WorkingDays workingDays = null;
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue());
final MonetaryCurrency currency = loan.getCurrency();
applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
calculatedRepaymentsStartingFromDate = this.loanAccountDomainService.getCalculatedRepaymentsStartingFromDate(
loan.getDisbursementDate(), loan, calendarInstance);
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
workingDays = this.workingDaysRepository.findOne();
}
ChangedTransactionDetail changedTransactionDetail = loan.recalculateSchedule(loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, restCalendarInstance,
existingTransactionIds, existingReversedTransactionIds);
this.loanRepository.save(loan);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
if (transferTransaction != null) {