FINERACT-2319: Add penalty-first payment allocation strategy for progressive loans with backdated repayment charge handling

This commit is contained in:
Oleksii Novikov
2025-07-10 13:38:56 +02:00
committed by Adam Saghy
parent 68b2fcb7e1
commit fc93db16b5
12 changed files with 219 additions and 49 deletions
@@ -49,11 +49,13 @@ import java.util.ListIterator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
@@ -79,6 +81,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRef
import org.apache.fineract.portfolio.rate.domain.Rate;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.lang.NonNull;
@Entity
@Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"),
@@ -1242,6 +1245,22 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.charges == null ? new HashSet<>() : this.charges.stream().filter(LoanCharge::isActive).collect(Collectors.toSet());
}
public boolean hasChargesAffectedByBackdatedRepaymentLikeTransaction(@NonNull final LoanTransaction transaction) {
if (!transaction.isRepaymentLikeType() || CollectionUtils.isEmpty(this.charges) || !isProgressiveSchedule()
|| !DateUtils.isBeforeBusinessDate(transaction.getTransactionDate())) {
return false;
}
final BiFunction<LocalDate, LocalDate, LocalDate> earlierDate = (date1, date2) -> DateUtils.isBefore(date1, date2) ? date1 : date2;
return this.charges.stream().filter(LoanCharge::isActive)
.filter(loanCharge -> loanCharge.isSpecifiedDueDate() || loanCharge.isOverdueInstallmentCharge())
.filter(loanCharge -> loanCharge.getDueLocalDate() != null).anyMatch(loanCharge -> {
final LocalDate comparisonDate = earlierDate.apply(loanCharge.getDueLocalDate(), loanCharge.getSubmittedOnDate());
return comparisonDate != null && comparisonDate.isAfter(transaction.getTransactionDate());
});
}
public LoanCharge fetchLoanChargesById(final Long id) {
LoanCharge charge = null;
for (LoanCharge loanCharge : this.charges) {
@@ -133,6 +133,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
boolean processLatest = isTransactionChronologicallyLatest //
&& adjustedTransaction == null // covers reversals
&& !loan.isForeclosure() //
&& !loan.hasChargesAffectedByBackdatedRepaymentLikeTransaction(loanTransaction)
&& loanTransactionProcessingService.canProcessLatestTransactionOnly(loan, loanTransaction, currentInstallment); //
if (processLatest) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction,