From 15c46685837642bd8c25fb7dcc24b50ab404a300 Mon Sep 17 00:00:00 2001 From: Pramod Nuthakki Date: Fri, 8 Aug 2014 03:37:19 +0530 Subject: [PATCH] MIFOSX-1421 : corrected interest calculation for different interest compounding types and corrected installment charges --- .../portfolio/loanaccount/domain/Loan.java | 65 ++++------ .../domain/LoanAccountDomainServiceJpa.java | 84 +++++++----- .../loanaccount/domain/LoanCharge.java | 68 +++++----- .../domain/LoanInstallmentCharge.java | 33 ++++- ...epaymentScheduleInstallmentRepository.java | 9 ++ .../loanaccount/domain/LoanTransaction.java | 1 + ...RepaymentScheduleTransactionProcessor.java | 67 ++++++---- .../domain/AbstractLoanScheduleGenerator.java | 33 +++-- .../domain/LoanApplicationTerms.java | 71 ++++++++++- ...WritePlatformServiceJpaRepositoryImpl.java | 56 ++++---- ...WritePlatformServiceJpaRepositoryImpl.java | 120 ++++++++++-------- 11 files changed, 372 insertions(+), 235 deletions(-) create mode 100755 mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallmentRepository.java diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index 6598be3c8..c24a82f30 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -964,7 +964,6 @@ public class Loan extends AbstractPersistable { } public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule) { - clearInstallmentLoanCharge(); this.repaymentScheduleInstallments.clear(); for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { @@ -984,14 +983,6 @@ public class Loan extends AbstractPersistable { } - private void clearInstallmentLoanCharge() { - for (final LoanCharge loanCharge : charges()) { - if (loanCharge.isInstalmentFee()) { - loanCharge.clearLoanInstallmentCharges(); - } - } - } - private void updateLoanScheduleDependentDerivedFields() { this.expectedMaturityDate = determineExpectedMaturityDate().toDate(); this.actualMaturityDate = determineExpectedMaturityDate().toDate(); @@ -1203,14 +1194,8 @@ public class Loan extends AbstractPersistable { final String chargesParamName = "charges"; if (isChargesModified) { - actualChanges.put(chargesParamName, getLoanCharges(possiblyModifedLoanCharges)); - actualChanges.put(chargesParamName, getLoanCharges(possiblyModifedLoanCharges)); actualChanges.put("recalculateLoanSchedule", true); - - for (final LoanCharge loanCharge : possiblyModifedLoanCharges) { - recalculateLoanCharge(loanCharge); - } } final String collateralParamName = "collateral"; @@ -1269,6 +1254,13 @@ public class Loan extends AbstractPersistable { return actualChanges; } + public void recalculateAllCharges() { + Set charges = this.charges(); + for (final LoanCharge loanCharge : charges) { + recalculateLoanCharge(loanCharge); + } + } + private boolean isInterestRecalculationEnabledForProduct() { return this.loanProduct.isInterestRecalculationEnabled(); } @@ -1723,12 +1715,12 @@ public class Loan extends AbstractPersistable { return ids; } - public void disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, final JsonCommand command, - final ApplicationCurrency currency, final List existingTransactionIds, final List existingReversedTransactionIds, - final Map actualChanges, final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, - final List holidays, final WorkingDays workingDays, final boolean allowTransactionsOnHoliday, - final boolean allowTransactionsOnNonWorkingDay, final boolean recalculateSchedule, - final CalendarInstance calendarInstanceForInterestRecalculation) { + public ChangedTransactionDetail disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, + final JsonCommand command, final ApplicationCurrency currency, final List existingTransactionIds, + final List existingReversedTransactionIds, final Map actualChanges, + final LocalDate calculatedRepaymentsStartingFromDate, final boolean isHolidayEnabled, final List holidays, + final WorkingDays workingDays, final boolean allowTransactionsOnHoliday, final boolean allowTransactionsOnNonWorkingDay, + final boolean recalculateSchedule, final CalendarInstance calendarInstanceForInterestRecalculation) { final LoanStatus statusEnum = this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, LoanStatus.fromInt(this.loanStatus)); @@ -1763,13 +1755,7 @@ public class Loan extends AbstractPersistable { if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate) || recalculateSchedule || isEmiAmountChanged) { regenerateRepaymentSchedule(loanScheduleFactory, currency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays); - if (recalculateSchedule) { - Set charges = this.charges(); - for (LoanCharge loanCharge : charges) { - recalculateLoanCharge(loanCharge); - } - updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); - } + updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); } if (this.repaymentScheduleDetail().isInterestRecalculationEnabled() && fetchRepaymentScheduleInstallment(1).getDueDate().isBefore(LocalDate.now())) { @@ -1794,8 +1780,7 @@ public class Loan extends AbstractPersistable { this.loanTransactions.add(interestAppliedTransaction); } - // changedTransactionDetail = - // reprocessTransactionForDisbursement(changedTransactionDetail); + return reprocessTransactionForDisbursement(); } @@ -1860,14 +1845,8 @@ public class Loan extends AbstractPersistable { return disburseAmount; } - public ChangedTransactionDetail reprocessTransactionForDisbursement() { + private ChangedTransactionDetail reprocessTransactionForDisbursement() { ChangedTransactionDetail changedTransactionDetail = null; - for (LoanCharge loanCharge : this.charges) { - if (loanCharge.isInstalmentFee() && loanCharge.hasNoLoanInstallmentCharges()) { - final Set chargePerInstallments = generateInstallmentLoanCharges(loanCharge); - loanCharge.addLoanInstallmentCharges(chargePerInstallments); - } - } if (this.loanProduct.isMultiDisburseLoan()) { if (!this.actualDisbursementDate.equals(fetchLastDisburseDetail().getDisbursementDate())) { @@ -2015,6 +1994,10 @@ public class Loan extends AbstractPersistable { isHolidayEnabled, holidays, workingDays); updateLoanSchedule(loanSchedule); + Set charges = this.charges(); + for (LoanCharge loanCharge : charges) { + recalculateLoanCharge(loanCharge); + } } private void handleDisbursementTransaction(final LocalDate disbursedOn) { @@ -2146,10 +2129,6 @@ public class Loan extends AbstractPersistable { regenerateRepaymentSchedule(loanScheduleFactory, applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays); if (isDisbueseAmtChanged) { - Set charges = charges(); - for (LoanCharge loanCharge : charges) { - recalculateLoanCharge(loanCharge); - } updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); } } @@ -4149,4 +4128,8 @@ public class Loan extends AbstractPersistable { } return cumulativeOurstanding; } + + public List fetchRepaymentScheduleInstallments() { + return this.repaymentScheduleInstallments; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 72db6bfe9..326aaa1b9 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -67,6 +67,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final LoanScheduleGeneratorFactory loanScheduleFactory; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; private final CalendarInstanceRepository calendarInstanceRepository; + private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository; @Autowired public LoanAccountDomainServiceJpa(final LoanAssembler loanAccountAssembler, final LoanRepository loanRepository, @@ -77,7 +78,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final JournalEntryWritePlatformService journalEntryWritePlatformService, final AccountTransferRepository accountTransferRepository, final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, - final CalendarInstanceRepository calendarInstanceRepository) { + final CalendarInstanceRepository calendarInstanceRepository, + final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository) { this.loanAccountAssembler = loanAccountAssembler; this.loanRepository = loanRepository; this.loanTransactionRepository = loanTransactionRepository; @@ -91,6 +93,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { this.loanScheduleFactory = loanScheduleFactory; this.applicationCurrencyRepository = applicationCurrencyRepository; this.calendarInstanceRepository = calendarInstanceRepository; + this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository; } @Transactional @@ -156,19 +159,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { holidays, workingDays, allowTransactionsOnNonWorkingDay, isHolidayEnabled, isRecoveryRepayment, this.loanScheduleFactory, applicationCurrency, calculatedRepaymentsStartingFromDate, restCalendarInstance); - try { - this.loanTransactionRepository.save(newRepaymentTransaction); - } catch (DataIntegrityViolationException e) { - final Throwable realCause = e.getCause(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); - if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").value(newRepaymentTransaction.getExternalId()) - .failWithCode("value.must.be.unique"); - } - if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", - "Validation errors exist.", dataValidationErrors); } - } + saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction); /*** * TODO Vishwas Batch save is giving me a @@ -178,11 +169,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { * recorded against the loan) ***/ - this.loanRepository.saveAndFlush(loan); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { for (Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - this.loanTransactionRepository.save(mapEntry.getValue()); + saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); // update loan with references to the newly created transactions loan.getLoanTransactions().add(mapEntry.getValue()); updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); @@ -204,6 +195,43 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { return newRepaymentTransaction; } + private void saveLoanTransactionWithDataIntegrityViolationChecks(LoanTransaction newRepaymentTransaction) { + try { + this.loanTransactionRepository.save(newRepaymentTransaction); + } catch (DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { + baseDataValidator.reset().parameter("externalId").value(newRepaymentTransaction.getExternalId()) + .failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } + } + + private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { + try { + List installments = loan.fetchRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.getId() == null) { + this.repaymentScheduleInstallmentRepository.save(installment); + } + } + this.loanRepository.saveAndFlush(loan); + } catch (final DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { + baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } + } + @Override @Transactional public LoanTransaction makeChargePayment(final Loan loan, final Long chargeId, final LocalDate transactionDate, @@ -233,8 +261,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay, newPaymentTransaction, installmentNumber); } - this.loanTransactionRepository.save(newPaymentTransaction); - this.loanRepository.saveAndFlush(loan); + saveLoanTransactionWithDataIntegrityViolationChecks(newPaymentTransaction); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanTransactionNote(loan, newPaymentTransaction, noteText); @@ -295,7 +323,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { loan.makeRefund(newRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); - this.loanTransactionRepository.save(newRefundTransaction); + saveLoanTransactionWithDataIntegrityViolationChecks(newRefundTransaction); this.loanRepository.save(loan); if (StringUtils.isNotBlank(noteText)) { @@ -327,20 +355,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { txnExternalId); disbursementTransaction.updateLoan(loan); loan.getLoanTransactions().add(disbursementTransaction); - try { - this.loanTransactionRepository.save(disbursementTransaction); - } catch (DataIntegrityViolationException e) { - final Throwable realCause = e.getCause(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); - if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").value(disbursementTransaction.getExternalId()) - .failWithCode("value.must.be.unique"); - } - if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", - "Validation errors exist.", dataValidationErrors); } - } - this.loanRepository.saveAndFlush(loan); + saveLoanTransactionWithDataIntegrityViolationChecks(disbursementTransaction); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanTransactionNote(loan, disbursementTransaction, noteText); @@ -355,7 +371,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Override public void reverseTransfer(final LoanTransaction loanTransaction) { loanTransaction.reverse(); - this.loanTransactionRepository.save(loanTransaction); + saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction); } @Override diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java index 34ee233d7..01a80d5fe 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java @@ -280,17 +280,7 @@ public class LoanCharge extends AbstractPersistable { } this.amountOrPercentage = chargeAmount; if (this.loan != null && isInstalmentFee()) { - final Set chargePerInstallments = this.loan.generateInstallmentLoanCharges(this); - if (this.loanInstallmentCharge.isEmpty()) { - this.loanInstallmentCharge.addAll(chargePerInstallments); - } else { - int index = 0; - final LoanInstallmentCharge[] loanChargePerInstallments = new LoanInstallmentCharge[chargePerInstallments.size()]; - final LoanInstallmentCharge[] loanChargePerInstallmentArray = chargePerInstallments.toArray(loanChargePerInstallments); - for (final LoanInstallmentCharge chargePerInstallment : this.loanInstallmentCharge) { - chargePerInstallment.copyFrom(loanChargePerInstallmentArray[index++]); - } - } + updateInstallmentCharges(); } } @@ -386,17 +376,7 @@ public class LoanCharge extends AbstractPersistable { this.amountOrPercentage = amount; this.amountOutstanding = calculateOutstanding(); if (this.loan != null && isInstalmentFee()) { - final Set chargePerInstallments = this.loan.generateInstallmentLoanCharges(this); - if (this.loanInstallmentCharge.isEmpty()) { - this.loanInstallmentCharge.addAll(chargePerInstallments); - } else { - int index = 0; - final LoanInstallmentCharge[] loanChargePerInstallments = new LoanInstallmentCharge[chargePerInstallments.size()]; - final LoanInstallmentCharge[] loanChargePerInstallmentArray = chargePerInstallments.toArray(loanChargePerInstallments); - for (final LoanInstallmentCharge chargePerInstallment : this.loanInstallmentCharge) { - chargePerInstallment.copyFrom(loanChargePerInstallmentArray[index++]); - } - } + updateInstallmentCharges(); } } } @@ -474,22 +454,36 @@ public class LoanCharge extends AbstractPersistable { } this.amountOrPercentage = newValue; if (isInstalmentFee()) { - final Set chargePerInstallments = this.loan.generateInstallmentLoanCharges(this); - if (this.loanInstallmentCharge.isEmpty()) { - this.loanInstallmentCharge.addAll(chargePerInstallments); - } else { - int index = 0; - final LoanInstallmentCharge[] loanChargePerInstallments = new LoanInstallmentCharge[chargePerInstallments.size()]; - final LoanInstallmentCharge[] loanChargePerInstallmentArray = chargePerInstallments.toArray(loanChargePerInstallments); - for (final LoanInstallmentCharge chargePerInstallment : this.loanInstallmentCharge) { - chargePerInstallment.copyFrom(loanChargePerInstallmentArray[index++]); - } - } + updateInstallmentCharges(); } } return actualChanges; } + private void updateInstallmentCharges() { + final Collection remove = new HashSet<>(); + final Set chargePerInstallments = this.loan.generateInstallmentLoanCharges(this); + if (this.loanInstallmentCharge.isEmpty()) { + this.loanInstallmentCharge.addAll(chargePerInstallments); + } else { + int index = 0; + final LoanInstallmentCharge[] loanChargePerInstallments = new LoanInstallmentCharge[chargePerInstallments.size()]; + final LoanInstallmentCharge[] loanChargePerInstallmentArray = chargePerInstallments.toArray(loanChargePerInstallments); + for (final LoanInstallmentCharge chargePerInstallment : this.loanInstallmentCharge) { + if (index == loanChargePerInstallmentArray.length) { + remove.add(chargePerInstallment); + chargePerInstallment.updateInstallment(null); + } else { + chargePerInstallment.copyFrom(loanChargePerInstallmentArray[index++]); + } + } + this.loanInstallmentCharge.removeAll(remove); + /*while (index < loanChargePerInstallmentArray.length - 1) { + chargePerInstallments.add(loanChargePerInstallmentArray[index++]); + }*/ + } + } + public boolean isDueAtDisbursement() { return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT); } @@ -811,7 +805,7 @@ public class LoanCharge extends AbstractPersistable { public List fetchRepaymentInstallment(final MonetaryCurrency currency) { List chargePaidDetails = new ArrayList<>(); for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { - if (loanChargePerInstallment.isPending() + if (!loanChargePerInstallment.isChargeAmountpaid(currency) && loanChargePerInstallment.getAmountThroughChargePayment(currency).isGreaterThanZero()) { LoanChargePaidDetail chargePaidDetail = new LoanChargePaidDetail( loanChargePerInstallment.getAmountThroughChargePayment(currency), @@ -857,11 +851,9 @@ public class LoanCharge extends AbstractPersistable { public void updateWaivedAmount(MonetaryCurrency currency) { if (isInstalmentFee()) { + this.amountWaived = BigDecimal.ZERO; for (final LoanInstallmentCharge chargePerInstallment : this.loanInstallmentCharge) { - final Money amountWaived = chargePerInstallment.updateWaivedAmount(currency); - if (this.amountWaived == null) { - this.amountWaived = BigDecimal.ZERO; - } + final Money amountWaived = chargePerInstallment.updateWaivedAndAmountPaidThroughChargePaymentAmount(currency); this.amountWaived = this.amountWaived.add(amountWaived.getAmount()); this.amountOutstanding = this.amountOutstanding.subtract(amountWaived.getAmount()); if (determineIfFullyPaid()) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java index 38c462481..d0240e9c8 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -145,6 +145,12 @@ public class LoanInstallmentCharge extends AbstractPersistable { return !(isPaid() || isWaived()); } + public boolean isChargeAmountpaid(MonetaryCurrency currency) { + Money amounPaidThroughChargePayment = Money.of(currency, this.amountThroughChargePayment); + Money paid = Money.of(currency, this.amountPaid); + return amounPaidThroughChargePayment.isEqualTo(paid); + } + public LoanRepaymentScheduleInstallment getRepaymentInstallment() { return this.installment; } @@ -199,16 +205,35 @@ public class LoanInstallmentCharge extends AbstractPersistable { this.amountPaid = BigDecimal.ZERO; this.amountWaived = BigDecimal.ZERO; this.amountWrittenOff = BigDecimal.ZERO; + this.amountThroughChargePayment = BigDecimal.ZERO; this.amountOutstanding = calculateAmountOutstanding(currency); this.paid = false; this.waived = false; + } public Money getAmountThroughChargePayment(final MonetaryCurrency currency) { return Money.of(currency, this.amountThroughChargePayment); } - public Money updateWaivedAmount(final MonetaryCurrency currency) { + public Money getUnpaidAmountThroughChargePayment(final MonetaryCurrency currency) { + return Money.of(currency, this.amountThroughChargePayment).minus(this.amountPaid); + } + + private void updateAmountThroughChargePayment(final MonetaryCurrency currency) { + Money amountThroughChargePayment = getAmountThroughChargePayment(currency); + if (amountThroughChargePayment.isGreaterThanZero() && amountThroughChargePayment.isGreaterThan(this.getAmount(currency))) { + this.amountThroughChargePayment = this.getAmount(); + } + } + + public Money updateWaivedAndAmountPaidThroughChargePaymentAmount(final MonetaryCurrency currency) { + updateWaivedAmount(currency); + updateAmountThroughChargePayment(currency); + return getAmountWaived(currency); + } + + private void updateWaivedAmount(final MonetaryCurrency currency) { Money waivedAmount = getAmountWaived(currency); if (waivedAmount.isGreaterThanZero()) { if (waivedAmount.isGreaterThan(this.getAmount(currency))) { @@ -221,7 +246,11 @@ public class LoanInstallmentCharge extends AbstractPersistable { this.waived = false; } } - return getAmountWaived(currency); + } + + + public void updateInstallment(LoanRepaymentScheduleInstallment installment) { + this.installment = installment; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallmentRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallmentRepository.java new file mode 100755 index 000000000..d8975cfcd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallmentRepository.java @@ -0,0 +1,9 @@ +package org.mifosplatform.portfolio.loanaccount.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanRepaymentScheduleInstallmentRepository extends JpaRepository, + JpaSpecificationExecutor { + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java index f2b6c0577..49635cf81 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java @@ -263,6 +263,7 @@ public final class LoanTransaction extends AbstractPersistable { this.interestPortion = null; this.feeChargesPortion = null; this.penaltyChargesPortion = null; + this.overPaymentPortion = null; } public void updateLoan(final Loan loan) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index cefc36ead..b4873e74f 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -86,8 +86,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen for (final LoanRepaymentScheduleInstallment installment : installments) { for (final LoanCharge loanCharge : transferCharges) { if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate())) { - LoanChargePaidDetail chargePaidDetail = new LoanChargePaidDetail( - installment.getFeeChargesOutstanding(currency), installment, loanCharge.isFeeCharge()); + Money amountForProcess = loanCharge.getAmount(currency); + if (amountForProcess.isGreaterThan(loanTransaction.getAmount(currency))) { + amountForProcess = loanTransaction.getAmount(currency); + } + LoanChargePaidDetail chargePaidDetail = new LoanChargePaidDetail(amountForProcess, installment, + loanCharge.isFeeCharge()); chargePaidDetails.add(chargePaidDetail); break; } @@ -98,11 +102,13 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen Money unprocessed = loanTransaction.getAmount(currency); for (LoanChargePaidDetail chargePaidDetail : chargePaidDetails) { final List 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); + Money processAmt = chargePaidDetail.getAmount(); + if(processAmt.isGreaterThan(unprocessed)){ + processAmt = unprocessed; + } + unprocessed = handleTransactionAndCharges(loanTransaction, currency, processInstallments, + transferCharges, processAmt, chargePaidDetail.isFeeCharge()); if (!unprocessed.isGreaterThanZero()) { break; } @@ -206,7 +212,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen if (loanTransaction.isNotWaiver()) { Money feeCharges = loanTransaction.getFeeChargesPortion(currency); Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); - if (chargeAmountToProcess != null) { + if (chargeAmountToProcess != null && feeCharges.isGreaterThan(chargeAmountToProcess)) { if (isFeeCharge) { feeCharges = chargeAmountToProcess; } else { @@ -235,23 +241,26 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { + if (transactionAmountUnprocessed.isGreaterThanZero()) { + if (currentInstallment.isNotFullyPaidOff()) { - if (currentInstallment.isNotFullyPaidOff()) { - - // is this transaction early/late/on-time with respect to the - // current installment? - if (isTransactionInAdvanceOfInstallment(installmentIndex, installments, transactionDate, transactionAmountUnprocessed)) { - transactionAmountUnprocessed = handleTransactionThatIsPaymentInAdvanceOfInstallment(currentInstallment, installments, - loanTransaction, transactionDate, transactionAmountUnprocessed); - } else if (isTransactionALateRepaymentOnInstallment(installmentIndex, installments, loanTransaction.getTransactionDate())) { - // does this result in a late payment of existing - // installment? - transactionAmountUnprocessed = handleTransactionThatIsALateRepaymentOfInstallment(currentInstallment, installments, - loanTransaction, transactionAmountUnprocessed); - } else { - // standard transaction - transactionAmountUnprocessed = handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, - transactionAmountUnprocessed); + // is this transaction early/late/on-time with respect to + // the + // current installment? + if (isTransactionInAdvanceOfInstallment(installmentIndex, installments, transactionDate, transactionAmountUnprocessed)) { + transactionAmountUnprocessed = handleTransactionThatIsPaymentInAdvanceOfInstallment(currentInstallment, + installments, loanTransaction, transactionDate, transactionAmountUnprocessed); + } else if (isTransactionALateRepaymentOnInstallment(installmentIndex, installments, + loanTransaction.getTransactionDate())) { + // does this result in a late payment of existing + // installment? + transactionAmountUnprocessed = handleTransactionThatIsALateRepaymentOfInstallment(currentInstallment, installments, + loanTransaction, transactionAmountUnprocessed); + } else { + // standard transaction + transactionAmountUnprocessed = handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, + loanTransaction, transactionAmountUnprocessed); + } } } @@ -292,10 +301,18 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining, installmentNumber, feeAmount); if (!amountPaidTowardsCharge.isZero()) { - if (!loanTransaction.isChargePayment()) { + Set chargesPaidBies = loanTransaction.getLoanChargesPaid(); + if (loanTransaction.isChargePayment()) { + for (final LoanChargePaidBy chargePaidBy : chargesPaidBies) { + LoanCharge loanCharge = chargePaidBy.getLoanCharge(); + if (loanCharge.getId().equals(unpaidCharge.getId())) { + chargePaidBy.setAmount(amountPaidTowardsCharge.getAmount()); + } + } + } else { final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, unpaidCharge, amountPaidTowardsCharge.getAmount()); - loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); + chargesPaidBies.add(loanChargePaidBy); } amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index bbcf5669e..ed4278460 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -231,15 +231,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener principalForThisPeriod = principalForThisPeriod.plus(actualOutstanding); } } - interestReducedDueToEarlyPayment = interestReducedDueToEarlyPayment.plus(loanApplicationTerms.interestRateFor( - this.paymentPeriodsInOneYearCalculator, mc, diffDays, detail.getAmount())); + this.paymentPeriodsInOneYearCalculator, mc, detail.getAmount(), detail.getStartDate(), scheduledDueDate)); } else if (detail.isLatePayment() && detail.isOverlapping(periodStartDate, scheduledDueDate) && periodStartDate.isBefore(LocalDate.now())) { LocalDate fromDate = periodStartDate; LocalDate toDate = scheduledDueDate; - if (!detail.getStartDate().isAfter(periodStartDate)) { + if (!detail.getStartDate().isBefore(periodStartDate)) { fromDate = detail.getStartDate(); } if (!detail.getToDate().isAfter(scheduledDueDate)) { @@ -248,9 +247,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener 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())); + this.paymentPeriodsInOneYearCalculator, mc, detail.getAmount(), fromDate, toDate)); } } @@ -333,18 +331,19 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener 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()); + if (!toDate.isAfter(LocalDate.now())) { + Money interestDueToLatePayment = loanApplicationTerms.interestRateFor(this.paymentPeriodsInOneYearCalculator, mc, + detail.getAmount(), fromDate, toDate); - totalInterestCharged = totalInterestCharged.add(interestDueToLatePayment.getAmount()); - totalRepaymentExpected = totalRepaymentExpected.add(interestDueToLatePayment.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++; + final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, toDate, + interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment, + interestDueToLatePayment.zero(), interestDueToLatePayment.zero(), interestDueToLatePayment); + periods.add(installment); + periodNumber++; + } } } @@ -711,8 +710,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } } - int days = Days.daysBetween(calculateInterestFrom, LocalDate.now()).getDays(); - Money interest = applicationTerms.interestRateFor(this.paymentPeriodsInOneYearCalculator, mc, days, prepaymentAmount); + Money interest = applicationTerms.interestRateFor(this.paymentPeriodsInOneYearCalculator, mc, prepaymentAmount, + calculateInterestFrom, LocalDate.now()); prepaymentAmount = prepaymentAmount.plus(interest).plus(amount); return prepaymentAmount; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 001e345fa..0844caa1f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -13,6 +13,8 @@ import java.util.List; import org.joda.time.Days; import org.joda.time.LocalDate; +import org.joda.time.Period; +import org.joda.time.PeriodType; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; @@ -661,15 +663,21 @@ 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); + public Money interestRateFor(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc, final Money outstandingBalance, + final LocalDate fromDate, final LocalDate toDate) { + + long loanTermPeriodsInOneYear = calculator.calculate(PeriodFrequencyType.DAYS).longValue(); + int repaymentEvery = Days.daysBetween(fromDate, toDate).getDays(); + if (isFallingInRepaymentPeriod(fromDate, toDate)) { + loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator); + repaymentEvery = getPeriodsBetween(fromDate, toDate); + } 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); + BigDecimal interestRate = oneDayOfYearInterestRate.multiply(BigDecimal.valueOf(repaymentEvery), mc); return outstandingBalance.multiplyRetainScale(interestRate, mc.getRoundingMode()); } @@ -967,4 +975,59 @@ public final class LoanApplicationTerms { public CalendarInstance getRestCalendarInstance() { return this.restCalendarInstance; } + + private boolean isFallingInRepaymentPeriod(LocalDate fromDate, LocalDate toDate) { + boolean isSameAsRepaymentPeriod = false; + if (this.interestCalculationPeriodMethod.getValue().equals(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD.getValue())) { + switch (this.repaymentPeriodFrequencyType) { + case WEEKS: + int days = getPeriodsBetween(fromDate, toDate); + isSameAsRepaymentPeriod = (days % 7) == 0; + break; + case MONTHS: + boolean isFromDateOnEndDate = false; + if (fromDate.getDayOfMonth() > fromDate.plusDays(1).getDayOfMonth()) { + isFromDateOnEndDate = true; + } + boolean isToDateOnEndDate = false; + if (toDate.getDayOfMonth() > toDate.plusDays(1).getDayOfMonth()) { + isToDateOnEndDate = true; + } + + if (isFromDateOnEndDate && isToDateOnEndDate) { + isSameAsRepaymentPeriod = true; + } else { + + int months = getPeriodsBetween(fromDate, toDate); + fromDate = fromDate.plusMonths(months); + isSameAsRepaymentPeriod = fromDate.isEqual(toDate); + } + + break; + default: + break; + } + } + return isSameAsRepaymentPeriod; + } + + private Integer getPeriodsBetween(LocalDate fromDate, LocalDate toDate) { + Integer numberOfPeriods = 0; + PeriodType periodType = PeriodType.yearMonthDay(); + Period difference = new Period(fromDate, toDate, periodType); + switch (this.repaymentPeriodFrequencyType) { + case WEEKS: + numberOfPeriods = difference.getDays(); + break; + case MONTHS: + numberOfPeriods = difference.getMonths(); + break; + case YEARS: + numberOfPeriods = difference.getYears(); + break; + default: + break; + } + return numberOfPeriods; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 4affeedcf..d8154291d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -54,9 +54,9 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData; import org.mifosplatform.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.domain.LoanChargeRepository; -import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; @@ -119,8 +119,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final CalendarInstanceRepository calendarInstanceRepository; private final SavingsAccountAssembler savingsAccountAssembler; private final AccountAssociationsRepository accountAssociationsRepository; - private final LoanChargeRepository loanChargeRepository; private final LoanReadPlatformService loanReadPlatformService; + private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository; @Autowired public LoanApplicationWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final FromJsonHelper fromJsonHelper, @@ -135,7 +135,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final CalendarRepository calendarRepository, final CalendarInstanceRepository calendarInstanceRepository, final SavingsAccountAssembler savingsAccountAssembler, final AccountAssociationsRepository accountAssociationsRepository, - final LoanChargeRepository loanChargeRepository, final LoanReadPlatformService loanReadPlatformService) { + final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository, + final LoanReadPlatformService loanReadPlatformService) { this.context = context; this.fromJsonHelper = fromJsonHelper; this.loanApplicationTransitionApiJsonValidator = loanApplicationTransitionApiJsonValidator; @@ -158,7 +159,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.calendarInstanceRepository = calendarInstanceRepository; this.savingsAccountAssembler = savingsAccountAssembler; this.accountAssociationsRepository = accountAssociationsRepository; - this.loanChargeRepository = loanChargeRepository; + this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository; this.loanReadPlatformService = loanReadPlatformService; } @@ -517,36 +518,22 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false); existingLoanApplication.updateLoanSchedule(loanSchedule); + existingLoanApplication.recalculateAllCharges(); } - this.loanRepository.saveAndFlush(existingLoanApplication); - - // FIXME: adding loan installment charges along with the other - // charges, - // is trying to add extra entries with null values. Because of that - // persisting charges separately final String chargesParamName = "charges"; if (changes.containsKey(chargesParamName)) { - final Set loanCharges = this.loanChargeAssembler.fromParsedJson(command.parsedJson()); - existingLoanApplication.updateLoanCharges(loanCharges); - this.loanRepository.saveAndFlush(existingLoanApplication); + existingLoanApplication.updateLoanCharges(possiblyModifedLoanCharges); } + saveAndFlushLoanWithDataIntegrityViolationChecks(existingLoanApplication); + final String submittedOnNote = command.stringValueOfParameterNamed("submittedOnNote"); if (StringUtils.isNotBlank(submittedOnNote)) { final Note note = Note.loanNote(existingLoanApplication, submittedOnNote); this.noteRepository.save(note); } - for (final LoanCharge loanCharge : existingLoanApplication.charges()) { - if (loanCharge.isInstalmentFee() && loanCharge.hasNoLoanInstallmentCharges()) { - final Set chargePerInstallments = existingLoanApplication - .generateInstallmentLoanCharges(loanCharge); - loanCharge.addLoanInstallmentCharges(chargePerInstallments); - this.loanChargeRepository.save(loanCharge); - } - } - final Long calendarId = command.longValueOfParameterNamed("calendarId"); Calendar calendar = null; if (calendarId != null && calendarId != 0) { @@ -896,4 +883,27 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } } + private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { + try { + List installments = loan.fetchRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.getId() == null) { + this.repaymentScheduleInstallmentRepository.save(installment); + } else { + break; + } + } + this.loanRepository.saveAndFlush(loan); + } catch (final DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.application"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { + baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 77cef2398..24ad4deb5 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -98,6 +98,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; @@ -166,6 +167,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final FromJsonHelper fromApiJsonHelper; private final AccountTransferRepository accountTransferRepository; private final CalendarRepository calendarRepository; + private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository; @Autowired public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, @@ -185,7 +187,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, final LoanChargeReadPlatformService loanChargeReadPlatformService, final LoanReadPlatformService loanReadPlatformService, final FromJsonHelper fromApiJsonHelper, final AccountTransferRepository accountTransferRepository, - final CalendarRepository calendarRepository) { + final CalendarRepository calendarRepository, + final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository) { this.context = context; this.loanEventApiJsonValidator = loanEventApiJsonValidator; this.loanAssembler = loanAssembler; @@ -213,6 +216,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.fromApiJsonHelper = fromApiJsonHelper; this.accountTransferRepository = accountTransferRepository; this.calendarRepository = calendarRepository; + this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository; } private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { @@ -267,6 +271,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf Money amountBeforeAdjust = loan.getPrincpal(); loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED); boolean canDisburse = loan.canDisburse(actualDisbursementDate); + ChangedTransactionDetail changedTransactionDetail = null; if (canDisburse) { Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); boolean recalcualteSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincpal()); @@ -286,9 +291,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue()); } - loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, - allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule, restCalendarInstance); + changedTransactionDetail = loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, + existingTransactionIds, existingReversedTransactionIds, changes, calculatedRepaymentsStartingFromDate, + isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, + recalcualteSchedule, restCalendarInstance); } if (!changes.isEmpty()) { saveAndFlushLoanWithDataIntegrityViolationChecks(loan); @@ -299,11 +305,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.noteRepository.save(note); } - // fix me - // adding loan installment charges along with the actual - // disbursement, is trying to add 2 extra entries with null values. - // which is causing issues - ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactionForDisbursement(); saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { @@ -367,6 +368,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { try { + List installments = loan.fetchRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.getId() == null) { + this.repaymentScheduleInstallmentRepository.save(installment); + } else { + break; + } + } this.loanRepository.saveAndFlush(loan); } catch (final DataIntegrityViolationException e) { final Throwable realCause = e.getCause(); @@ -380,6 +389,29 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } + private void saveLoanWithDataIntegrityViolationChecks(final Loan loan) { + try { + List installments = loan.fetchRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.getId() == null) { + this.repaymentScheduleInstallmentRepository.save(installment); + } else { + break; + } + } + this.loanRepository.save(loan); + } catch (final DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { + baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } + } + /**** * TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base * @@ -426,6 +458,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED); updateLoanCounters(loan, actualDisbursementDate); boolean canDisburse = loan.canDisburse(actualDisbursementDate); + ChangedTransactionDetail changedTransactionDetail = null; if (canDisburse) { Money amountBeforeAdjust = loan.getPrincpal(); Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); @@ -445,24 +478,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_DETAIL.getValue()); } - loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, firstRepaymentOnDate, isHolidayEnabled, holidays, workingDays, - allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule, restCalendarInstance); + changedTransactionDetail = loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, + existingTransactionIds, existingReversedTransactionIds, changes, firstRepaymentOnDate, isHolidayEnabled, holidays, + workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay, recalcualteSchedule, + restCalendarInstance); } if (!changes.isEmpty()) { - try { - this.loanRepository.save(loan); - } catch (final DataIntegrityViolationException e) { - final Throwable realCause = e.getCause(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource("loan.transaction"); - if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); - } - if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException( - "validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } - } + + saveLoanWithDataIntegrityViolationChecks(loan); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { @@ -470,11 +493,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.noteRepository.save(note); } - // fix me - // adding loan installment charges along with the actual - // disbursement, is trying to add 2 extra entries with null - // values. which is causing issues - ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactionForDisbursement(); saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { @@ -542,7 +560,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf existingReversedTransactionIds, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays); if (!changes.isEmpty()) { - this.loanRepository.saveAndFlush(loan); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); this.accountTransfersWritePlatformService.reverseAllTransactions(loanId, PortfolioAccountType.LOAN); String noteText = null; if (command.hasParameter("note")) { @@ -719,7 +737,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf * only in edge cases (when a adjustment is made before the latest * payment recorded against the loan) ***/ - this.loanRepository.saveAndFlush(loan); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { this.loanTransactionRepository.save(mapEntry.getValue()); @@ -821,7 +839,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf * only in edge cases (when a waiver is made before the latest payment * recorded against the loan) ***/ - this.loanRepository.saveAndFlush(loan); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { this.loanTransactionRepository.save(mapEntry.getValue()); @@ -898,7 +916,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanTransactionRepository.save(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { changes.put("note", noteText); @@ -968,7 +986,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanTransactionRepository.save(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { @@ -1026,7 +1044,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.closeAsMarkedForReschedule(command, defaultLoanLifecycleStateMachine(), changes); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { @@ -1120,7 +1138,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf * entered only in edge cases (when a payment is made before the * latest payment recorded against the loan) ***/ - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { this.loanTransactionRepository.save(mapEntry.getValue()); @@ -1155,7 +1173,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Map changes = loan.updateLoanCharge(loanCharge, command); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -1220,7 +1238,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber); this.loanTransactionRepository.save(waiveTransaction); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); @@ -1251,7 +1269,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf LOAN_CHARGE_CANNOT_BE_DELETED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); } loan.removeLoanCharge(loanCharge); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -1448,7 +1466,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.setLoanStatus(LoanStatus.TRANSFER_IN_PROGRESS.getValue()); this.loanTransactionRepository.save(newTransferTransaction); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); @@ -1477,7 +1495,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } this.loanTransactionRepository.save(newTransferAcceptanceTransaction); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); @@ -1497,7 +1515,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.setLoanStatus(LoanStatus.ACTIVE.getValue()); this.loanTransactionRepository.save(newTransferAcceptanceTransaction); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); @@ -1509,7 +1527,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf public void rejectLoanTransfer(final Long accountId) { final Loan loan = this.loanAssembler.assembleFrom(accountId); loan.setLoanStatus(LoanStatus.TRANSFER_ON_HOLD.getValue()); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); } @Transactional @@ -1534,7 +1552,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -1570,7 +1588,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (!loan.hasLoanOfficer(fromLoanOfficer)) { throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); } loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); } this.loanRepository.flush(); @@ -1598,7 +1616,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.removeLoanOfficer(dateOfLoanOfficerunAssigned); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -1644,7 +1662,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate()); loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, holidays, workingDays); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); } } } @@ -2022,7 +2040,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } } - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); return new CommandProcessingResultBuilder() // @@ -2064,7 +2082,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf existingTransactionIds, existingReversedTransactionIds, changes, this.loanScheduleFactory, applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, restCalendarInstance); - this.loanRepository.saveAndFlush(loan); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { for (Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { @@ -2191,7 +2209,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, restCalendarInstance, existingTransactionIds, existingReversedTransactionIds); - this.loanRepository.save(loan); + saveLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {