diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 1f97a4954..1baf13430 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -9629,7 +9629,7 @@ Request Body:

Calculate Interest on Savings Account

- Calculates interest earned on a savings account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of another API that will be called by overnight process. + Calculates interest earned on a savings account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.

@@ -9651,10 +9651,37 @@ Request Body:
+   +
+
+

Post Interest on Savings Account

+

+ Calculates and Posts interest earned on a savings account based on todays date and whether an interest posting or crediting event is due. +

+
+
+ POST https://Domain Name/api/v1/savingsaccounts/{savingsId}?command=postInterest + POST savingsaccount/1?command=postInterest +Content-Type: application/json +Request Body: +{ +} + + +{ + "officeId": 1, + "clientId": 1, + "savingsId": 1, + "resourceId": 1 +} + +
+
+  
-

List Savings Accounts

+

List Savings Accounts

Example Requests:

savingsaccounts


@@ -13498,6 +13525,14 @@ GET https://DomainName/api/v1/search?query=000000111 + + + savingsaccounts/{accountId}?command=postInterest + Post Interest on a Savings Account + + + + Savings Transactions savingsaccounts/{accountId}/transactions?command=deposit diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransactionComparator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransactionComparator.java index fc70470f6..c6d34c1ce 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransactionComparator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransactionComparator.java @@ -14,22 +14,22 @@ public class LoanTransactionComparator implements Comparator { @Override public int compare(final LoanTransaction o1, final LoanTransaction o2) { - int compareResult = 0; - final int comparsion = o1.getTransactionDate().compareTo(o2.getTransactionDate()); - if (comparsion == 0) { - // equal transaction dates - if (o1.isWaiver() && o2.isNotWaiver()) { - compareResult = -1; - } else if (o1.isNotWaiver() && o2.isWaiver()) { - compareResult = 1; - } else { - compareResult = 0; - } - } else { - compareResult = comparsion; - } - - return compareResult; + int compareResult = 0; + final int comparsion = o1.getTransactionDate().compareTo(o2.getTransactionDate()); + if (comparsion == 0) { + // equal transaction dates + if (o1.isWaiver() && o2.isNotWaiver()) { + compareResult = -1; + } else if (o1.isNotWaiver() && o2.isWaiver()) { + compareResult = 1; + } else { + compareResult = 0; + } + } else { + compareResult = comparsion; + } + + return compareResult; } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java index 1b17d0966..fbee92c03 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java @@ -3,12 +3,12 @@ package org.mifosplatform.portfolio.savings.domain; import java.math.BigDecimal; import java.math.MathContext; +import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; public class InterestCompoundingPeriodSummary { - @SuppressWarnings("unused") private final LocalDateInterval periodInterval; @SuppressWarnings("unused") private final BigDecimal openingBalance; @@ -50,4 +50,8 @@ public class InterestCompoundingPeriodSummary { public BigDecimal interestUnrounded() { return this.interestEarnedUnrounded; } + + public boolean fallsBefore(final LocalDate dateToCheck) { + return this.periodInterval.fallsBefore(dateToCheck); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java index 03fd5c781..83d8adc68 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java @@ -40,4 +40,8 @@ public class LocalDateInterval { private boolean isBetweenInclusive(final LocalDate start, final LocalDate end, final LocalDate target) { return !target.isBefore(start) && !target.isAfter(end); } + + public boolean fallsBefore(final LocalDate dateToCheck) { + return this.endDate.isBefore(dateToCheck); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java index 131f47ec2..1c70ebe0e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java @@ -10,6 +10,7 @@ import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -46,6 +47,8 @@ import org.mifosplatform.portfolio.group.domain.Group; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.savings.api.SavingsApiConstants; import org.mifosplatform.portfolio.savings.exception.InsufficientAccountBalanceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.jpa.domain.AbstractPersistable; @Entity @@ -53,6 +56,8 @@ import org.springframework.data.jpa.domain.AbstractPersistable; @UniqueConstraint(columnNames = { "external_id" }, name = "sa_external_id_UNIQUE") }) public class SavingsAccount extends AbstractPersistable { + private final static Logger logger = LoggerFactory.getLogger(SavingsAccount.class); + @Column(name = "account_no", length = 20, unique = true, nullable = false) private String accountNumber; @@ -123,7 +128,7 @@ public class SavingsAccount extends AbstractPersistable { private Integer lockinPeriodFrequencyType; /** - * When account is becomes active this field is derived if + * When account becomes active this field is derived if * lockinPeriodFrequency and * lockinPeriodFrequencyType details are present. */ @@ -163,8 +168,8 @@ public class SavingsAccount extends AbstractPersistable { } private SavingsAccount(final Client client, final Group group, final SavingsProduct product, final String accountNo, - final String externalId, final SavingsAccountStatusType status, final LocalDate activationDate, final BigDecimal nominalAnnualInterestRate, - final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, + final String externalId, final SavingsAccountStatusType status, final LocalDate activationDate, + final BigDecimal nominalAnnualInterestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsInterestPostingPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, final SavingsInterestCalculationDaysInYearType interestCalculationDaysInYearType, final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency, final SavingsPeriodFrequencyType lockinPeriodFrequencyType) { @@ -278,15 +283,80 @@ public class SavingsAccount extends AbstractPersistable { public void postInterest(final LocalDate interestPostingUpToDate) { final SavingsInterestPostingPeriodType postingPeriodType = SavingsInterestPostingPeriodType.fromInt(this.interestPostingPeriodType); - List postingLocalDates = determineInterestPostingDates(getActivationLocalDate(), interestPostingUpToDate, + final List postingLocalDates = determineInterestPostingDates(getActivationLocalDate(), interestPostingUpToDate, postingPeriodType); - List compoundingPeriods = calculateInterest(interestPostingUpToDate); + final List compoundingPeriods = calculateInterest(interestPostingUpToDate); - // - for (InterestCompoundingPeriodSummary interestCompoundingPeriodSummary : compoundingPeriods) { + Money interestPostedToDate = Money.zero(this.currency); + boolean recalucateDailyBalanceDetails = false; + for (LocalDate postingDate : postingLocalDates) { + Money interestEarnedToBePostedForPeriod = findInterestEarnedToBePostedOn(interestPostedToDate, postingDate, compoundingPeriods); + + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + + SavingsAccountTransaction postingTransaction = findInterestPostingTransactionFor(postingDate); + if (postingTransaction == null) { + final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, postingDate, + interestEarnedToBePostedForPeriod); + this.transactions.add(newPostingTransaction); + recalucateDailyBalanceDetails = true; + } else { + boolean correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); + if (correctionRequired) { + postingTransaction.reverse(); + final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, postingDate, + interestEarnedToBePostedForPeriod); + this.transactions.add(newPostingTransaction); + recalucateDailyBalanceDetails = true; + } + } } + + if (recalucateDailyBalanceDetails) { + // no openingBalance concept supported yet but probably will to + // allow + // for migrations. + final Money openingAccountBalance = Money.zero(this.currency); + + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(openingAccountBalance); + } + + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + private SavingsAccountTransaction findInterestPostingTransactionFor(final LocalDate postingDate) { + + SavingsAccountTransaction postingTransation = null; + + for (SavingsAccountTransaction transaction : this.transactions) { + if (transaction.isInterestPosting() && transaction.occursOn(postingDate)) { + postingTransation = transaction; + break; + } + } + + return postingTransation; + } + + private Money findInterestEarnedToBePostedOn(final Money interestPostedToDate, final LocalDate postingDate, + final List compoundingPeriods) { + + Money interestToBePosted = Money.zero(this.currency); + + BigDecimal lastRelevantInterest = BigDecimal.ZERO; + for (InterestCompoundingPeriodSummary interestCompoundingPeriodSummary : compoundingPeriods) { + if (interestCompoundingPeriodSummary.fallsBefore(postingDate)) { + interestToBePosted = Money.of(this.currency, interestCompoundingPeriodSummary.compoundedInterest()); + lastRelevantInterest = interestCompoundingPeriodSummary.compoundedInterest(); + } + } + logger.info("Interest is: " + lastRelevantInterest + " :>> "); + + return interestToBePosted.minus(interestPostedToDate); } private List determineInterestPostingDates(final LocalDate activationLocalDate, final LocalDate interestPostingUpToDate, @@ -300,7 +370,9 @@ public class SavingsAccount extends AbstractPersistable { while (!periodStartDate.isAfter(interestPostingUpToDate) && !periodEndDate.isAfter(interestPostingUpToDate)) { final LocalDate interestPostingLocalDate = determineInterestPostingPeriodEndDateFrom(periodStartDate, postingPeriodType); - postingDates.add(interestPostingLocalDate); + if (!interestPostingLocalDate.isAfter(DateUtils.getLocalDateOfTenant())) { + postingDates.add(interestPostingLocalDate); + } periodEndDate = interestPostingLocalDate; periodStartDate = interestPostingLocalDate; @@ -558,11 +630,25 @@ public class SavingsAccount extends AbstractPersistable { return periodsInOneYear; } + private List retreiveListOfTransactions() { + List listOfTransactionsSorted = new ArrayList(); + for (SavingsAccountTransaction transaction : this.transactions) { + listOfTransactionsSorted.add(transaction); + } + + SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); + Collections.sort(listOfTransactionsSorted, transactionComparator); + + return listOfTransactionsSorted; + } + private void recalculateDailyBalances(final Money openingAccountBalance) { Money runningBalance = openingAccountBalance.copy(); - for (SavingsAccountTransaction transaction : this.transactions) { + List accountTransactionsSorted = retreiveListOfTransactions(); + + for (SavingsAccountTransaction transaction : accountTransactionsSorted) { if (transaction.isReversed()) { transaction.zeroBalanceFields(); } else { @@ -582,13 +668,13 @@ public class SavingsAccount extends AbstractPersistable { // loop over transactions in reverse LocalDate endOfBalanceDate = DateUtils.getLocalDateOfTenant(); - for (int i = this.transactions.size() - 1; i >= 0; i--) { - SavingsAccountTransaction transaction = this.transactions.get(i); + for (int i = accountTransactionsSorted.size() - 1; i >= 0; i--) { + SavingsAccountTransaction transaction = accountTransactionsSorted.get(i); transaction.updateCumulativeBalanceAndDates(this.currency, endOfBalanceDate); // this transactions transaction date is end of balance date for // previous transaction. - endOfBalanceDate = transaction.localDate().minusDays(1); + endOfBalanceDate = transaction.transactionLocalDate().minusDays(1); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java index 2d2b0c45c..09529aeea 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java @@ -76,6 +76,12 @@ public final class SavingsAccountTransaction extends AbstractPersistable { return new SavingsAccountTransaction(savingsAccount, SavingsAccountTransactionType.WITHDRAWAL.getValue(), date, amount, isReversed); } + public static SavingsAccountTransaction interestPosting(final SavingsAccount savingsAccount, final LocalDate date, final Money amount) { + final boolean isReversed = false; + return new SavingsAccountTransaction(savingsAccount, SavingsAccountTransactionType.INTEREST_POSTING.getValue(), date, amount, + isReversed); + } + private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Integer typeOf, final LocalDate transactionLocalDate, final Money amount, final boolean isReversed) { this.savingsAccount = savingsAccount; @@ -85,7 +91,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable { this.reversed = isReversed; } - public LocalDate localDate() { + public LocalDate transactionLocalDate() { return new LocalDate(this.dateOf); } @@ -106,7 +112,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable { } public boolean isInterestPosting() { - return SavingsAccountTransactionType.fromInt(this.typeOf).isInterestPosting(); + return SavingsAccountTransactionType.fromInt(this.typeOf).isInterestPosting() && isNotReversed(); } public boolean isNotReversed() { @@ -117,6 +123,10 @@ public final class SavingsAccountTransaction extends AbstractPersistable { return this.reversed; } + public boolean occursOn(final LocalDate occursOnDate) { + return getTransactionLocalDate().isEqual(occursOnDate); + } + public void zeroBalanceFields() { this.runningBalance = null; this.cumulativeBalance = null; @@ -130,8 +140,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable { public void updateCumulativeBalanceAndDates(final MonetaryCurrency currency, final LocalDate endOfBalanceDate) { this.balanceEndDate = endOfBalanceDate.toDate(); - this.balanceNumberOfDays = LocalDateInterval.create(getTransactionLocalDate(), endOfBalanceDate) - .daysInPeriodInclusiveOfEndDate(); + this.balanceNumberOfDays = LocalDateInterval.create(getTransactionLocalDate(), endOfBalanceDate).daysInPeriodInclusiveOfEndDate(); this.cumulativeBalance = Money.of(currency, this.runningBalance).multipliedBy(this.balanceNumberOfDays).getAmount(); } @@ -175,4 +184,9 @@ public final class SavingsAccountTransaction extends AbstractPersistable { final LocalDateInterval interestPeriodInterval = LocalDateInterval.create(getTransactionLocalDate(), balanceValidTo); return SavingsAccountDailyBalance.createFrom(interestPeriodInterval, this.runningBalance, compoundedInterestToDate); } + + public boolean hasNotAmount(final Money amountToCheck) { + final Money transactionAmount = getAmount(amountToCheck.getCurrency()); + return transactionAmount.isNotEqualTo(amountToCheck); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionComparator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionComparator.java new file mode 100644 index 000000000..840f48750 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransactionComparator.java @@ -0,0 +1,35 @@ +/** + * 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.domain; + +import java.util.Comparator; + +/** + * Sort savings account transactions by transaction date and transaction type + * placing + */ +public class SavingsAccountTransactionComparator implements Comparator { + + @Override + public int compare(final SavingsAccountTransaction o1, final SavingsAccountTransaction o2) { + int compareResult = 0; + final int comparsion = o1.transactionLocalDate().compareTo(o2.transactionLocalDate()); + if (comparsion == 0) { + // equal transaction dates + if (o1.isInterestPosting() && !o2.isInterestPosting()) { + compareResult = -1; + } else if (!o1.isInterestPosting() && o2.isInterestPosting()) { + compareResult = 1; + } else { + compareResult = 0; + } + } else { + compareResult = comparsion; + } + + return compareResult; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 085fb74c9..d5c6b439d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -289,7 +289,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead @Override public Collection retrieveAllTransactions(final Long savingsId) { - final String sql = "select " + this.transactionsMapper.schema() + " where sa.id = ?"; + final String sql = "select " + this.transactionsMapper.schema() + + " where sa.id = ? and tr.is_reversed=0 order by tr.transaction_date DESC, tr.id DESC"; return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId }); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index c6b09b42a..13c53a014 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -333,6 +333,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .build(); } + @Transactional @Override public CommandProcessingResult postInterest(final Long savingsId, final JsonCommand command) { this.context.authenticatedUser();