Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_PYMNT_ZERO_INTEREST_CHARGE_OFF_DELINQUENT_REASON_INTEREST_RECALC_CAPITALIZED_INCOME, //
LP2_ADV_PYMNT_360_30_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY, //
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION, //
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION, //
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_OVER_APPLIED_PERCENTAGE_CAPITALIZED_INCOME, //
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_OVER_APPLIED_FLAT_CAPITALIZED_INCOME, //
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_APPROVED_OVER_APPLIED_PERCENTAGE_CAPITALIZED_INCOME, //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4058,6 +4058,39 @@ public void initialize() throws Exception {
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY,
responseLoanProductsResponseAdvCustomPaymentAllocationProgressiveLoanInterestDailyEmi36030InterestRecalculationDaily);

// LP2 with progressive loan schedule + horizontal + interest EMI + 360/30 + multidisbursement +
// contract termination with interest recognition
// (LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION)
final String name148 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION
.getName();

final PostLoanProductsRequest loanProductsRequestAdvCustomContractTerminationProgressiveLoanScheduleIntRecalcRecog = loanProductsRequestFactory
.defaultLoanProductsRequestLP2InterestDailyRecalculation()//
.interestRecognitionOnDisbursementDate(true) //
.name(name148)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT",
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PENALTY, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_FEE, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_INTEREST, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PRINCIPAL, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PENALTY, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_FEE, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PRINCIPAL, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_INTEREST, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PENALTY, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_FEE, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PRINCIPAL, //
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_INTEREST), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
final Response<PostLoanProductsResponse> responseLoanProductsRequestAdvCustomContractTerminationProgressiveLoanScheduleIntRecalcRecog = loanProductsApi
.createLoanProduct(loanProductsRequestAdvCustomContractTerminationProgressiveLoanScheduleIntRecalcRecog).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION,
responseLoanProductsRequestAdvCustomContractTerminationProgressiveLoanScheduleIntRecalcRecog);

// (LP1_WITH_OVERRIDES) - Loan product with all attribute overrides ENABLED
final String nameWithOverrides = DefaultLoanProduct.LP1_WITH_OVERRIDES.getName();
final PostLoanProductsRequest loanProductsRequestWithOverrides = loanProductsRequestFactory.defaultLoanProductsRequestLP1() //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ public abstract class TestContextKey {
public static final String LOAN_INTEREST_REFUND_RESPONSE = "loanInterestRefundResponse";
public static final String INTEREST_PAUSE_VARIATION_ID = "interestPauseVariationId";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationContractTermination";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationContractTerminationIntRecognition";
public static final String LOAN_CONTRACT_TERMINATION_RESPONSE = "loanContractTerminationResponse";
public static final String LOAN_UNDO_CONTRACT_TERMINATION_RESPONSE = "loanUndoContractTerminationResponse";
public static final String LOAN_BUY_DOWN_FEE_RESPONSE = "loanBuyDownFeeResponse";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1317,3 +1317,48 @@ Feature: Contract Termination
| 31 March 2024 | Accrual Adjustment | 0.15 | 0.0 | 0.15 | 0.0 | 0.0 | 0.0 | false | false |
| 31 March 2024 | Contract Termination | 57.37 | 57.05 | 0.32 | 0.0 | 0.0 | 0.0 | true | true |
And Global configuration "is-principal-compounding-disabled-for-overdue-loans" is disabled

@TestRailId:C4133
Scenario: Contract termination on disbursement date
When Admin sets the business date to "01 January 2025"
And Admin creates a client with random data
And 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_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION | 01 January 2025 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2025" with "100" amount and expected disbursement date on "01 January 2025"
And Admin successfully disburse the loan on "01 January 2025" with "100" EUR transaction amount
And Admin successfully terminates loan contract
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 |
| | | 01 January 2025 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2025 | | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
And 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 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 January 2025 | Contract Termination | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false |

@TestRailId:C4134
Scenario: Contract termination on disbursement date with interest recognition
When Admin sets the business date to "01 January 2025"
And Admin creates a client with random data
And 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_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION | 01 January 2025 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2025" with "100" amount and expected disbursement date on "01 January 2025"
And Admin successfully disburse the loan on "01 January 2025" with "100" EUR transaction amount
And Admin successfully terminates loan contract
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 |
| | | 01 January 2025 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2025 | | 0.0 | 100.0 | 0.02 | 0.0 | 0.0 | 100.02| 0.0 | 0.0 | 0.0 | 100.02 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 0.02 | 0.0 | 0.0 | 100.02 | 0.0 | 0.0 | 0.0 | 100.02 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 January 2025 | Contract Termination | 100.02 | 100.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 01 January 2025 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
Original file line number Diff line number Diff line change
Expand Up @@ -1410,10 +1410,17 @@ public void addLoanRepaymentScheduleInstallment(final LoanRepaymentScheduleInsta
* @param date
* @return a schedule installment is related to the provided date
**/
public LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(LocalDate date) {
return getRepaymentScheduleInstallment(
e -> (e.isFirstNormalInstallment() && DateUtils.isDateInRangeInclusive(date, e.getFromDate(), e.getDueDate()))
|| DateUtils.isDateInRangeFromExclusiveToInclusive(date, e.getFromDate(), e.getDueDate()));
public LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(final LocalDate date) {
return getRepaymentScheduleInstallment(e -> (DateUtils.isDateInRangeFromExclusiveToInclusive(date, e.getFromDate(), e.getDueDate())
|| (e.isFirstNormalInstallment(getRepaymentScheduleInstallments())
&& DateUtils.isDateInRangeInclusive(date, e.getFromDate(), e.getDueDate()))));
}

public List<LoanRepaymentScheduleInstallment> getInstallmentsUpToTransactionDate(final LocalDate transactionDate) {
return getRepaymentScheduleInstallments().stream()
.filter(i -> (transactionDate.isAfter(i.getFromDate())
|| (i.isFirstNormalInstallment(getRepaymentScheduleInstallments()) && !transactionDate.isBefore(i.getFromDate()))))
.collect(Collectors.toCollection(ArrayList::new));
}

public LoanRepaymentScheduleInstallment fetchRepaymentScheduleInstallment(final Integer installmentNumber) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1216,8 +1216,7 @@ private BigDecimal setScaleAndDefaultToNullIfZero(final BigDecimal value) {
return value.setScale(6, MoneyHelper.getRoundingMode());
}

public boolean isFirstNormalInstallment() {
return loan.getRepaymentScheduleInstallments().stream().filter(rp -> !rp.isDownPayment()).findFirst().stream()
.anyMatch(rp -> rp.equals(this));
public boolean isFirstNormalInstallment(List<LoanRepaymentScheduleInstallment> installments) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why to change this?

return installments.stream().filter(rp -> !rp.isDownPayment()).findFirst().stream().anyMatch(rp -> rp.equals(this));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2255,4 +2255,8 @@ public void updateVariationDays(final long daysToAdd) {
this.variationDays += daysToAdd;
}

public boolean isInterestRecognitionOnDisbursementDate() {
return this.interestRecognitionOnDisbursementDate;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ ChangedTransactionDetail processLatestTransaction(String transactionProcessingSt

ChangedTransactionDetail reprocessLoanTransactions(String transactionProcessingStrategyCode, LocalDate disbursementDate,
List<LoanTransaction> repaymentsOrWaivers, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges);
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges,
boolean interestRecognitionOnDisbursementDate);

LoanRepaymentScheduleTransactionProcessor getTransactionProcessor(String transactionProcessingStrategyCode);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1845,13 +1845,15 @@ private void handleAccelerateMaturityDate(final LoanTransaction loanTransaction,
final Loan loan = loanTransaction.getLoan();
final LoanRepaymentScheduleInstallment currentInstallment = loan.getRelatedRepaymentScheduleInstallment(transactionDate);

if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate())) {
if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate()) && currentInstallment != null) {
if (currentInstallment.isNotFullyPaidOff()) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
&& loan.isInterestBearingAndInterestRecalculationEnabled()) {
final BigDecimal interestOutstanding = currentInstallment.getInterestOutstanding(loan.getCurrency()).getAmount();
final LocalDate tillDate = (loan.getLoanProductRelatedDetail().isInterestRecognitionOnDisbursementDate()
&& transactionDate.equals(loan.getDisbursementDate())) ? transactionDate.plusDays(1L) : transactionDate;
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
currentInstallment.getDueDate(), transactionDate, true).getAmount();
currentInstallment.getDueDate(), tillDate, true).getAmount();
if (interestOutstanding.compareTo(BigDecimal.ZERO) > 0 || newInterest.compareTo(BigDecimal.ZERO) > 0) {
currentInstallment.updateInterestCharged(newInterest);
}
Expand Down Expand Up @@ -1902,9 +1904,8 @@ private void handleAccelerateMaturityDate(final LoanTransaction loanTransaction,
MathUtil.nullToZero(currentInstallment.getTotalPaidInAdvance()).add(futureTotalPaidInAdvance));
}

final List<LoanRepaymentScheduleInstallment> installmentsUpToTransactionDate = installments.stream()
.filter(installment -> transactionDate.isAfter(installment.getFromDate()))
.collect(Collectors.toCollection(ArrayList::new));
final List<LoanRepaymentScheduleInstallment> installmentsUpToTransactionDate = loan
.getInstallmentsUpToTransactionDate(transactionDate);

final List<LoanTransaction> transactionsToBeReprocessed = loan.getLoanTransactions().stream()
.filter(transaction -> transaction.getTransactionDate().isBefore(transactionDate))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,4 +440,8 @@ public Function<Long, LocalDate> resolveRepaymentPEriodLengthGeneratorFunction(L
default -> throw new UnsupportedOperationException();
};
}

public boolean isInterestRecognitionOnDisbursementDate() {
return loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,17 @@ private Loan getLoan(List<LoanTransaction> loanTransactions, List<LoanRepaymentS
@Override
public ChangedTransactionDetail reprocessLoanTransactions(String transactionProcessingStrategyCode, LocalDate disbursementDate,
List<LoanTransaction> loanTransactions, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges) {
Set<LoanCharge> charges, boolean interestRecognitionOnDisbursementDate) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(
transactionProcessingStrategyCode);
if (loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor) {
LocalDate currentDate = DateUtils.getBusinessLocalDate();
LocalDate interestForDate = DateUtils.getBusinessLocalDate();
if (interestRecognitionOnDisbursementDate && interestForDate.equals(disbursementDate)) {
Copy link
Contributor

@adamsaghy adamsaghy Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think its correct to use current date + 1 which effectively a future date. We cannot work against future date for transaction processing. Also interest is payable on the current date anyway, we dont need this. interestRecognitionOnDisbursementDate is ONLY for accruals, nothing else!

interestForDate = interestForDate.plusDays(1L);
}
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> result = advancedProcessor
.reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges);
.reprocessProgressiveLoanTransactions(disbursementDate, interestForDate, loanTransactions, currency, installments,
charges);
if (!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
modelRepository.writeInterestScheduleModel(getLoan(loanTransactions, installments, charges), result.getRight());
}
Expand Down
Loading
Loading