FINERACT-2312: Post interest with adjustments for savings accounts
This commit is contained in:
committed by
Adam Saghy
parent
ef1e2e1095
commit
a6fa1adc99
+10
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+54
-17
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ public class CompoundInterestValues {
|
||||
return this.uncompoundedInterest;
|
||||
}
|
||||
|
||||
public void setcompoundedInterest(BigDecimal interestToBeCompounded) {
|
||||
public void setCompoundedInterest(BigDecimal interestToBeCompounded) {
|
||||
this.compoundedInterest = interestToBeCompounded;
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -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,
|
||||
|
||||
+131
-12
@@ -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
|
||||
|
||||
+17
@@ -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);
|
||||
}
|
||||
|
||||
+18
@@ -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) {
|
||||
|
||||
+25
-25
@@ -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() });
|
||||
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[] { 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() });
|
||||
}
|
||||
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() });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+443
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user