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

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