From 4a2a85d20e0540e9797d6fa77ac54a263a5ba328 Mon Sep 17 00:00:00 2001 From: Pramod Nuthakki Date: Tue, 24 Feb 2015 10:45:10 +0530 Subject: [PATCH] MIFOSX-1850 : Recurring deposits without maturity date --- .../infrastructure/jobs/service/JobName.java | 3 +- .../savings/DepositAccountUtils.java | 47 ++++++ .../data/DepositAccountDataValidator.java | 19 ++- .../DepositAccountDomainServiceJpa.java | 2 +- .../DepositAccountTermAndPreClosure.java | 5 + .../domain/RecurringDepositAccount.java | 156 ++++++++++-------- .../DepositAccountReadPlatformService.java | 3 + ...DepositAccountReadPlatformServiceImpl.java | 39 ++++- ...WritePlatformServiceJpaRepositoryImpl.java | 17 +- .../service/ScheduledJobRunnerService.java | 2 + .../ScheduledJobRunnerServiceImpl.java | 66 ++++++++ .../core_db/V242__open_rd_changes.sql | 2 + 12 files changed, 274 insertions(+), 87 deletions(-) create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/DepositAccountUtils.java create mode 100644 mifosng-provider/src/main/resources/sql/migrations/core_db/V242__open_rd_changes.sql diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/jobs/service/JobName.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/jobs/service/JobName.java index 9a67d2e4b..e07bb85ee 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/jobs/service/JobName.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/jobs/service/JobName.java @@ -23,7 +23,8 @@ public enum JobName { 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"), - RECALCULATE_INTEREST_FOR_LOAN("Recalculate Interest For Loans"); + RECALCULATE_INTEREST_FOR_LOAN("Recalculate Interest For Loans"), + GENERATE_RD_SCEHDULE("Generate Mandatory Savings Schedule"); private final String name; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/DepositAccountUtils.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/DepositAccountUtils.java new file mode 100644 index 000000000..058dc72ea --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/DepositAccountUtils.java @@ -0,0 +1,47 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.savings; + +import org.joda.time.LocalDate; +import org.mifosplatform.portfolio.calendar.domain.CalendarFrequencyType; +import org.mifosplatform.portfolio.calendar.service.CalendarUtils; +import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType; + +public class DepositAccountUtils { + + public static final int GENERATE_MINIMUM_NUMBER_OF_FUTURE_INSTALMENTS = 5; + + public static LocalDate calculateNextDepositDate(final LocalDate lastDepositDate, final PeriodFrequencyType frequency, + final int recurringEvery) { + LocalDate nextDepositDate = lastDepositDate; + + switch (frequency) { + case DAYS: + nextDepositDate = lastDepositDate.plusDays(recurringEvery); + break; + case WEEKS: + nextDepositDate = lastDepositDate.plusWeeks(recurringEvery); + break; + case MONTHS: + nextDepositDate = lastDepositDate.plusMonths(recurringEvery); + break; + case YEARS: + nextDepositDate = lastDepositDate.plusYears(recurringEvery); + break; + case INVALID: + break; + } + return nextDepositDate; + } + + public static LocalDate calculateNextDepositDate(final LocalDate lastDepositDate, final String recurrence) { + final PeriodFrequencyType frequencyType = CalendarFrequencyType.from(CalendarUtils.getFrequency(recurrence)); + Integer frequency = CalendarUtils.getInterval(recurrence); + frequency = frequency == -1 ? 1 : frequency; + return calculateNextDepositDate(lastDepositDate, frequencyType, frequency); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/DepositAccountDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/DepositAccountDataValidator.java index 1264eb9af..ace5064ec 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/DepositAccountDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/DepositAccountDataValidator.java @@ -125,7 +125,7 @@ public class DepositAccountDataValidator { validateDepositDetailsForUpdate(element, baseDataValidator); validatePreClosureDetailForUpdate(element, baseDataValidator); - validateDepositTermDeatilForUpdate(element, baseDataValidator); + validateDepositTermDeatilForUpdate(element, baseDataValidator, DepositAccountType.FIXED_DEPOSIT); // validateSavingsCharges(element, baseDataValidator); throwExceptionIfValidationWarningsExist(dataValidationErrors); @@ -166,7 +166,7 @@ public class DepositAccountDataValidator { validateDepositDetailsForUpdate(element, baseDataValidator); validatePreClosureDetailForUpdate(element, baseDataValidator); - validateDepositTermDeatilForUpdate(element, baseDataValidator); + validateDepositTermDeatilForUpdate(element, baseDataValidator, DepositAccountType.RECURRING_DEPOSIT); validateRecurringDetailForUpdate(element, baseDataValidator); // validateSavingsCharges(element, baseDataValidator); @@ -494,8 +494,10 @@ public class DepositAccountDataValidator { baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).notNull().positiveAmount(); } - final Integer depositPeriod = fromApiJsonHelper.extractIntegerSansLocaleNamed(depositPeriodParamName, element); - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull().integerGreaterThanZero(); + if (depositType.isFixedDeposit() || fromApiJsonHelper.parameterExists(depositPeriodParamName, element)) { + final Integer depositPeriod = fromApiJsonHelper.extractIntegerSansLocaleNamed(depositPeriodParamName, element); + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull().integerGreaterThanZero(); + } final Integer depositPeriodFrequencyId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(depositPeriodFrequencyIdParamName, element); @@ -503,7 +505,8 @@ public class DepositAccountDataValidator { .isOneOfTheseValues(SavingsPeriodFrequencyType.integerValues()); } - private void validateDepositTermDeatilForUpdate(final JsonElement element, final DataValidatorBuilder baseDataValidator) { + private void validateDepositTermDeatilForUpdate(final JsonElement element, final DataValidatorBuilder baseDataValidator, + final DepositAccountType depositAccountType) { this.productDataValidator.validateDepositTermDetailForUpdate(element, baseDataValidator); if (fromApiJsonHelper.parameterExists(depositAmountParamName, element)) { @@ -513,7 +516,11 @@ public class DepositAccountDataValidator { if (fromApiJsonHelper.parameterExists(depositPeriodParamName, element)) { final Integer depositPeriod = fromApiJsonHelper.extractIntegerSansLocaleNamed(depositPeriodParamName, element); - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull().integerGreaterThanZero(); + if (depositAccountType.isFixedDeposit()) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull().integerGreaterThanZero(); + } else { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull().integerGreaterThanZero(); + } } if (fromApiJsonHelper.parameterExists(depositPeriodFrequencyIdParamName, element)) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/DepositAccountDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/DepositAccountDomainServiceJpa.java index d796e3cfc..da9fb1ed9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/DepositAccountDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/DepositAccountDomainServiceJpa.java @@ -221,7 +221,7 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); Long savingsTransactionId = null; - account.postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + account.postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, closedDate); final BigDecimal transactionAmount = account.getAccountBalance(); final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/DepositAccountTermAndPreClosure.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/DepositAccountTermAndPreClosure.java index 9e28b9401..8ac7f03fd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/DepositAccountTermAndPreClosure.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/DepositAccountTermAndPreClosure.java @@ -195,6 +195,11 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistable { this.maturityAmount = this.depositAmount.add(interestPayable); this.maturityDate = maturityDate.toDate(); } + + public void updateDepositAmount(final BigDecimal depositAmount) { + this.depositAmount = depositAmount; + } + public LocalDate getMaturityLocalDate() { LocalDate maturityLocalDate = null; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/RecurringDepositAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/RecurringDepositAccount.java index 6ac60fdfe..61c6e5a15 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/RecurringDepositAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/RecurringDepositAccount.java @@ -49,6 +49,7 @@ import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.group.domain.Group; import org.mifosplatform.portfolio.interestratechart.domain.InterestRateChart; import org.mifosplatform.portfolio.savings.DepositAccountOnClosureType; +import org.mifosplatform.portfolio.savings.DepositAccountUtils; import org.mifosplatform.portfolio.savings.DepositsApiConstants; import org.mifosplatform.portfolio.savings.PreClosurePenalInterestOnType; import org.mifosplatform.portfolio.savings.SavingsApiConstants; @@ -192,6 +193,10 @@ public class RecurringDepositAccount extends SavingsAccount { } } + if (depositCloseDate == null) { + depositCloseDate = LocalDate.now(); + } + final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount(); BigDecimal applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client); @@ -209,8 +214,16 @@ public class RecurringDepositAccount extends SavingsAccount { public void updateMaturityDateAndAmount(final MathContext mc, final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { final LocalDate maturityDate = calculateMaturityDate(); - final LocalDate interestCalculationUpto = maturityDate.minusDays(1); - final List allTransactions = generateIncludingFutureTransactions(interestCalculationUpto); + LocalDate interestCalculationUpto = null; + List allTransactions = null; + if (maturityDate == null) { + interestCalculationUpto = DateUtils.getLocalDateOfTenant(); + allTransactions = getTransactions(interestCalculationUpto, false); + } else { + interestCalculationUpto = maturityDate.minusDays(1); + allTransactions = getTransactions(interestCalculationUpto, true); + + } final List postingPeriods = calculateInterestPayable(mc, interestCalculationUpto, allTransactions, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); @@ -220,8 +233,12 @@ public class RecurringDepositAccount extends SavingsAccount { totalInterestPayable = totalInterestPayable.plus(postingPeriod.getInterestEarned()); totalDepositAmount = totalDepositAmount.plus(postingPeriod.closingBalance()).minus(postingPeriod.openingBalance()); } - - this.accountTermAndPreClosure.updateMaturityDetails(totalDepositAmount.getAmount(), totalInterestPayable.getAmount(), maturityDate); + if (maturityDate == null) { + this.accountTermAndPreClosure.updateDepositAmount(totalDepositAmount.getAmount()); + } else { + this.accountTermAndPreClosure.updateMaturityDetails(totalDepositAmount.getAmount(), totalInterestPayable.getAmount(), + maturityDate); + } } public void updateMaturityStatus(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { @@ -239,7 +256,7 @@ public class RecurringDepositAccount extends SavingsAccount { if (!this.maturityDate().isAfter(todayDate)) { // update account status this.status = SavingsAccountStatusType.MATURED.getValue(); - postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, todayDate); } } @@ -248,6 +265,7 @@ public class RecurringDepositAccount extends SavingsAccount { final LocalDate startDate = depositStartDate(); LocalDate maturityDate = null; final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); + if (depositPeriod == null) { return maturityDate; } switch (this.accountTermAndPreClosure.depositPeriodFrequencyType()) { case DAYS: maturityDate = startDate.plusDays(depositPeriod); @@ -315,16 +333,18 @@ public class RecurringDepositAccount extends SavingsAccount { return allPostingPeriods; } - private List generateIncludingFutureTransactions(final LocalDate depositEndDate) { + private List getTransactions(final LocalDate depositEndDate, final boolean generateFutureTransactions) { List allTransactions = new ArrayList<>(); // add existing transactions allTransactions.addAll(retreiveOrderedNonInterestPostingTransactions()); - for (RecurringDepositScheduleInstallment installment : depositScheduleInstallments()) { - if (installment.isPrincipalNotCompleted(getCurrency())) { - final SavingsAccountTransaction transaction = SavingsAccountTransaction - .deposit(null, office(), null, installment.dueDate(), installment.getDepositAmountOutstanding(getCurrency()), - installment.dueDate().toDate(), null); - allTransactions.add(transaction); + if (generateFutureTransactions) { + for (RecurringDepositScheduleInstallment installment : depositScheduleInstallments()) { + if (installment.isPrincipalNotCompleted(getCurrency())) { + final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(null, office(), null, + installment.dueDate(), installment.getDepositAmountOutstanding(getCurrency()), installment.dueDate().toDate(), + null); + allTransactions.add(transaction); + } } } @@ -358,29 +378,6 @@ public class RecurringDepositAccount extends SavingsAccount { return allTransactions; } - private LocalDate calculateNextDepositDate(final LocalDate lastDepositDate, final PeriodFrequencyType frequency, - final int recurringEvery) { - LocalDate nextDepositDate = lastDepositDate; - - switch (frequency) { - case DAYS: - nextDepositDate = lastDepositDate.plusDays(recurringEvery); - break; - case WEEKS: - nextDepositDate = lastDepositDate.plusWeeks(recurringEvery); - break; - case MONTHS: - nextDepositDate = lastDepositDate.plusMonths(recurringEvery); - break; - case YEARS: - nextDepositDate = lastDepositDate.plusYears(recurringEvery); - break; - case INVALID: - break; - } - return nextDepositDate; - } - public LocalDate depositStartDate() { final LocalDate depositStartDate = accountTermAndPreClosure.getExpectedFirstDepositOnDate(); if (depositStartDate == null) return accountSubmittedOrActivationDate(); @@ -500,7 +497,7 @@ public class RecurringDepositAccount extends SavingsAccount { .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.closeAction); final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); - if (!SavingsAccountStatusType.MATURED.hasStateOf(currentStatus)) { + if (!SavingsAccountStatusType.MATURED.hasStateOf(currentStatus) && this.maturityDate() != null) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.in.matured.state"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } @@ -514,7 +511,7 @@ public class RecurringDepositAccount extends SavingsAccount { .failWithCode("must.be.after.activation.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } - if (closedDate.isBefore(maturityDate())) { + if (maturityDate() != null && closedDate.isBefore(maturityDate())) { baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) .failWithCode("must.be.after.account.maturity.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } @@ -564,11 +561,15 @@ public class RecurringDepositAccount extends SavingsAccount { this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); } - public void postMaturityInterest(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - final LocalDate interestPostingUpToDate = maturityDate(); + public void postMaturityInterest(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate closeDate) { + LocalDate interestPostingUpToDate = maturityDate(); + if (interestPostingUpToDate == null) { + interestPostingUpToDate = closeDate; + } final MathContext mc = MathContext.DECIMAL64; boolean isInterestTransfer = false; - final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, + final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate.minusDays(1), isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); Money interestPostedToDate = Money.zero(this.currency); @@ -895,7 +896,7 @@ public class RecurringDepositAccount extends SavingsAccount { private boolean isBeforeMaturityDate(final LocalDate compareDate) { final LocalDate maturityDate = this.maturityDate(); - return compareDate.isBefore(maturityDate); + return maturityDate == null ? true : compareDate.isBefore(maturityDate); } private boolean isBeforeDepositStartDate(LocalDate compareDate) { @@ -923,30 +924,35 @@ public class RecurringDepositAccount extends SavingsAccount { } final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); - final SavingsPeriodFrequencyType depositPeriodFrequencyType = this.accountTermAndPreClosure.depositPeriodFrequencyType(); - final boolean isValidDepositPeriod = this.accountTermAndPreClosure.depositTermDetail().isDepositBetweenMinAndMax( - depositStartDate(), calculateMaturityDate()); - if (!isValidDepositPeriod) { - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) - .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.between.min.and.max.deposit.term"); - } else { - final Integer inMultiplesOf = this.accountTermAndPreClosure.depositTermDetail().inMultiplesOfDepositTerm(); - if (inMultiplesOf != null) { - final boolean isValid = this.accountTermAndPreClosure.depositTermDetail().isValidInMultiplesOfPeriod(depositPeriod, - depositPeriodFrequencyType); - if (!isValid) { - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) - .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.multiple.of.term"); + if (this.accountTermAndPreClosure.depositTermDetail().maxDepositTerm() != null) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull(); + } + if (depositPeriod != null) { + final SavingsPeriodFrequencyType depositPeriodFrequencyType = this.accountTermAndPreClosure.depositPeriodFrequencyType(); + final boolean isValidDepositPeriod = this.accountTermAndPreClosure.depositTermDetail().isDepositBetweenMinAndMax( + depositStartDate(), calculateMaturityDate()); + if (!isValidDepositPeriod) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) + .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.between.min.and.max.deposit.term"); + } else { + final Integer inMultiplesOf = this.accountTermAndPreClosure.depositTermDetail().inMultiplesOfDepositTerm(); + if (inMultiplesOf != null) { + final boolean isValid = this.accountTermAndPreClosure.depositTermDetail().isValidInMultiplesOfPeriod(depositPeriod, + depositPeriodFrequencyType); + if (!isValid) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) + .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.multiple.of.term"); + } } } - } - if (isAccountLocked(calculateMaturityDate())) { - baseDataValidator - .reset() - .parameter(depositPeriodParamName) - .value(depositPeriod) - .failWithCode("deposit.period.must.be.greater.than.lock.in.period", - "Deposit period must be greater than account lock-in period."); + if (isAccountLocked(calculateMaturityDate())) { + baseDataValidator + .reset() + .parameter(depositPeriodParamName) + .value(depositPeriod) + .failWithCode("deposit.period.must.be.greater.than.lock.in.period", + "Deposit period must be greater than account lock-in period."); + } } if (firstDepositDateBeforeAccountSubmittedOrActivationDate()) { @@ -969,8 +975,8 @@ public class RecurringDepositAccount extends SavingsAccount { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); - - if (this.chart != null) { + LocalDate maturityDate = calculateMaturityDate(); + if (this.chart != null && maturityDate != null) { final LocalDate chartFromDate = this.chart.getFromDateAsLocalDate(); LocalDate chartEndDate = this.chart.getEndDateAsLocalDate(); chartEndDate = chartEndDate == null ? DateUtils.getLocalDateOfTenant() : chartEndDate; @@ -981,8 +987,8 @@ public class RecurringDepositAccount extends SavingsAccount { } final BigDecimal maturityAmount = this.accountTermAndPreClosure.maturityAmount(); - BigDecimal applicableInterestRate = this.chart.getApplicableInterestRate(maturityAmount, depositStartDate(), - calculateMaturityDate(), this.client); + BigDecimal applicableInterestRate = this.chart.getApplicableInterestRate(maturityAmount, depositStartDate(), maturityDate, + this.client); if (applicableInterestRate.equals(BigDecimal.ZERO)) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( @@ -1088,18 +1094,28 @@ public class RecurringDepositAccount extends SavingsAccount { installmentDate = depositStartDate(); } - final LocalDate maturityDate = calculateMaturityDate(); int installmentNumber = 1; + final LocalDate maturityDate = calcualteScheduleTillDate(frequency, recurringEvery); final BigDecimal depositAmount = this.recurringDetail.mandatoryRecommendedDepositAmount(); while (maturityDate.isAfter(installmentDate)) { final RecurringDepositScheduleInstallment installment = RecurringDepositScheduleInstallment.installment(this, installmentNumber, installmentDate.toDate(), depositAmount); depositScheduleInstallments.add(installment); - installmentDate = calculateNextDepositDate(installmentDate, frequency, recurringEvery); + installmentDate = DepositAccountUtils.calculateNextDepositDate(installmentDate, frequency, recurringEvery); installmentNumber += 1; } } + private LocalDate calcualteScheduleTillDate(final PeriodFrequencyType frequency, final Integer recurringEvery) { + LocalDate tillDate = calculateMaturityDate(); + if (tillDate == null) { + final LocalDate today = DateUtils.getLocalDateOfTenant(); + tillDate = DepositAccountUtils.calculateNextDepositDate(today, frequency, recurringEvery + * (DepositAccountUtils.GENERATE_MINIMUM_NUMBER_OF_FUTURE_INSTALMENTS + 1)); + } + return tillDate; + } + private List depositScheduleInstallments() { if (this.depositScheduleInstallments == null) { this.depositScheduleInstallments = new ArrayList<>(); @@ -1113,7 +1129,7 @@ public class RecurringDepositAccount extends SavingsAccount { public void updateOverduePayments(final LocalDate todayDate) { LocalDate overdueUptoDate = this.maturityDate(); - if (overdueUptoDate.isAfter(todayDate)) { + if (overdueUptoDate == null || overdueUptoDate.isAfter(todayDate)) { overdueUptoDate = todayDate; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountReadPlatformService.java index d9e319a31..cc444d083 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountReadPlatformService.java @@ -6,6 +6,7 @@ package org.mifosplatform.portfolio.savings.service; import java.util.Collection; +import java.util.Map; import org.mifosplatform.infrastructure.core.data.PaginationParameters; import org.mifosplatform.infrastructure.core.service.Page; @@ -38,4 +39,6 @@ public interface DepositAccountReadPlatformService { SavingsAccountTransactionData retrieveRecurringAccountDepositTransactionTemplate(final Long accountId); Collection retrieveDataForInterestTransfer(); + + Collection> retriveDataForRDScheduleCreation(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java index ddb8aff31..42629583a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java @@ -10,14 +10,18 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; +import java.util.Map; import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.infrastructure.codes.data.CodeValueData; import org.mifosplatform.infrastructure.codes.service.CodeValueReadPlatformService; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.data.PaginationParameters; import org.mifosplatform.infrastructure.core.data.PaginationParametersDataValidator; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.core.service.PaginationHelper; import org.mifosplatform.infrastructure.core.service.RoutingDataSource; @@ -32,6 +36,7 @@ import org.mifosplatform.portfolio.account.domain.AccountTransferType; import org.mifosplatform.portfolio.calendar.data.CalendarData; import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; import org.mifosplatform.portfolio.calendar.domain.CalendarFrequencyType; +import org.mifosplatform.portfolio.calendar.domain.CalendarType; import org.mifosplatform.portfolio.calendar.service.CalendarReadPlatformService; import org.mifosplatform.portfolio.charge.data.ChargeData; import org.mifosplatform.portfolio.charge.service.ChargeReadPlatformService; @@ -103,6 +108,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead private final RecurringAccountDepositTransactionTemplateMapper rdTransactionTemplateMapper; private final DropdownReadPlatformService dropdownReadPlatformService; private final CalendarReadPlatformService calendarReadPlatformService; + private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); @Autowired public DepositAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource, @@ -197,9 +203,13 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead sqlBuilder.append(this.depositAccountForMaturityRowMapper.schema()); sqlBuilder.append(" WHERE da.deposit_type_enum in (?, ?) and da.status_enum = ?"); - return this.jdbcTemplate.query(sqlBuilder.toString(), this.depositAccountForMaturityRowMapper, new Object[] { - DepositAccountType.FIXED_DEPOSIT.getValue(), DepositAccountType.RECURRING_DEPOSIT.getValue(), - SavingsAccountStatusType.ACTIVE.getValue() }); + LocalDate today = DateUtils.getLocalDateOfTenant(); + + return this.jdbcTemplate.query( + sqlBuilder.toString(), + this.depositAccountForMaturityRowMapper, + new Object[] { formatter.print(today), DepositAccountType.FIXED_DEPOSIT.getValue(), + DepositAccountType.RECURRING_DEPOSIT.getValue(), SavingsAccountStatusType.ACTIVE.getValue() }); } @Override @@ -482,6 +492,27 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead SavingsAccountTransactionType.INTEREST_POSTING.getValue(), SavingsAccountStatusType.ACTIVE.getValue() }); } + @Override + public Collection> retriveDataForRDScheduleCreation() { + final StringBuilder sb = new StringBuilder(300); + sb.append(" select rd.savings_account_id savingsId, rd.mandatory_recommended_deposit_amount as amount,"); + sb.append(" mc.recurrence as recurrence ,"); + sb.append(" max(ms.duedate) as dueDate , max(ms.installment) as installment,"); + sb.append(" count(ms.installment) as futureInstallemts"); + sb.append(" from m_deposit_account_term_and_preclosure dat "); + sb.append(" inner join m_savings_account sa on sa.id = dat.savings_account_id and sa.status_enum = ?"); + sb.append(" inner join m_deposit_account_recurring_detail rd on rd.savings_account_id = dat.savings_account_id "); + sb.append(" inner join m_calendar_instance mci on mci.entity_type_enum = ? and mci.entity_id = dat.savings_account_id "); + sb.append(" inner join m_calendar mc on mc.id = mci.calendar_id and mc.calendar_type_enum = ?"); + sb.append(" inner join m_mandatory_savings_schedule ms on ms.savings_account_id = dat.savings_account_id and ms.duedate > ?"); + sb.append(" where dat.deposit_period is null"); + sb.append(" group by ms.savings_account_id"); + + return this.jdbcTemplate.queryForList(sb.toString(), SavingsAccountStatusType.ACTIVE.getValue(), + CalendarEntityType.SAVINGS.getValue(), CalendarType.COLLECTION.getValue(), + formatter.print(DateUtils.getLocalDateOfTenant())); + } + private static abstract class DepositAccountMapper implements RowMapper { private final String selectFieldsSql; @@ -1300,6 +1331,8 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead sqlBuilder.append("da.account_no as accountNumber, "); sqlBuilder.append("da.deposit_type_enum as depositTypeId "); sqlBuilder.append("FROM m_savings_account da "); + sqlBuilder.append("inner join m_deposit_account_term_and_preclosure dat on dat.savings_account_id = da.id "); + sqlBuilder.append("and dat.maturity_date is not null and dat.maturity_date <= ? "); this.schemaSql = sqlBuilder.toString(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 23e87ed18..b381e2ea7 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -5,6 +5,7 @@ */ package org.mifosplatform.portfolio.savings.service; +import static org.mifosplatform.portfolio.savings.DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.amountParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.chargeIdParamName; @@ -312,8 +313,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo financialYearBeginningMonth); } - final LocalDate overdueUptoDate = account.maturityDate().isAfter(DateUtils.getLocalDateOfTenant()) ? DateUtils - .getLocalDateOfTenant() : account.maturityDate(); + final LocalDate overdueUptoDate = DateUtils.getLocalDateOfTenant(); account.updateOverduePayments(overdueUptoDate); final boolean isInterestTransfer = false; if (account.isBeforeLastPostingPeriod(account.getActivationLocalDate())) { @@ -586,8 +586,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final boolean isPreMatureClosure = false; account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - final LocalDate overdueUptoDate = account.maturityDate().isAfter(DateUtils.getLocalDateOfTenant()) ? DateUtils - .getLocalDateOfTenant() : account.maturityDate(); + final LocalDate overdueUptoDate = DateUtils.getLocalDateOfTenant(); if (savingsAccountTransaction.isDeposit()) { account.updateScheduleInstallments(); @@ -682,8 +681,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo if (savingsAccountTransaction.isDeposit()) { account.handleScheduleInstallments(savingsAccountTransaction); } - final LocalDate overdueUptoDate = account.maturityDate().isAfter(DateUtils.getLocalDateOfTenant()) ? DateUtils - .getLocalDateOfTenant() : account.maturityDate(); + final LocalDate overdueUptoDate = DateUtils.getLocalDateOfTenant(); account.updateOverduePayments(overdueUptoDate); postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); @@ -833,6 +831,13 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, DepositAccountType.RECURRING_DEPOSIT); checkClientOrGroupActive(account); + if (account.maturityDate() == null) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("can.not.close.as.premature"); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } this.depositAccountDomainService.handleRDAccountPreMatureClosure(account, paymentDetail, user, command, DateUtils.getLocalDateOfTenant(), changes); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java index 9b8a3f094..6268d5653 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java @@ -20,4 +20,6 @@ public interface ScheduledJobRunnerService { void updateNPA(); void updateMaturityDetailsOfDepositAccounts(); + + void generateRDSchedule(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java index e7fa3836a..bbd1d81c3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java @@ -5,17 +5,25 @@ */ package org.mifosplatform.scheduledjobs.service; +import java.math.BigDecimal; import java.util.Collection; +import java.util.Date; import java.util.List; +import java.util.Map; +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.core.service.RoutingDataSourceServiceFactory; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.jobs.annotation.CronTarget; import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException; import org.mifosplatform.infrastructure.jobs.service.JobName; import org.mifosplatform.portfolio.savings.DepositAccountType; +import org.mifosplatform.portfolio.savings.DepositAccountUtils; import org.mifosplatform.portfolio.savings.data.DepositAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountAnnualFeeData; import org.mifosplatform.portfolio.savings.service.DepositAccountReadPlatformService; @@ -33,6 +41,8 @@ import org.springframework.transaction.annotation.Transactional; public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService { private final static Logger logger = LoggerFactory.getLogger(ScheduledJobRunnerServiceImpl.class); + private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); + private final DateTimeFormatter formatterWithTime = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); private final RoutingDataSourceServiceFactory dataSourceServiceFactory; private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; @@ -285,4 +295,60 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Deposit accounts affected by update: " + depositAccounts.size()); } + @Override + @CronTarget(jobName = JobName.GENERATE_RD_SCEHDULE) + public void generateRDSchedule() { + final JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSourceServiceFactory.determineDataSourceService().retrieveDataSource()); + final Collection> scheduleDetails = this.depositAccountReadPlatformService.retriveDataForRDScheduleCreation(); + String insertSql = "INSERT INTO `m_mandatory_savings_schedule` (`savings_account_id`, `duedate`, `installment`, `deposit_amount`, `completed_derived`, `created_date`, `lastmodified_date`) VALUES "; + StringBuilder sb = new StringBuilder(); + String currentDate = formatterWithTime.print(DateUtils.getLocalDateTimeOfTenant()); + int iterations = 0; + for (Map details : scheduleDetails) { + Long count = (Long) details.get("futureInstallemts"); + if (count == null) { + count = 0l; + } + final Long savingsId = (Long) details.get("savingsId"); + final BigDecimal amount = (BigDecimal) details.get("amount"); + final String recurrence = (String) details.get("recurrence"); + Date date = (Date) details.get("dueDate"); + LocalDate lastDepositDate = new LocalDate(date); + Integer installmentNumber = (Integer) details.get("installment"); + while (count < DepositAccountUtils.GENERATE_MINIMUM_NUMBER_OF_FUTURE_INSTALMENTS) { + count++; + installmentNumber++; + lastDepositDate = DepositAccountUtils.calculateNextDepositDate(lastDepositDate, recurrence); + + if (sb.length() > 0) { + sb.append(", "); + } + sb.append("("); + sb.append(savingsId); + sb.append(",'"); + sb.append(formatter.print(lastDepositDate)); + sb.append("',"); + sb.append(installmentNumber); + sb.append(","); + sb.append(amount); + sb.append(", b'0','"); + sb.append(currentDate); + sb.append("','"); + sb.append(currentDate); + sb.append("')"); + iterations++; + if (iterations > 200) { + jdbcTemplate.update(insertSql + sb.toString()); + sb = new StringBuilder(); + } + + } + } + + if (sb.length() > 0) { + jdbcTemplate.update(insertSql + sb.toString()); + } + + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V242__open_rd_changes.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V242__open_rd_changes.sql new file mode 100644 index 000000000..d87c8a376 --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V242__open_rd_changes.sql @@ -0,0 +1,2 @@ +INSERT INTO `job` (`name`, `display_name`, `cron_expression`, `create_time`) VALUES ('Generate Mandatory Savings Schedule', 'Generate Mandatory Savings Schedule', '0 5 0 1/1 * ? *', now()); +