blob: 26b7310312cae98f1b5210817594de3aafef1f73 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.domain.interest;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
public class PostingPeriod {
@SuppressWarnings("unused")
private final LocalDateInterval periodInterval;
private final MonetaryCurrency currency;
private final SavingsCompoundingInterestPeriodType interestCompoundingType;
private final BigDecimal interestRateAsFraction;
private final long daysInYear;
private final List<CompoundingPeriod> compoundingPeriods;
// interest posting details
private final LocalDate dateOfPostingTransaction;
private BigDecimal interestEarnedUnrounded;
private Money interestEarnedRounded;
// opening/closing details
private final Money openingBalance;
private final Money closingBalance;
private final SavingsInterestCalculationType interestCalculationType;
// include in compounding interest
private boolean interestTransfered = false;
// minimum balance for interest calculation
private final Money minBalanceForInterestCalculation;
private BigDecimal overdraftInterestRateAsFraction;
private Money minOverdraftForInterestCalculation;
public static PostingPeriod createFrom(final LocalDateInterval periodInterval, final Money periodStartingBalance,
final List<SavingsAccountTransaction> orderedListOfTransactions, final MonetaryCurrency currency,
final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType,
final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear,
final LocalDate upToInterestCalculationDate, Collection<Long> interestPostTransactions, boolean isInterestTransfer,
final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd) {
final BigDecimal overdraftInterestRateAsFraction = BigDecimal.ZERO;
final Money minOverdraftForInterestCalculation = Money.zero(currency);
return createFrom(periodInterval, periodStartingBalance, orderedListOfTransactions, currency,
interestCompoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYear,
upToInterestCalculationDate, interestPostTransactions, isInterestTransfer,
minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation);
}
// isInterestTransfer boolean is to identify newly created transaction is
// interest transfer
public static PostingPeriod createFrom(final LocalDateInterval periodInterval, final Money periodStartingBalance,
final List<SavingsAccountTransaction> orderedListOfTransactions, final MonetaryCurrency currency,
final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType,
final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear,
final LocalDate upToInterestCalculationDate, Collection<Long> interestPostTransactions, boolean isInterestTransfer,
final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd,
final BigDecimal overdraftInterestRateAsFraction, final Money minOverdraftForInterestCalculation) {
final List<EndOfDayBalance> accountEndOfDayBalances = new ArrayList<>();
boolean interestTransfered = false;
Money openingDayBalance = periodStartingBalance;
Money closeOfDayBalance = openingDayBalance;
for (final SavingsAccountTransaction transaction : orderedListOfTransactions) {
if (transaction.fallsWithin(periodInterval)) {
// the balance of the transaction falls entirely within this
// period so no need to do any cropping/bounding
final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalance(openingDayBalance);
accountEndOfDayBalances.add(endOfDayBalance);
openingDayBalance = endOfDayBalance.closingBalance();
} else if (transaction.spansAnyPortionOf(periodInterval)) {
final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalanceBoundedBy(openingDayBalance, periodInterval);
accountEndOfDayBalances.add(endOfDayBalance);
closeOfDayBalance = endOfDayBalance.closingBalance();
openingDayBalance = closeOfDayBalance;
}
// this check is to make sure to add interest if withdrawal is
// happened for already
if (transaction.occursOn(periodInterval.endDate().plusDays(1))) {
if (transaction.getId() == null) {
interestTransfered = isInterestTransfer;
} else if (interestPostTransactions.contains(transaction.getId())) {
interestTransfered = true;
}
}
}
if (accountEndOfDayBalances.isEmpty()) {
LocalDate balanceStartDate = periodInterval.startDate();
LocalDate balanceEndDate = periodInterval.endDate();
Integer numberOfDaysOfBalance = periodInterval.daysInPeriodInclusiveOfEndDate();
if (balanceEndDate.isAfter(upToInterestCalculationDate)) {
balanceEndDate = upToInterestCalculationDate;
final LocalDateInterval spanOfBalance = LocalDateInterval.create(balanceStartDate, balanceEndDate);
numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate();
}
final EndOfDayBalance endOfDayBalance = EndOfDayBalance.from(balanceStartDate, openingDayBalance, closeOfDayBalance,
numberOfDaysOfBalance);
accountEndOfDayBalances.add(endOfDayBalance);
closeOfDayBalance = endOfDayBalance.closingBalance();
openingDayBalance = closeOfDayBalance;
}
final List<CompoundingPeriod> compoundingPeriods = compoundingPeriodsInPostingPeriod(periodInterval, interestCompoundingPeriodType,
accountEndOfDayBalances, upToInterestCalculationDate);
return new PostingPeriod(periodInterval, currency, periodStartingBalance, openingDayBalance, interestCompoundingPeriodType,
interestCalculationType, interestRateAsFraction, daysInYear, compoundingPeriods, interestTransfered,
minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation);
}
private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurrency currency, final Money openingBalance,
final Money closingBalance, final SavingsCompoundingInterestPeriodType interestCompoundingType,
final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear,
final List<CompoundingPeriod> compoundingPeriods, boolean interestTransfered, final Money minBalanceForInterestCalculation,
final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final BigDecimal overdraftInterestRateAsFraction,
final Money minOverdraftForInterestCalculation) {
this.periodInterval = periodInterval;
this.currency = currency;
this.openingBalance = openingBalance;
this.closingBalance = closingBalance;
this.interestCompoundingType = interestCompoundingType;
this.interestCalculationType = interestCalculationType;
this.interestRateAsFraction = interestRateAsFraction;
this.daysInYear = daysInYear;
this.compoundingPeriods = compoundingPeriods;
if (isSavingsInterestPostingAtCurrentPeriodEnd)
this.dateOfPostingTransaction = periodInterval.endDate();
else
this.dateOfPostingTransaction = periodInterval.endDate().plusDays(1);
this.interestTransfered = interestTransfered;
this.minBalanceForInterestCalculation = minBalanceForInterestCalculation;
this.overdraftInterestRateAsFraction = overdraftInterestRateAsFraction;
this.minOverdraftForInterestCalculation = minOverdraftForInterestCalculation;
}
public Money interest() {
return this.interestEarnedRounded;
}
public LocalDate dateOfPostingTransaction() {
return this.dateOfPostingTransaction;
}
public Money closingBalance() {
return this.closingBalance;
}
public Money openingBalance() {
return this.openingBalance;
}
public BigDecimal calculateInterest(final BigDecimal interestFromPreviousPostingPeriod) {
BigDecimal interestEarned = BigDecimal.ZERO;
// for each compounding period accumulate the amount of interest
// to be applied to the balanced for interest calculation
BigDecimal interestCompounded = interestFromPreviousPostingPeriod;
for (final CompoundingPeriod compoundingPeriod : this.compoundingPeriods) {
final BigDecimal interestUnrounded = compoundingPeriod.calculateInterest(this.interestCompoundingType,
this.interestCalculationType, interestCompounded, this.interestRateAsFraction, this.daysInYear,
this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
this.minOverdraftForInterestCalculation.getAmount());
interestCompounded = interestCompounded.add(interestUnrounded);
interestEarned = interestEarned.add(interestUnrounded);
}
this.interestEarnedUnrounded = interestEarned;
this.interestEarnedRounded = Money.of(this.currency, this.interestEarnedUnrounded);
return interestEarned;
}
public Money getInterestEarned() {
return this.interestEarnedRounded;
}
private static List<CompoundingPeriod> compoundingPeriodsInPostingPeriod(final LocalDateInterval postingPeriodInterval,
final SavingsCompoundingInterestPeriodType interestPeriodType, final List<EndOfDayBalance> allEndOfDayBalances,
final LocalDate upToInterestCalculationDate) {
final List<CompoundingPeriod> compoundingPeriods = new ArrayList<>();
CompoundingPeriod compoundingPeriod = null;
switch (interestPeriodType) {
case INVALID:
break;
case DAILY:
compoundingPeriod = DailyCompoundingPeriod.create(postingPeriodInterval, allEndOfDayBalances, upToInterestCalculationDate);
compoundingPeriods.add(compoundingPeriod);
break;
case MONTHLY:
final LocalDate postingPeriodEndDate = postingPeriodInterval.endDate();
LocalDate periodStartDate = postingPeriodInterval.startDate();
LocalDate periodEndDate = periodStartDate;
while (!periodStartDate.isAfter(postingPeriodEndDate) && !periodEndDate.isAfter(postingPeriodEndDate)) {
periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate);
if (periodEndDate.isAfter(postingPeriodEndDate)) {
periodEndDate = postingPeriodEndDate;
}
final LocalDateInterval compoundingPeriodInterval = LocalDateInterval.create(periodStartDate, periodEndDate);
if (postingPeriodInterval.contains(compoundingPeriodInterval)) {
compoundingPeriod = MonthlyCompoundingPeriod.create(compoundingPeriodInterval, allEndOfDayBalances,
upToInterestCalculationDate);
compoundingPeriods.add(compoundingPeriod);
}
// move periodStartDate forward to day after this period
periodStartDate = periodEndDate.plusDays(1);
}
break;
// case WEEKLY:
// break;
// case BIWEEKLY:
// break;
case QUATERLY:
final LocalDate qPostingPeriodEndDate = postingPeriodInterval.endDate();
periodStartDate = postingPeriodInterval.startDate();
periodEndDate = periodStartDate;
while (!periodStartDate.isAfter(qPostingPeriodEndDate) && !periodEndDate.isAfter(qPostingPeriodEndDate)) {
periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate);
if (periodEndDate.isAfter(qPostingPeriodEndDate)) {
periodEndDate = qPostingPeriodEndDate;
}
final LocalDateInterval compoundingPeriodInterval = LocalDateInterval.create(periodStartDate, periodEndDate);
if (postingPeriodInterval.contains(compoundingPeriodInterval)) {
compoundingPeriod = QuarterlyCompoundingPeriod.create(compoundingPeriodInterval, allEndOfDayBalances,
upToInterestCalculationDate);
compoundingPeriods.add(compoundingPeriod);
}
// move periodStartDate forward to day after this period
periodStartDate = periodEndDate.plusDays(1);
}
break;
case BI_ANNUAL:
final LocalDate bPostingPeriodEndDate = postingPeriodInterval.endDate();
periodStartDate = postingPeriodInterval.startDate();
periodEndDate = periodStartDate;
while (!periodStartDate.isAfter(bPostingPeriodEndDate) && !periodEndDate.isAfter(bPostingPeriodEndDate)) {
periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate);
if (periodEndDate.isAfter(bPostingPeriodEndDate)) {
periodEndDate = bPostingPeriodEndDate;
}
final LocalDateInterval compoundingPeriodInterval = LocalDateInterval.create(periodStartDate, periodEndDate);
if (postingPeriodInterval.contains(compoundingPeriodInterval)) {
compoundingPeriod = BiAnnualCompoundingPeriod.create(compoundingPeriodInterval, allEndOfDayBalances,
upToInterestCalculationDate);
compoundingPeriods.add(compoundingPeriod);
}
// move periodStartDate forward to day after this period
periodStartDate = periodEndDate.plusDays(1);
}
break;
case ANNUAL:
final LocalDate aPostingPeriodEndDate = postingPeriodInterval.endDate();
periodStartDate = postingPeriodInterval.startDate();
periodEndDate = periodStartDate;
while (!periodStartDate.isAfter(aPostingPeriodEndDate) && !periodEndDate.isAfter(aPostingPeriodEndDate)) {
periodEndDate = determineInterestPeriodEndDateFrom(periodStartDate, interestPeriodType, upToInterestCalculationDate);
if (periodEndDate.isAfter(aPostingPeriodEndDate)) {
periodEndDate = aPostingPeriodEndDate;
}
final LocalDateInterval compoundingPeriodInterval = LocalDateInterval.create(periodStartDate, periodEndDate);
if (postingPeriodInterval.contains(compoundingPeriodInterval)) {
compoundingPeriod = AnnualCompoundingPeriod.create(compoundingPeriodInterval, allEndOfDayBalances,
upToInterestCalculationDate);
compoundingPeriods.add(compoundingPeriod);
}
// move periodStartDate forward to day after this period
periodStartDate = periodEndDate.plusDays(1);
}
break;
// case NO_COMPOUNDING_SIMPLE_INTEREST:
// break;
}
return compoundingPeriods;
}
private static LocalDate determineInterestPeriodEndDateFrom(final LocalDate periodStartDate,
final SavingsCompoundingInterestPeriodType interestPeriodType, final LocalDate upToInterestCalculationDate) {
LocalDate periodEndDate = upToInterestCalculationDate;
switch (interestPeriodType) {
case INVALID:
break;
case DAILY:
periodEndDate = periodStartDate;
break;
// case WEEKLY:
// periodEndDate = periodStartDate.dayOfWeek().withMaximumValue();
// break;
// case BIWEEKLY:
// final LocalDate closestEndOfWeek =
// periodStartDate.dayOfWeek().withMaximumValue();
// periodEndDate = closestEndOfWeek.plusWeeks(1);
// break;
case MONTHLY:
// produce period end date on last day of current month
periodEndDate = periodStartDate.dayOfMonth().withMaximumValue();
break;
case QUATERLY:
// // jan 1st to mar 31st, 1st apr to jun 30, jul 1st to sept
// 30,
// // oct 1st to dec 31
int year = periodStartDate.getYearOfEra();
int monthofYear = periodStartDate.getMonthOfYear();
if (monthofYear <= 3) {
periodEndDate = new DateTime().withDate(year, 3, 31).toLocalDate();
} else if (monthofYear <= 6) {
periodEndDate = new DateTime().withDate(year, 6, 30).toLocalDate();
} else if (monthofYear <= 9) {
periodEndDate = new DateTime().withDate(year, 9, 30).toLocalDate();
} else if (monthofYear <= 12) {
periodEndDate = new DateTime().withDate(year, 12, 31).toLocalDate();
}
break;
case BI_ANNUAL:
// // jan 1st to 30, jul 1st to dec 31
year = periodStartDate.getYearOfEra();
monthofYear = periodStartDate.getMonthOfYear();
if (monthofYear <= 6) {
periodEndDate = new DateTime().withDate(year, 6, 30).toLocalDate();
} else if (monthofYear <= 12) {
periodEndDate = new DateTime().withDate(year, 12, 31).toLocalDate();
}
break;
case ANNUAL:
periodEndDate = periodStartDate.monthOfYear().withMaximumValue();
periodEndDate = periodEndDate.dayOfMonth().withMaximumValue();
break;
// case NO_COMPOUNDING_SIMPLE_INTEREST:
// periodEndDate = periodStartDate.monthOfYear().withMaximumValue();
// periodEndDate = periodEndDate.dayOfMonth().withMaximumValue();
// break;
}
return periodEndDate;
}
public boolean isInterestTransfered() {
return this.interestTransfered;
}
}