MIFOSX-250: interest posting/crediting added.

This commit is contained in:
Keith Woodlock
2013-04-23 11:52:45 +01:00
parent db35e26d0a
commit 46c5565fb9
9 changed files with 216 additions and 36 deletions
+37 -2
View File
@@ -9629,7 +9629,7 @@ Request Body:
<div class="method-description">
<h2>Calculate Interest on Savings Account</h2>
<p>
Calculates interest earned on a savings account based on todays date. It does not attempt to <b>post or credit</b> 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 <b>post or credit</b> the interest on the account. That is responsibility of the <a href="#savingsaccounts_post_interest" >Post Interest API</a> that will likely be called by overnight process.
</p>
</div>
<div class="method-example">
@@ -9651,10 +9651,37 @@ Request Body:
</div>
</div>
<a id="savingsaccounts_post_interest" name="savingsaccounts_post_interest" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h2>Post Interest on Savings Account</h2>
<p>
Calculates and Posts interest earned on a savings account based on todays date and whether an interest posting or crediting event is due.
</p>
</div>
<div class="method-example">
<code class="method-declaration">POST https://Domain Name/api/v1/savingsaccounts/{savingsId}?command=postInterest</code>
<code class="method-request">POST savingsaccount/1?command=postInterest
Content-Type: application/json
Request Body:
{
}
</code>
<code class="method-response">
{
"officeId": 1,
"clientId": 1,
"savingsId": 1,
"resourceId": 1
}
</code>
</div>
</div>
<a id="savingsaccounts_list" name="savingsaccounts_list" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>List Savings Accounts</h4>
<h2>List Savings Accounts</h2>
<p>Example Requests:</p>
<div class=apiClick>savingsaccounts</div>
<br><br>
@@ -13498,6 +13525,14 @@ GET https://DomainName/api/v1/search?query=000000111
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>savingsaccounts/{accountId}?command=postInterest</td>
<td><a href="#savingsaccounts_post_interest">Post Interest on a Savings Account</a></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr class="alt">
<td><a href="#savingsaccounts_transactions">Savings Transactions</a></td>
<td>savingsaccounts/{accountId}/transactions?command=deposit</td>
@@ -14,22 +14,22 @@ public class LoanTransactionComparator implements Comparator<LoanTransaction> {
@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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<Long> {
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<Long> {
private Integer lockinPeriodFrequencyType;
/**
* When account is becomes <code>active</code> this field is derived if
* When account becomes <code>active</code> this field is derived if
* <code>lockinPeriodFrequency</code> and
* <code>lockinPeriodFrequencyType</code> details are present.
*/
@@ -163,8 +168,8 @@ public class SavingsAccount extends AbstractPersistable<Long> {
}
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<Long> {
public void postInterest(final LocalDate interestPostingUpToDate) {
final SavingsInterestPostingPeriodType postingPeriodType = SavingsInterestPostingPeriodType.fromInt(this.interestPostingPeriodType);
List<LocalDate> postingLocalDates = determineInterestPostingDates(getActivationLocalDate(), interestPostingUpToDate,
final List<LocalDate> postingLocalDates = determineInterestPostingDates(getActivationLocalDate(), interestPostingUpToDate,
postingPeriodType);
List<InterestCompoundingPeriodSummary> compoundingPeriods = calculateInterest(interestPostingUpToDate);
final List<InterestCompoundingPeriodSummary> 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<InterestCompoundingPeriodSummary> 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<LocalDate> determineInterestPostingDates(final LocalDate activationLocalDate, final LocalDate interestPostingUpToDate,
@@ -300,7 +370,9 @@ public class SavingsAccount extends AbstractPersistable<Long> {
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<Long> {
return periodsInOneYear;
}
private List<SavingsAccountTransaction> retreiveListOfTransactions() {
List<SavingsAccountTransaction> listOfTransactionsSorted = new ArrayList<SavingsAccountTransaction>();
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<SavingsAccountTransaction> accountTransactionsSorted = retreiveListOfTransactions();
for (SavingsAccountTransaction transaction : accountTransactionsSorted) {
if (transaction.isReversed()) {
transaction.zeroBalanceFields();
} else {
@@ -582,13 +668,13 @@ public class SavingsAccount extends AbstractPersistable<Long> {
// 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);
}
}
@@ -76,6 +76,12 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
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<Long> {
this.reversed = isReversed;
}
public LocalDate localDate() {
public LocalDate transactionLocalDate() {
return new LocalDate(this.dateOf);
}
@@ -106,7 +112,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
}
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<Long> {
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<Long> {
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<Long> {
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);
}
}
@@ -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<SavingsAccountTransaction> {
@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;
}
}
@@ -289,7 +289,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
@Override
public Collection<SavingsAccountTransactionData> 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 });
}
@@ -333,6 +333,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
.build();
}
@Transactional
@Override
public CommandProcessingResult postInterest(final Long savingsId, final JsonCommand command) {
this.context.authenticatedUser();