FINERACT-2011: Savings Account - transaction order is incorrectly mixed after certain activities

This commit is contained in:
jmarta
2023-11-20 11:50:31 +01:00
committed by Adam Saghy
parent d0341e69cb
commit 4f2400ca3e
8 changed files with 371 additions and 221 deletions
@@ -64,10 +64,9 @@ public class GlobalConfigurationProperty extends AbstractPersistableCustom {
private boolean isTrapDoor;
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(7);
if (this.isTrapDoor == true) {
if (this.isTrapDoor) {
throw new GlobalConfigurationPropertyCannotBeModfied(this.getId());
}
@@ -109,7 +108,6 @@ public class GlobalConfigurationProperty extends AbstractPersistableCustom {
}
return actualChanges;
}
public static GlobalConfigurationProperty newSurveyConfiguration(final String name) {
@@ -120,6 +118,5 @@ public class GlobalConfigurationProperty extends AbstractPersistableCustom {
return new GlobalConfigurationPropertyData().setName(getName()).setEnabled(isEnabled()).setValue(getValue())
.setDateValue(getDateValue()).setStringValue(getStringValue()).setId(this.getId()).setDescription(this.description)
.setTrapDoor(this.isTrapDoor);
}
}
@@ -83,6 +83,7 @@ import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator;
import org.apache.fineract.interoperation.domain.InteropIdentifier;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -510,6 +511,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom {
final List<PostingPeriod> postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer,
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
postReversals);
if (postingPeriods.isEmpty()) {
return;
}
Money interestPostedToDate = Money.zero(this.currency);
@@ -811,109 +815,103 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom {
boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth,
final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final boolean postReversals) {
// no openingBalance concept supported yet but probably will to allow
// for migrations.
Money openingAccountBalance = null;
// no openingBalance concept supported yet but probably will to allow for migrations.
// Check global configurations and 'pivot' date is null
if (backdatedTxnsAllowedTill) {
openingAccountBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate());
} else {
openingAccountBalance = Money.zero(this.currency);
}
// update existing transactions so derived balance fields are
// correct.
Money openingAccountBalance = backdatedTxnsAllowedTill ? Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate())
: Money.zero(this.currency);
// update existing transactions so derived balance fields are correct.
recalculateDailyBalances(openingAccountBalance, upToInterestCalculationDate, backdatedTxnsAllowedTill, postReversals);
// 1. default to calculate interest based on entire history OR
// 2. determine latest 'posting period' and find interest credited to
// that period
// A generate list of EndOfDayBalances (not including interest postings)
final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType);
final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType
.fromInt(this.interestCompoundingPeriodType);
final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType
.fromInt(this.interestCalculationDaysInYearType);
List<LocalDate> postedAsOnDates = null;
if (backdatedTxnsAllowedTill) {
postedAsOnDates = getManualPostingDatesWithPivotConfig();
} else {
postedAsOnDates = getManualPostingDates();
}
if (postInterestOnDate != null) {
postedAsOnDates.add(postInterestOnDate);
}
final List<LocalDateInterval> postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(
getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth,
postedAsOnDates);
final List<PostingPeriod> allPostingPeriods = new ArrayList<>();
if (hasInterestCalculation() || hasOverdraftInterestCalculation()) {
// 1. default to calculate interest based on entire history OR
// 2. determine latest 'posting period' and find interest credited to that period
Money periodStartingBalance;
if (this.startInterestCalculationDate != null && !this.getStartInterestCalculationDate().equals(this.getActivationDate())) {
LocalDate startInterestCalculationDate = this.startInterestCalculationDate;
SavingsAccountTransaction transaction = null;
// A generate list of EndOfDayBalances (not including interest postings)
final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType
.fromInt(this.interestPostingPeriodType);
final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType
.fromInt(this.interestCompoundingPeriodType);
final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType
.fromInt(this.interestCalculationDaysInYearType);
List<LocalDate> postedAsOnDates = null;
if (backdatedTxnsAllowedTill) {
transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate);
postedAsOnDates = getManualPostingDatesWithPivotConfig();
} else {
transaction = findLastTransaction(startInterestCalculationDate);
postedAsOnDates = getManualPostingDates();
}
if (postInterestOnDate != null) {
postedAsOnDates.add(postInterestOnDate);
}
final List<LocalDateInterval> postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(
getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth,
postedAsOnDates);
if (transaction == null) {
Money periodStartingBalance;
if (this.startInterestCalculationDate != null && !this.getStartInterestCalculationDate().equals(this.getActivationDate())) {
LocalDate startInterestCalculationDate = this.startInterestCalculationDate;
SavingsAccountTransaction transaction = null;
if (backdatedTxnsAllowedTill) {
transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate);
} else {
transaction = findLastTransaction(startInterestCalculationDate);
}
if (transaction == null) {
periodStartingBalance = Money.zero(this.currency);
} else {
periodStartingBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate());
}
} else {
periodStartingBalance = Money.zero(this.currency);
} else {
periodStartingBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate());
}
} else {
periodStartingBalance = Money.zero(this.currency);
final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType
.fromInt(this.interestCalculationType);
final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate);
final BigDecimal overdraftInterestRateAsFraction = getEffectiveOverdraftInterestRateAsFraction(mc);
final Collection<Long> interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId());
final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation());
final Money minOverdraftForInterestCalculation = Money.of(getCurrency(), this.minOverdraftForInterestCalculation);
for (final LocalDateInterval periodInterval : postingPeriodIntervals) {
boolean isUserPosting = false;
if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) {
isUserPosting = true;
}
PostingPeriod postingPeriod = null;
List<SavingsAccountTransaction> orderedNonInterestPostingTransactions = null;
if (backdatedTxnsAllowedTill) {
orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig();
} else {
orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingTransactions();
}
List<SavingsAccountTransactionDetailsForPostingPeriod> savingsAccountTransactionDetailsForPostingPeriod = toSavingsAccountTransactionDetailsForPostingPeriodList(
orderedNonInterestPostingTransactions);
postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance,
savingsAccountTransactionDetailsForPostingPeriod, this.currency, compoundingPeriodType, interestCalculationType,
interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, interestPostTransactions,
isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth);
periodStartingBalance = postingPeriod.closingBalance();
allPostingPeriods.add(postingPeriod);
}
this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(),
isTransferInterestToOtherAccount());
this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods);
}
final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType);
final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate);
final BigDecimal overdraftInterestRateAsFraction = getEffectiveOverdraftInterestRateAsFraction(mc);
final Collection<Long> interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId());
final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation());
final Money minOverdraftForInterestCalculation = Money.of(getCurrency(), this.minOverdraftForInterestCalculation);
for (final LocalDateInterval periodInterval : postingPeriodIntervals) {
boolean isUserPosting = false;
if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) {
isUserPosting = true;
}
PostingPeriod postingPeriod = null;
List<SavingsAccountTransaction> orderedNonInterestPostingTransactions = null;
if (backdatedTxnsAllowedTill) {
orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig();
} else {
orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingTransactions();
}
List<SavingsAccountTransactionDetailsForPostingPeriod> savingsAccountTransactionDetailsForPostingPeriod = toSavingsAccountTransactionDetailsForPostingPeriodList(
orderedNonInterestPostingTransactions);
postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance,
savingsAccountTransactionDetailsForPostingPeriod, this.currency, compoundingPeriodType, interestCalculationType,
interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, interestPostTransactions,
isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd,
overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth);
periodStartingBalance = postingPeriod.closingBalance();
allPostingPeriods.add(postingPeriod);
}
this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(),
isTransferInterestToOtherAccount());
this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods);
if (backdatedTxnsAllowedTill) {
this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, null,
this.savingsAccountTransactions);
@@ -933,6 +931,14 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom {
return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc);
}
private boolean hasInterestCalculation() {
return !MathUtil.isEmpty(nominalAnnualInterestRate);
}
private boolean hasOverdraftInterestCalculation() {
return isAllowOverdraft() && !MathUtil.isEmpty(getOverdraftLimit()) && !MathUtil.isEmpty(nominalAnnualInterestRateOverdraft);
}
protected List<SavingsAccountTransaction> retreiveOrderedNonInterestPostingTransactions() {
final List<SavingsAccountTransaction> listOfTransactionsSorted = retrieveListOfTransactions();
@@ -982,7 +988,8 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom {
protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate,
final boolean backdatedTxnsAllowedTill, boolean postReversals) {
Money runningBalance = openingAccountBalance.copy();
Money runningBalance = openingAccountBalance;
boolean calculateInterest = hasInterestCalculation() || hasOverdraftInterestCalculation();
List<SavingsAccountTransaction> accountTransactionsSorted = null;
@@ -1017,15 +1024,14 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom {
}
runningBalance = runningBalance.plus(transactionAmount);
if (!transaction.getRunningBalance(this.currency).isEqualTo(transactionAmount)) {
transaction.setRunningBalance(runningBalance);
transaction.setRunningBalance(runningBalance);
if (MathUtil.isEmpty(overdraftAmount) && runningBalance.isLessThanZero() && !transaction.isAmountOnHold()) {
overdraftAmount = runningBalance.negated();
}
if (overdraftAmount.isZero() && runningBalance.isLessThanZero() && !transaction.isAmountOnHold()) {
overdraftAmount = overdraftAmount.plus(runningBalance.getAmount().negate());
}
if (transaction.getId() == null && overdraftAmount.isGreaterThanZero()) {
if (!calculateInterest || transaction.getId() == null) {
transaction.setOverdraftAmount(overdraftAmount);
} else if (overdraftAmount.isNotEqualTo(transaction.getOverdraftAmount(this.currency))) {
} else if (!MathUtil.isEqualTo(overdraftAmount, transaction.getOverdraftAmount(this.currency))) {
SavingsAccountTransaction accountTransaction = SavingsAccountTransaction.copyTransaction(transaction);
if (transaction.isChargeTransaction()) {
Set<SavingsAccountChargePaidBy> chargesPaidBy = transaction.getSavingsAccountChargesPaid();
@@ -1039,7 +1045,7 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom {
if (postReversals) {
reversal = SavingsAccountTransaction.reversal(transaction);
}
if (overdraftAmount.isGreaterThanZero()) {
if (MathUtil.isGreaterThanZero(overdraftAmount)) {
accountTransaction.setOverdraftAmount(overdraftAmount);
}
accountTransaction.setRunningBalance(runningBalance);
@@ -63,6 +63,7 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidati
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum;
import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService;
@@ -579,8 +580,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (savingsAccountData.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0 || (savingsAccountData.isAllowOverdraft()
&& savingsAccountData.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) {
if (MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRate()) || (savingsAccountData.isAllowOverdraft()
&& MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRateOverdraft()))) {
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds);