FINERACT-2312: Post interest with adjustments for savings accounts

This commit is contained in:
Juan-Pablo-Alvarez
2025-09-04 10:24:42 +02:00
committed by Adam Saghy
parent ef1e2e1095
commit a6fa1adc99
11 changed files with 725 additions and 60 deletions
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
@@ -38,7 +38,7 @@ public class CompoundInterestValues {
return this.uncompoundedInterest;
}
public void setcompoundedInterest(BigDecimal interestToBeCompounded) {
public void setCompoundedInterest(BigDecimal interestToBeCompounded) {
this.compoundedInterest = interestToBeCompounded;
}
@@ -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,