FINERACT-2319: Add penalty-first payment allocation strategy for progressive loans with backdated repayment charge handling
This commit is contained in:
committed by
Adam Saghy
parent
68b2fcb7e1
commit
fc93db16b5
+1
@@ -109,6 +109,7 @@ public enum DefaultLoanProduct implements LoanProduct {
|
||||
LP2_ADV_PYMNT_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR, //
|
||||
LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST, //
|
||||
LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST, //
|
||||
LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_INTEREST_PENALTY_FEE_PRINCIPAL, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_INTEREST_FEE_PRINCIPAL, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_PRINCIPAL_INTEREST_FEE, //
|
||||
|
||||
+32
@@ -3302,6 +3302,38 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
|
||||
TestContext.INSTANCE.set(
|
||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_INTEREST_FLAT_ADV_PMT_ALLOC_MULTIDISBURSE_PART_PERIOD_CALC_DISABLED,
|
||||
responseLoanProductsRequestDownPaymentAdvInterestFlatMultiDisbPartPeriodIntCalcDisabled);
|
||||
|
||||
// LP2 without Down-payment + interest recalculation disabled + advanced payment allocation + progressive loan
|
||||
// schedule + horizontal + allocation penalty first
|
||||
// (LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST)
|
||||
String name127 = DefaultLoanProduct.LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST.getName();
|
||||
PostLoanProductsRequest loanProductsRequestNoInterestRecalculationAllocationPenaltyFirst = loanProductsRequestFactory
|
||||
.defaultLoanProductsRequestLP2()//
|
||||
.name(name127)//
|
||||
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
|
||||
.loanScheduleType("PROGRESSIVE") //
|
||||
.loanScheduleProcessingType("HORIZONTAL")//
|
||||
.enableDownPayment(false)//
|
||||
.enableAutoRepaymentForDownPayment(null)//
|
||||
.disbursedAmountPercentageForDownPayment(null)//
|
||||
.paymentAllocation(List.of(//
|
||||
createPaymentAllocation("DEFAULT", "LAST_INSTALLMENT",
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PENALTY, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_INTEREST, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PRINCIPAL, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_FEE, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PENALTY, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_INTEREST, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PRINCIPAL, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_FEE, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PENALTY, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_INTEREST, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PRINCIPAL, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_FEE)));//
|
||||
Response<PostLoanProductsResponse> responseLoanProductsRequestNoInterestRecalculationAllocationPenaltyFirst = loanProductsApi
|
||||
.createLoanProduct(loanProductsRequestNoInterestRecalculationAllocationPenaltyFirst).execute();
|
||||
TestContext.INSTANCE.set(TestContextKey.LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST_RESPONSE,
|
||||
responseLoanProductsRequestNoInterestRecalculationAllocationPenaltyFirst);
|
||||
}
|
||||
|
||||
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
|
||||
|
||||
+1
@@ -169,6 +169,7 @@ public abstract class TestContextKey {
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity";
|
||||
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationInterestFirst";
|
||||
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationPrincipalFirst";
|
||||
public static final String LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationAllocationPenaltyFirst";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAccelerateMaturityChargeOffBehaviourLastInstallmentStrategy";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY = "loanProductCreateResponseLP2AdvancedPaymentAccelerateMaturityChargeOffBehaviourLastInstallmentStrategy";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_REST_FREQUENCY_DATE_LAST_INSTALLMENT = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyTillRestFrequencyDateLastInstallment";
|
||||
|
||||
@@ -5406,3 +5406,86 @@ Feature: LoanRepayment
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
|
||||
| 01 February 2024 | Repayment | 40.0 | 39.42 | 0.58 | 0.0 | 0.0 | 60.58 | false | false |
|
||||
When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
|
||||
|
||||
Scenario: Verify progressive loan repayment reversals with penalty charge and backdated repayment
|
||||
When Admin sets the business date to "20 October 2024"
|
||||
When Admin creates a client with random data
|
||||
When Admin creates a fully customized loan with the following data:
|
||||
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||
| LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST | 20 October 2024 | 100 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 30 | DAYS | 30 | DAYS | 1 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||
And Admin successfully approves the loan on "20 October 2024" with "100" amount and expected disbursement date on "20 October 2024"
|
||||
And Admin successfully disburse the loan on "20 October 2024" with "100" EUR transaction amount
|
||||
Then Loan Repayment schedule has 1 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 20 October 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 30 | 19 November 2024 | | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
|
||||
When Admin sets the business date to "22 October 2024"
|
||||
And Customer makes "AUTOPAY" repayment on "22 October 2024" with 100 EUR transaction amount
|
||||
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
||||
Then Loan Repayment schedule has 1 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 20 October 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 30 | 19 November 2024 | 22 October 2024 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 100.0 | 100.0 | 0.0 | 0.0 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 20 October 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 |
|
||||
| 22 October 2024 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||
When Admin sets the business date to "24 October 2024"
|
||||
And Customer makes a repayment undo on "22 October 2024"
|
||||
Then Loan status will be "ACTIVE"
|
||||
And Loan has 100 outstanding amount
|
||||
Then Loan Repayment schedule has 1 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 20 October 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 30 | 19 November 2024 | | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 20 October 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
|
||||
| 22 October 2024 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | true | false |
|
||||
When Admin sets the business date to "26 October 2024"
|
||||
And Customer makes "AUTOPAY" repayment on "26 October 2024" with 100 EUR transaction amount
|
||||
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
||||
Then Loan Repayment schedule has 1 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 20 October 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 30 | 19 November 2024 | 26 October 2024 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 100.0 | 100.0 | 0.0 | 0.0 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 20 October 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
|
||||
| 22 October 2024 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | true | false |
|
||||
| 26 October 2024 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
When Admin sets the business date to "28 October 2024"
|
||||
And Customer makes a repayment undo on "26 October 2024"
|
||||
Then Loan status will be "ACTIVE"
|
||||
And Loan has 100 outstanding amount
|
||||
Then Loan Repayment schedule has 1 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 20 October 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 30 | 19 November 2024 | | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
|
||||
When Admin adds "LOAN_NSF_FEE" due date charge with "28 October 2024" due date and 10 EUR transaction amount
|
||||
Then Loan Repayment schedule has 1 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 20 October 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 30 | 19 November 2024 | | 0.0 | 100.0 | 0.0 | 0.0 | 10.0 | 110.0 | 0.0 | 0.0 | 0.0 | 110.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 100.0 | 0.0 | 0.0 | 10.0 | 110.0 | 0.0 | 0.0 | 0.0 | 110.0 |
|
||||
And Customer makes "AUTOPAY" repayment on "26 October 2024" with 101 EUR transaction amount
|
||||
Then Loan Repayment schedule has 1 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 20 October 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 30 | 19 November 2024 | | 0.0 | 100.0 | 0.0 | 0.0 | 10.0 | 110.0 | 101.0 | 101.0 | 0.0 | 9.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 100.0 | 0.0 | 0.0 | 10.0 | 110.0 | 101.0 | 101.0 | 0.0 | 9.0 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 20 October 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
|
||||
| 22 October 2024 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | true | false |
|
||||
| 26 October 2024 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | true | false |
|
||||
| 26 October 2024 | Repayment | 101.0 | 100.0 | 0.0 | 0.0 | 1.0 | 0.0 | false | false |
|
||||
When Customer makes "AUTOPAY" repayment on "27 October 2024" with 9 EUR transaction amount
|
||||
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
||||
+19
@@ -49,11 +49,13 @@ import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
@@ -79,6 +81,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRef
|
||||
import org.apache.fineract.portfolio.rate.domain.Rate;
|
||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"),
|
||||
@@ -1242,6 +1245,22 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.charges == null ? new HashSet<>() : this.charges.stream().filter(LoanCharge::isActive).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public boolean hasChargesAffectedByBackdatedRepaymentLikeTransaction(@NonNull final LoanTransaction transaction) {
|
||||
if (!transaction.isRepaymentLikeType() || CollectionUtils.isEmpty(this.charges) || !isProgressiveSchedule()
|
||||
|| !DateUtils.isBeforeBusinessDate(transaction.getTransactionDate())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final BiFunction<LocalDate, LocalDate, LocalDate> earlierDate = (date1, date2) -> DateUtils.isBefore(date1, date2) ? date1 : date2;
|
||||
|
||||
return this.charges.stream().filter(LoanCharge::isActive)
|
||||
.filter(loanCharge -> loanCharge.isSpecifiedDueDate() || loanCharge.isOverdueInstallmentCharge())
|
||||
.filter(loanCharge -> loanCharge.getDueLocalDate() != null).anyMatch(loanCharge -> {
|
||||
final LocalDate comparisonDate = earlierDate.apply(loanCharge.getDueLocalDate(), loanCharge.getSubmittedOnDate());
|
||||
return comparisonDate != null && comparisonDate.isAfter(transaction.getTransactionDate());
|
||||
});
|
||||
}
|
||||
|
||||
public LoanCharge fetchLoanChargesById(final Long id) {
|
||||
LoanCharge charge = null;
|
||||
for (LoanCharge loanCharge : this.charges) {
|
||||
|
||||
+1
@@ -133,6 +133,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
boolean processLatest = isTransactionChronologicallyLatest //
|
||||
&& adjustedTransaction == null // covers reversals
|
||||
&& !loan.isForeclosure() //
|
||||
&& !loan.hasChargesAffectedByBackdatedRepaymentLikeTransaction(loanTransaction)
|
||||
&& loanTransactionProcessingService.canProcessLatestTransactionOnly(loan, loanTransaction, currentInstallment); //
|
||||
if (processLatest) {
|
||||
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction,
|
||||
|
||||
+1
@@ -904,6 +904,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
|
||||
boolean processLatest = isTransactionChronologicallyLatest //
|
||||
&& !loan.isForeclosure() //
|
||||
&& !loan.hasChargesAffectedByBackdatedRepaymentLikeTransaction(refundTransaction) //
|
||||
&& loanTransactionProcessingService.canProcessLatestTransactionOnly(loan, refundTransaction, currentInstallment); //
|
||||
if (processLatest) {
|
||||
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), refundTransaction,
|
||||
|
||||
+3
@@ -74,6 +74,9 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
|
||||
if (!DateUtils.isEqualBusinessDate(loanTransaction.getTransactionDate())) {
|
||||
return false;
|
||||
}
|
||||
if (loan.hasChargesAffectedByBackdatedRepaymentLikeTransaction(loanTransaction)) {
|
||||
return false;
|
||||
}
|
||||
LoanInterestRecalculationDetails interestRecalculationDetails = loan.getLoanInterestRecalculationDetails();
|
||||
if (interestRecalculationDetails != null && ((interestRecalculationDetails.getRestFrequencyType().isSameAsRepayment()
|
||||
&& interestRecalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillPreClosureDateEnabled())
|
||||
|
||||
+46
-15
@@ -645,26 +645,57 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
protected void verifyTransactions(Long loanId, TransactionExt... transactions) {
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
||||
protected void verifyTransactions(final Long loanId, final TransactionExt... transactions) {
|
||||
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
||||
if (transactions == null || transactions.length == 0) {
|
||||
assertNull(loanDetails.getTransactions(), "No transaction is expected on loan " + loanId);
|
||||
} else {
|
||||
Assertions.assertNotNull(loanDetails.getTransactions());
|
||||
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size(), "Number of transactions on loan " + loanId);
|
||||
|
||||
Arrays.stream(transactions).forEach(tr -> {
|
||||
boolean found = loanDetails.getTransactions().stream()
|
||||
.anyMatch(item -> Objects.equals(Utils.getDoubleValue(item.getAmount()), tr.amount) //
|
||||
&& Objects.equals(item.getType().getValue(), tr.type) //
|
||||
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)) //
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getOutstandingLoanBalance()), tr.outstandingPrincipal) //
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getPrincipalPortion()), tr.principalPortion) //
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getInterestPortion()), tr.interestPortion) //
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getFeeChargesPortion()), tr.feePortion) //
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getPenaltyChargesPortion()), tr.penaltyPortion) //
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getOverpaymentPortion()), tr.overpaymentPortion) //
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getUnrecognizedIncomePortion()), tr.unrecognizedPortion) //
|
||||
);
|
||||
Assertions.assertTrue(found, "Required transaction not found: " + tr + " on loan " + loanId);
|
||||
final List<GetLoansLoanIdTransactions> transactionsByDate = loanDetails.getTransactions().stream()
|
||||
.filter(item -> Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter))).toList();
|
||||
|
||||
if (transactionsByDate.isEmpty()) {
|
||||
Assertions.fail("No transactions found for date " + tr.date + " on loan " + loanId);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean found = transactionsByDate.stream()
|
||||
.anyMatch(item -> Objects.equals(Utils.getDoubleValue(item.getAmount()), tr.amount)
|
||||
&& Objects.equals(item.getType().getValue(), tr.type)
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getOutstandingLoanBalance()), tr.outstandingPrincipal)
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getPrincipalPortion()), tr.principalPortion)
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getInterestPortion()), tr.interestPortion)
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getFeeChargesPortion()), tr.feePortion)
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getPenaltyChargesPortion()), tr.penaltyPortion)
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getOverpaymentPortion()), tr.overpaymentPortion)
|
||||
&& Objects.equals(Utils.getDoubleValue(item.getUnrecognizedIncomePortion()), tr.unrecognizedPortion));
|
||||
|
||||
if (!found) {
|
||||
final StringBuilder errorMessage = new StringBuilder();
|
||||
errorMessage.append("Required transaction not found: ").append(tr).append(" on loan ").append(loanId);
|
||||
errorMessage.append("\nTransactions found for date ").append(tr.date).append(":");
|
||||
|
||||
for (int i = 0; i < transactionsByDate.size(); i++) {
|
||||
GetLoansLoanIdTransactions item = transactionsByDate.get(i);
|
||||
errorMessage.append("\n Transaction ").append(i + 1).append(": ");
|
||||
errorMessage.append("amount=").append(Utils.getDoubleValue(item.getAmount()));
|
||||
errorMessage.append(", type=").append(item.getType().getValue());
|
||||
errorMessage.append(", date=").append(item.getDate().format(dateTimeFormatter));
|
||||
errorMessage.append(", outstandingPrincipal=").append(Utils.getDoubleValue(item.getOutstandingLoanBalance()));
|
||||
errorMessage.append(", principalPortion=").append(Utils.getDoubleValue(item.getPrincipalPortion()));
|
||||
errorMessage.append(", interestPortion=").append(Utils.getDoubleValue(item.getInterestPortion()));
|
||||
errorMessage.append(", feePortion=").append(Utils.getDoubleValue(item.getFeeChargesPortion()));
|
||||
errorMessage.append(", penaltyPortion=").append(Utils.getDoubleValue(item.getPenaltyChargesPortion()));
|
||||
errorMessage.append(", unrecognizedPortion=").append(Utils.getDoubleValue(item.getUnrecognizedIncomePortion()));
|
||||
errorMessage.append(", overpaymentPortion=").append(Utils.getDoubleValue(item.getOverpaymentPortion()));
|
||||
errorMessage.append(", reversed=").append(item.getManuallyReversed() != null ? item.getManuallyReversed() : false);
|
||||
}
|
||||
|
||||
Assertions.fail(errorMessage.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+22
-24
@@ -714,7 +714,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
final String creationBusinessDay = "15 January 2023";
|
||||
AtomicReference<Long> loanId = new AtomicReference<>();
|
||||
runAt(creationBusinessDay, () -> {
|
||||
|
||||
Long localLoanProductId = createLoanProductAccountingAccrualPeriodicAdvancedPaymentAllocation();
|
||||
loanId.set(applyForLoanApplicationAdvancedPaymentAllocation(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000),
|
||||
disbursementDay, BigDecimal.ZERO));
|
||||
@@ -723,53 +722,52 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
.dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en"));
|
||||
|
||||
loanTransactionHelper.disburseLoan(loanId.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay)
|
||||
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
|
||||
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000)).locale("en"));
|
||||
|
||||
chargePenalty(loanId.get(), 20.0, chargeDueDate1st);
|
||||
|
||||
addRepaymentForLoan(loanId.get(), 50.0, "10 January 2023");
|
||||
verifyTransactions(loanId.get(), //
|
||||
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
transaction(50.0, "Repayment", "10 January 2023", 970, 30, 0, 0, 20, 0.0, 0.0));
|
||||
transaction(1000, "Disbursement", disbursementDay, 1000, 0, 0, 0, 0, 0, 0),
|
||||
transaction(50, "Repayment", "10 January 2023", 950, 50, 0, 0, 0, 0, 0));
|
||||
|
||||
});
|
||||
runAt(repaymentPeriod1CloseDate, () -> {
|
||||
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
|
||||
verifyTransactions(loanId.get(), //
|
||||
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
|
||||
transaction(50.0, "Repayment", "10 January 2023", 970, 30, 0, 0, 20, 0.0, 0.0),
|
||||
transaction(20.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 0.0, 20.0, 0.0, 0.0));
|
||||
transaction(1000, "Disbursement", disbursementDay, 1000, 0, 0, 0, 0, 0, 0),
|
||||
transaction(20, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0, 0),
|
||||
transaction(50, "Repayment", "10 January 2023", 950, 50, 0, 0, 0, 0, 0),
|
||||
transaction(20, "Accrual Activity", "01 February 2023", 0, 0, 0, 0, 20, 0, 0));
|
||||
|
||||
});
|
||||
runAt(repaymentPeriod1OneDayAfterCloseDate, () -> {
|
||||
|
||||
addRepaymentForLoan(loanId.get(), 220.0, "8 January 2023");
|
||||
|
||||
verifyTransactions(loanId.get(), //
|
||||
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
|
||||
transaction(50.0, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0.0, 0.0),
|
||||
transaction(220.0, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0.0, 0.0),
|
||||
transaction(20.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 0.0, 20.0, 0.0, 0.0));
|
||||
transaction(1000, "Disbursement", disbursementDay, 1000, 0, 0, 0, 0, 0, 0),
|
||||
transaction(20, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0, 0),
|
||||
transaction(50, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0, 0),
|
||||
transaction(220, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0, 0),
|
||||
transaction(20, "Accrual Activity", "01 February 2023", 0, 0, 0, 0, 20, 0, 0));
|
||||
|
||||
chargePenalty(loanId.get(), 33.0, chargeDueDate2st);
|
||||
|
||||
verifyTransactions(loanId.get(), //
|
||||
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
|
||||
transaction(50.0, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0.0, 0.0),
|
||||
transaction(220.0, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0.0, 0.0),
|
||||
transaction(53.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 0.0, 53.0, 0.0, 0.0));
|
||||
transaction(1000, "Disbursement", disbursementDay, 1000, 0, 0, 0, 0, 0, 0),
|
||||
transaction(20, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0, 0),
|
||||
transaction(50, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0, 0),
|
||||
transaction(220, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0, 0),
|
||||
transaction(53, "Accrual Activity", "01 February 2023", 0, 0, 0, 0, 53, 0, 0));
|
||||
|
||||
chargeFee(loanId.get(), 12.0, chargeDueDate3st);
|
||||
|
||||
verifyTransactions(loanId.get(), //
|
||||
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
|
||||
transaction(50.0, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0.0, 0.0),
|
||||
transaction(220.0, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0.0, 0.0),
|
||||
transaction(65.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 12.0, 53.0, 0.0, 0.0));
|
||||
transaction(1000, "Disbursement", disbursementDay, 1000, 0, 0, 0, 0, 0, 0),
|
||||
transaction(20, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0, 0),
|
||||
transaction(50, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0, 0),
|
||||
transaction(220, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0, 0),
|
||||
transaction(65, "Accrual Activity", "01 February 2023", 0, 0, 0, 12, 53, 0, 0));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ public class LoanTransactionReprocessForAdvancedPaymentAllocationTest extends Ba
|
||||
.transactionAmount(50.0).externalId(loanTransactionExternalIdStr));
|
||||
|
||||
// verify transaction amounts
|
||||
verifyTransaction(LocalDate.of(2023, 2, 20), 50.0f, 0.0f, 0.0f, 50.0f, 0.0f, loanId, "repayment");
|
||||
verifyTransaction(LocalDate.of(2023, 2, 20), 50.0f, 50.0f, 0.0f, 0.0f, 0.0f, loanId, "repayment");
|
||||
|
||||
// add loan charge for a date later than repayment date
|
||||
// apply penalty
|
||||
|
||||
+9
-9
@@ -389,11 +389,11 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest extends BaseL
|
||||
assertEquals(LocalDate.of(2023, 1, 31), firstRepaymentInstallment.getDueDate());
|
||||
|
||||
assertEquals(feePortion, Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesDue()));
|
||||
assertEquals(0.00, Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesOutstanding()));
|
||||
assertEquals(50.00, Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesOutstanding()));
|
||||
assertEquals(penaltyPortion, Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesDue()));
|
||||
assertEquals(0.00, Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesOutstanding()));
|
||||
assertEquals(100.00, Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesOutstanding()));
|
||||
assertEquals(400.00, Utils.getDoubleValue(secondRepaymentInstallment.getTotalDueForPeriod()));
|
||||
assertEquals(0.00, Utils.getDoubleValue(secondRepaymentInstallment.getTotalOutstandingForPeriod()));
|
||||
assertEquals(150.00, Utils.getDoubleValue(secondRepaymentInstallment.getTotalOutstandingForPeriod()));
|
||||
assertEquals(LocalDate.of(2023, 3, 2), secondRepaymentInstallment.getDueDate());
|
||||
|
||||
assertEquals(0.00, Utils.getDoubleValue(thirdRepaymentInstallment.getFeeChargesDue()));
|
||||
@@ -401,7 +401,7 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest extends BaseL
|
||||
assertEquals(0.00, Utils.getDoubleValue(thirdRepaymentInstallment.getPenaltyChargesDue()));
|
||||
assertEquals(0.00, Utils.getDoubleValue(thirdRepaymentInstallment.getPenaltyChargesOutstanding()));
|
||||
assertEquals(250.00, Utils.getDoubleValue(thirdRepaymentInstallment.getTotalDueForPeriod()));
|
||||
assertEquals(240.00, Utils.getDoubleValue(thirdRepaymentInstallment.getTotalOutstandingForPeriod()));
|
||||
assertEquals(90.00, Utils.getDoubleValue(thirdRepaymentInstallment.getTotalOutstandingForPeriod()));
|
||||
assertEquals(LocalDate.of(2023, 4, 1), thirdRepaymentInstallment.getDueDate());
|
||||
|
||||
loanTransactionHelper.makeRefundByCash("28 January 2023", 15.0f, loanId);
|
||||
@@ -424,13 +424,13 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest extends BaseL
|
||||
assertEquals(LocalDate.of(2023, 1, 31), firstRepaymentInstallment.getDueDate());
|
||||
|
||||
assertEquals(feePortion, Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesDue()));
|
||||
assertEquals(0.00, Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesOutstanding()));
|
||||
assertEquals(50.00, Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesOutstanding()));
|
||||
assertEquals(penaltyPortion, Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesDue()));
|
||||
assertEquals(0.00, Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesOutstanding()));
|
||||
assertEquals(100.00, Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesOutstanding()));
|
||||
assertEquals(250.00, Utils.getDoubleValue(secondRepaymentInstallment.getPrincipalDue()));
|
||||
assertEquals(5.00, Utils.getDoubleValue(secondRepaymentInstallment.getPrincipalOutstanding()));
|
||||
assertEquals(0.00, Utils.getDoubleValue(secondRepaymentInstallment.getPrincipalOutstanding()));
|
||||
assertEquals(400.00, Utils.getDoubleValue(secondRepaymentInstallment.getTotalDueForPeriod()));
|
||||
assertEquals(5.00, Utils.getDoubleValue(secondRepaymentInstallment.getTotalOutstandingForPeriod()));
|
||||
assertEquals(150.00, Utils.getDoubleValue(secondRepaymentInstallment.getTotalOutstandingForPeriod()));
|
||||
assertEquals(LocalDate.of(2023, 3, 2), secondRepaymentInstallment.getDueDate());
|
||||
|
||||
assertEquals(0.00, Utils.getDoubleValue(thirdRepaymentInstallment.getFeeChargesDue()));
|
||||
@@ -438,7 +438,7 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest extends BaseL
|
||||
assertEquals(0.00, Utils.getDoubleValue(thirdRepaymentInstallment.getPenaltyChargesDue()));
|
||||
assertEquals(0.00, Utils.getDoubleValue(thirdRepaymentInstallment.getPenaltyChargesOutstanding()));
|
||||
assertEquals(250.00, Utils.getDoubleValue(thirdRepaymentInstallment.getTotalDueForPeriod()));
|
||||
assertEquals(250.00, Utils.getDoubleValue(thirdRepaymentInstallment.getTotalOutstandingForPeriod()));
|
||||
assertEquals(105.0, Utils.getDoubleValue(thirdRepaymentInstallment.getTotalOutstandingForPeriod()));
|
||||
assertEquals(LocalDate.of(2023, 4, 1), thirdRepaymentInstallment.getDueDate());
|
||||
|
||||
// fully unpaying the second installment
|
||||
|
||||
Reference in New Issue
Block a user