FINERACT-2059: Re-aging repayment schedule handling
This commit is contained in:
+2
-1
@@ -24,7 +24,8 @@ public interface LoanReAgingApiConstants {
|
|||||||
String dateFormatParameterName = "dateFormat";
|
String dateFormatParameterName = "dateFormat";
|
||||||
String externalIdParameterName = "externalId";
|
String externalIdParameterName = "externalId";
|
||||||
|
|
||||||
String frequency = "frequency";
|
String frequencyType = "frequencyType";
|
||||||
|
String frequencyNumber = "frequencyNumber";
|
||||||
String startDate = "startDate";
|
String startDate = "startDate";
|
||||||
String numberOfInstallments = "numberOfInstallments";
|
String numberOfInstallments = "numberOfInstallments";
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-5
@@ -2166,8 +2166,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private LocalDate determineExpectedMaturityDate() {
|
private LocalDate determineExpectedMaturityDate() {
|
||||||
final int numberOfInstallments = this.repaymentScheduleInstallments.size();
|
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments().stream()
|
||||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
.filter(i -> !i.isDownPayment() && !i.isAdditional()).toList();
|
||||||
|
final int numberOfInstallments = installments.size();
|
||||||
LocalDate maturityDate = installments.get(numberOfInstallments - 1).getDueDate();
|
LocalDate maturityDate = installments.get(numberOfInstallments - 1).getDueDate();
|
||||||
ListIterator<LoanRepaymentScheduleInstallment> iterator = installments.listIterator(numberOfInstallments);
|
ListIterator<LoanRepaymentScheduleInstallment> iterator = installments.listIterator(numberOfInstallments);
|
||||||
while (iterator.hasPrevious()) {
|
while (iterator.hasPrevious()) {
|
||||||
@@ -3432,7 +3433,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
|
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
|
||||||
List<LoanTransaction> trans = getLoanTransactions();
|
List<LoanTransaction> trans = getLoanTransactions();
|
||||||
for (final LoanTransaction transaction : trans) {
|
for (final LoanTransaction transaction : trans) {
|
||||||
if (transaction.isNotReversed() && (transaction.isChargeOff() || !transaction.isNonMonetaryTransaction())) {
|
if (transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge() || transaction.isReAmortize()
|
||||||
|
|| !transaction.isNonMonetaryTransaction())) {
|
||||||
repaymentsOrWaivers.add(transaction);
|
repaymentsOrWaivers.add(transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3670,10 +3672,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private LocalDate getNextUnpaidInstallmentDueDate() {
|
private LocalDate getNextUnpaidInstallmentDueDate() {
|
||||||
LocalDate nextUnpaidInstallmentDate = null;
|
|
||||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||||
LocalDate currentBusinessDate = DateUtils.getBusinessLocalDate();
|
LocalDate currentBusinessDate = DateUtils.getBusinessLocalDate();
|
||||||
LocalDate expectedMaturityDate = determineExpectedMaturityDate();
|
LocalDate expectedMaturityDate = determineExpectedMaturityDate();
|
||||||
|
LocalDate nextUnpaidInstallmentDate = expectedMaturityDate;
|
||||||
|
|
||||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||||
boolean isCurrentDateBeforeInstallmentAndLoanPeriod = DateUtils.isBefore(currentBusinessDate, installment.getDueDate())
|
boolean isCurrentDateBeforeInstallmentAndLoanPeriod = DateUtils.isBefore(currentBusinessDate, installment.getDueDate())
|
||||||
@@ -5664,7 +5666,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
lastCompoundingDate = compoundingDetail.getEffectiveDate();
|
lastCompoundingDate = compoundingDetail.getEffectiveDate();
|
||||||
}
|
}
|
||||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||||
LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1);
|
LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment
|
||||||
|
.getLastNonDownPaymentInstallment(installments);
|
||||||
reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate());
|
reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate());
|
||||||
reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate());
|
reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate());
|
||||||
}
|
}
|
||||||
|
|||||||
+42
@@ -29,6 +29,7 @@ import jakarta.persistence.Table;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
@@ -145,6 +146,9 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
|||||||
@Column(name = "is_down_payment", nullable = false)
|
@Column(name = "is_down_payment", nullable = false)
|
||||||
private boolean isDownPayment;
|
private boolean isDownPayment;
|
||||||
|
|
||||||
|
@Column(name = "is_re_aged", nullable = false)
|
||||||
|
private boolean isReAged;
|
||||||
|
|
||||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "loanRepaymentScheduleInstallment")
|
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "loanRepaymentScheduleInstallment")
|
||||||
private Set<LoanInterestRecalcualtionAdditionalDetails> loanCompoundingDetails = new HashSet<>();
|
private Set<LoanInterestRecalcualtionAdditionalDetails> loanCompoundingDetails = new HashSet<>();
|
||||||
|
|
||||||
@@ -223,6 +227,36 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
|||||||
this.obligationsMet = false;
|
this.obligationsMet = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoanRepaymentScheduleInstallment(Loan loan, Integer installmentNumber, LocalDate fromDate, LocalDate dueDate,
|
||||||
|
BigDecimal principal, BigDecimal interestCharged, BigDecimal feeChargesCharged, BigDecimal penaltyCharges,
|
||||||
|
BigDecimal creditedPrincipal, BigDecimal creditedFee, BigDecimal creditedPenalty, boolean additional, boolean isDownPayment,
|
||||||
|
boolean isReAged) {
|
||||||
|
this.loan = loan;
|
||||||
|
this.installmentNumber = installmentNumber;
|
||||||
|
this.fromDate = fromDate;
|
||||||
|
this.dueDate = dueDate;
|
||||||
|
this.principal = principal;
|
||||||
|
this.interestCharged = interestCharged;
|
||||||
|
this.feeChargesCharged = feeChargesCharged;
|
||||||
|
this.penaltyCharges = penaltyCharges;
|
||||||
|
this.creditedPrincipal = creditedPrincipal;
|
||||||
|
this.creditedFee = creditedFee;
|
||||||
|
this.creditedPenalty = creditedPenalty;
|
||||||
|
this.additional = additional;
|
||||||
|
this.isDownPayment = isDownPayment;
|
||||||
|
this.isReAged = isReAged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LoanRepaymentScheduleInstallment newReAgedInstallment(final Loan loan, final Integer installmentNumber,
|
||||||
|
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal) {
|
||||||
|
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal, null, null, null, null, null,
|
||||||
|
null, false, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LoanRepaymentScheduleInstallment getLastNonDownPaymentInstallment(List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
|
return installments.stream().filter(i -> !i.isDownPayment()).reduce((first, second) -> second).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
private BigDecimal defaultToNullIfZero(final BigDecimal value) {
|
private BigDecimal defaultToNullIfZero(final BigDecimal value) {
|
||||||
BigDecimal result = value;
|
BigDecimal result = value;
|
||||||
if (BigDecimal.ZERO.compareTo(value) == 0) {
|
if (BigDecimal.ZERO.compareTo(value) == 0) {
|
||||||
@@ -400,6 +434,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
|||||||
return this.installmentNumber.compareTo(o.installmentNumber);
|
return this.installmentNumber.compareTo(o.installmentNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int compareToByDueDate(LoanRepaymentScheduleInstallment o) {
|
||||||
|
return this.dueDate.compareTo(o.dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) {
|
public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) {
|
||||||
return !isPrincipalCompleted(currency);
|
return !isPrincipalCompleted(currency);
|
||||||
}
|
}
|
||||||
@@ -1022,4 +1060,8 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
|||||||
public enum PaymentAction {
|
public enum PaymentAction {
|
||||||
PAY, UNPAY
|
PAY, UNPAY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isReAged() {
|
||||||
|
return isReAged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -865,7 +865,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
|| LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf())
|
|| LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf())
|
||||||
|| LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf())
|
|| LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf())
|
||||||
|| LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf())
|
|| LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf())
|
||||||
|| LoanTransactionType.CHARGE_OFF.equals(getTypeOf()));
|
|| LoanTransactionType.CHARGE_OFF.equals(getTypeOf()) || LoanTransactionType.REAMORTIZE.equals(getTypeOf())
|
||||||
|
|| LoanTransactionType.REAGE.equals(getTypeOf()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
|
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
|
||||||
|
|||||||
+3
-1
@@ -61,7 +61,9 @@ public enum LoanTransactionType {
|
|||||||
CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), //
|
CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), //
|
||||||
CHARGE_OFF(27, "loanTransactionType.chargeOff"), //
|
CHARGE_OFF(27, "loanTransactionType.chargeOff"), //
|
||||||
DOWN_PAYMENT(28, "loanTransactionType.downPayment"), //
|
DOWN_PAYMENT(28, "loanTransactionType.downPayment"), //
|
||||||
REAGE(29, "loanTransactionType.reAge"), REAMORTIZE(30, "loanTransactionType.reAmortize");
|
REAGE(29, "loanTransactionType.reAge"), //
|
||||||
|
REAMORTIZE(30, "loanTransactionType.reAmortize"), //
|
||||||
|
;
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
|
|||||||
+5
-2
@@ -40,8 +40,11 @@ public class LoanReAgeParameter extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
private Long loanTransactionId;
|
private Long loanTransactionId;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(name = "frequency", nullable = false)
|
@Column(name = "frequency_type", nullable = false)
|
||||||
private PeriodFrequencyType frequency;
|
private PeriodFrequencyType frequencyType;
|
||||||
|
|
||||||
|
@Column(name = "frequency_number", nullable = false)
|
||||||
|
private Integer frequencyNumber;
|
||||||
|
|
||||||
@Column(name = "start_date", nullable = false)
|
@Column(name = "start_date", nullable = false)
|
||||||
private LocalDate startDate;
|
private LocalDate startDate;
|
||||||
|
|||||||
+6
-5
@@ -479,7 +479,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
loanTransaction.resetDerivedComponents();
|
loanTransaction.resetDerivedComponents();
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
|
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
|
||||||
installments.sort(byDate);
|
List<LoanRepaymentScheduleInstallment> installmentToBeProcessed = installments.stream().filter(i -> !i.isDownPayment())
|
||||||
|
.sorted(byDate).toList();
|
||||||
final Money zeroMoney = Money.zero(currency);
|
final Money zeroMoney = Money.zero(currency);
|
||||||
Money transactionAmount = loanTransaction.getAmount(currency);
|
Money transactionAmount = loanTransaction.getAmount(currency);
|
||||||
Money principalPortion = MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaymentHolder.getMoneyObject()));
|
Money principalPortion = MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaymentHolder.getMoneyObject()));
|
||||||
@@ -492,7 +493,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||||
boolean loanTransactionMapped = false;
|
boolean loanTransactionMapped = false;
|
||||||
LocalDate pastDueDate = null;
|
LocalDate pastDueDate = null;
|
||||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
for (final LoanRepaymentScheduleInstallment currentInstallment : installmentToBeProcessed) {
|
||||||
pastDueDate = currentInstallment.getDueDate();
|
pastDueDate = currentInstallment.getDueDate();
|
||||||
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
|
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
|
||||||
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
|
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
|
||||||
@@ -526,7 +527,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
// New installment will be added (N+1 scenario)
|
// New installment will be added (N+1 scenario)
|
||||||
if (!loanTransactionMapped) {
|
if (!loanTransactionMapped) {
|
||||||
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
|
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
|
||||||
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installments.size() - 1);
|
LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed.get(installmentToBeProcessed.size() - 1);
|
||||||
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
|
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
|
||||||
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
|
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
|
||||||
if (repaidAmount.isGreaterThanZero()) {
|
if (repaidAmount.isGreaterThanZero()) {
|
||||||
@@ -848,7 +849,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
protected void addChargeOnlyRepaymentInstallmentIfRequired(Set<LoanCharge> charges,
|
protected void addChargeOnlyRepaymentInstallmentIfRequired(Set<LoanCharge> charges,
|
||||||
List<LoanRepaymentScheduleInstallment> installments) {
|
List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
if (!CollectionUtils.isEmpty(charges) && !CollectionUtils.isEmpty(installments)) {
|
if (!CollectionUtils.isEmpty(charges) && !CollectionUtils.isEmpty(installments)) {
|
||||||
LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = installments.get(installments.size() - 1);
|
LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = installments.stream().filter(i -> !i.isDownPayment())
|
||||||
|
.reduce((first, second) -> second).orElseThrow();
|
||||||
LocalDate installmentDueDate = null;
|
LocalDate installmentDueDate = null;
|
||||||
|
|
||||||
LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges);
|
LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges);
|
||||||
@@ -867,7 +869,6 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null);
|
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null);
|
||||||
installment.markAsAdditional();
|
installment.markAsAdditional();
|
||||||
loan.addLoanRepaymentScheduleInstallment(installment);
|
loan.addLoanRepaymentScheduleInstallment(installment);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-2
@@ -22,7 +22,6 @@ import static java.math.BigDecimal.ZERO;
|
|||||||
import static java.util.stream.Collectors.mapping;
|
import static java.util.stream.Collectors.mapping;
|
||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK;
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK;
|
||||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REAMORTIZE;
|
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
||||||
@@ -44,11 +43,14 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.NotImplementedException;
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
@@ -71,6 +73,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||||
@@ -84,12 +88,15 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||||
|
|
||||||
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
|
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
|
||||||
|
|
||||||
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||||
|
|
||||||
|
private final LoanReAgingParameterRepository reAgingParameterRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCode() {
|
public String getCode() {
|
||||||
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
||||||
@@ -144,6 +151,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Remove re-aged and additional (N+1) installments (if applicable), those will be recreated during the
|
||||||
|
// reprocessing
|
||||||
|
installments.removeIf(LoanRepaymentScheduleInstallment::isReAged);
|
||||||
|
installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional);
|
||||||
|
|
||||||
addChargeOnlyRepaymentInstallmentIfRequired(charges, installments);
|
addChargeOnlyRepaymentInstallmentIfRequired(charges, installments);
|
||||||
|
|
||||||
@@ -185,6 +196,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
ctx.getOverpaymentHolder());
|
ctx.getOverpaymentHolder());
|
||||||
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
|
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
|
||||||
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||||
|
case REAGE -> handleReAge(loanTransaction, ctx);
|
||||||
// TODO: Cover rest of the transaction types
|
// TODO: Cover rest of the transaction types
|
||||||
default -> {
|
default -> {
|
||||||
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
||||||
@@ -201,7 +213,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
.toList();
|
.toList();
|
||||||
List<LoanRepaymentScheduleInstallment> futureInstallments = installments.stream() //
|
List<LoanRepaymentScheduleInstallment> futureInstallments = installments.stream() //
|
||||||
.filter(installment -> installment.getDueDate().isAfter(transactionDate)) //
|
.filter(installment -> installment.getDueDate().isAfter(transactionDate)) //
|
||||||
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment()) //
|
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment() && !installment.isReAged()) //
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
BigDecimal overallOverDuePrincipal = ZERO;
|
BigDecimal overallOverDuePrincipal = ZERO;
|
||||||
@@ -1279,4 +1291,62 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
private Money aggregatedInterestPortion;
|
private Money aggregatedInterestPortion;
|
||||||
private Money aggregatedPenaltyChargesPortion;
|
private Money aggregatedPenaltyChargesPortion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
|
MonetaryCurrency currency = ctx.getCurrency();
|
||||||
|
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
||||||
|
// Either we have the transaction id or we need to fetch it from context
|
||||||
|
Long loanTransactionId = loanTransaction.getId() != null ? loanTransaction.getId()
|
||||||
|
: ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction);
|
||||||
|
LoanReAgeParameter reAgeParameter = reAgingParameterRepository.findByLoanTransactionId(loanTransactionId).orElseThrow();
|
||||||
|
AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency));
|
||||||
|
installments.forEach(i -> {
|
||||||
|
Money principalOutstanding = i.getPrincipalOutstanding(currency);
|
||||||
|
if (principalOutstanding.isGreaterThanZero()) {
|
||||||
|
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
|
||||||
|
i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Money calculatedPrincipal = outstandingPrincipalBalance.get().dividedBy(reAgeParameter.getNumberOfInstallments(),
|
||||||
|
MoneyHelper.getRoundingMode());
|
||||||
|
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||||
|
if (installmentAmountInMultiplesOf != null) {
|
||||||
|
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
|
||||||
|
}
|
||||||
|
Money adjustCalculatedPrincipal = outstandingPrincipalBalance.get()
|
||||||
|
.minus(calculatedPrincipal.multipliedBy(reAgeParameter.getNumberOfInstallments()));
|
||||||
|
LoanRepaymentScheduleInstallment lastNormalInstallment = installments.stream().filter(i -> !i.isDownPayment())
|
||||||
|
.reduce((first, second) -> second).orElseThrow();
|
||||||
|
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(
|
||||||
|
lastNormalInstallment.getLoan(), lastNormalInstallment.getInstallmentNumber() + 1, lastNormalInstallment.getDueDate(),
|
||||||
|
reAgeParameter.getStartDate(), calculatedPrincipal.getAmount());
|
||||||
|
installments.add(reAgedInstallment);
|
||||||
|
for (int i = 1; i < reAgeParameter.getNumberOfInstallments(); i++) {
|
||||||
|
LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(reAgeParameter, reAgedInstallment.getDueDate());
|
||||||
|
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
|
||||||
|
reAgedInstallment.getInstallmentNumber() + 1, reAgedInstallment.getDueDate(), calculatedDueDate,
|
||||||
|
calculatedPrincipal.getAmount());
|
||||||
|
installments.add(reAgedInstallment);
|
||||||
|
}
|
||||||
|
reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal);
|
||||||
|
|
||||||
|
reprocessInstallmentsOrder(installments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reprocessInstallmentsOrder(List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
|
AtomicInteger counter = new AtomicInteger(0);
|
||||||
|
installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate)
|
||||||
|
.forEachOrdered(i -> i.updateInstallmentNumber(counter.getAndIncrement()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate calculateReAgedInstallmentDueDate(LoanReAgeParameter reAgeParameter, LocalDate dueDate) {
|
||||||
|
return switch (reAgeParameter.getFrequencyType()) {
|
||||||
|
case DAYS -> dueDate.plusDays(reAgeParameter.getFrequencyNumber());
|
||||||
|
case WEEKS -> dueDate.plusWeeks(reAgeParameter.getFrequencyNumber());
|
||||||
|
case MONTHS -> dueDate.plusMonths(reAgeParameter.getFrequencyNumber());
|
||||||
|
case YEARS -> dueDate.plusYears(reAgeParameter.getFrequencyNumber());
|
||||||
|
default -> throw new UnsupportedOperationException(reAgeParameter.getFrequencyType().getCode());
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -42,4 +42,5 @@
|
|||||||
<include relativeToChangelogFile="true" file="parts/1017_add_fee_and_penalty_adjustments_to_loan.xml"/>
|
<include relativeToChangelogFile="true" file="parts/1017_add_fee_and_penalty_adjustments_to_loan.xml"/>
|
||||||
<include relativeToChangelogFile="true" file="parts/1018_rename_credited_principal_back_to_credits_amount.xml"/>
|
<include relativeToChangelogFile="true" file="parts/1018_rename_credited_principal_back_to_credits_amount.xml"/>
|
||||||
<include relativeToChangelogFile="true" file="parts/1019_add_fixed_length.xml"/>
|
<include relativeToChangelogFile="true" file="parts/1019_add_fixed_length.xml"/>
|
||||||
|
<include relativeToChangelogFile="true" file="parts/1020_add_re_aged_flag_to_loan_installment.xml"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
<changeSet author="fineract" id="1">
|
||||||
|
<addColumn tableName="m_loan_repayment_schedule">
|
||||||
|
<column name="is_re_aged" type="boolean" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+4
-2
@@ -285,8 +285,10 @@ final class LoanTransactionsApiResourceSwagger {
|
|||||||
public Long writeoffReasonId;
|
public Long writeoffReasonId;
|
||||||
|
|
||||||
// command=reAge START
|
// command=reAge START
|
||||||
@Schema(example = "frequency")
|
@Schema(example = "frequencyType")
|
||||||
public String frequency;
|
public String frequencyType;
|
||||||
|
@Schema(example = "frequencyNumber")
|
||||||
|
public Integer frequencyNumber;
|
||||||
@Schema(example = "startDate")
|
@Schema(example = "startDate")
|
||||||
public String startDate;
|
public String startDate;
|
||||||
@Schema(example = "numberOfInstallments")
|
@Schema(example = "numberOfInstallments")
|
||||||
|
|||||||
+2
-1
@@ -1854,7 +1854,8 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
|
|||||||
holidayDetailDTO);
|
holidayDetailDTO);
|
||||||
updateMapWithAmount(principalPortionMap, unprocessed, applicableDate);
|
updateMapWithAmount(principalPortionMap, unprocessed, applicableDate);
|
||||||
installment.addPrincipalAmount(unprocessed);
|
installment.addPrincipalAmount(unprocessed);
|
||||||
LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1);
|
LoanRepaymentScheduleInstallment lastInstallment = installments.stream().filter(i -> !i.isDownPayment())
|
||||||
|
.reduce((first, second) -> second).orElseThrow();
|
||||||
lastInstallment.updatePrincipal(lastInstallment.getPrincipal(unprocessed.getCurrency()).plus(unprocessed).getAmount());
|
lastInstallment.updatePrincipal(lastInstallment.getPrincipal(unprocessed.getCurrency()).plus(unprocessed).getAmount());
|
||||||
lastInstallment.payPrincipalComponent(detail.getTransactionDate(), unprocessed);
|
lastInstallment.payPrincipalComponent(detail.getTransactionDate(), unprocessed);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -181,6 +181,12 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the disbursement happened after maturity date
|
||||||
|
if (loanApplicationTerms.isMultiDisburseLoan()) {
|
||||||
|
processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods,
|
||||||
|
DateUtils.getBusinessLocalDate().plusDays(1));
|
||||||
|
}
|
||||||
|
|
||||||
// determine fees and penalties for charges which depends on total
|
// determine fees and penalties for charges which depends on total
|
||||||
// loan interest
|
// loan interest
|
||||||
updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc);
|
updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc);
|
||||||
|
|||||||
+1
-1
@@ -498,7 +498,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!changes.isEmpty()) {
|
if (!changes.isEmpty()) {
|
||||||
|
loan.updateLoanScheduleDependentDerivedFields();
|
||||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||||
|
|
||||||
final String noteText = command.stringValueOfParameterNamed("note");
|
final String noteText = command.stringValueOfParameterNamed("note");
|
||||||
|
|||||||
+53
-9
@@ -24,6 +24,7 @@ import java.math.BigDecimal;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
@@ -41,12 +42,18 @@ import org.apache.fineract.organisation.monetary.domain.Money;
|
|||||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
|
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
|
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||||
|
import org.apache.fineract.portfolio.note.domain.Note;
|
||||||
|
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -61,6 +68,8 @@ public class LoanReAgingServiceImpl {
|
|||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final LoanTransactionRepository loanTransactionRepository;
|
private final LoanTransactionRepository loanTransactionRepository;
|
||||||
private final LoanReAgingParameterRepository reAgingParameterRepository;
|
private final LoanReAgingParameterRepository reAgingParameterRepository;
|
||||||
|
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
|
||||||
|
private final NoteRepository noteRepository;
|
||||||
|
|
||||||
public CommandProcessingResult reAge(Long loanId, JsonCommand command) {
|
public CommandProcessingResult reAge(Long loanId, JsonCommand command) {
|
||||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||||
@@ -77,6 +86,15 @@ public class LoanReAgingServiceImpl {
|
|||||||
LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command);
|
LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command);
|
||||||
reAgingParameterRepository.saveAndFlush(reAgeParameter);
|
reAgingParameterRepository.saveAndFlush(reAgeParameter);
|
||||||
|
|
||||||
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||||
|
.determineProcessor(loan.transactionProcessingStrategy());
|
||||||
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction,
|
||||||
|
new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
|
||||||
|
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney())));
|
||||||
|
|
||||||
|
loan.updateLoanScheduleDependentDerivedFields();
|
||||||
|
persistNote(loan, command, changes);
|
||||||
|
|
||||||
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
|
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeBusinessEvent(loan));
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeTransactionBusinessEvent(reAgeTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeTransactionBusinessEvent(reAgeTransaction));
|
||||||
@@ -91,15 +109,6 @@ public class LoanReAgingServiceImpl {
|
|||||||
.with(changes).build();
|
.with(changes).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction, JsonCommand command) {
|
|
||||||
// TODO: these parameters should be checked when the validations are implemented
|
|
||||||
PeriodFrequencyType periodFrequencyType = command.enumValueOfParameterNamed(LoanReAgingApiConstants.frequency,
|
|
||||||
PeriodFrequencyType.class);
|
|
||||||
LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
|
|
||||||
Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
|
|
||||||
return new LoanReAgeParameter(reAgeTransaction.getId(), periodFrequencyType, startDate, numberOfInstallments);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) {
|
public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) {
|
||||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||||
reAgingValidator.validateUndoReAge(loan, command);
|
reAgingValidator.validateUndoReAge(loan, command);
|
||||||
@@ -115,6 +124,10 @@ public class LoanReAgingServiceImpl {
|
|||||||
reverseReAgeTransaction(reAgeTransaction, command);
|
reverseReAgeTransaction(reAgeTransaction, command);
|
||||||
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
||||||
|
|
||||||
|
reProcessLoanTransactions(reAgeTransaction.getLoan());
|
||||||
|
loan.updateLoanScheduleDependentDerivedFields();
|
||||||
|
persistNote(loan, command, changes);
|
||||||
|
|
||||||
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
|
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAgeBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAgeBusinessEvent(loan));
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAgeTransactionBusinessEvent(reAgeTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAgeTransactionBusinessEvent(reAgeTransaction));
|
||||||
@@ -156,4 +169,35 @@ public class LoanReAgingServiceImpl {
|
|||||||
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE.getValue(), transactionDate, txPrincipalAmount,
|
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE.getValue(), transactionDate, txPrincipalAmount,
|
||||||
txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId);
|
txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction, JsonCommand command) {
|
||||||
|
// TODO: these parameters should be checked when the validations are implemented
|
||||||
|
PeriodFrequencyType periodFrequencyType = command.enumValueOfParameterNamed(LoanReAgingApiConstants.frequencyType,
|
||||||
|
PeriodFrequencyType.class);
|
||||||
|
LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
|
||||||
|
Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
|
||||||
|
Integer periodFrequencyNumber = command.integerValueOfParameterNamed(LoanReAgingApiConstants.frequencyNumber);
|
||||||
|
return new LoanReAgeParameter(reAgeTransaction.getId(), periodFrequencyType, periodFrequencyNumber, startDate,
|
||||||
|
numberOfInstallments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reProcessLoanTransactions(Loan loan) {
|
||||||
|
final List<LoanTransaction> filteredTransactions = loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed)
|
||||||
|
.filter(t -> t.isChargeOff() || !t.isNonMonetaryTransaction()).sorted(LoanTransactionComparator.INSTANCE).toList();
|
||||||
|
|
||||||
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||||
|
.determineProcessor(loan.transactionProcessingStrategy());
|
||||||
|
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), filteredTransactions,
|
||||||
|
loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistNote(Loan loan, JsonCommand command, Map<String, Object> changes) {
|
||||||
|
if (command.hasParameter("note")) {
|
||||||
|
final String note = command.stringValueOfParameterNamed("note");
|
||||||
|
final Note newNote = Note.loanNote(loan, note);
|
||||||
|
changes.put("note", note);
|
||||||
|
|
||||||
|
this.noteRepository.saveAndFlush(newNote);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.starter;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
|
||||||
@@ -103,8 +104,9 @@ public class LoanAccountAutoStarter {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
|
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
|
||||||
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor() {
|
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(
|
||||||
return new AdvancedPaymentScheduleTransactionProcessor();
|
LoanReAgingParameterRepository reAgingParameterRepository) {
|
||||||
|
return new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -71,4 +71,12 @@
|
|||||||
</column>
|
</column>
|
||||||
</addColumn>
|
</addColumn>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
<changeSet id="4" author="fineract">
|
||||||
|
<addColumn tableName="m_loan_reage_parameter">
|
||||||
|
<column name="frequency_number" type="SMALLINT" defaultValueNumeric="1">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
<renameColumn tableName="m_loan_reage_parameter" oldColumnName="frequency" newColumnName="frequency_type" columnDataType="VARCHAR(100)"/>
|
||||||
|
</changeSet>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+3
-1
@@ -61,6 +61,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||||
@@ -89,6 +90,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1);
|
private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1);
|
||||||
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
|
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
|
||||||
private AdvancedPaymentScheduleTransactionProcessor underTest;
|
private AdvancedPaymentScheduleTransactionProcessor underTest;
|
||||||
|
private LoanReAgingParameterRepository reAgingParameterRepository = Mockito.mock(LoanReAgingParameterRepository.class);
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
public static void init() {
|
public static void init() {
|
||||||
@@ -102,7 +104,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
underTest = new AdvancedPaymentScheduleTransactionProcessor();
|
underTest = new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
|
||||||
|
|
||||||
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
|
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
|
||||||
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
||||||
|
|||||||
+11
-2
@@ -378,11 +378,12 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
|
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void reAgeLoan(Long loanId, String frequency, String startDate, Integer numberOfInstallments) {
|
protected void reAgeLoan(Long loanId, String frequencyType, int frequencyNumber, String startDate, Integer numberOfInstallments) {
|
||||||
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
|
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
|
||||||
request.setDateFormat(DATETIME_PATTERN);
|
request.setDateFormat(DATETIME_PATTERN);
|
||||||
request.setLocale("en");
|
request.setLocale("en");
|
||||||
request.setFrequency(frequency);
|
request.setFrequencyType(frequencyType);
|
||||||
|
request.setFrequencyNumber(frequencyNumber);
|
||||||
request.setStartDate(startDate);
|
request.setStartDate(startDate);
|
||||||
request.setNumberOfInstallments(numberOfInstallments);
|
request.setNumberOfInstallments(numberOfInstallments);
|
||||||
loanTransactionHelper.reAge(loanId, request);
|
loanTransactionHelper.reAge(loanId, request);
|
||||||
@@ -798,6 +799,13 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
|
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void checkMaturityDates(long loanId, LocalDate expectedMaturityDate, LocalDate actualMaturityDate) {
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
|
||||||
|
assertEquals(expectedMaturityDate, loanDetails.getTimeline().getExpectedMaturityDate());
|
||||||
|
assertEquals(actualMaturityDate, loanDetails.getTimeline().getActualMaturityDate());
|
||||||
|
}
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public static class BatchRequestBuilder {
|
public static class BatchRequestBuilder {
|
||||||
|
|
||||||
@@ -937,6 +945,7 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
|
|
||||||
public static final Integer MONTHS = 2;
|
public static final Integer MONTHS = 2;
|
||||||
public static final String MONTHS_STRING = "MONTHS";
|
public static final String MONTHS_STRING = "MONTHS";
|
||||||
|
public static final String DAYS_STRING = "DAYS";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class InterestCalculationPeriodType {
|
public static class InterestCalculationPeriodType {
|
||||||
|
|||||||
-7
@@ -120,9 +120,6 @@ public class LoanDelinquencyDetailsNextPaymentDateConfigurationTest extends Base
|
|||||||
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("25 December 2023")
|
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("25 December 2023")
|
||||||
.dateFormat(DATETIME_PATTERN).locale("en"));
|
.dateFormat(DATETIME_PATTERN).locale("en"));
|
||||||
|
|
||||||
// delinquency null next payment date for date after maturity date
|
|
||||||
verifyLoanDelinquencyNextPaymentDate(loanId, "", true);
|
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
// reset global config
|
// reset global config
|
||||||
GlobalConfigurationHelper.updateLoanNextPaymentDateConfiguration(this.requestSpec, this.responseSpec,
|
GlobalConfigurationHelper.updateLoanNextPaymentDateConfiguration(this.requestSpec, this.responseSpec,
|
||||||
@@ -238,10 +235,6 @@ public class LoanDelinquencyDetailsNextPaymentDateConfigurationTest extends Base
|
|||||||
|
|
||||||
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("25 December 2023")
|
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("25 December 2023")
|
||||||
.dateFormat(DATETIME_PATTERN).locale("en"));
|
.dateFormat(DATETIME_PATTERN).locale("en"));
|
||||||
|
|
||||||
// delinquency null next payment date for date after maturity date
|
|
||||||
verifyLoanDelinquencyNextPaymentDate(loanId, "", true);
|
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
// reset global config
|
// reset global config
|
||||||
GlobalConfigurationHelper.updateLoanNextPaymentDateConfiguration(this.requestSpec, this.responseSpec,
|
GlobalConfigurationHelper.updateLoanNextPaymentDateConfiguration(this.requestSpec, this.responseSpec,
|
||||||
|
|||||||
+150
-72
@@ -19,10 +19,15 @@
|
|||||||
package org.apache.fineract.integrationtests.loan.reaging;
|
package org.apache.fineract.integrationtests.loan.reaging;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import org.apache.fineract.client.models.PostChargesResponse;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansRequest;
|
import org.apache.fineract.client.models.PostLoansRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansResponse;
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
||||||
@@ -40,13 +45,17 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
|
|||||||
// Create Client
|
// Create Client
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
int numberOfRepayments = 1;
|
int numberOfRepayments = 3;
|
||||||
int repaymentEvery = 1;
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
// Create Loan Product
|
// Create Loan Product
|
||||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() //
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() //
|
||||||
.numberOfRepayments(numberOfRepayments) //
|
.numberOfRepayments(numberOfRepayments) //
|
||||||
.repaymentEvery(repaymentEvery) //
|
.repaymentEvery(repaymentEvery) //
|
||||||
|
.installmentAmountInMultiplesOf(null) //
|
||||||
|
.enableDownPayment(true) //
|
||||||
|
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) //
|
||||||
|
.enableAutoRepaymentForDownPayment(true) //
|
||||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
|
||||||
|
|
||||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
@@ -74,102 +83,75 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
|
|||||||
|
|
||||||
// verify transactions
|
// verify transactions
|
||||||
verifyTransactions(loanId, //
|
verifyTransactions(loanId, //
|
||||||
transaction(1250.0, "Disbursement", "01 January 2023") //
|
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(312.5, "Down Payment", "01 January 2023") //
|
||||||
);
|
);
|
||||||
|
|
||||||
// verify schedule
|
// verify schedule
|
||||||
verifyRepaymentSchedule(loanId, //
|
verifyRepaymentSchedule(loanId, //
|
||||||
installment(0, null, "01 January 2023"), //
|
installment(1250, null, "01 January 2023"), //
|
||||||
installment(1250.0, false, "01 February 2023") //
|
installment(312.5, true, "01 January 2023"), //
|
||||||
|
installment(312.5, false, "01 February 2023"), //
|
||||||
|
installment(312.5, false, "01 March 2023"), //
|
||||||
|
installment(312.5, false, "01 April 2023") //
|
||||||
);
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), LocalDate.of(2023, 4, 1));
|
||||||
createdLoanId.set(loanId);
|
createdLoanId.set(loanId);
|
||||||
});
|
});
|
||||||
|
|
||||||
runAt("02 February 2023", () -> {
|
runAt("11 April 2023", () -> {
|
||||||
|
|
||||||
long loanId = createdLoanId.get();
|
long loanId = createdLoanId.get();
|
||||||
|
|
||||||
// create re-age transaction
|
// create charge
|
||||||
reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, "02 February 2023", 6);
|
double chargeAmount = 10.0;
|
||||||
|
PostChargesResponse chargeResult = createCharge(chargeAmount);
|
||||||
|
Long chargeId = chargeResult.getResourceId();
|
||||||
|
|
||||||
// verify transactions
|
// add charge after maturity
|
||||||
verifyTransactions(loanId, //
|
PostLoansLoanIdChargesResponse loanChargeResult = addLoanCharge(loanId, chargeId, "11 April 2023", chargeAmount);
|
||||||
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
|
||||||
transaction(1250.0, "Re-age", "02 February 2023") //
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: verify installments when schedule generation is implemented
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void test_LoanUndoReAgeTransaction_Works() {
|
|
||||||
AtomicLong createdLoanId = new AtomicLong();
|
|
||||||
|
|
||||||
runAt("01 January 2023", () -> {
|
|
||||||
// Create Client
|
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
|
||||||
|
|
||||||
int numberOfRepayments = 1;
|
|
||||||
int repaymentEvery = 1;
|
|
||||||
|
|
||||||
// Create Loan Product
|
|
||||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() //
|
|
||||||
.numberOfRepayments(numberOfRepayments) //
|
|
||||||
.repaymentEvery(repaymentEvery) //
|
|
||||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
|
|
||||||
|
|
||||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
|
||||||
Long loanProductId = loanProductResponse.getResourceId();
|
|
||||||
|
|
||||||
// Apply and Approve Loan
|
|
||||||
double amount = 1250.0;
|
|
||||||
|
|
||||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
|
||||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
|
|
||||||
.repaymentEvery(repaymentEvery)//
|
|
||||||
.loanTermFrequency(numberOfRepayments)//
|
|
||||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
|
|
||||||
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
|
|
||||||
|
|
||||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
|
||||||
|
|
||||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
|
||||||
approveLoanRequest(amount, "01 January 2023"));
|
|
||||||
|
|
||||||
Long loanId = approvedLoanResult.getLoanId();
|
|
||||||
|
|
||||||
// disburse Loan
|
|
||||||
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
|
|
||||||
|
|
||||||
// verify transactions
|
|
||||||
verifyTransactions(loanId, //
|
|
||||||
transaction(1250.0, "Disbursement", "01 January 2023") //
|
|
||||||
);
|
|
||||||
|
|
||||||
// verify schedule
|
// verify schedule
|
||||||
verifyRepaymentSchedule(loanId, //
|
verifyRepaymentSchedule(loanId, //
|
||||||
installment(0, null, "01 January 2023"), //
|
installment(1250, null, "01 January 2023"), //
|
||||||
installment(1250.0, false, "01 February 2023") //
|
installment(312.5, true, "01 January 2023"), //
|
||||||
|
installment(312.5, false, "01 February 2023"), //
|
||||||
|
installment(312.5, false, "01 March 2023"), //
|
||||||
|
installment(312.5, false, "01 April 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023") //
|
||||||
);
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), LocalDate.of(2023, 4, 1));
|
||||||
createdLoanId.set(loanId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
runAt("02 February 2023", () -> {
|
runAt("12 April 2023", () -> {
|
||||||
long loanId = createdLoanId.get();
|
long loanId = createdLoanId.get();
|
||||||
|
|
||||||
// create re-age transaction
|
// create re-age transaction
|
||||||
reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, "02 February 2023", 6);
|
reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 April 2023", 4);
|
||||||
|
|
||||||
// verify transactions
|
// verify transactions
|
||||||
verifyTransactions(loanId, //
|
verifyTransactions(loanId, //
|
||||||
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
||||||
transaction(1250.0, "Re-age", "02 February 2023") //
|
transaction(312.5, "Down Payment", "01 January 2023"), //
|
||||||
|
transaction(937.5, "Re-age", "12 April 2023") //
|
||||||
);
|
);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(1250, null, "01 January 2023"), //
|
||||||
|
installment(312.5, true, "01 January 2023"), //
|
||||||
|
installment(0, true, "01 February 2023"), //
|
||||||
|
installment(0, true, "01 March 2023"), //
|
||||||
|
installment(0, true, "01 April 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023"), //
|
||||||
|
installment(234.38, false, "12 April 2023"), //
|
||||||
|
installment(234.38, false, "12 May 2023"), //
|
||||||
|
installment(234.38, false, "12 June 2023"), //
|
||||||
|
installment(234.36, false, "12 July 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), LocalDate.of(2023, 7, 12));
|
||||||
});
|
});
|
||||||
|
|
||||||
runAt("03 February 2023", () -> {
|
runAt("13 April 2023", () -> {
|
||||||
long loanId = createdLoanId.get();
|
long loanId = createdLoanId.get();
|
||||||
|
|
||||||
// create re-age transaction
|
// create re-age transaction
|
||||||
@@ -178,10 +160,106 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
|
|||||||
// verify transactions
|
// verify transactions
|
||||||
verifyTransactions(loanId, //
|
verifyTransactions(loanId, //
|
||||||
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
||||||
reversedTransaction(1250.0, "Re-age", "02 February 2023") //
|
transaction(312.5, "Down Payment", "01 January 2023"), //
|
||||||
|
reversedTransaction(937.5, "Re-age", "12 April 2023") //
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: verify installments when schedule generation is implemented
|
// verify schedule
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(1250, null, "01 January 2023"), //
|
||||||
|
installment(312.5, true, "01 January 2023"), //
|
||||||
|
installment(312.5, false, "01 February 2023"), //
|
||||||
|
installment(312.5, false, "01 March 2023"), //
|
||||||
|
installment(312.5, false, "01 April 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), LocalDate.of(2023, 4, 1));
|
||||||
|
});
|
||||||
|
String repaymentExternalId = UUID.randomUUID().toString();
|
||||||
|
runAt("13 April 2023", () -> {
|
||||||
|
long loanId = createdLoanId.get();
|
||||||
|
|
||||||
|
loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionDate("13 April 2023").locale("en").transactionAmount(100.0).externalId(repaymentExternalId));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(312.5, "Down Payment", "01 January 2023"), //
|
||||||
|
reversedTransaction(937.5, "Re-age", "12 April 2023"), //
|
||||||
|
transaction(100.0, "Repayment", "13 April 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify schedule
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(1250, null, "01 January 2023"), //
|
||||||
|
installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023"), //
|
||||||
|
installment(312.5, 0, 0, 0, 212.5, false, "01 February 2023"), //
|
||||||
|
installment(312.5, 0, 0, 0, 312.5, false, "01 March 2023"), //
|
||||||
|
installment(312.5, 0, 0, 0, 312.5, false, "01 April 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
// create re-age transaction
|
||||||
|
reAgeLoan(loanId, RepaymentFrequencyType.DAYS_STRING, 30, "13 April 2023", 3);
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(312.5, "Down Payment", "01 January 2023"), //
|
||||||
|
reversedTransaction(937.5, "Re-age", "12 April 2023"), //
|
||||||
|
transaction(100.0, "Repayment", "13 April 2023"), //
|
||||||
|
transaction(837.5, "Re-age", "13 April 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify schedule
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(1250, null, "01 January 2023"), //
|
||||||
|
installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023"), //
|
||||||
|
installment(100.0, 0, 0, 0, 0.0, true, "01 February 2023"), //
|
||||||
|
installment(0, 0, 0, 0, 0.0, true, "01 March 2023"), //
|
||||||
|
installment(0, 0, 0, 0, 0.0, true, "01 April 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 10.0, false, "11 April 2023"), //
|
||||||
|
installment(279.17, 0, 0, 0, 279.17, false, "13 April 2023"), //
|
||||||
|
installment(279.17, 0, 0, 0, 279.17, false, "13 May 2023"), //
|
||||||
|
installment(279.16, 0, 0, 0, 279.16, false, "12 June 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 6, 12), LocalDate.of(2023, 6, 12));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("14 April 2023", () -> {
|
||||||
|
long loanId = createdLoanId.get();
|
||||||
|
|
||||||
|
// disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(100.0), "14 April 2023");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(1250.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(312.5, "Down Payment", "01 January 2023"), //
|
||||||
|
reversedTransaction(937.5, "Re-age", "12 April 2023"), //
|
||||||
|
transaction(100.0, "Repayment", "13 April 2023"), //
|
||||||
|
transaction(837.5, "Re-age", "13 April 2023"), //
|
||||||
|
transaction(100.0, "Disbursement", "14 April 2023"), //
|
||||||
|
transaction(25.0, "Down Payment", "14 April 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify schedule
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(1250, null, "01 January 2023"), //
|
||||||
|
installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023"), //
|
||||||
|
installment(100.0, 0, 0, 0, 0.0, true, "01 February 2023"), //
|
||||||
|
installment(0, 0, 0, 0, 0.0, true, "01 March 2023"), //
|
||||||
|
installment(0, 0, 0, 0, 0.0, true, "01 April 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 0.0, true, "11 April 2023"), //
|
||||||
|
installment(279.17, 0, 0, 0, 264.17, false, "13 April 2023"), //
|
||||||
|
installment(100, null, "14 April 2023"), //
|
||||||
|
installment(25.0, 0, 0, 0, 25.0, false, "14 April 2023"), //
|
||||||
|
installment(316.67, 0, 0, 0, 316.67, false, "13 May 2023"), //
|
||||||
|
installment(316.66, 0, 0, 0, 316.66, false, "12 June 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 6, 12), LocalDate.of(2023, 6, 12));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user