MIFOSX-250: interest posting/crediting added.
This commit is contained in:
+37
-2
@@ -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"> </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"> </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>
|
||||
|
||||
+16
-16
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
@@ -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);
|
||||
}
|
||||
}
|
||||
+98
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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 });
|
||||
}
|
||||
|
||||
+1
@@ -333,6 +333,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult postInterest(final Long savingsId, final JsonCommand command) {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
Reference in New Issue
Block a user