FINERACT-2059: Support reverse-replay of Re-age transaction
This commit is contained in:
+3
@@ -66,6 +66,9 @@ public abstract class AbstractPlatformException extends RuntimeException {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Object[] filterThrowableCause(Object[] defaultUserMessageArgs) {
|
private static Object[] filterThrowableCause(Object[] defaultUserMessageArgs) {
|
||||||
|
if (defaultUserMessageArgs == null) {
|
||||||
|
return NO_ARGS;
|
||||||
|
}
|
||||||
List<Object> filteredDefaultUserMessageArgs = new ArrayList<>(defaultUserMessageArgs.length);
|
List<Object> filteredDefaultUserMessageArgs = new ArrayList<>(defaultUserMessageArgs.length);
|
||||||
for (Object defaultUserMessageArg : defaultUserMessageArgs) {
|
for (Object defaultUserMessageArg : defaultUserMessageArgs) {
|
||||||
if (!(defaultUserMessageArg instanceof Throwable)) {
|
if (!(defaultUserMessageArg instanceof Throwable)) {
|
||||||
|
|||||||
+16
@@ -25,6 +25,7 @@ import jakarta.persistence.FetchType;
|
|||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.OneToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import jakarta.persistence.UniqueConstraint;
|
import jakarta.persistence.UniqueConstraint;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -47,6 +48,7 @@ import org.apache.fineract.organisation.office.domain.Office;
|
|||||||
import org.apache.fineract.portfolio.account.data.AccountTransferData;
|
import org.apache.fineract.portfolio.account.data.AccountTransferData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
|
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData;
|
import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData;
|
||||||
@@ -135,6 +137,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "fromTransaction")
|
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "fromTransaction")
|
||||||
private Set<LoanTransactionRelation> loanTransactionRelations = new HashSet<>();
|
private Set<LoanTransactionRelation> loanTransactionRelations = new HashSet<>();
|
||||||
|
|
||||||
|
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
|
||||||
|
private LoanReAgeParameter loanReAgeParameter;
|
||||||
|
|
||||||
protected LoanTransaction() {}
|
protected LoanTransaction() {}
|
||||||
|
|
||||||
public static LoanTransaction incomePosting(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount,
|
public static LoanTransaction incomePosting(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount,
|
||||||
@@ -292,6 +297,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
if (LoanTransactionType.CHARGE_PAYMENT.equals(loanTransaction.getTypeOf())) {
|
if (LoanTransactionType.CHARGE_PAYMENT.equals(loanTransaction.getTypeOf())) {
|
||||||
newTransaction.getLoanChargesPaid().addAll(loanTransaction.getLoanChargesPaid());
|
newTransaction.getLoanChargesPaid().addAll(loanTransaction.getLoanChargesPaid());
|
||||||
}
|
}
|
||||||
|
if (LoanTransactionType.REAGE.equals(loanTransaction.getTypeOf())) {
|
||||||
|
newTransaction.setLoanReAgeParameter(loanTransaction.getLoanReAgeParameter().getCopy(newTransaction));
|
||||||
|
}
|
||||||
return newTransaction;
|
return newTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1000,6 +1008,14 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
return DateUtils.isEqual(getTransactionDate(), date);
|
return DateUtils.isEqual(getTransactionDate(), date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoanReAgeParameter getLoanReAgeParameter() {
|
||||||
|
return loanReAgeParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoanReAgeParameter(LoanReAgeParameter loanReAgeParameter) {
|
||||||
|
this.loanReAgeParameter = loanReAgeParameter;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO missing hashCode(), equals(Object obj), but probably OK as long as
|
// TODO missing hashCode(), equals(Object obj), but probably OK as long as
|
||||||
// this is never stored in a Collection.
|
// this is never stored in a Collection.
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -22,12 +22,15 @@ import jakarta.persistence.Column;
|
|||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.EnumType;
|
import jakarta.persistence.EnumType;
|
||||||
import jakarta.persistence.Enumerated;
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "m_loan_reage_parameter")
|
@Table(name = "m_loan_reage_parameter")
|
||||||
@@ -35,9 +38,9 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
|||||||
@Getter
|
@Getter
|
||||||
public class LoanReAgeParameter extends AbstractAuditableWithUTCDateTimeCustom {
|
public class LoanReAgeParameter extends AbstractAuditableWithUTCDateTimeCustom {
|
||||||
|
|
||||||
// intentionally not doing a JPA relationship since it's not necessary
|
@OneToOne
|
||||||
@Column(name = "loan_transaction_id", nullable = false)
|
@JoinColumn(name = "loan_transaction_id", nullable = false)
|
||||||
private Long loanTransactionId;
|
private LoanTransaction loanTransaction;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(name = "frequency_type", nullable = false)
|
@Column(name = "frequency_type", nullable = false)
|
||||||
@@ -54,4 +57,8 @@ public class LoanReAgeParameter extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
|
|
||||||
// for JPA, don't use
|
// for JPA, don't use
|
||||||
protected LoanReAgeParameter() {}
|
protected LoanReAgeParameter() {}
|
||||||
|
|
||||||
|
public LoanReAgeParameter getCopy(LoanTransaction loanTransaction) {
|
||||||
|
return new LoanReAgeParameter(loanTransaction, frequencyType, frequencyNumber, startDate, numberOfInstallments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-15
@@ -1293,12 +1293,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
|
loanTransaction.resetDerivedComponents();
|
||||||
MonetaryCurrency currency = ctx.getCurrency();
|
MonetaryCurrency currency = ctx.getCurrency();
|
||||||
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
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));
|
AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency));
|
||||||
installments.forEach(i -> {
|
installments.forEach(i -> {
|
||||||
Money principalOutstanding = i.getPrincipalOutstanding(currency);
|
Money principalOutstanding = i.getPrincipalOutstanding(currency);
|
||||||
@@ -1308,29 +1306,39 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Money calculatedPrincipal = outstandingPrincipalBalance.get().dividedBy(reAgeParameter.getNumberOfInstallments(),
|
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency),
|
||||||
MoneyHelper.getRoundingMode());
|
Money.zero(currency));
|
||||||
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
|
||||||
if (installmentAmountInMultiplesOf != null) {
|
Money calculatedPrincipal = Money.zero(currency);
|
||||||
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
|
Money adjustCalculatedPrincipal = Money.zero(currency);
|
||||||
|
if (outstandingPrincipalBalance.get().isGreaterThanZero()) {
|
||||||
|
calculatedPrincipal = outstandingPrincipalBalance.get()
|
||||||
|
.dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getRoundingMode());
|
||||||
|
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||||
|
if (installmentAmountInMultiplesOf != null) {
|
||||||
|
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
|
||||||
|
}
|
||||||
|
adjustCalculatedPrincipal = outstandingPrincipalBalance.get()
|
||||||
|
.minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()));
|
||||||
}
|
}
|
||||||
Money adjustCalculatedPrincipal = outstandingPrincipalBalance.get()
|
|
||||||
.minus(calculatedPrincipal.multipliedBy(reAgeParameter.getNumberOfInstallments()));
|
|
||||||
LoanRepaymentScheduleInstallment lastNormalInstallment = installments.stream().filter(i -> !i.isDownPayment())
|
LoanRepaymentScheduleInstallment lastNormalInstallment = installments.stream().filter(i -> !i.isDownPayment())
|
||||||
.reduce((first, second) -> second).orElseThrow();
|
.reduce((first, second) -> second).orElseThrow();
|
||||||
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(
|
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(
|
||||||
lastNormalInstallment.getLoan(), lastNormalInstallment.getInstallmentNumber() + 1, lastNormalInstallment.getDueDate(),
|
lastNormalInstallment.getLoan(), lastNormalInstallment.getInstallmentNumber() + 1, lastNormalInstallment.getDueDate(),
|
||||||
reAgeParameter.getStartDate(), calculatedPrincipal.getAmount());
|
loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount());
|
||||||
installments.add(reAgedInstallment);
|
installments.add(reAgedInstallment);
|
||||||
for (int i = 1; i < reAgeParameter.getNumberOfInstallments(); i++) {
|
reAgedInstallment.updateDerivedFields(currency, loanTransaction.getTransactionDate());
|
||||||
LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(reAgeParameter, reAgedInstallment.getDueDate());
|
|
||||||
|
for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) {
|
||||||
|
LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(),
|
||||||
|
reAgedInstallment.getDueDate());
|
||||||
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
|
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
|
||||||
reAgedInstallment.getInstallmentNumber() + 1, reAgedInstallment.getDueDate(), calculatedDueDate,
|
reAgedInstallment.getInstallmentNumber() + 1, reAgedInstallment.getDueDate(), calculatedDueDate,
|
||||||
calculatedPrincipal.getAmount());
|
calculatedPrincipal.getAmount());
|
||||||
installments.add(reAgedInstallment);
|
installments.add(reAgedInstallment);
|
||||||
|
reAgedInstallment.updateDerivedFields(currency, loanTransaction.getTransactionDate());
|
||||||
}
|
}
|
||||||
reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal);
|
reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal);
|
||||||
|
|
||||||
reprocessInstallmentsOrder(installments);
|
reprocessInstallmentsOrder(installments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -28,6 +28,10 @@ import org.springframework.dao.EmptyResultDataAccessException;
|
|||||||
*/
|
*/
|
||||||
public class LoanTransactionNotFoundException extends AbstractPlatformResourceNotFoundException {
|
public class LoanTransactionNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public LoanTransactionNotFoundException(final String msg) {
|
||||||
|
super("error.msg.loan.transaction.not.found", msg);
|
||||||
|
}
|
||||||
|
|
||||||
public LoanTransactionNotFoundException(final Long id) {
|
public LoanTransactionNotFoundException(final Long id) {
|
||||||
super("error.msg.loan.transaction.id.invalid", "Transaction with identifier " + id + " does not exist", id);
|
super("error.msg.loan.transaction.id.invalid", "Transaction with identifier " + id + " does not exist", id);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-7
@@ -51,6 +51,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet
|
|||||||
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.LoanRepaymentScheduleTransactionProcessor;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
||||||
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.Note;
|
||||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||||
@@ -80,11 +81,10 @@ public class LoanReAgingServiceImpl {
|
|||||||
changes.put(LoanReAgingApiConstants.dateFormatParameterName, command.dateFormat());
|
changes.put(LoanReAgingApiConstants.dateFormatParameterName, command.dateFormat());
|
||||||
|
|
||||||
LoanTransaction reAgeTransaction = createReAgeTransaction(loan, command);
|
LoanTransaction reAgeTransaction = createReAgeTransaction(loan, command);
|
||||||
// important to do a flush before creating the reage parameter since it needs the ID
|
|
||||||
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
|
||||||
|
|
||||||
LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command);
|
LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command);
|
||||||
reAgingParameterRepository.saveAndFlush(reAgeParameter);
|
reAgeTransaction.setLoanReAgeParameter(reAgeParameter);
|
||||||
|
|
||||||
|
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
||||||
|
|
||||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||||
.determineProcessor(loan.transactionProcessingStrategy());
|
.determineProcessor(loan.transactionProcessingStrategy());
|
||||||
@@ -119,7 +119,7 @@ public class LoanReAgingServiceImpl {
|
|||||||
|
|
||||||
LoanTransaction reAgeTransaction = findLatestNonReversedReAgeTransaction(loan);
|
LoanTransaction reAgeTransaction = findLatestNonReversedReAgeTransaction(loan);
|
||||||
if (reAgeTransaction == null) {
|
if (reAgeTransaction == null) {
|
||||||
// TODO: when validations implemented; throw exception if there isn't a reage transaction available
|
throw new LoanTransactionNotFoundException("Re-Age transaction for loan was not found");
|
||||||
}
|
}
|
||||||
reverseReAgeTransaction(reAgeTransaction, command);
|
reverseReAgeTransaction(reAgeTransaction, command);
|
||||||
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
||||||
@@ -177,8 +177,7 @@ public class LoanReAgingServiceImpl {
|
|||||||
LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
|
LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
|
||||||
Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
|
Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
|
||||||
Integer periodFrequencyNumber = command.integerValueOfParameterNamed(LoanReAgingApiConstants.frequencyNumber);
|
Integer periodFrequencyNumber = command.integerValueOfParameterNamed(LoanReAgingApiConstants.frequencyNumber);
|
||||||
return new LoanReAgeParameter(reAgeTransaction.getId(), periodFrequencyType, periodFrequencyNumber, startDate,
|
return new LoanReAgeParameter(reAgeTransaction, periodFrequencyType, periodFrequencyNumber, startDate, numberOfInstallments);
|
||||||
numberOfInstallments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reProcessLoanTransactions(Loan loan) {
|
private void reProcessLoanTransactions(Loan loan) {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule</class>
|
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule</class>
|
||||||
<class>org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType</class>
|
<class>org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType</class>
|
||||||
<class>org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTypeListConverter</class>
|
<class>org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTypeListConverter</class>
|
||||||
|
<class>org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter</class>
|
||||||
<!-- Fineract Savings module -->
|
<!-- Fineract Savings module -->
|
||||||
<class>org.apache.fineract.interoperation.domain.InteropIdentifier</class>
|
<class>org.apache.fineract.interoperation.domain.InteropIdentifier</class>
|
||||||
<class>org.apache.fineract.portfolio.interestratechart.domain.InterestIncentives</class>
|
<class>org.apache.fineract.portfolio.interestratechart.domain.InterestIncentives</class>
|
||||||
|
|||||||
+8
@@ -90,6 +90,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
|||||||
import org.apache.fineract.integrationtests.common.system.CodeHelper;
|
import org.apache.fineract.integrationtests.common.system.CodeHelper;
|
||||||
import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
|
import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
|
||||||
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
|
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||||
@@ -824,6 +825,12 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
assertEquals(actualMaturityDate, loanDetails.getTimeline().getActualMaturityDate());
|
assertEquals(actualMaturityDate, loanDetails.getTimeline().getActualMaturityDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void verifyLoanStatus(long loanId, LoanStatus loanStatus) {
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
|
||||||
|
assertEquals(loanStatus.getCode(), loanDetails.getStatus().getCode());
|
||||||
|
}
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public static class BatchRequestBuilder {
|
public static class BatchRequestBuilder {
|
||||||
|
|
||||||
@@ -964,6 +971,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 Integer DAYS = 0;
|
||||||
public static final String DAYS_STRING = "DAYS";
|
public static final String DAYS_STRING = "DAYS";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+224
@@ -18,6 +18,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.integrationtests.loan.reaging;
|
package org.apache.fineract.integrationtests.loan.reaging;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -28,12 +32,15 @@ import org.apache.fineract.client.models.PostLoanProductsResponse;
|
|||||||
import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
|
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.PostLoansLoanIdTransactionsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
|
||||||
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.client.util.CallFailedRuntimeException;
|
||||||
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
||||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
|
public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
|
||||||
@@ -406,4 +413,221 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
|
|||||||
checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), LocalDate.of(2023, 7, 12));
|
checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), LocalDate.of(2023, 7, 12));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanReAgeReverseReplay_Works() {
|
||||||
|
AtomicLong createdLoanId = new AtomicLong();
|
||||||
|
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
// Create Client
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 3;
|
||||||
|
int repaymentEvery = 15;
|
||||||
|
|
||||||
|
// Create Loan Product
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() //
|
||||||
|
.numberOfRepayments(numberOfRepayments) //
|
||||||
|
.repaymentEvery(repaymentEvery) //
|
||||||
|
.installmentAmountInMultiplesOf(null) //
|
||||||
|
.enableDownPayment(true) //
|
||||||
|
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) //
|
||||||
|
.enableAutoRepaymentForDownPayment(true) //
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.DAYS.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 * repaymentEvery)//
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.DAYS)//
|
||||||
|
.loanTermFrequencyType(RepaymentFrequencyType.DAYS);
|
||||||
|
|
||||||
|
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(500.0), "01 January 2023");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(500.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Down Payment", "01 January 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify schedule
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(500.0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 2, 15), LocalDate.of(2023, 2, 15));
|
||||||
|
createdLoanId.set(loanId);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("27 February 2023", () -> {
|
||||||
|
|
||||||
|
long loanId = createdLoanId.get();
|
||||||
|
|
||||||
|
// create re-age transaction
|
||||||
|
reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "01 March 2023", 6);
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(500.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Down Payment", "01 January 2023"), //
|
||||||
|
transaction(375.0, "Re-age", "27 February 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(500.0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(0.0, true, "15 February 2023"), //
|
||||||
|
installment(62.5, false, "01 March 2023"), //
|
||||||
|
installment(62.5, false, "01 April 2023"), //
|
||||||
|
installment(62.5, false, "01 May 2023"), //
|
||||||
|
installment(62.5, false, "01 June 2023"), //
|
||||||
|
installment(62.5, false, "01 July 2023"), //
|
||||||
|
installment(62.5, false, "01 August 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1));
|
||||||
|
|
||||||
|
loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionDate("01 February 2023").locale("en").transactionAmount(125.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(500.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Down Payment", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(250.0, "Re-age", "27 February 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(500.0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(0.0, true, "15 February 2023"), //
|
||||||
|
installment(41.67, false, "01 March 2023"), //
|
||||||
|
installment(41.67, false, "01 April 2023"), //
|
||||||
|
installment(41.67, false, "01 May 2023"), //
|
||||||
|
installment(41.67, false, "01 June 2023"), //
|
||||||
|
installment(41.67, false, "01 July 2023"), //
|
||||||
|
installment(41.65, false, "01 August 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1));
|
||||||
|
});
|
||||||
|
runAt("28 February 2023", () -> {
|
||||||
|
|
||||||
|
long loanId = createdLoanId.get();
|
||||||
|
PostLoansLoanIdTransactionsResponse repaymentResponse = loanTransactionHelper.makeLoanRepayment(loanId,
|
||||||
|
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("02 February 2023").locale("en")
|
||||||
|
.transactionAmount(250.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(500.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Down Payment", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "02 February 2023"), //
|
||||||
|
transaction(0.0, "Re-age", "27 February 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(500.0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "16 January 2023"), //
|
||||||
|
installment(125.0, true, "31 January 2023"), //
|
||||||
|
installment(125.0, true, "15 February 2023"), //
|
||||||
|
installment(0.0, true, "01 March 2023"), //
|
||||||
|
installment(0.0, true, "01 April 2023"), //
|
||||||
|
installment(0.0, true, "01 May 2023"), //
|
||||||
|
installment(0.0, true, "01 June 2023"), //
|
||||||
|
installment(0.0, true, "01 July 2023"), //
|
||||||
|
installment(0.0, true, "01 August 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 2, 2));
|
||||||
|
|
||||||
|
verifyLoanStatus(loanId, LoanStatus.CLOSED_OBLIGATIONS_MET);
|
||||||
|
|
||||||
|
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentResponse.getResourceId(),
|
||||||
|
new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("28 February 2023")
|
||||||
|
.transactionAmount(0.0).locale("en"));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(500.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Down Payment", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment", "01 February 2023"), //
|
||||||
|
reversedTransaction(250.0, "Repayment", "02 February 2023"), //
|
||||||
|
transaction(250.0, "Re-age", "27 February 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(500.0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(0.0, true, "15 February 2023"), //
|
||||||
|
installment(41.67, false, "01 March 2023"), //
|
||||||
|
installment(41.67, false, "01 April 2023"), //
|
||||||
|
installment(41.67, false, "01 May 2023"), //
|
||||||
|
installment(41.67, false, "01 June 2023"), //
|
||||||
|
installment(41.67, false, "01 July 2023"), //
|
||||||
|
installment(41.65, false, "01 August 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyLoanStatus(loanId, LoanStatus.ACTIVE);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 March 2023", () -> {
|
||||||
|
|
||||||
|
long loanId = createdLoanId.get();
|
||||||
|
// create re-age transaction
|
||||||
|
undoReAgeLoan(loanId);
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(500.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Down Payment", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment", "01 February 2023"), //
|
||||||
|
reversedTransaction(250.0, "Repayment", "02 February 2023"), //
|
||||||
|
reversedTransaction(250.0, "Re-age", "27 February 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(500.0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023") //
|
||||||
|
);
|
||||||
|
checkMaturityDates(loanId, LocalDate.of(2023, 2, 15), LocalDate.of(2023, 2, 15));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("02 March 2023", () -> {
|
||||||
|
|
||||||
|
long loanId = createdLoanId.get();
|
||||||
|
// create re-age transaction
|
||||||
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||||
|
() -> loanTransactionHelper.undoReAge(loanId, new PostLoansLoanIdTransactionsRequest()));
|
||||||
|
assertEquals(404, exception.getResponse().code());
|
||||||
|
assertTrue(exception.getMessage().contains("error.msg.loan.transaction.not.found"));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user