This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new a6fa1adc99 FINERACT-2312: Post interest with adjustments for savings
accounts
a6fa1adc99 is described below
commit a6fa1adc99f62af7882930e5b4dbaee01077d7ff
Author: Juan-Pablo-Alvarez <[email protected]>
AuthorDate: Wed Jul 23 18:09:25 2025 -0600
FINERACT-2312: Post interest with adjustments for savings accounts
---
.../portfolio/savings/data/SavingsAccountData.java | 10 +
.../data/SavingsAccountTransactionData.java | 71 +++-
.../domain/interest/CompoundInterestHelper.java | 8 +-
.../domain/interest/CompoundInterestValues.java | 2 +-
.../savings/domain/interest/PostingPeriod.java | 10 +-
.../SavingsAccountInterestPostingServiceImpl.java | 143 ++++++-
.../SavingsAccountReadPlatformServiceImpl.java | 17 +
...countWritePlatformServiceJpaRepositoryImpl.java | 18 +
.../service/SavingsSchedularInterestPoster.java | 52 +--
.../SavingsInterestPostingTest.java | 443 +++++++++++++++++++++
.../common/savings/SavingsProductHelper.java | 13 +-
11 files changed, 726 insertions(+), 61 deletions(-)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java
index e1394661ce..42c5ab2fb3 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java
@@ -29,6 +29,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Set;
import lombok.Getter;
+import lombok.Setter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@@ -47,6 +48,7 @@ import org.apache.fineract.portfolio.tax.data.TaxGroupData;
/**
* Immutable data object representing a savings account.
*/
+@Setter
@Getter
@JsonLocalDateArrayFormat
public final class SavingsAccountData implements Serializable {
@@ -144,6 +146,13 @@ public final class SavingsAccountData implements
Serializable {
private transient Long glAccountIdForSavingsControl;
private transient Long glAccountIdForInterestOnSavings;
+ private Long glAccountIdForInterestPayable;
+ private Long glAccountIdForOverdraftPorfolio;
+ private Long glAccountIdForInterestReceivable;
+
+ private BigDecimal interestPosting;
+ private BigDecimal overdraftPosting;
+
public static SavingsAccountData importInstanceIndividual(Long clientId,
Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
BigDecimal nominalAnnualInterestRate, EnumOptionData
interestCompoundingPeriodTypeEnum,
EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData
interestCalculationTypeEnum,
@@ -964,4 +973,5 @@ public final class SavingsAccountData implements
Serializable {
public boolean isIsDormancyTrackingActive() {
return this.isDormancyTrackingActive;
}
+
}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java
index bf1028b5fd..47236df405 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java
@@ -105,6 +105,10 @@ public final class SavingsAccountTransactionData
implements Serializable {
private BigDecimal overdraftAmount;
private transient Long modifiedId;
private transient String refNo;
+ private Boolean isOverdraft;
+
+ private Long accountCredit;
+ private Long accountDebit;
private SavingsAccountTransactionData(final Long id, final
SavingsAccountTransactionEnumData transactionType,
final PaymentDetailData paymentDetailData, final Long savingsId,
final String savingsAccountNo, final LocalDate transactionDate,
@@ -112,7 +116,7 @@ public final class SavingsAccountTransactionData implements
Serializable {
final boolean reversed, final AccountTransferData transfer, final
Collection<PaymentTypeData> paymentTypeOptions,
final LocalDate submittedOnDate, final boolean
interestedPostedAsOn, final String submittedByUsername, final String note,
final Boolean isReversal, final Long originalTransactionId,
boolean isManualTransaction, final Boolean lienTransaction,
- final Long releaseTransactionId, final String reasonForBlock) {
+ final Long releaseTransactionId, final String reasonForBlock,
final Boolean isOverdraft) {
this.id = id;
this.transactionType = transactionType;
TransactionEntryType entryType = null;
@@ -146,6 +150,7 @@ public final class SavingsAccountTransactionData implements
Serializable {
this.lienTransaction = lienTransaction;
this.releaseTransactionId = releaseTransactionId;
this.reasonForBlock = reasonForBlock;
+ this.isOverdraft = isOverdraft;
}
private static SavingsAccountTransactionData createData(final Long id,
final SavingsAccountTransactionEnumData transactionType,
@@ -156,7 +161,7 @@ public final class SavingsAccountTransactionData implements
Serializable {
final Boolean lienTransaction) {
return new SavingsAccountTransactionData(id, transactionType,
paymentDetailData, accountId, accountNo, date, currency, amount,
outstandingChargeAmount, runningBalance, reversed, transfer,
paymentTypeOptions, submittedOnDate, interestedPostedAsOn,
- submittedByUsername, note, null, null, false, lienTransaction,
null, null);
+ submittedByUsername, note, null, null, false, lienTransaction,
null, null, false);
}
public static SavingsAccountTransactionData create(final Long id, final
SavingsAccountTransactionEnumData transactionType,
@@ -167,7 +172,8 @@ public final class SavingsAccountTransactionData implements
Serializable {
final Boolean lienTransaction, final Long releaseTransactionId,
final String reasonForBlock) {
return new SavingsAccountTransactionData(id, transactionType,
paymentDetailData, savingsId, savingsAccountNo, date, currency,
amount, outstandingChargeAmount, runningBalance, reversed,
transfer, null, submittedOnDate, interestedPostedAsOn,
- submittedByUsername, note, isReversal, originalTransactionId,
false, lienTransaction, releaseTransactionId, reasonForBlock);
+ submittedByUsername, note, isReversal, originalTransactionId,
false, lienTransaction, releaseTransactionId, reasonForBlock,
+ false);
}
public static SavingsAccountTransactionData create(final Long id, final
SavingsAccountTransactionEnumData transactionType,
@@ -235,10 +241,10 @@ public final class SavingsAccountTransactionData
implements Serializable {
private static SavingsAccountTransactionData createImport(final
SavingsAccountTransactionEnumData transactionType,
final PaymentDetailData paymentDetailData, final Long
savingsAccountId, final String accountNumber,
final LocalDate transactionDate, final BigDecimal
transactionAmount, final boolean reversed, final LocalDate submittedOnDate,
- boolean isManualTransaction, final Boolean lienTransaction) {
+ boolean isManualTransaction, final Boolean lienTransaction, final
Boolean isOverdraft) {
SavingsAccountTransactionData data = new
SavingsAccountTransactionData(null, transactionType, paymentDetailData,
savingsAccountId,
accountNumber, transactionDate, null, transactionAmount, null,
null, reversed, null, null, submittedOnDate, false, null,
- null, null, null, isManualTransaction, lienTransaction, null,
null);
+ null, null, null, isManualTransaction, lienTransaction, null,
null, isOverdraft);
// duplicated import fields
data.savingsAccountId = savingsAccountId;
data.accountNumber = accountNumber;
@@ -251,14 +257,14 @@ public final class SavingsAccountTransactionData
implements Serializable {
return createImport(accountTransaction.getTransactionType(),
accountTransaction.getPaymentDetailData(),
accountTransaction.getSavingsAccountId(), null,
accountTransaction.getTransactionDate(), accountTransaction.getAmount(),
accountTransaction.isReversed(),
accountTransaction.getSubmittedOnDate(),
accountTransaction.isManualTransaction(),
- accountTransaction.getLienTransaction());
+ accountTransaction.getLienTransaction(), false);
}
public static SavingsAccountTransactionData importInstance(BigDecimal
transactionAmount, LocalDate transactionDate, Long paymentTypeId,
String accountNumber, String checkNumber, String routingCode,
String receiptNumber, String bankNumber, String note,
Long savingsAccountId, SavingsAccountTransactionEnumData
transactionType, Integer rowIndex, String locale, String dateFormat) {
SavingsAccountTransactionData data = createImport(transactionType,
null, savingsAccountId, accountNumber, transactionDate,
- transactionAmount, false, transactionDate, false, false);
+ transactionAmount, false, transactionDate, false, false,
false);
data.rowIndex = rowIndex;
data.paymentTypeId = paymentTypeId;
data.checkNumber = checkNumber;
@@ -272,10 +278,11 @@ public final class SavingsAccountTransactionData
implements Serializable {
}
private static SavingsAccountTransactionData
createImport(SavingsAccountTransactionEnumData transactionType, Long
savingsAccountId,
- LocalDate transactionDate, BigDecimal transactionAmount, final
LocalDate submittedOnDate, boolean isManualTransaction) {
+ LocalDate transactionDate, BigDecimal transactionAmount, final
LocalDate submittedOnDate, boolean isManualTransaction,
+ Boolean isOverdraft) {
// import transaction
return createImport(transactionType, null, savingsAccountId, null,
transactionDate, transactionAmount, false, submittedOnDate,
- isManualTransaction, false);
+ isManualTransaction, false, isOverdraft);
}
public static SavingsAccountTransactionData interestPosting(final
SavingsAccountData savingsAccount, final LocalDate date,
@@ -285,17 +292,28 @@ public final class SavingsAccountTransactionData
implements Serializable {
SavingsAccountTransactionEnumData transactionType = new
SavingsAccountTransactionEnumData(
savingsAccountTransactionType.getValue().longValue(),
savingsAccountTransactionType.getCode(),
savingsAccountTransactionType.getValue().toString());
- return createImport(transactionType, savingsAccount.getId(), date,
amount.getAmount(), submittedOnDate, isManualTransaction);
+ return createImport(transactionType, savingsAccount.getId(), date,
amount.getAmount(), submittedOnDate, isManualTransaction, false);
+ }
+
+ public static SavingsAccountTransactionData accrual(final
SavingsAccountData savingsAccount, final LocalDate date, final Money amount,
+ final boolean isManualTransaction) {
+ final LocalDate submittedOnDate = DateUtils.getBusinessLocalDate();
+ final SavingsAccountTransactionType savingsAccountTransactionType =
SavingsAccountTransactionType.ACCRUAL;
+ SavingsAccountTransactionEnumData transactionType = new
SavingsAccountTransactionEnumData(
+ savingsAccountTransactionType.getValue().longValue(),
savingsAccountTransactionType.getCode(),
+ savingsAccountTransactionType.getValue().toString());
+ return createImport(transactionType, savingsAccount.getId(), date,
amount.getAmount(), submittedOnDate, isManualTransaction, false);
}
public static SavingsAccountTransactionData overdraftInterest(final
SavingsAccountData savingsAccount, final LocalDate date,
- final Money amount, final boolean isManualTransaction) {
+ final Money amount, final boolean isManualTransaction, final
Boolean isOverdraft) {
final LocalDate submittedOnDate = DateUtils.getBusinessLocalDate();
final SavingsAccountTransactionType savingsAccountTransactionType =
SavingsAccountTransactionType.OVERDRAFT_INTEREST;
SavingsAccountTransactionEnumData transactionType = new
SavingsAccountTransactionEnumData(
savingsAccountTransactionType.getValue().longValue(),
savingsAccountTransactionType.getCode(),
savingsAccountTransactionType.getValue().toString());
- return createImport(transactionType, savingsAccount.getId(), date,
amount.getAmount(), submittedOnDate, isManualTransaction);
+ return createImport(transactionType, savingsAccount.getId(), date,
amount.getAmount(), submittedOnDate, isManualTransaction,
+ isOverdraft);
}
public static SavingsAccountTransactionData withHoldTax(final
SavingsAccountData savingsAccount, final LocalDate date,
@@ -306,7 +324,7 @@ public final class SavingsAccountTransactionData implements
Serializable {
savingsAccountTransactionType.getValue().longValue(),
savingsAccountTransactionType.getCode(),
savingsAccountTransactionType.getValue().toString());
SavingsAccountTransactionData accountTransaction =
createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(),
- submittedOnDate, false);
+ submittedOnDate, false, false);
accountTransaction.addTaxDetails(taxDetails);
return accountTransaction;
}
@@ -387,10 +405,11 @@ public final class SavingsAccountTransactionData
implements Serializable {
final MonetaryCurrency currency = openingBalance.getCurrency();
Money endOfDayBalance = openingBalance.copy();
if (isDeposit() || isDividendPayoutAndNotReversed()) {
- endOfDayBalance = openingBalance.plus(getAmount());
+ endOfDayBalance = Money.of(currency, this.runningBalance);
} else if (isWithdrawal() || isChargeTransactionAndNotReversed()) {
-
- if (openingBalance.isGreaterThanZero()) {
+ if (isWithdrawal()) {
+ endOfDayBalance = Money.of(currency, this.runningBalance);
+ } else if (openingBalance.isGreaterThanZero()) {
endOfDayBalance = openingBalance.minus(getAmount());
} else {
endOfDayBalance = Money.of(currency, this.runningBalance);
@@ -400,6 +419,14 @@ public final class SavingsAccountTransactionData
implements Serializable {
return EndOfDayBalance.from(getTransactionDate(), openingBalance,
endOfDayBalance, this.balanceNumberOfDays);
}
+ public EndOfDayBalance toEndOfDayBalanceDates(final Money openingBalance,
LocalDateInterval date) {
+ final MonetaryCurrency currency = openingBalance.getCurrency();
+ Money endOfDayBalance = Money.of(currency, this.runningBalance);
+
+ return EndOfDayBalance.from(getTransactionDate(), openingBalance,
endOfDayBalance,
+ this.balanceNumberOfDays != null ? this.balanceNumberOfDays :
date.endDate().getDayOfMonth());
+ }
+
public boolean isChargeTransactionAndNotReversed() {
return this.transactionType.isChargeTransaction() && isNotReversed();
}
@@ -427,7 +454,9 @@ public final class SavingsAccountTransactionData implements
Serializable {
if (isDeposit() || isDividendPayoutAndNotReversed()) {
endOfDayBalance = endOfDayBalance.plus(getAmount());
} else if (isWithdrawal() || isChargeTransactionAndNotReversed()) {
- if (endOfDayBalance.isGreaterThanZero() || isAllowOverdraft) {
+ if (endOfDayBalance.isLessThanZero() && isAllowOverdraft) {
+ endOfDayBalance = Money.of(currency, this.runningBalance);
+ } else if (endOfDayBalance.isGreaterThanZero() ||
isAllowOverdraft) {
endOfDayBalance = endOfDayBalance.minus(getAmount());
} else {
endOfDayBalance = Money.of(currency, this.runningBalance);
@@ -658,4 +687,12 @@ public final class SavingsAccountTransactionData
implements Serializable {
public TransactionEntryType getEntryType() {
return entryType;
}
+
+ public void setAccountCredit(Long accountCredit) {
+ this.accountCredit = accountCredit;
+ }
+
+ public void setAccountDebit(Long accountDebit) {
+ this.accountDebit = accountDebit;
+ }
}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java
index 4b918c0198..46f588dd66 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java
@@ -47,9 +47,14 @@ public class CompoundInterestHelper {
// total interest earned in previous periods but not yet recognised
BigDecimal compoundedInterest = BigDecimal.ZERO;
BigDecimal unCompoundedInterest = BigDecimal.ZERO;
+ LocalDate endDay = DateUtils.getBusinessLocalDate();
final CompoundInterestValues compoundInterestValues = new
CompoundInterestValues(compoundedInterest, unCompoundedInterest);
for (final PostingPeriod postingPeriod : allPeriods) {
+ if (postingPeriod.dateOfPostingTransaction().getMonth() !=
endDay.getMonth()) {
+
compoundInterestValues.setCompoundedInterest(interestEarned.getAmount());
+ }
+
final BigDecimal interestEarnedThisPeriod =
postingPeriod.calculateInterest(compoundInterestValues);
final Money moneyToBePostedForPeriod = Money.of(currency,
interestEarnedThisPeriod);
@@ -61,8 +66,9 @@ public class CompoundInterestHelper {
// calculation.
if (!(postingPeriod.isInterestTransfered() ||
!interestTransferEnabled
|| (lockUntil != null &&
!DateUtils.isAfter(postingPeriod.dateOfPostingTransaction(), lockUntil)))) {
- compoundInterestValues.setcompoundedInterest(BigDecimal.ZERO);
+ compoundInterestValues.setCompoundedInterest(BigDecimal.ZERO);
}
+ endDay = postingPeriod.dateOfPostingTransaction();
}
return interestEarned;
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java
index 09a871e8db..68cfc07cd7 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java
@@ -38,7 +38,7 @@ public class CompoundInterestValues {
return this.uncompoundedInterest;
}
- public void setcompoundedInterest(BigDecimal interestToBeCompounded) {
+ public void setCompoundedInterest(BigDecimal interestToBeCompounded) {
this.compoundedInterest = interestToBeCompounded;
}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java
index a9decdc097..65130e998c 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java
@@ -26,6 +26,8 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.TreeSet;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -34,6 +36,8 @@ import
org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodTyp
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
import
org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
+@Setter
+@Getter
public final class PostingPeriod {
private final LocalDateInterval periodInterval;
@@ -64,6 +68,8 @@ public final class PostingPeriod {
private Integer financialYearBeginningMonth;
+ private boolean overdraftInterest = false;
+
public void setOverdraftInterestRateAsFraction(BigDecimal
overdraftInterestRateAsFraction) {
this.overdraftInterestRateAsFraction = overdraftInterestRateAsFraction;
}
@@ -301,7 +307,7 @@ public final class PostingPeriod {
if
(compoundingPeriodEndDate.equals(compoundingPeriod.getPeriodInterval().endDate()))
{
BigDecimal interestCompounded =
compoundInterestValues.getcompoundedInterest().add(unCompoundedInterest);
-
compoundInterestValues.setcompoundedInterest(interestCompounded);
+
compoundInterestValues.setCompoundedInterest(interestCompounded);
compoundInterestValues.setZeroForInterestToBeUncompounded();
}
interestEarned = interestEarned.add(interestUnrounded);
@@ -314,7 +320,7 @@ public final class PostingPeriod {
}
public Money getInterestEarned() {
- return this.interestEarnedRounded;
+ return this.interestEarnedRounded != null ? this.interestEarnedRounded
: Money.zero(this.currency);
}
private static List<CompoundingPeriod>
compoundingPeriodsInPostingPeriod(final LocalDateInterval postingPeriodInterval,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
index b952b24a51..79831d54f5 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java
@@ -32,6 +32,7 @@ import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.savings.DepositAccountType;
@@ -81,11 +82,17 @@ public class SavingsAccountInterestPostingServiceImpl
implements SavingsAccountI
for (final PostingPeriod interestPostingPeriod : postingPeriods) {
final LocalDate interestPostingTransactionDate =
interestPostingPeriod.dateOfPostingTransaction();
final Money interestEarnedToBePostedForPeriod =
interestPostingPeriod.getInterestEarned();
+ final Boolean isOverdraft =
interestPostingPeriod.isOverdraftInterest();
if (!DateUtils.isAfter(interestPostingTransactionDate,
interestPostingUpToDate)) {
interestPostedToDate =
interestPostedToDate.plus(interestEarnedToBePostedForPeriod);
- final SavingsAccountTransactionData postingTransaction =
findInterestPostingTransactionFor(interestPostingTransactionDate,
- savingsAccountData);
+ SavingsAccountTransactionData postingTransaction = null;
+ if
(this.depositAccountType(savingsAccountData).isSavingsDeposit() &&
savingsAccountData.isAllowOverdraft()) {
+ postingTransaction =
findInterestPostingTransactionForInterest(interestPostingTransactionDate,
savingsAccountData,
+ isOverdraft);
+ } else {
+ postingTransaction =
findInterestPostingTransactionFor(interestPostingTransactionDate,
savingsAccountData);
+ }
if (postingTransaction == null) {
SavingsAccountTransactionData newPostingTransaction;
@@ -95,7 +102,7 @@ public class SavingsAccountInterestPostingServiceImpl
implements SavingsAccountI
} else {
newPostingTransaction =
SavingsAccountTransactionData.overdraftInterest(savingsAccountData,
interestPostingTransactionDate,
interestEarnedToBePostedForPeriod.negated(),
- interestPostingPeriod.isUserPosting());
+ interestPostingPeriod.isUserPosting(),
isOverdraft);
}
savingsAccountData.updateTransactions(newPostingTransaction);
@@ -131,7 +138,7 @@ public class SavingsAccountInterestPostingServiceImpl
implements SavingsAccountI
} else {
newPostingTransaction =
SavingsAccountTransactionData.overdraftInterest(savingsAccountData,
interestPostingTransactionDate,
interestEarnedToBePostedForPeriod.negated(),
- interestPostingPeriod.isUserPosting());
+ interestPostingPeriod.isUserPosting(),
isOverdraft);
}
savingsAccountData.updateTransactions(newPostingTransaction);
@@ -190,6 +197,36 @@ public class SavingsAccountInterestPostingServiceImpl
implements SavingsAccountI
return transaction;
}
+ private Money appendPostingPeriodIfAny(final LocalDateInterval
periodInterval, Money periodStartingBalance,
+ final List<SavingsAccountTransactionData> txs, final
MonetaryCurrency monetaryCurrency,
+ final SavingsCompoundingInterestPeriodType compoundingPeriodType,
final SavingsInterestCalculationType interestCalculationType,
+ final BigDecimal interestRateAsFraction, final int daysInYear,
final LocalDate upToInterestCalculationDate,
+ final Collection<Long> interestPostTransactions, final boolean
isInterestTransfer, final Money minBalanceForInterestCalculation,
+ final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final
BigDecimal overdraftInterestRateAsFraction,
+ final Money minOverdraftForInterestCalculation, final boolean
isUserPosting, final Integer financialYearBeginningMonth,
+ final boolean allowOverdraft, final List<PostingPeriod>
allPostingPeriods, Boolean isOverdraftTransacction) {
+
+ if (txs == null || txs.isEmpty()) {
+ return periodStartingBalance;
+ }
+
+ final PostingPeriod postingPeriod =
PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, txs,
monetaryCurrency,
+ compoundingPeriodType, interestCalculationType,
interestRateAsFraction, daysInYear, upToInterestCalculationDate,
+ interestPostTransactions, isInterestTransfer,
minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd,
+ overdraftInterestRateAsFraction,
minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth,
+ allowOverdraft);
+
+ periodStartingBalance = postingPeriod.closingBalance();
+ postingPeriod.setOverdraftInterest(isOverdraftTransacction);
+
+ if (!(MathUtil.isZero(postingPeriod.getOpeningBalance().getAmount())
+ &&
MathUtil.isZero(postingPeriod.closingBalance().getAmount()))) {
+ allPostingPeriods.add(postingPeriod);
+ }
+
+ return periodStartingBalance;
+ }
+
public List<PostingPeriod> calculateInterestUsing(final MathContext mc,
final LocalDate upToInterestCalculationDate,
boolean isInterestTransfer, final boolean
isSavingsInterestPostingAtCurrentPeriodEnd, final Integer
financialYearBeginningMonth,
final LocalDate postInterestOnDate, final boolean
backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) {
@@ -268,16 +305,40 @@ public class SavingsAccountInterestPostingServiceImpl
implements SavingsAccountI
if
(postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) {
isUserPosting = true;
}
- final PostingPeriod postingPeriod =
PostingPeriod.createFromDTO(periodInterval, periodStartingBalance,
-
retreiveOrderedNonInterestPostingTransactions(savingsAccountData),
monetaryCurrency, compoundingPeriodType,
- interestCalculationType, interestRateAsFraction,
daysInYearType.getValue(), upToInterestCalculationDate,
- interestPostTransactions, isInterestTransfer,
minBalanceForInterestCalculation,
- isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
- isUserPosting, financialYearBeginningMonth,
savingsAccountData.isAllowOverdraft());
+ if (savingsAccountData.isAllowOverdraft() &&
!MathUtil.isZero(savingsAccountData.getGlAccountIdForInterestReceivable())) {
- periodStartingBalance = postingPeriod.closingBalance();
+ List<SavingsAccountTransactionData> overdraftTxs =
listForOverdraft(savingsAccountData, periodInterval);
+ List<SavingsAccountTransactionData> interestPostingTxs =
listForInterestPosting(savingsAccountData, periodInterval,
+ monetaryCurrency);
- allPostingPeriods.add(postingPeriod);
+ boolean isOverdraftAccountType =
isOverdraftAccount(savingsAccountData, periodInterval, monetaryCurrency);
+
+ List<SavingsAccountTransactionData> primaryInterestPublication
= isOverdraftAccountType ? overdraftTxs : interestPostingTxs;
+ List<SavingsAccountTransactionData>
secondaryInterestPublication = isOverdraftAccountType ? interestPostingTxs
+ : overdraftTxs;
+
+ periodStartingBalance =
appendPostingPeriodIfAny(periodInterval, periodStartingBalance,
primaryInterestPublication,
+ monetaryCurrency, compoundingPeriodType,
interestCalculationType, interestRateAsFraction, daysInYearType.getValue(),
+ upToInterestCalculationDate, interestPostTransactions,
isInterestTransfer, minBalanceForInterestCalculation,
+ isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
+ isUserPosting, financialYearBeginningMonth,
savingsAccountData.isAllowOverdraft(), allPostingPeriods,
+ isOverdraftAccountType ? true : false);
+
+ periodStartingBalance =
appendPostingPeriodIfAny(periodInterval, periodStartingBalance,
secondaryInterestPublication,
+ monetaryCurrency, compoundingPeriodType,
interestCalculationType, interestRateAsFraction, daysInYearType.getValue(),
+ upToInterestCalculationDate, interestPostTransactions,
isInterestTransfer, minBalanceForInterestCalculation,
+ isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
+ isUserPosting, financialYearBeginningMonth,
savingsAccountData.isAllowOverdraft(), allPostingPeriods,
+ isOverdraftAccountType ? false : true);
+
+ } else {
+ periodStartingBalance =
appendPostingPeriodIfAny(periodInterval, periodStartingBalance,
+
retreiveOrderedNonInterestPostingTransactions(savingsAccountData),
monetaryCurrency, compoundingPeriodType,
+ interestCalculationType, interestRateAsFraction,
daysInYearType.getValue(), upToInterestCalculationDate,
+ interestPostTransactions, isInterestTransfer,
minBalanceForInterestCalculation,
+ isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation,
+ isUserPosting, financialYearBeginningMonth,
savingsAccountData.isAllowOverdraft(), allPostingPeriods, false);
+ }
}
this.savingsHelper.calculateInterestForAllPostingPeriods(monetaryCurrency,
allPostingPeriods,
@@ -297,6 +358,52 @@ public class SavingsAccountInterestPostingServiceImpl
implements SavingsAccountI
return allPostingPeriods;
}
+ private List<SavingsAccountTransactionData> listForOverdraft(final
SavingsAccountData savingsAccountData,
+ final LocalDateInterval periodInterval) {
+ List<SavingsAccountTransactionData> overdraftTransactionsInPeriod =
new ArrayList<>();
+ for (SavingsAccountTransactionData lists :
retreiveOrderedNonInterestPostingTransactions(savingsAccountData)) {
+ if (MathUtil.isLessThanZero(lists.getRunningBalance()) &&
periodInterval.startDate().getMonth() == lists.getDate().getMonth()) {
+ overdraftTransactionsInPeriod.add(lists);
+
+ }
+ }
+ return overdraftTransactionsInPeriod;
+
+ }
+
+ private List<SavingsAccountTransactionData> listForInterestPosting(final
SavingsAccountData savingsAccountData,
+ final LocalDateInterval periodInterval, final MonetaryCurrency
currency) {
+
+ final List<SavingsAccountTransactionData> nonOverdraftTransactions =
new ArrayList<>();
+
+ for (final SavingsAccountTransactionData tx :
retreiveOrderedNonInterestPostingTransactions(savingsAccountData)) {
+ if (periodInterval.startDate().getMonth() ==
tx.getDate().getMonth()) {
+ final Money runningBalance = Money.of(currency,
tx.getRunningBalance());
+
+ if (runningBalance.isGreaterThanZero() &&
!runningBalance.isZero()) {
+ nonOverdraftTransactions.add(tx);
+ }
+ }
+ }
+ return nonOverdraftTransactions;
+ }
+
+ private Boolean isOverdraftAccount(final SavingsAccountData
savingsAccountData, final LocalDateInterval periodInterval,
+ final MonetaryCurrency currency) {
+
+ for (SavingsAccountTransactionData tx :
retreiveOrderedNonInterestPostingTransactions(savingsAccountData)) {
+ if (MathUtil.isLessThanZero(tx.getRunningBalance()) &&
periodInterval.startDate().getMonth() == tx.getDate().getMonth()) {
+ return true;
+ } else if (periodInterval.startDate().getMonth() ==
tx.getDate().getMonth()) {
+ final Money runningBalance = Money.of(currency,
tx.getRunningBalance());
+ if (!runningBalance.isZero()) {
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+
private List<SavingsAccountTransactionData>
retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData
savingsAccountData) {
final List<SavingsAccountTransactionData> listOfTransactionsSorted =
retrieveListOfTransactions(savingsAccountData);
@@ -509,6 +616,18 @@ public class SavingsAccountInterestPostingServiceImpl
implements SavingsAccountI
return postingTransation;
}
+ protected SavingsAccountTransactionData
findInterestPostingTransactionForInterest(final LocalDate postingDate,
+ final SavingsAccountData savingsAccountData, boolean isOverdraft) {
+ SavingsAccountTransactionData postingTransation = null;
+ List<SavingsAccountTransactionData> transactions =
savingsAccountData.getSavingsAccountTransactionData();
+ postingTransation = transactions.stream().filter(t -> {
+ Boolean interestSearch = isOverdraft ?
t.isOverdraftInterestAndNotReversed() : t.isInterestPostingAndNotReversed();
+ return interestSearch && t.occursOn(postingDate) &&
!t.isReversalTransaction();
+ }).findFirst().orElse(null);
+
+ return postingTransation;
+ }
+
protected void resetAccountTransactionsEndOfDayBalances(final
List<SavingsAccountTransactionData> accountTransactionsSorted,
final LocalDate interestPostingUpToDate, final SavingsAccountData
savingsAccountData) {
// loop over transactions in reverse
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
index a5de90375b..96084d007b 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java
@@ -337,6 +337,9 @@ public class SavingsAccountReadPlatformServiceImpl
implements SavingsAccountRead
"msac.id as chargeId, msac.amount as chargeAmount,
msac.charge_time_enum as chargeTimeType, msac.is_penalty as isPenaltyCharge, ");
sqlBuilder.append("txd.id as taxDetailsId, txd.amount as
taxAmount, ");
sqlBuilder.append("apm.gl_account_id as
glAccountIdForInterestOnSavings, apm1.gl_account_id as
glAccountIdForSavingsControl, ");
+ sqlBuilder.append(
+ "apm2.gl_account_id as
glAccountIdForInterestReceivable,apm3.gl_account_id as
glAccountIdForOverdraftPorfolio, ");
+ sqlBuilder.append("apm4.gl_account_id as
glAccountIdForInterestPayable, ");
sqlBuilder.append(
"mtc.id as taxComponentId, mtc.debit_account_id as
debitAccountId, mtc.credit_account_id as creditAccountId, mtc.percentage as
taxPercentage ");
sqlBuilder.append("from m_savings_account sa ");
@@ -356,6 +359,9 @@ public class SavingsAccountReadPlatformServiceImpl
implements SavingsAccountRead
"left join acc_product_mapping apm on apm.product_type = 2
and apm.product_id = sp.id and apm.financial_account_type=3 ");
sqlBuilder.append(
"left join acc_product_mapping apm1 on apm1.product_type =
2 and apm1.product_id = sp.id and apm1.financial_account_type=2 ");
+ sqlBuilder.append("left join acc_product_mapping apm2 on
apm2.product_id = sp.id and apm2.financial_account_type=18 ");
+ sqlBuilder.append("left join acc_product_mapping apm3 on
apm3.product_id = sp.id and apm3.financial_account_type = 11 ");
+ sqlBuilder.append("left join acc_product_mapping apm4 on
apm4.product_id = sp.id and apm4.financial_account_type = 17 ");
this.schemaSql = sqlBuilder.toString();
}
@@ -409,6 +415,11 @@ public class SavingsAccountReadPlatformServiceImpl
implements SavingsAccountRead
final Long glAccountIdForInterestOnSavings =
rs.getLong("glAccountIdForInterestOnSavings");
final Long glAccountIdForSavingsControl =
rs.getLong("glAccountIdForSavingsControl");
+ final Long glAccountIdForOverdraftPorfolio =
rs.getLong("glAccountIdForOverdraftPorfolio");
+ final Long glAccountIdForInterestReceivable =
rs.getLong("glAccountIdForInterestReceivable");
+
+ final Long glAccountIdForInterestPayable =
rs.getLong("glAccountIdForInterestPayable");
+
final Long productId = rs.getLong("productId");
final Integer accountType = rs.getInt("accountingType");
final AccountingRuleType accountingRuleType =
AccountingRuleType.fromInt(accountType);
@@ -565,6 +576,12 @@ public class SavingsAccountReadPlatformServiceImpl
implements SavingsAccountRead
savingsAccountData.setClientData(clientData);
savingsAccountData.setGroupGeneralData(groupGeneralData);
savingsAccountData.setSavingsProduct(savingsProductData);
+
+
savingsAccountData.setGlAccountIdForInterestReceivable(glAccountIdForInterestReceivable);
+
savingsAccountData.setGlAccountIdForOverdraftPorfolio(glAccountIdForOverdraftPorfolio);
+
+
savingsAccountData.setGlAccountIdForInterestPayable(glAccountIdForInterestPayable);
+
savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings);
savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl);
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
index b6705759cb..f30712b509 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
@@ -595,6 +595,7 @@ public class
SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
for (SavingsAccountTransactionData accountTransaction :
transactions) {
if (accountTransaction.getId() == null) {
savingsAccountData.setNewSavingsAccountTransactionData(accountTransaction);
+ selectAccountId(accountTransaction,
savingsAccountData);
}
}
}
@@ -604,6 +605,23 @@ public class
SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
return savingsAccountData;
}
+ public void selectAccountId(SavingsAccountTransactionData
accountTransaction, SavingsAccountData savingsAccountData) {
+ SavingsAccountTransactionType transactionType =
SavingsAccountTransactionType
+
.fromInt(accountTransaction.getTransactionType().getId().intValue());
+ if (transactionType.isOverDraftInterestPosting()) {
+ if
(MathUtil.isGreaterThanZero(accountTransaction.getRunningBalance())) {
+
accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForSavingsControl());
+
accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable());
+ } else {
+
accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForOverdraftPorfolio());
+
accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable());
+ }
+ } else {
+
accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForInterestPayable());
+
accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForSavingsControl());
+ }
+ }
+
@Override
public CommandProcessingResult reverseTransaction(final Long savingsId,
final Long transactionId,
final boolean allowAccountTransferModification, final JsonCommand
command) {
diff --git
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
index 295d3f55ae..913fbdb31e 100644
---
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
+++
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
@@ -108,33 +108,33 @@ public class SavingsSchedularInterestPoster {
for (SavingsAccountTransactionData savingsAccountTransactionData :
savingsAccountTransactionDataList) {
if (savingsAccountTransactionData.getId() == null) {
final String key =
savingsAccountTransactionData.getRefNo();
- if (savingsAccountTransactionDataHashMap.containsKey(key))
{
- final SavingsAccountTransactionData dataFromFetch =
savingsAccountTransactionDataHashMap.get(key);
-
savingsAccountTransactionData.setId(dataFromFetch.getId());
- if
(savingsAccountData.getGlAccountIdForSavingsControl() != 0
- &&
savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) {
- OffsetDateTime auditDatetime =
DateUtils.getAuditOffsetDateTime();
- paramsForGLInsertion.add(new Object[] {
savingsAccountData.getGlAccountIdForSavingsControl(),
- savingsAccountData.getOfficeId(), null,
currencyCode,
- SAVINGS_TRANSACTION_IDENTIFIER +
savingsAccountTransactionData.getId().toString(),
- savingsAccountTransactionData.getId(),
null, false, null, false,
-
savingsAccountTransactionData.getTransactionDate(),
JournalEntryType.CREDIT.getValue().longValue(),
- savingsAccountTransactionData.getAmount(),
null, JournalEntryType.CREDIT.getValue().longValue(),
- savingsAccountData.getId(), auditDatetime,
auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null,
-
savingsAccountTransactionData.getTransactionDate(), null, userId, userId,
- DateUtils.getBusinessLocalDate() });
-
- paramsForGLInsertion.add(new Object[] {
savingsAccountData.getGlAccountIdForInterestOnSavings(),
- savingsAccountData.getOfficeId(), null,
currencyCode,
- SAVINGS_TRANSACTION_IDENTIFIER +
savingsAccountTransactionData.getId().toString(),
- savingsAccountTransactionData.getId(),
null, false, null, false,
-
savingsAccountTransactionData.getTransactionDate(),
JournalEntryType.DEBIT.getValue().longValue(),
- savingsAccountTransactionData.getAmount(),
null, JournalEntryType.DEBIT.getValue().longValue(),
- savingsAccountData.getId(), auditDatetime,
auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null,
-
savingsAccountTransactionData.getTransactionDate(), null, userId, userId,
- DateUtils.getBusinessLocalDate() });
- }
+ final Boolean isOverdraft =
savingsAccountTransactionData.getIsOverdraft();
+ final SavingsAccountTransactionData dataFromFetch =
savingsAccountTransactionDataHashMap.get(key);
+ savingsAccountTransactionData.setId(dataFromFetch.getId());
+ if (savingsAccountData.getGlAccountIdForSavingsControl()
!= 0
+ &&
savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) {
+ OffsetDateTime auditDatetime =
DateUtils.getAuditOffsetDateTime();
+ paramsForGLInsertion.add(
+ new Object[] {
savingsAccountTransactionData.getAccountCredit(),
savingsAccountData.getOfficeId(), null,
+ currencyCode,
SAVINGS_TRANSACTION_IDENTIFIER +
savingsAccountTransactionData.getId().toString(),
+ savingsAccountTransactionData.getId(),
null, false, null, false,
+
savingsAccountTransactionData.getTransactionDate(),
JournalEntryType.CREDIT.getValue().longValue(),
+
savingsAccountTransactionData.getAmount(), null,
JournalEntryType.CREDIT.getValue().longValue(),
+ savingsAccountData.getId(),
auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO,
+ null,
savingsAccountTransactionData.getTransactionDate(), null, userId, userId,
+ DateUtils.getBusinessLocalDate() });
+
+ paramsForGLInsertion
+ .add(new Object[] {
savingsAccountTransactionData.getAccountDebit(),
savingsAccountData.getOfficeId(), null,
+ currencyCode,
SAVINGS_TRANSACTION_IDENTIFIER +
savingsAccountTransactionData.getId().toString(),
+ savingsAccountTransactionData.getId(),
null, false, null, false,
+
savingsAccountTransactionData.getTransactionDate(),
JournalEntryType.DEBIT.getValue().longValue(),
+
savingsAccountTransactionData.getAmount(), null,
JournalEntryType.DEBIT.getValue().longValue(),
+ savingsAccountData.getId(),
auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO,
+ null,
savingsAccountTransactionData.getTransactionDate(), null, userId, userId,
+ DateUtils.getBusinessLocalDate() });
}
+
}
}
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
new file mode 100644
index 0000000000..7e03e49487
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
@@ -0,0 +1,443 @@
+/**
+ * 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.integrationtests;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
+import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.CommonConstants;
+import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
+import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
+import
org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
+import
org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
+import
org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SavingsInterestPostingTest {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(SavingsInterestPostingTest.class);
+ private static ResponseSpecification responseSpec;
+ private static RequestSpecification requestSpec;
+ private AccountHelper accountHelper;
+ private SavingsAccountHelper savingsAccountHelper;
+ private SchedulerJobHelper schedulerJobHelper;
+ public static final String MINIMUM_OPENING_BALANCE = "1000.0";
+ private GlobalConfigurationHelper globalConfigurationHelper;
+ private SavingsProductHelper productHelper;
+ private JournalEntryHelper journalEntryHelper;
+
+ @BeforeEach
+ public void setup() {
+ Utils.initializeRESTAssured();
+ this.requestSpec = new
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ this.requestSpec.header("Authorization", "Basic " +
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+ this.responseSpec = new
ResponseSpecBuilder().expectStatusCode(200).build();
+ this.schedulerJobHelper = new SchedulerJobHelper(this.requestSpec);
+ this.accountHelper = new AccountHelper(this.requestSpec,
this.responseSpec);
+ this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec,
this.responseSpec);
+ this.journalEntryHelper = new JournalEntryHelper(this.requestSpec,
this.responseSpec);
+ this.globalConfigurationHelper = new GlobalConfigurationHelper();
+ }
+
+ @Test
+ public void testPostInterestWithOverdraftProduct() {
+ try {
+ final String amount = "10000";
+ final String jobName = "Post Interest For Savings";
+
+ final Account assetAccount = accountHelper.createAssetAccount();
+ final Account incomeAccount = accountHelper.createIncomeAccount();
+ final Account expenseAccount =
accountHelper.createExpenseAccount();
+ final Account liabilityAccount =
accountHelper.createLiabilityAccount();
+ final Account interestReceivableAccount =
accountHelper.createAssetAccount("interestReceivableAccount");
+ final Account savingsControlAccount =
accountHelper.createLiabilityAccount("Savings Control");
+ final Account interestPayableAccount =
accountHelper.createLiabilityAccount("Interest Payable");
+
+ final Integer productId =
createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(
+ interestPayableAccount.getAccountID().toString(),
savingsControlAccount.getAccountID().toString(),
+ interestReceivableAccount.getAccountID().toString(),
assetAccount, incomeAccount, expenseAccount, liabilityAccount);
+
+ final Integer clientId = ClientHelper.createClient(requestSpec,
responseSpec, "01 January 2025");
+ final LocalDate startDate =
LocalDate.of(LocalDate.now(Utils.getZoneIdOfTenant()).getYear(), 2, 1);
+ final String startDateString = DateTimeFormatter.ofPattern("dd
MMMM yyyy", Locale.US).format(startDate);
+
+ final Integer accountId =
savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
+ SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL,
startDateString);
+ savingsAccountHelper.approveSavingsOnDate(accountId,
startDateString);
+ savingsAccountHelper.activateSavings(accountId, startDateString);
+ savingsAccountHelper.depositToSavingsAccount(accountId, amount,
startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ // Simulate time passing - update business date to March
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+ new PutGlobalConfigurationsRequest().enabled(true));
+ LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
+ BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec,
BusinessDateType.BUSINESS_DATE, marchDate);
+
+ schedulerJobHelper.executeAndAwaitJob(jobName);
+
+ long days = ChronoUnit.DAYS.between(startDate, marchDate);
+ BigDecimal expected = calcInterestPosting(productHelper, amount,
days);
+
+ List<HashMap> txs = getInterestTransactions(accountId);
+ for (HashMap tx : txs) {
+ Assertions.assertEquals(expected, BigDecimal.valueOf(((Double)
tx.get("amount"))));
+ }
+
+ long interestCount = countInterestOnDate(accountId, marchDate);
+ long overdraftCount = countOverdraftOnDate(accountId, marchDate);
+ Assertions.assertEquals(1L, interestCount, "Expected exactly one
INTEREST posting on posting date");
+ Assertions.assertEquals(0L, overdraftCount, "Expected NO OVERDRAFT
posting on posting date");
+ } finally {
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+ new PutGlobalConfigurationsRequest().enabled(false));
+ }
+ }
+
+ @Test
+ public void testOverdraftInterestWithOverdraftProduct() {
+ try {
+ final String amount = "10000";
+ final String jobName = "Post Interest For Savings";
+
+ final Account assetAccount = accountHelper.createAssetAccount();
+ final Account incomeAccount = accountHelper.createIncomeAccount();
+ final Account expenseAccount =
accountHelper.createExpenseAccount();
+ final Account liabilityAccount =
accountHelper.createLiabilityAccount();
+ final Account interestReceivableAccount =
accountHelper.createAssetAccount("interestReceivableAccount");
+ final Account savingsControlAccount =
accountHelper.createLiabilityAccount("Savings Control");
+ final Account interestPayableAccount =
accountHelper.createLiabilityAccount("Interest Payable");
+
+ final Integer productId =
createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(
+ interestPayableAccount.getAccountID().toString(),
savingsControlAccount.getAccountID().toString(),
+ interestReceivableAccount.getAccountID().toString(),
assetAccount, incomeAccount, expenseAccount, liabilityAccount);
+
+ final Integer clientId = ClientHelper.createClient(requestSpec,
responseSpec, "01 January 2025");
+ final LocalDate startDate =
LocalDate.of(LocalDate.now(Utils.getZoneIdOfTenant()).getYear(), 2, 1);
+ final String startDateString = DateTimeFormatter.ofPattern("dd
MMMM yyyy", Locale.US).format(startDate);
+
+ final Integer accountId =
savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
+ SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL,
startDateString);
+ savingsAccountHelper.approveSavingsOnDate(accountId,
startDateString);
+ savingsAccountHelper.activateSavings(accountId, startDateString);
+ savingsAccountHelper.withdrawalFromSavingsAccount(accountId,
amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ // Simulate time passing - update business date to March
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+ new PutGlobalConfigurationsRequest().enabled(true));
+ LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
+ BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec,
BusinessDateType.BUSINESS_DATE, marchDate);
+
+ schedulerJobHelper.executeAndAwaitJob(jobName);
+
+ long days = ChronoUnit.DAYS.between(startDate, marchDate);
+ BigDecimal expected = calcOverdraftPosting(productHelper, amount,
days);
+
+ List<HashMap> txs = getInterestTransactions(accountId);
+ Assertions.assertEquals(expected, BigDecimal.valueOf(((Double)
txs.get(0).get("amount"))));
+
+ BigDecimal runningBalance = BigDecimal.valueOf(((Double)
txs.get(0).get("runningBalance")));
+ Assertions.assertTrue(MathUtil.isLessThanZero(runningBalance),
"Running balance is not less than zero");
+
+ long interestCount = countInterestOnDate(accountId, marchDate);
+ long overdraftCount = countOverdraftOnDate(accountId, marchDate);
+ Assertions.assertEquals(0L, interestCount, "Expected NO INTEREST
posting on posting date");
+ Assertions.assertEquals(1L, overdraftCount, "Expected exactly one
OVERDRAFT posting on posting date");
+ } finally {
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+ new PutGlobalConfigurationsRequest().enabled(false));
+ }
+ }
+
+ @Test
+ public void
testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceLessZero() {
+ try {
+ final String amountDeposit = "10000";
+ final String amountWithdrawal = "20000";
+ final String jobName = "Post Interest For Savings";
+
+ final Account assetAccount = accountHelper.createAssetAccount();
+ final Account incomeAccount = accountHelper.createIncomeAccount();
+ final Account expenseAccount =
accountHelper.createExpenseAccount();
+ final Account liabilityAccount =
accountHelper.createLiabilityAccount();
+ final Account interestReceivableAccount =
accountHelper.createAssetAccount("interestReceivableAccount");
+ final Account savingsControlAccount =
accountHelper.createLiabilityAccount("Savings Control");
+ final Account interestPayableAccount =
accountHelper.createLiabilityAccount("Interest Payable");
+
+ final Integer productId =
createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(
+ interestPayableAccount.getAccountID().toString(),
savingsControlAccount.getAccountID().toString(),
+ interestReceivableAccount.getAccountID().toString(),
assetAccount, incomeAccount, expenseAccount, liabilityAccount);
+
+ final Integer clientId = ClientHelper.createClient(requestSpec,
responseSpec, "01 January 2025");
+ final LocalDate startDate =
LocalDate.of(LocalDate.now(Utils.getZoneIdOfTenant()).getYear(), 2, 1);
+ final String startStr = DateTimeFormatter.ofPattern("dd MMMM
yyyy", Locale.US).format(startDate);
+
+ final Integer accountId =
savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
+ SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startStr);
+ savingsAccountHelper.approveSavingsOnDate(accountId, startStr);
+ savingsAccountHelper.activateSavings(accountId, startStr);
+ savingsAccountHelper.depositToSavingsAccount(accountId,
amountDeposit, startStr, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ final LocalDate withdrawalDate = LocalDate.of(startDate.getYear(),
2, 16);
+ final String withdrawalStr = DateTimeFormatter.ofPattern("dd MMMM
yyyy", Locale.US).format(withdrawalDate);
+ savingsAccountHelper.withdrawalFromSavingsAccount(accountId,
amountWithdrawal, withdrawalStr,
+ CommonConstants.RESPONSE_RESOURCE_ID);
+
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+ new PutGlobalConfigurationsRequest().enabled(true));
+ LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
+ BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec,
BusinessDateType.BUSINESS_DATE, marchDate);
+
+ schedulerJobHelper.executeAndAwaitJob(jobName);
+
+ List<HashMap> txs = getInterestTransactions(accountId);
+ for (HashMap tx : txs) {
+ BigDecimal amt = BigDecimal.valueOf(((Double)
tx.get("amount")));
+ @SuppressWarnings("unchecked")
+ Map<String, Object> typeMap = (Map<String, Object>)
tx.get("transactionType");
+ SavingsAccountTransactionType type =
SavingsAccountTransactionType.fromInt(((Double) typeMap.get("id")).intValue());
+
+ if (type.isInterestPosting()) {
+ long days = ChronoUnit.DAYS.between(startDate,
withdrawalDate);
+ BigDecimal expected = calcInterestPosting(productHelper,
amountDeposit, days);
+ Assertions.assertEquals(expected, amt);
+ } else {
+ long days = ChronoUnit.DAYS.between(withdrawalDate,
marchDate);
+ BigDecimal overdraftBase = new
BigDecimal(amountWithdrawal).subtract(new BigDecimal(amountDeposit));
+ BigDecimal expected = calcOverdraftPosting(productHelper,
overdraftBase.toString(), days);
+ Assertions.assertEquals(expected, amt);
+ }
+ }
+
+ Assertions.assertEquals(1L, countInterestOnDate(accountId,
marchDate), "Expected exactly one INTEREST posting on posting date");
+ Assertions.assertEquals(1L, countOverdraftOnDate(accountId,
marchDate),
+ "Expected exactly one OVERDRAFT posting on posting date");
+ } finally {
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+ new PutGlobalConfigurationsRequest().enabled(false));
+ }
+ }
+
+ @Test
+ public void
testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceGreaterZero() {
+ try {
+ final String amountDeposit = "20000";
+ final String amountWithdrawal = "10000";
+ final String jobName = "Post Interest For Savings";
+
+ final Account assetAccount = accountHelper.createAssetAccount();
+ final Account incomeAccount = accountHelper.createIncomeAccount();
+ final Account expenseAccount =
accountHelper.createExpenseAccount();
+ final Account liabilityAccount =
accountHelper.createLiabilityAccount();
+ final Account interestReceivableAccount =
accountHelper.createAssetAccount("interestReceivableAccount");
+ final Account savingsControlAccount =
accountHelper.createLiabilityAccount("Savings Control");
+ final Account interestPayableAccount =
accountHelper.createLiabilityAccount("Interest Payable");
+
+ final Integer productId =
createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(
+ interestPayableAccount.getAccountID().toString(),
savingsControlAccount.getAccountID().toString(),
+ interestReceivableAccount.getAccountID().toString(),
assetAccount, incomeAccount, expenseAccount, liabilityAccount);
+
+ final Integer clientId = ClientHelper.createClient(requestSpec,
responseSpec, "01 January 2025");
+ final LocalDate startDate =
LocalDate.of(LocalDate.now(Utils.getZoneIdOfTenant()).getYear(), 2, 1);
+ final String startStr = DateTimeFormatter.ofPattern("dd MMMM
yyyy", Locale.US).format(startDate);
+
+ final Integer accountId =
savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
+ SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startStr);
+ savingsAccountHelper.approveSavingsOnDate(accountId, startStr);
+ savingsAccountHelper.activateSavings(accountId, startStr);
+ savingsAccountHelper.withdrawalFromSavingsAccount(accountId,
amountWithdrawal, startStr, CommonConstants.RESPONSE_RESOURCE_ID);
+
+ final LocalDate depositDate = LocalDate.of(startDate.getYear(), 2,
16);
+ final String depositStr = DateTimeFormatter.ofPattern("dd MMMM
yyyy", Locale.US).format(depositDate);
+ savingsAccountHelper.depositToSavingsAccount(accountId,
amountDeposit, depositStr, CommonConstants.RESPONSE_RESOURCE_ID);
+
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+ new PutGlobalConfigurationsRequest().enabled(true));
+ LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
+ BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec,
BusinessDateType.BUSINESS_DATE, marchDate);
+
+ schedulerJobHelper.executeAndAwaitJob(jobName);
+
+ List<HashMap> txs = getInterestTransactions(accountId);
+ for (HashMap tx : txs) {
+ BigDecimal amt = BigDecimal.valueOf(((Double)
tx.get("amount")));
+ @SuppressWarnings("unchecked")
+ Map<String, Object> typeMap = (Map<String, Object>)
tx.get("transactionType");
+ SavingsAccountTransactionType type =
SavingsAccountTransactionType.fromInt(((Double) typeMap.get("id")).intValue());
+
+ if (type.isOverDraftInterestPosting()) {
+ long days = ChronoUnit.DAYS.between(startDate,
depositDate);
+ BigDecimal expected = calcOverdraftPosting(productHelper,
amountWithdrawal, days);
+ Assertions.assertEquals(expected, amt);
+ } else {
+ long days = ChronoUnit.DAYS.between(depositDate,
marchDate);
+ BigDecimal positiveBase = new
BigDecimal(amountDeposit).subtract(new BigDecimal(amountWithdrawal));
+ BigDecimal expected = calcInterestPosting(productHelper,
positiveBase.toString(), days);
+ Assertions.assertEquals(expected, amt);
+ }
+ }
+
+ Assertions.assertEquals(1L, countOverdraftOnDate(accountId,
marchDate),
+ "Expected exactly one OVERDRAFT posting on posting date");
+ Assertions.assertEquals(1L, countInterestOnDate(accountId,
marchDate), "Expected exactly one INTEREST posting on posting date");
+ } finally {
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+ new PutGlobalConfigurationsRequest().enabled(false));
+ }
+ }
+
+ private List<HashMap> getInterestTransactions(Integer savingsAccountId) {
+ List<HashMap> all =
savingsAccountHelper.getSavingsTransactions(savingsAccountId);
+ List<HashMap> filtered = new ArrayList<>();
+ for (HashMap tx : all) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> txType = (Map<String, Object>)
tx.get("transactionType");
+ SavingsAccountTransactionType type =
SavingsAccountTransactionType.fromInt(((Double) txType.get("id")).intValue());
+ if (type.isInterestPosting() || type.isOverDraftInterestPosting())
{
+ filtered.add(tx);
+ }
+ }
+ return filtered;
+ }
+
+ public Integer
createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(final String
interestPayableAccount,
+ final String savingsControlAccount, final String
interestReceivableAccount, final Account... accounts) {
+
+ LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT
WITHOUT OVERDRAFT ---------------------------------------");
+ this.productHelper = new
SavingsProductHelper().withOverDraftRate("100000", "21")
+
.withAccountInterestReceivables(interestReceivableAccount).withSavingsControlAccountId(savingsControlAccount)
+
.withInterestPayableAccountId(interestPayableAccount).withInterestCompoundingPeriodTypeAsAnnually()
+
.withInterestPostingPeriodTypeAsMonthly().withInterestCalculationPeriodTypeAsDailyBalance()
+ .withAccountingRuleAsAccrualBased(accounts);
+
+ final String savingsProductJSON = this.productHelper.build();
+ return SavingsProductHelper.createSavingsProduct(savingsProductJSON,
requestSpec, responseSpec);
+ }
+
+ private BigDecimal calcInterestPosting(SavingsProductHelper productHelper,
String amount, long days) {
+ BigDecimal rate =
productHelper.getNominalAnnualInterestRate().divide(new BigDecimal("100.00"));
+ BigDecimal principal = new BigDecimal(amount);
+ BigDecimal dayFactor =
BigDecimal.ONE.divide(productHelper.getInterestCalculationDaysInYearType(),
MathContext.DECIMAL64);
+ BigDecimal dailyRate = rate.multiply(dayFactor, MathContext.DECIMAL64);
+ BigDecimal periodRate = dailyRate.multiply(BigDecimal.valueOf(days),
MathContext.DECIMAL64);
+ return principal.multiply(periodRate,
MathContext.DECIMAL64).setScale(productHelper.getDecimalCurrency(),
RoundingMode.HALF_EVEN);
+ }
+
+ private BigDecimal calcOverdraftPosting(SavingsProductHelper
productHelper, String amount, long days) {
+ BigDecimal rate =
productHelper.getNominalAnnualInterestRateOverdraft().divide(new
BigDecimal("100.00"));
+ BigDecimal principal = new BigDecimal(amount);
+ BigDecimal dayFactor =
BigDecimal.ONE.divide(productHelper.getInterestCalculationDaysInYearType(),
MathContext.DECIMAL64);
+ BigDecimal dailyRate = rate.multiply(dayFactor, MathContext.DECIMAL64);
+ BigDecimal periodRate = dailyRate.multiply(BigDecimal.valueOf(days),
MathContext.DECIMAL64);
+ return principal.multiply(periodRate,
MathContext.DECIMAL64).setScale(productHelper.getDecimalCurrency(),
RoundingMode.HALF_EVEN);
+ }
+
+ // =========================
+ // Helpers robustos de FECHA/TIPO
+ // =========================
+
+ @SuppressWarnings("unchecked")
+ private LocalDate coerceToLocalDate(HashMap tx) {
+ // Posibles claves de fecha devueltas por Fineract
+ String[] candidateKeys = new String[] { "date", "transactionDate",
"submittedOnDate", "createdDate" };
+
+ for (String key : candidateKeys) {
+ Object v = tx.get(key);
+ if (v == null) {
+ continue;
+ }
+
+ // Caso 1: arreglo [yyyy, MM, dd]
+ if (v instanceof List<?>) {
+ List<?> arr = (List<?>) v;
+ if (arr.size() >= 3 && arr.get(0) instanceof Number &&
arr.get(1) instanceof Number && arr.get(2) instanceof Number) {
+ int year = ((Number) arr.get(0)).intValue();
+ int month = ((Number) arr.get(1)).intValue();
+ int day = ((Number) arr.get(2)).intValue();
+ return LocalDate.of(year, month, day);
+ }
+ }
+
+ // Caso 2: string con distintos formatos
+ if (v instanceof String) {
+ String s = (String) v;
+ DateTimeFormatter[] fmts = new DateTimeFormatter[] {
DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US),
+ DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.US),
DateTimeFormatter.ofPattern("yyyy-MM-dd") };
+ for (DateTimeFormatter f : fmts) {
+ try {
+ return LocalDate.parse(s, f);
+ } catch (Exception ignore) {
+ // intentionally ignored
+ }
+ }
+ }
+ }
+ // Si ninguna clave/forma se pudo parsear
+ return null;
+ }
+
+ private boolean isDate(HashMap tx, LocalDate expected) {
+ LocalDate got = coerceToLocalDate(tx);
+ return got != null && got.isEqual(expected);
+ }
+
+ @SuppressWarnings("unchecked")
+ private SavingsAccountTransactionType txType(HashMap tx) {
+ Map<String, Object> typeMap = (Map<String, Object>)
tx.get("transactionType");
+ return SavingsAccountTransactionType.fromInt(((Double)
typeMap.get("id")).intValue());
+ }
+
+ private long countInterestOnDate(Integer accountId, LocalDate date) {
+ List<HashMap> all =
savingsAccountHelper.getSavingsTransactions(accountId);
+ return all.stream().filter(tx -> isDate(tx,
date)).map(this::txType).filter(SavingsAccountTransactionType::isInterestPosting)
+ .count();
+ }
+
+ private long countOverdraftOnDate(Integer accountId, LocalDate date) {
+ List<HashMap> all =
savingsAccountHelper.getSavingsTransactions(accountId);
+ return all.stream().filter(tx -> isDate(tx, date)).map(this::txType)
+
.filter(SavingsAccountTransactionType::isOverDraftInterestPosting).count();
+ }
+}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java
index 2aa7725cc0..89a4f7487d 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java
@@ -168,8 +168,8 @@ public class SavingsProductHelper {
map.put("interestReceivableAccountId",
this.interestReceivableAccountId);
}
if (this.accountingRule.equals(ACCRUAL_PERIODIC)) {
- if (this.interestReceivableAccountId != null) {
- map.put("interestReceivableAccountId",
this.interestReceivableAccountId);
+ if (this.savingsControlAccountId != null) {
+ map.put("savingsControlAccountId",
this.savingsControlAccountId);
}
}
@@ -202,6 +202,11 @@ public class SavingsProductHelper {
return this;
}
+ public SavingsProductHelper withInterestCompoundingPeriodTypeAsAnnually() {
+ this.interestCompoundingPeriodType = ANNUAL;
+ return this;
+ }
+
public SavingsProductHelper withInterestPostingPeriodTypeAsMonthly() {
this.interestPostingPeriodType = MONTHLY;
return this;
@@ -357,6 +362,10 @@ public class SavingsProductHelper {
return new BigDecimal(nominalAnnualInterestRate);
}
+ public BigDecimal getNominalAnnualInterestRateOverdraft() {
+ return new BigDecimal(nominalAnnualInterestRateOverdraft);
+ }
+
public BigDecimal getInterestCalculationDaysInYearType() {
return new BigDecimal(interestCalculationDaysInYearType);
}